Compare commits

..

No commits in common. "6f66aa3c06d3ed29cdc0d5e680840e4125fccb04" and "1ba2709fdae04e62442a045fb3892aa8710e9d81" have entirely different histories.

397
sdn.cpp
View File

@ -26,7 +26,6 @@
#include <cwchar>
#include <climits>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
@ -265,8 +264,6 @@ fun decode_attrs (const vector<string> &attrs) -> chtype {
// --- Application -------------------------------------------------------------
enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define KEY(name) (SYM | KEY_ ## name)
#define CTRL 31 &
struct entry {
@ -283,47 +280,6 @@ 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") \
XX(DIRECTORY, "di") XX(SYMLINK, "ln") XX(MULTIHARDLINK, "mh") \
XX(FIFO, "pi") XX(SOCKET, "so") XX(DOOR, "do") XX(BLOCK, "bd") \
@ -339,14 +295,6 @@ enum { LS(XX) LS_COUNT };
static const char *g_ls_colors[] = {LS(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 {
string cwd; ///< Current working directory
string start_dir; ///< Starting directory
@ -370,9 +318,6 @@ static struct {
map<int, chtype> ls_colors; ///< LS_COLORS decoded
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():
@ -381,34 +326,17 @@ static struct {
struct tm now; ///< Current local time for display
} g;
// 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 {
fun ls_format (const string &filename, const struct stat &info) -> chtype {
int type = LS_ORPHAN;
auto set = [&](int t) { if (ls_is_colored (t)) type = t; };
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)) {
auto set = [&](int t) { if (g.ls_colors.count (t)) type = t; };
// TODO: LS_MISSING if available and this is a missing symlink target
if (S_ISREG (info.st_mode)) {
type = LS_FILE;
if (info.st_nlink > 1)
set (LS_MULTIHARDLINK);
if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
set (LS_EXECUTABLE);
if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
if (lgetxattr (filename.c_str (), "security.capability", NULL, 0) >= 0)
set (LS_CAPABILITY);
if ((info.st_mode & S_ISGID))
set (LS_SETGID);
@ -423,10 +351,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
if ((info.st_mode & S_ISVTX) && (info.st_mode & S_IWOTH))
set (LS_STICKY_OTHER_WRITABLE);
} 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;
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)) {
type = LS_FIFO;
} else if (S_ISSOCK (info.st_mode)) {
@ -442,9 +370,9 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
if (x != g.ls_colors.end ())
format = x->second;
auto dot = name.find_last_of ('.');
auto dot = filename.find_last_of ('.');
if (dot != string::npos && type == LS_FILE) {
const auto x = g.ls_exts.find (name.substr (++dot));
const auto x = g.ls_exts.find (filename.substr (++dot));
if (x != g.ls_exts.end ())
format = x->second;
}
@ -467,8 +395,8 @@ fun make_entry (const struct dirent *f) -> entry {
e.cols[entry::USER] = e.cols[entry::GROUP] =
e.cols[entry::SIZE] = e.cols[entry::MTIME] = apply_attrs (L"?", 0);
e.cols[entry::FILENAME] =
apply_attrs (to_wide (e.filename), ls_format (e, false));
auto format = ls_format (e.filename, info);
e.cols[entry::FILENAME] = apply_attrs (to_wide (e.filename), format);
return e;
}
@ -479,8 +407,7 @@ fun make_entry (const struct dirent *f) -> entry {
e.target_path = "?";
} else {
e.target_path = buf;
// If a symlink links to another symlink, we follow all the way
(void) stat (buf, &e.target_info);
(void) lstat (buf, &e.target_info);
}
}
@ -514,10 +441,11 @@ fun make_entry (const struct dirent *f) -> entry {
e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0);
auto &fn = e.cols[entry::FILENAME] =
apply_attrs (to_wide (e.filename), ls_format (e, false));
apply_attrs (to_wide (e.filename), ls_format (e.filename, info));
if (!e.target_path.empty ()) {
fn.append (apply_attrs (to_wide (" -> "), 0));
fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
fn.append (apply_attrs (to_wide (e.target_path),
ls_format (e.target_path, e.target_info)));
}
return e;
}
@ -621,6 +549,30 @@ fun search (const wstring &needle) {
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) {
if (chdir (path.c_str ())) {
beep ();
@ -642,118 +594,97 @@ fun choose (const entry &entry) -> bool {
return true;
}
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 {
fun handle (wint_t c, bool is_char) -> bool {
// If an editor is active, let it handle the key instead and eat it
if (g.editor) {
handle_editor (c);
handle_editor (c, is_char);
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];
auto i = g_normal_actions.find (c);
switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
case ACTION_CHOOSE_FULL:
switch (c) {
case ALT | L'\r':
case ALT | KEY_ENTER:
g.chosen_full = true;
g.chosen = current.filename;
return false;
case ACTION_CHOOSE:
case L'\r':
case KEY_ENTER:
if (choose (current))
break;
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;
case ACTION_UP:
case L'k': case CTRL L'p': case KEY_UP:
g.cursor--;
break;
case ACTION_DOWN:
case L'j': case CTRL L'n': case KEY_DOWN:
g.cursor++;
break;
case ACTION_TOP:
case L'g': case ALT | L'<': case KEY_HOME:
g.cursor = 0;
break;
case ACTION_BOTTOM:
case L'G': case ALT | L'>': case KEY_END:
g.cursor = int (g.entries.size ()) - 1;
break;
case ACTION_PAGE_PREVIOUS:
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 KEY_PPAGE: g.cursor -= LINES; break;
case KEY_NPAGE: g.cursor += LINES; break;
case ACTION_GO_START:
case CTRL L'e': g.offset++; break;
case CTRL L'y': g.offset--; break;
case '&':
change_dir (g.start_dir);
break;
case ACTION_GO_HOME:
case '~':
if (const auto *home = getenv ("HOME"))
change_dir (home);
else if (const auto *pw = getpwuid (getuid ()))
change_dir (pw->pw_dir);
break;
case ACTION_SEARCH:
g.editor = c;
break;
case ACTION_RENAME_PREFILL:
g.editor_line = to_wide (current.filename);
// Fall-through
case ACTION_RENAME:
g.editor = c & ~ALT;
break;
case ACTION_TOGGLE_FULL:
case L't':
case ALT | L't':
g.full_view = !g.full_view;
break;
case ACTION_REDRAW:
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;
break;
case CTRL L'L':
clear ();
break;
case ACTION_RELOAD:
case L'r':
reload ();
break;
case KEY_RESIZE:
case WEOF:
break;
default:
if (c != KEY (RESIZE) && c != WEOF)
beep ();
beep ();
}
g.cursor = max (g.cursor, 0);
g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
@ -836,11 +767,11 @@ fun load_ls_colors (vector<string> colors) {
auto equal = pair.find ('=');
if (equal == string::npos)
continue;
auto key = pair.substr (0, equal), value = 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, ";"));
attrs[pair.substr (0, equal)] =
decode_ansi_sgr (split (pair.substr (equal + 1), ";"));
}
// `LINK target` i.e. `ln=target` is not supported now
for (int i = 0; i < LS_COUNT; i++) {
auto m = attrs.find (g_ls_colors[i]);
if (m != attrs.end ())
@ -852,17 +783,15 @@ fun load_ls_colors (vector<string> colors) {
}
}
fun load_colors () {
// Bail out on dumb terminals, there's not much one can do about them
if (!has_colors () || start_color () == ERR || use_default_colors () == ERR)
return;
if (const char *colors = getenv ("LS_COLORS"))
load_ls_colors (split (colors, ":"));
fun load_configuration () {
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
if (!has_colors () || start_color () == ERR || use_default_colors () == ERR)
return;
string line;
while (getline (*config, line)) {
auto tokens = split (line, " ");
@ -873,117 +802,9 @@ fun load_colors () {
if (name == g.attr_names[i])
g.attrs[i] = decode_attrs (tokens);
}
}
fun read_key (wint_t &c) -> bool {
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;
}
if (const char *colors = getenv ("LS_COLORS"))
load_ls_colors (split (colors, ":"));
}
int main (int argc, char *argv[]) {
@ -1007,14 +828,12 @@ int main (int argc, char *argv[]) {
}
locale::global (locale (""));
load_bindings ();
if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR) {
cerr << "cannot initialize screen" << endl;
return 1;
}
load_colors ();
load_configuration ();
reload ();
g.start_dir = g.cwd;
update ();
@ -1023,13 +842,17 @@ int main (int argc, char *argv[]) {
// which would worsen start-up flickering
if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
endwin ();
cerr << "cannot configure input" << endl;
cerr << "cannot initialize screen" << endl;
return 1;
}
wint_t c;
while (!read_key (c) || handle (c))
while (1) {
inotify_check ();
int res = get_wch (&c);
if (res != ERR && !handle (c, res == OK))
break;
}
endwin ();
// Presumably it is going to end up as an argument, so quote it