Compare commits

...

7 Commits

Author SHA1 Message Date
Přemysl Eric Janouch d6846e6327
Add an action for chdir 2018-11-02 15:09:27 +01:00
Přemysl Eric Janouch 314ba114a1
Implement messages to the user 2018-11-02 15:05:04 +01:00
Přemysl Eric Janouch 4de89faf7e
Store runtime and configuration to a file
Added a toggle for gravity, now turned off by default.
2018-11-02 14:48:16 +01:00
Přemysl Eric Janouch beee2e2683
Unnecessary c_str() 2018-11-02 12:35:03 +01:00
Přemysl Eric Janouch 4ab0db3c04
Make sure to quote empty strings 2018-11-02 12:08:43 +01:00
Přemysl Eric Janouch 3624636c2f
New config parser
Basically a subset of Bourne shell.
2018-11-01 22:42:49 +01:00
Přemysl Eric Janouch e80c56e249
Cleanup 2018-11-01 22:19:31 +01:00
1 changed files with 177 additions and 41 deletions

218
sdn.cpp
View File

@ -114,7 +114,7 @@ fun needs_shell_quoting (const string &v) -> bool {
for (auto c : v)
if (strchr ("|&;<>()$`\\\"' \t\n" "*?[#˜=%" "!", c))
return true;
return false;
return v.empty ();
}
fun shell_escape (const string &v) -> string {
@ -130,6 +130,59 @@ fun shell_escape (const string &v) -> string {
return "'" + result + "'";
}
fun parse_line (istream &is, vector<string> &out) -> bool {
enum {STA, DEF, COM, ESC, WOR, QUO, STATES};
enum {TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6};
enum {TWOR = TAKE | WOR};
// We never transition back to the start state, so it can stay as a noop
static char table[STATES][7] = {
// state EOF SP, TAB ' # \ LF default
/* STA */ {ERROR, DEF, QUO, COM, ESC, STOP, TWOR},
/* DEF */ {STOP, 0, QUO, COM, ESC, STOP, TWOR},
/* COM */ {STOP, 0, 0, 0, 0, STOP, 0},
/* ESC */ {ERROR, TWOR, TWOR, TWOR, TWOR, TWOR, TWOR},
/* WOR */ {STOP | PUSH, DEF | PUSH, QUO, TAKE, ESC, STOP | PUSH, TAKE},
/* QUO */ {ERROR, TAKE, WOR, TAKE, TAKE, TAKE, TAKE},
};
out.clear (); string token; int state = STA;
constexpr auto eof = istream::traits_type::eof ();
while (1) {
int ch = is.get (), edge = 0;
switch (ch) {
case eof: edge = table[state][0]; break;
case '\t':
case ' ': edge = table[state][1]; break;
case '\'': edge = table[state][2]; break;
case '#': edge = table[state][3]; break;
case '\\': edge = table[state][4]; break;
case '\n': edge = table[state][5]; break;
default: edge = table[state][6]; break;
}
if (edge & TAKE)
token += ch;
if (edge & PUSH) {
out.push_back (token);
token.clear ();
}
if (edge & STOP)
return true;
if (edge & ERROR)
return false;
if (edge &= 7)
state = edge;
}
}
fun write_line (ostream &os, const vector<string> &in) {
if (!in.empty ())
os << shell_escape (in.at (0));
for (size_t i = 1; i < in.size (); i++)
os << " " << shell_escape (in.at (i));
os << endl;
}
fun decode_type (mode_t m) -> wchar_t {
if (S_ISDIR (m)) return L'd'; if (S_ISBLK (m)) return L'b';
if (S_ISCHR (m)) return L'c'; if (S_ISLNK (m)) return L'l';
@ -140,7 +193,7 @@ fun decode_type (mode_t m) -> wchar_t {
/// Return the modes of a file in the usual stat/ls format
fun decode_mode (mode_t m) -> wstring {
return { decode_type (m),
return {decode_type (m),
L"r-"[!(m & S_IRUSR)],
L"w-"[!(m & S_IWUSR)],
((m & S_ISUID) ? L"sS" : L"x-")[!(m & S_IXUSR)],
@ -149,8 +202,7 @@ fun decode_mode (mode_t m) -> wstring {
((m & S_ISGID) ? L"sS" : L"x-")[!(m & S_IXGRP)],
L"r-"[!(m & S_IROTH)],
L"w-"[!(m & S_IWOTH)],
((m & S_ISVTX) ? L"tT" : L"x-")[!(m & S_IXOTH)],
};
((m & S_ISVTX) ? L"tT" : L"x-")[!(m & S_IXOTH)]};
}
template<class T> fun shift (vector<T> &v) -> T {
@ -191,12 +243,23 @@ fun xdg_config_find (const string &suffix) -> unique_ptr<ifstream> {
for (const auto &dir : dirs) {
if (dir[0] != '/')
continue;
if (ifstream ifs {dir + suffix})
if (ifstream ifs {dir + "/" PROJECT_NAME "/" + suffix})
return make_unique<ifstream> (move (ifs));
}
return nullptr;
}
fun xdg_config_write (const string &suffix) -> unique_ptr<fstream> {
auto dir = xdg_config_home ();
if (dir[0] == '/') {
// TODO: try to create the end directory
if (fstream fs {dir + "/" PROJECT_NAME "/" + suffix,
fstream::in | fstream::out | fstream::trunc})
return make_unique<fstream> (move (fs));
}
return nullptr;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
using ncstring = basic_string<cchar_t>;
@ -307,7 +370,7 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define ACTIONS(XX) XX(NONE) XX(CHOOSE) XX(CHOOSE_FULL) XX(HELP) XX(QUIT) \
XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(PAGE_PREVIOUS) XX(PAGE_NEXT) \
XX(SCROLL_UP) XX(SCROLL_DOWN) XX(GO_START) XX(GO_HOME) \
XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CHDIR) XX(GO_START) XX(GO_HOME) \
XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) \
XX(TOGGLE_FULL) XX(REDRAW) XX(RELOAD) \
XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE)
@ -320,7 +383,7 @@ enum action { ACTIONS(XX) ACTION_COUNT };
static const char *g_action_names[] = {ACTIONS(XX)};
#undef XX
static map<wint_t, action> g_normal_actions = {
static map<wint_t, action> g_normal_actions {
{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE}, {'h', ACTION_HELP},
// M-o ought to be the same shortcut the navigator is launched with
@ -331,18 +394,18 @@ static map<wint_t, action> g_normal_actions = {
{'G', ACTION_BOTTOM}, {ALT | '>', ACTION_BOTTOM}, {KEY(END), ACTION_BOTTOM},
{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
{CTRL 'y', ACTION_SCROLL_UP}, {CTRL 'e', ACTION_SCROLL_DOWN},
{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
{'c', ACTION_CHDIR}, {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH},
{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
{'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL},
{CTRL 'L', ACTION_REDRAW}, {'r', ACTION_RELOAD},
};
static map<wint_t, action> g_input_actions = {
static map<wint_t, action> g_input_actions {
{27, ACTION_INPUT_ABORT}, {CTRL 'g', ACTION_INPUT_ABORT},
{L'\r', ACTION_INPUT_CONFIRM}, {KEY (ENTER), ACTION_INPUT_CONFIRM},
{KEY (BACKSPACE), ACTION_INPUT_B_DELETE},
};
static const map<string, map<wint_t, action>*> g_binding_contexts = {
static const map<string, map<wint_t, action>*> g_binding_contexts {
{"normal", &g_normal_actions}, {"input", &g_input_actions},
};
@ -395,8 +458,12 @@ static struct {
vector<level> levels; ///< Upper directory levels
int offset, cursor; ///< Scroll offset and cursor position
bool full_view; ///< Show extended information
bool gravity; ///< Entries are shoved to the bottom
int max_widths[entry::COLUMNS]; ///< Column widths
wstring message; ///< Message for the user
int message_ttl; ///< Time to live for the message
string chosen; ///< Chosen item for the command line
bool chosen_full; ///< Use the full path
@ -581,7 +648,7 @@ fun update () {
auto index = g.offset + i;
bool selected = index == g.cursor;
attrset (selected ? g.attrs[g.AT_CURSOR] : 0);
move (available - used + i, 0);
move (g.gravity ? (available - used + i) : i, 0);
auto used = 0;
for (int col = start_column; col < entry::COLUMNS; col++) {
@ -603,13 +670,16 @@ fun update () {
hline (' ', COLS - print (bar, COLS));
attrset (g.attrs[g.AT_INPUT]);
curs_set (0);
if (g.editor) {
move (LINES - 1, 0);
auto p = apply_attrs (wstring (g.editor) + L": ", 0);
move (LINES - 1, print (p + apply_attrs (g.editor_line, 0), COLS - 1));
curs_set (1);
} else
curs_set (0);
} else if (!g.message.empty ()) {
move (LINES - 1, 0);
print (apply_attrs (g.message, 0), COLS);
}
refresh ();
}
@ -654,7 +724,11 @@ fun reload () {
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
}
// TODO: we should be able to signal failures to the user
fun show_message (const string &message, int ttl = 30) {
g.message = to_wide (message);
g.message_ttl = ttl;
}
fun run_pager (FILE *contents) {
// We don't really need to set O_CLOEXEC, so we're not going to
rewind (contents);
@ -735,12 +809,28 @@ fun search (const wstring &needle) {
fun is_ancestor_dir (const string &ancestor, const string &of) -> bool {
if (strncmp (ancestor.c_str (), of.c_str (), ancestor.length ()))
return false;
return of.c_str ()[ancestor.length ()] == '/'
|| (ancestor == "/" && ancestor != of);
return of[ancestor.length ()] == '/' || (ancestor == "/" && ancestor != of);
}
fun pop_levels () {
string anchor; auto i = g.levels.rbegin ();
while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) {
if (i->path == g.cwd) {
g.offset = i->offset;
g.cursor = i->cursor;
anchor = i->filename;
}
i++;
g.levels.pop_back ();
}
if (!anchor.empty () && (g.cursor >= g.entries.size ()
|| g.entries[g.cursor].filename != anchor))
search (to_wide (anchor));
}
fun change_dir (const string &path) {
if (chdir (path.c_str ())) {
show_message (strerror (errno));
beep ();
return;
}
@ -752,20 +842,7 @@ fun change_dir (const string &path) {
g.levels.push_back (last);
g.offset = g.cursor = 0;
} else {
string anchor;
auto i = g.levels.rbegin ();
while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) {
if (i->path == g.cwd) {
g.offset = i->offset;
g.cursor = i->cursor;
anchor = i->filename;
}
i++;
g.levels.pop_back ();
}
if (!anchor.empty () && (g.cursor >= g.entries.size ()
|| g.entries[g.cursor].filename != anchor))
search (to_wide (anchor));
pop_levels ();
}
}
@ -858,6 +935,12 @@ fun handle (wint_t c) -> bool {
g.offset--;
break;
case ACTION_CHDIR:
g.editor = L"chdir";
g.editor_on_confirm = [] {
change_dir (to_mb (g.editor_line));
};
break;
case ACTION_GO_START:
change_dir (g.start_dir);
break;
@ -1003,14 +1086,13 @@ fun load_colors () {
if (const char *colors = getenv ("LS_COLORS"))
load_ls_colors (split (colors, ":"));
auto config = xdg_config_find ("/" PROJECT_NAME "/look");
auto config = xdg_config_find ("look");
if (!config)
return;
string line;
while (getline (*config, line)) {
auto tokens = split (line, " ");
if (tokens.empty () || line.front () == '#')
vector<string> tokens;
while (parse_line (*config, tokens)) {
if (tokens.empty ())
continue;
auto name = shift (tokens);
for (int i = 0; i < g.AT_COUNT; i++)
@ -1091,7 +1173,7 @@ fun load_bindings () {
learn_named_key (filtered, SYM | kc);
}
auto config = xdg_config_find ("/" PROJECT_NAME "/bindings");
auto config = xdg_config_find ("bindings");
if (!config)
return;
@ -1106,10 +1188,9 @@ fun load_bindings () {
actions[name] = action (a++);
}
string line;
while (getline (*config, line)) {
auto tokens = split (line, " ");
if (tokens.empty () || line.front () == '#')
vector<string> tokens;
while (parse_line (*config, tokens)) {
if (tokens.empty ())
continue;
if (tokens.size () < 3) {
cerr << "bindings: expected: context binding action";
@ -1134,6 +1215,53 @@ fun load_bindings () {
}
}
fun load_history_level (const vector<string> &v) {
if (v.size () != 6)
return;
// Not checking the hostname and parent PID right now since we can't merge
g.levels.push_back ({stoi (v.at (4)), stoi (v.at (5)), v.at (3), v.at (6)});
}
fun load_config () {
auto config = xdg_config_find ("config");
if (!config)
return;
vector<string> tokens;
while (parse_line (*config, tokens)) {
if (tokens.empty ())
continue;
if (tokens.front () == "full-view")
g.full_view = tokens.size () > 1 && tokens.at (1) == "1";
else if (tokens.front () == "gravity")
g.gravity = tokens.size () > 1 && tokens.at (1) == "1";
else if (tokens.front () == "history")
load_history_level (tokens);
}
}
fun save_config () {
auto config = xdg_config_write ("config");
if (!config)
return;
write_line (*config, {"full-view", g.full_view ? "1" : "0"});
write_line (*config, {"gravity", g.gravity ? "1" : "0"});
char hostname[256];
if (gethostname (hostname, sizeof hostname))
*hostname = 0;
auto ppid = std::to_string (getppid ());
for (auto i = g.levels.begin (); i != g.levels.end (); i++)
write_line (*config, {"history", hostname, ppid, i->path,
to_string (i->offset), to_string (i->cursor), i->filename});
write_line (*config, {"history", hostname, ppid, g.cwd,
to_string (g.offset), to_string (g.cursor),
g.entries[g.cursor].filename});
}
int main (int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -1159,6 +1287,7 @@ int main (int argc, char *argv[]) {
locale::global (locale (""));
load_bindings ();
load_config ();
if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR) {
cerr << "cannot initialize screen" << endl;
@ -1168,6 +1297,7 @@ int main (int argc, char *argv[]) {
load_colors ();
reload ();
g.start_dir = g.cwd;
pop_levels ();
update ();
// Invoking keypad() earlier would make ncurses flush its output buffer,
@ -1179,9 +1309,15 @@ int main (int argc, char *argv[]) {
}
wint_t c;
while (!read_key (c) || handle (c))
while (!read_key (c) || handle (c)) {
inotify_check ();
if (g.message_ttl && !--g.message_ttl) {
g.message.clear ();
update ();
}
}
endwin ();
save_config ();
// Presumably it is going to end up as an argument, so quote it
if (!g.chosen.empty ())