Compare commits

...

4 Commits

Author SHA1 Message Date
6f66aa3c06
Configurable key bindings 2018-10-25 23:05:34 +02:00
86b520006c
Look up bindings through a map 2018-10-25 23:05:34 +02:00
2484c94b39
Fix key binding collisions
Still not fixed in handle_editor() though.
2018-10-25 23:05:33 +02:00
6e34f480a9
Finish copying the GNU ls coloring algorithm 2018-10-25 23:05:33 +02:00

393
sdn.cpp
View File

@ -26,6 +26,7 @@
#include <cwchar> #include <cwchar>
#include <climits> #include <climits>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <fstream> #include <fstream>
#include <map> #include <map>
@ -264,6 +265,8 @@ fun decode_attrs (const vector<string> &attrs) -> chtype {
// --- Application ------------------------------------------------------------- // --- Application -------------------------------------------------------------
enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define KEY(name) (SYM | KEY_ ## name)
#define CTRL 31 & #define CTRL 31 &
struct entry { struct entry {
@ -280,6 +283,47 @@ struct entry {
} }
}; };
#define ACTIONS(XX) XX(NONE) XX(CHOOSE) XX(CHOOSE_FULL) 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(SEARCH) XX(RENAME) XX(RENAME_PREFILL) \
XX(TOGGLE_FULL) XX(REDRAW) XX(RELOAD) \
XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE)
#define XX(name) ACTION_ ## name,
enum action { ACTIONS(XX) ACTION_COUNT };
#undef XX
#define XX(name) #name,
static const char *g_action_names[] = {ACTIONS(XX)};
#undef XX
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},
// M-o ought to be the same shortcut the navigator is launched with
{ALT | 'o', ACTION_QUIT}, {'q', ACTION_QUIT},
{'k', ACTION_UP}, {CTRL 'p', ACTION_UP}, {KEY (UP), ACTION_UP},
{'j', ACTION_DOWN}, {CTRL 'n', ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
{'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},
{'/', 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 = {
{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 = {
{"normal", &g_normal_actions}, {"input", &g_input_actions},
};
#define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \ #define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \
XX(DIRECTORY, "di") XX(SYMLINK, "ln") XX(MULTIHARDLINK, "mh") \ XX(DIRECTORY, "di") XX(SYMLINK, "ln") XX(MULTIHARDLINK, "mh") \
XX(FIFO, "pi") XX(SOCKET, "so") XX(DOOR, "do") XX(BLOCK, "bd") \ XX(FIFO, "pi") XX(SOCKET, "so") XX(DOOR, "do") XX(BLOCK, "bd") \
@ -295,6 +339,14 @@ enum { LS(XX) LS_COUNT };
static const char *g_ls_colors[] = {LS(XX)}; static const char *g_ls_colors[] = {LS(XX)};
#undef XX #undef XX
struct stringcaseless {
bool operator () (const string &a, const string &b) const {
const auto &c = locale::classic();
return lexicographical_compare (begin (a), end (a), begin (b), end (b),
[&](char m, char n) { return tolower (m, c) < tolower (n, c); });
}
};
static struct { static struct {
string cwd; ///< Current working directory string cwd; ///< Current working directory
string start_dir; ///< Starting directory string start_dir; ///< Starting directory
@ -318,6 +370,9 @@ static struct {
map<int, chtype> ls_colors; ///< LS_COLORS decoded map<int, chtype> ls_colors; ///< LS_COLORS decoded
map<string, chtype> ls_exts; ///< LS_COLORS file extensions map<string, chtype> ls_exts; ///< LS_COLORS file extensions
bool ls_symlink_as_target; ///< ln=target in dircolors
map<string, wint_t, stringcaseless> key_names;
// Refreshed by reload(): // Refreshed by reload():
@ -326,17 +381,34 @@ static struct {
struct tm now; ///< Current local time for display struct tm now; ///< Current local time for display
} g; } g;
fun ls_format (const string &filename, const struct stat &info) -> chtype { // The coloring logic has been more or less exactly copied from GNU ls,
// simplified and rewritten to reflect local implementation specifics
fun ls_is_colored (int type) -> bool {
auto i = g.ls_colors.find (type);
return i != g.ls_colors.end () && i->second != 0;
}
fun ls_format (const entry &e, bool for_target) -> chtype {
int type = LS_ORPHAN; int type = LS_ORPHAN;
auto set = [&](int t) { if (g.ls_colors.count (t)) type = t; }; auto set = [&](int t) { if (ls_is_colored (t)) type = t; };
// TODO: LS_MISSING if available and this is a missing symlink target
if (S_ISREG (info.st_mode)) { const auto &name = for_target
? e.target_path : e.filename;
const auto &info =
(for_target || (g.ls_symlink_as_target && e.target_info.st_mode))
? e.target_info : e.info;
if (for_target && info.st_mode == 0) {
// This differs from GNU ls: we use ORPHAN when MISSING is not set,
// but GNU ls colors by dirent::d_type
set (LS_MISSING);
} else if (S_ISREG (info.st_mode)) {
type = LS_FILE; type = LS_FILE;
if (info.st_nlink > 1) if (info.st_nlink > 1)
set (LS_MULTIHARDLINK); set (LS_MULTIHARDLINK);
if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
set (LS_EXECUTABLE); set (LS_EXECUTABLE);
if (lgetxattr (filename.c_str (), "security.capability", NULL, 0) >= 0) if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
set (LS_CAPABILITY); set (LS_CAPABILITY);
if ((info.st_mode & S_ISGID)) if ((info.st_mode & S_ISGID))
set (LS_SETGID); set (LS_SETGID);
@ -351,10 +423,10 @@ fun ls_format (const string &filename, const struct stat &info) -> chtype {
if ((info.st_mode & S_ISVTX) && (info.st_mode & S_IWOTH)) if ((info.st_mode & S_ISVTX) && (info.st_mode & S_IWOTH))
set (LS_STICKY_OTHER_WRITABLE); set (LS_STICKY_OTHER_WRITABLE);
} else if (S_ISLNK (info.st_mode)) { } else if (S_ISLNK (info.st_mode)) {
// TODO: LS_ORPHAN when symlink target is missing and either
// a/ "li" is "target", or
// b/ LS_ORPHAN is available
type = LS_SYMLINK; type = LS_SYMLINK;
if (!e.target_info.st_mode
&& (ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
type = LS_ORPHAN;
} else if (S_ISFIFO (info.st_mode)) { } else if (S_ISFIFO (info.st_mode)) {
type = LS_FIFO; type = LS_FIFO;
} else if (S_ISSOCK (info.st_mode)) { } else if (S_ISSOCK (info.st_mode)) {
@ -370,9 +442,9 @@ fun ls_format (const string &filename, const struct stat &info) -> chtype {
if (x != g.ls_colors.end ()) if (x != g.ls_colors.end ())
format = x->second; format = x->second;
auto dot = filename.find_last_of ('.'); auto dot = name.find_last_of ('.');
if (dot != string::npos && type == LS_FILE) { if (dot != string::npos && type == LS_FILE) {
const auto x = g.ls_exts.find (filename.substr (++dot)); const auto x = g.ls_exts.find (name.substr (++dot));
if (x != g.ls_exts.end ()) if (x != g.ls_exts.end ())
format = x->second; format = x->second;
} }
@ -395,8 +467,8 @@ fun make_entry (const struct dirent *f) -> entry {
e.cols[entry::USER] = e.cols[entry::GROUP] = e.cols[entry::USER] = e.cols[entry::GROUP] =
e.cols[entry::SIZE] = e.cols[entry::MTIME] = apply_attrs (L"?", 0); e.cols[entry::SIZE] = e.cols[entry::MTIME] = apply_attrs (L"?", 0);
auto format = ls_format (e.filename, info); e.cols[entry::FILENAME] =
e.cols[entry::FILENAME] = apply_attrs (to_wide (e.filename), format); apply_attrs (to_wide (e.filename), ls_format (e, false));
return e; return e;
} }
@ -407,7 +479,8 @@ fun make_entry (const struct dirent *f) -> entry {
e.target_path = "?"; e.target_path = "?";
} else { } else {
e.target_path = buf; e.target_path = buf;
(void) lstat (buf, &e.target_info); // If a symlink links to another symlink, we follow all the way
(void) stat (buf, &e.target_info);
} }
} }
@ -441,11 +514,10 @@ fun make_entry (const struct dirent *f) -> entry {
e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0); e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0);
auto &fn = e.cols[entry::FILENAME] = auto &fn = e.cols[entry::FILENAME] =
apply_attrs (to_wide (e.filename), ls_format (e.filename, info)); apply_attrs (to_wide (e.filename), ls_format (e, false));
if (!e.target_path.empty ()) { if (!e.target_path.empty ()) {
fn.append (apply_attrs (to_wide (" -> "), 0)); fn.append (apply_attrs (to_wide (" -> "), 0));
fn.append (apply_attrs (to_wide (e.target_path), fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
ls_format (e.target_path, e.target_info)));
} }
return e; return e;
} }
@ -549,30 +621,6 @@ fun search (const wstring &needle) {
g.cursor = best; g.cursor = best;
} }
fun handle_editor (wint_t c, bool is_char) {
if (c == 27 || c == (CTRL L'g')) {
g.editor_line.clear ();
g.editor = 0;
} else if (c == L'\r' || (!is_char && c == KEY_ENTER)) {
if (g.editor == L'e') {
auto mb = to_mb (g.editor_line);
rename (g.entries[g.cursor].filename.c_str (), mb.c_str ());
reload ();
}
g.editor_line.clear ();
g.editor = 0;
} else if (is_char) {
g.editor_line += c;
if (g.editor == L'/'
|| g.editor == L's')
search (g.editor_line);
} else if (c == KEY_BACKSPACE) {
if (!g.editor_line.empty ())
g.editor_line.erase (g.editor_line.length () - 1);
} else
beep ();
}
fun change_dir (const string& path) { fun change_dir (const string& path) {
if (chdir (path.c_str ())) { if (chdir (path.c_str ())) {
beep (); beep ();
@ -594,96 +642,117 @@ fun choose (const entry &entry) -> bool {
return true; return true;
} }
fun handle (wint_t c, bool is_char) -> bool { fun handle_editor (wint_t c) {
// FIXME: do not check editor actions by the prompt letter
auto i = g_input_actions.find (c);
switch (i == g_input_actions.end () ? ACTION_NONE : i->second) {
case ACTION_INPUT_ABORT:
g.editor_line.clear ();
g.editor = 0;
break;
case ACTION_INPUT_CONFIRM:
if (g.editor == L'e') {
auto mb = to_mb (g.editor_line);
rename (g.entries[g.cursor].filename.c_str (), mb.c_str ());
reload ();
}
g.editor_line.clear ();
g.editor = 0;
break;
case ACTION_INPUT_B_DELETE:
if (!g.editor_line.empty ())
g.editor_line.erase (g.editor_line.length () - 1);
break;
default:
if (c & (ALT | SYM)) {
beep ();
} else {
g.editor_line += c;
if (g.editor == L'/'
|| g.editor == L's')
search (g.editor_line);
}
}
}
fun handle (wint_t c) -> bool {
// If an editor is active, let it handle the key instead and eat it // If an editor is active, let it handle the key instead and eat it
if (g.editor) { if (g.editor) {
handle_editor (c, is_char); handle_editor (c);
c = WEOF; c = WEOF;
} }
// Translate the Alt key into a bit outside the range of Unicode
enum { ALT = 1 << 24 };
if (c == 27) {
if (get_wch (&c) == ERR) {
beep ();
return true;
}
c |= ALT;
}
const auto &current = g.entries[g.cursor]; const auto &current = g.entries[g.cursor];
switch (c) { auto i = g_normal_actions.find (c);
case ALT | L'\r': switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
case ALT | KEY_ENTER: case ACTION_CHOOSE_FULL:
g.chosen_full = true; g.chosen_full = true;
g.chosen = current.filename; g.chosen = current.filename;
return false; return false;
case L'\r': case ACTION_CHOOSE:
case KEY_ENTER:
if (choose (current)) if (choose (current))
break; break;
return false; return false;
case ACTION_QUIT:
// M-o ought to be the same shortcut the navigator is launched with
case ALT | L'o':
case L'q':
return false; return false;
case L'k': case CTRL L'p': case KEY_UP: case ACTION_UP:
g.cursor--; g.cursor--;
break; break;
case L'j': case CTRL L'n': case KEY_DOWN: case ACTION_DOWN:
g.cursor++; g.cursor++;
break; break;
case L'g': case ALT | L'<': case KEY_HOME: case ACTION_TOP:
g.cursor = 0; g.cursor = 0;
break; break;
case L'G': case ALT | L'>': case KEY_END: case ACTION_BOTTOM:
g.cursor = int (g.entries.size ()) - 1; g.cursor = int (g.entries.size ()) - 1;
break; break;
case KEY_PPAGE: g.cursor -= LINES; break; case ACTION_PAGE_PREVIOUS:
case KEY_NPAGE: g.cursor += LINES; break; g.cursor -= LINES;
break;
case ACTION_PAGE_NEXT:
g.cursor += LINES;
break;
case ACTION_SCROLL_DOWN:
g.offset++;
break;
case ACTION_SCROLL_UP:
g.offset--;
break;
case CTRL L'e': g.offset++; break; case ACTION_GO_START:
case CTRL L'y': g.offset--; break;
case '&':
change_dir (g.start_dir); change_dir (g.start_dir);
break; break;
case '~': case ACTION_GO_HOME:
if (const auto *home = getenv ("HOME")) if (const auto *home = getenv ("HOME"))
change_dir (home); change_dir (home);
else if (const auto *pw = getpwuid (getuid ())) else if (const auto *pw = getpwuid (getuid ()))
change_dir (pw->pw_dir); change_dir (pw->pw_dir);
break; break;
case L't': case ACTION_SEARCH:
case ALT | L't':
g.full_view = !g.full_view;
break;
case ALT | L'e':
g.editor_line = to_wide (current.filename);
// Fall-through
case L'e':
g.editor = c & ~ALT;
break;
case L'/':
case L's':
g.editor = c; g.editor = c;
break; break;
case ACTION_RENAME_PREFILL:
g.editor_line = to_wide (current.filename);
// Fall-through
case ACTION_RENAME:
g.editor = c & ~ALT;
break;
case CTRL L'L': case ACTION_TOGGLE_FULL:
g.full_view = !g.full_view;
break;
case ACTION_REDRAW:
clear (); clear ();
break; break;
case L'r': case ACTION_RELOAD:
reload (); reload ();
break; break;
case KEY_RESIZE:
case WEOF:
break;
default: default:
if (c != KEY (RESIZE) && c != WEOF)
beep (); beep ();
} }
g.cursor = max (g.cursor, 0); g.cursor = max (g.cursor, 0);
@ -767,11 +836,11 @@ fun load_ls_colors (vector<string> colors) {
auto equal = pair.find ('='); auto equal = pair.find ('=');
if (equal == string::npos) if (equal == string::npos)
continue; continue;
attrs[pair.substr (0, equal)] = auto key = pair.substr (0, equal), value = pair.substr (equal + 1);
decode_ansi_sgr (split (pair.substr (equal + 1), ";")); if (key != g_ls_colors[LS_SYMLINK]
|| !(g.ls_symlink_as_target = value == "target"))
attrs[key] = decode_ansi_sgr (split (value, ";"));
} }
// `LINK target` i.e. `ln=target` is not supported now
for (int i = 0; i < LS_COUNT; i++) { for (int i = 0; i < LS_COUNT; i++) {
auto m = attrs.find (g_ls_colors[i]); auto m = attrs.find (g_ls_colors[i]);
if (m != attrs.end ()) if (m != attrs.end ())
@ -783,14 +852,16 @@ fun load_ls_colors (vector<string> colors) {
} }
} }
fun load_configuration () { fun load_colors () {
auto config = xdg_config_find ("/" PROJECT_NAME "/look");
if (!config)
return;
// Bail out on dumb terminals, there's not much one can do about them // Bail out on dumb terminals, there's not much one can do about them
if (!has_colors () || start_color () == ERR || use_default_colors () == ERR) if (!has_colors () || start_color () == ERR || use_default_colors () == ERR)
return; return;
if (const char *colors = getenv ("LS_COLORS"))
load_ls_colors (split (colors, ":"));
auto config = xdg_config_find ("/" PROJECT_NAME "/look");
if (!config)
return;
string line; string line;
while (getline (*config, line)) { while (getline (*config, line)) {
@ -802,9 +873,117 @@ fun load_configuration () {
if (name == g.attr_names[i]) if (name == g.attr_names[i])
g.attrs[i] = decode_attrs (tokens); g.attrs[i] = decode_attrs (tokens);
} }
}
if (const char *colors = getenv ("LS_COLORS")) fun read_key (wint_t &c) -> bool {
load_ls_colors (split (colors, ":")); int res = get_wch (&c);
if (res == ERR)
return false;
wint_t metafied{};
if (c == 27 && (res = get_wch (&metafied)) != ERR)
c = ALT | metafied;
if (res == KEY_CODE_YES)
c |= SYM;
return true;
}
fun parse_key (const string &key_name) -> wint_t {
wint_t c{};
auto p = key_name.c_str ();
if (!strncmp (p, "M-", 2)) {
c |= ALT;
p += 2;
}
if (!strncmp (p, "C-", 2)) {
p += 2;
if (*p < 32) {
cerr << "bindings: invalid combination: " << key_name << endl;
return WEOF;
}
c |= CTRL *p;
p += 1;
} else if (g.key_names.count (p)) {
c |= g.key_names.at (p);
p += strlen (p);
} else {
wchar_t w; mbstate_t mb {};
auto len = strlen (p) + 1, res = mbrtowc (&w, p, len, &mb);
if (res == 0) {
cerr << "bindings: missing key name: " << key_name << endl;
return WEOF;
}
if (res == size_t (-1) || res == size_t (-2)) {
cerr << "bindings: invalid encoding: " << key_name << endl;
return WEOF;
}
c |= w;
p += res;
}
if (*p) {
cerr << "key name has unparsable trailing part: " << key_name << endl;
return WEOF;
}
return c;
}
fun load_bindings () {
g.key_names["space"] = ' ';
for (int kc = KEY_MIN; kc < KEY_MAX; kc++) {
const char *name = keyname (kc);
if (!name)
continue;
if (!strncmp (name, "KEY_", 4))
name += 4;
string filtered;
for (; *name; name++) {
if (*name != '(' && *name != ')')
filtered += *name;
}
g.key_names[filtered] = kc;
}
auto config = xdg_config_find ("/" PROJECT_NAME "/bindings");
if (!config)
return;
// Stringization in the preprocessor is a bit limited, we want lisp-case
map<string, action> actions;
int a;
for (auto p : g_action_names) {
string name;
for (; *p; p++)
name += *p == '_' ? '-' : *p + 'a' - 'A';
actions[name] = action (a++);
}
string line;
while (getline (*config, line)) {
auto tokens = split (line, " ");
if (tokens.empty () || line.front () == '#')
continue;
if (tokens.size () < 3) {
cerr << "bindings: expected: context binding action";
continue;
}
auto context = tokens[0], key_name = tokens[1], action = tokens[2];
auto m = g_binding_contexts.find (context);
if (m == g_binding_contexts.end ()) {
cerr << "bindings: invalid context: " << context << endl;
continue;
}
wint_t c = parse_key (key_name);
if (c == WEOF)
continue;
auto i = actions.find (action);
if (i == actions.end ()) {
cerr << "bindings: invalid action: " << action << endl;
continue;
}
(*m->second)[c] = i->second;
}
} }
int main (int argc, char *argv[]) { int main (int argc, char *argv[]) {
@ -828,12 +1007,14 @@ int main (int argc, char *argv[]) {
} }
locale::global (locale ("")); locale::global (locale (""));
load_bindings ();
if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR) { if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR) {
cerr << "cannot initialize screen" << endl; cerr << "cannot initialize screen" << endl;
return 1; return 1;
} }
load_configuration (); load_colors ();
reload (); reload ();
g.start_dir = g.cwd; g.start_dir = g.cwd;
update (); update ();
@ -842,17 +1023,13 @@ int main (int argc, char *argv[]) {
// which would worsen start-up flickering // which would worsen start-up flickering
if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) { if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
endwin (); endwin ();
cerr << "cannot initialize screen" << endl; cerr << "cannot configure input" << endl;
return 1; return 1;
} }
wint_t c; wint_t c;
while (1) { while (!read_key (c) || handle (c))
inotify_check (); inotify_check ();
int res = get_wch (&c);
if (res != ERR && !handle (c, res == OK))
break;
}
endwin (); endwin ();
// Presumably it is going to end up as an argument, so quote it // Presumably it is going to end up as an argument, so quote it