diff options
Diffstat (limited to 'sdn.cpp')
-rw-r--r-- | sdn.cpp | 716 |
1 files changed, 514 insertions, 202 deletions
@@ -1,7 +1,7 @@ // // sdn: simple directory navigator // -// Copyright (c) 2017 - 2018, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name> // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. @@ -18,37 +18,44 @@ // May be required for ncursesw and we generally want it all anyway #define _XOPEN_SOURCE_EXTENDED -#include <string> -#include <vector> -#include <locale> -#include <iostream> #include <algorithm> -#include <cwchar> #include <climits> #include <cstdlib> #include <cstring> +#include <cwchar> #include <fstream> +#include <iostream> +#include <locale> #include <map> -#include <tuple> #include <memory> +#include <string> +#include <tuple> +#include <vector> -#include <unistd.h> #include <dirent.h> -#include <sys/stat.h> -#include <sys/types.h> -#include <sys/acl.h> #include <fcntl.h> -#include <pwd.h> +#include <fnmatch.h> #include <grp.h> #include <libgen.h> +#include <pwd.h> +#include <signal.h> +#include <sys/acl.h> +#include <sys/stat.h> +#include <sys/types.h> #include <time.h> +#include <unistd.h> +#include <acl/libacl.h> +#include <ncurses.h> #include <sys/inotify.h> -#include <sys/xattr.h> #include <sys/types.h> #include <sys/wait.h> -#include <acl/libacl.h> -#include <ncurses.h> +#include <sys/xattr.h> + +// To implement cbreak() with disabled ^S that gets reënabled on endwin() +#define NCURSES_INTERNALS +#include <term.h> +#undef CTRL // term.h -> termios.h -> sys/ttydefaults.h, too simplistic // Unicode is complex enough already and we might make assumptions #ifndef __STDC_ISO_10646__ @@ -92,8 +99,8 @@ fun to_mb (const wstring &wide) -> string { return mb; } -fun prefix_length (const wstring &in, const wstring &of) -> int { - int score = 0; +fun prefix_length (const wstring &in, const wstring &of) -> size_t { + size_t score = 0; for (size_t i = 0; i < of.size () && in.size () >= i && in[i] == of[i]; i++) score++; return score; @@ -113,6 +120,28 @@ fun split (const string &s, const string &sep) -> vector<string> { vector<string> result; split (s, sep, result); return result; } +fun untilde (const string &path) -> string { + if (path.empty ()) + return path; + + string tail = path.substr (1); + if (path[0] == '\\') + return tail; + if (path[0] != '~') + return path; + + // If there is something between the ~ and the first / (or the EOS) + if (size_t until_slash = strcspn (tail.c_str (), "/")) { + if (const auto *pw = getpwnam (tail.substr (0, until_slash).c_str ())) + return pw->pw_dir + tail.substr (until_slash); + } else if (const auto *home = getenv ("HOME")) { + return home + tail; + } else if (const auto *pw = getpwuid (getuid ())) { + return pw->pw_dir + tail; + } + return path; +} + fun needs_shell_quoting (const string &v) -> bool { // IEEE Std 1003.1 sh + the exclamation mark because of csh/bash // history expansion, implicitly also the NUL character @@ -136,11 +165,11 @@ fun shell_escape (const string &v) -> string { } 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}; + 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 + // We never transition back to the start state, so it can stay as a no-op static char table[STATES][7] = { // state EOF SP, TAB ' # \ LF default /* STA */ {ERROR, DEF, QUO, COM, ESC, STOP, TWOR}, @@ -221,8 +250,9 @@ fun capitalize (const string &s) -> string { return result; } -/// Underlining for teletypes, also imitated in more(1) and less(1) -fun underline (const string& s) -> string { +/// Underlining for teletypes (also called overstriking), +/// also imitated in more(1) and less(1) +fun underline (const string &s) -> string { string result; for (auto c : s) result.append ({c, 8, '_'}); @@ -244,7 +274,7 @@ fun xdg_config_home () -> string { fun xdg_config_find (const string &suffix) -> unique_ptr<ifstream> { vector<string> dirs {xdg_config_home ()}; const char *system_dirs = getenv ("XDG_CONFIG_DIRS"); - split (system_dirs ? system_dirs : "/etc/xdg", ":", dirs); + split ((system_dirs && *system_dirs) ? system_dirs : "/etc/xdg", ":", dirs); for (const auto &dir : dirs) { if (dir[0] != '/') continue; @@ -294,16 +324,16 @@ fun invert (cchar_t &ch) { } fun apply_attrs (const wstring &w, attr_t attrs) -> ncstring { - ncstring res; - for (auto c : w) - res += cchar (attrs, c); + ncstring res (w.size (), cchar_t {}); + for (size_t i = 0; i < w.size (); i++) + res[i] = cchar (attrs, w[i]); return res; } fun sanitize_char (chtype attrs, wchar_t c) -> ncstring { - if (c < 32) + if (c < 32 || c == 0x7f) return {cchar (attrs | A_REVERSE, L'^'), - cchar (attrs | A_REVERSE, c + 64)}; + cchar (attrs | A_REVERSE, (c + 64) & 0x7f)}; if (!iswprint (c)) return {cchar (attrs | A_REVERSE, L'?')}; return {cchar (attrs, c)}; @@ -330,13 +360,6 @@ fun print (const ncstring &nc, int limit) -> int { return total_width; } -fun compute_width (const wstring &w) -> int { - int total = 0; - for (const auto &c : w) - total += wcwidth (c); - return total; -} - fun compute_width (const ncstring &nc) -> int { int total = 0; for (const auto &c : nc) @@ -383,16 +406,19 @@ fun decode_attrs (const vector<string> &attrs) -> chtype { enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode #define KEY(name) (SYM | KEY_ ## name) -#define CTRL 31 & +#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f) #define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \ XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \ XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \ - XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) \ + XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \ XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \ - XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) \ + XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \ XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \ - XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) + XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) XX(INPUT_DELETE) \ + XX(INPUT_B_KILL_WORD) XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) \ + XX(INPUT_QUOTED_INSERT) \ + XX(INPUT_BACKWARD) XX(INPUT_FORWARD) XX(INPUT_BEGINNING) XX(INPUT_END) #define XX(name) ACTION_ ## name, enum action { ACTIONS(XX) ACTION_COUNT }; @@ -405,32 +431,51 @@ static const char *g_action_names[] = {ACTIONS(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}, - {KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT}, {'h', ACTION_HELP}, + {KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP}, + {KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT}, {'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR}, // M-o ought to be the same shortcut the navigator is launched with {ALT | 'o', ACTION_QUIT}, {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT}, - {'k', ACTION_UP}, {CTRL 'p', ACTION_UP}, {KEY (UP), ACTION_UP}, - {'j', ACTION_DOWN}, {CTRL 'n', ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN}, + {'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}, {'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW}, {KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT}, - {CTRL 'y', ACTION_SCROLL_UP}, {CTRL 'e', ACTION_SCROLL_DOWN}, - {'c', ACTION_CHDIR}, {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME}, - {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, + {CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN}, + {'z', ACTION_CENTER}, + {'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT}, + {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME}, + {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH}, {ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME}, + {KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR}, {'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL}, {'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN}, - {CTRL 'L', ACTION_REDRAW}, {'r', ACTION_RELOAD}, + {CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD}, }; static map<wint_t, action> g_input_actions { - {27, ACTION_INPUT_ABORT}, {CTRL 'g', ACTION_INPUT_ABORT}, + {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}, + // Sometimes terminfo is wrong, we need to accept both of these + {L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE}, + {KEY (BACKSPACE), ACTION_INPUT_B_DELETE}, {KEY (DC), ACTION_INPUT_DELETE}, + {CTRL ('W'), ACTION_INPUT_B_KILL_WORD}, {CTRL ('D'), ACTION_INPUT_DELETE}, + {CTRL ('U'), ACTION_INPUT_B_KILL_LINE}, + {CTRL ('K'), ACTION_INPUT_KILL_LINE}, + {CTRL ('V'), ACTION_INPUT_QUOTED_INSERT}, + {CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD}, + {CTRL ('F'), ACTION_INPUT_FORWARD}, {KEY (RIGHT), ACTION_INPUT_FORWARD}, + {CTRL ('A'), ACTION_INPUT_BEGINNING}, {KEY (HOME), ACTION_INPUT_BEGINNING}, + {CTRL ('E'), ACTION_INPUT_END}, {KEY (END), ACTION_INPUT_END}, +}; +static map<wint_t, action> g_search_actions { + {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP}, + {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN}, }; static const map<string, map<wint_t, action>*> g_binding_contexts { {"normal", &g_normal_actions}, {"input", &g_input_actions}, + {"search", &g_search_actions}, }; #define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \ @@ -450,7 +495,7 @@ static const char *g_ls_colors[] = {LS(XX)}; struct stringcaseless { bool operator () (const string &a, const string &b) const { - const auto &c = locale::classic(); + 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); }); } @@ -470,6 +515,7 @@ struct level { }; static struct { + ncstring cmdline; ///< Outer command line string cwd; ///< Current working directory string start_dir; ///< Starting directory vector<entry> entries; ///< Current directory entries @@ -479,6 +525,7 @@ static struct { bool gravity; ///< Entries are shoved to the bottom bool reverse_sort; ///< Reverse sort bool show_hidden; ///< Show hidden files + bool ext_helpers; ///< Launch helpers externally int max_widths[entry::COLUMNS]; ///< Column widths int sort_column = entry::FILENAME; ///< Sorting column int sort_flash_ttl; ///< Sorting column flash TTL @@ -487,6 +534,7 @@ static struct { int message_ttl; ///< Time to live for the message string chosen; ///< Chosen item for the command line + string ext_helper; ///< External helper to run bool no_chdir; ///< Do not tell the shell to chdir bool quitting; ///< Whether we should quit already @@ -494,13 +542,17 @@ static struct { bool out_of_date; ///< Entries may be out of date const wchar_t *editor; ///< Prompt string for editing + wstring editor_info; ///< Right-side prompt while editing wstring editor_line; ///< Current user input + int editor_cursor = 0; ///< Cursor position + bool editor_inserting; ///< Inserting a literal character void (*editor_on_change) (); ///< Callback on editor change - void (*editor_on_confirm) (); ///< Callback on editor confirmation + map<action, void (*) ()> editor_on; ///< Handlers for custom actions - enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_COUNT }; - chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0}; - const char *attr_names[AT_COUNT] = {"cursor", "bar", "cwd", "input"}; + enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE, AT_COUNT }; + chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0}; + const char *attr_names[AT_COUNT] = + {"cursor", "bar", "cwd", "input", "info", "cmdline"}; map<int, chtype> ls_colors; ///< LS_COLORS decoded map<string, chtype> ls_exts; ///< LS_COLORS file extensions @@ -508,12 +560,13 @@ static struct { map<string, wint_t, stringcaseless> name_to_key; map<wint_t, string> key_to_name; + map<string, wint_t> custom_keys; string action_names[ACTION_COUNT]; ///< Stylized action names // Refreshed by reload(): - map<uid_t, string> unames; ///< User names by UID - map<gid_t, string> gnames; ///< Group names by GID + map<uid_t, wstring> unames; ///< User names by UID + map<gid_t, wstring> gnames; ///< Group names by GID struct tm now; ///< Current local time for display } g; @@ -560,8 +613,8 @@ fun ls_format (const entry &e, bool for_target) -> chtype { set (LS_STICKY_OTHER_WRITABLE); } else if (S_ISLNK (info.st_mode)) { type = LS_SYMLINK; - if (!e.target_info.st_mode - && (ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target)) + 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; @@ -587,15 +640,33 @@ fun ls_format (const entry &e, bool for_target) -> chtype { return format; } +fun suffixize (off_t size, unsigned shift, wchar_t suffix, std::wstring &out) + -> bool { + // Prevent implementation-defined and undefined behaviour + if (size < 0 || shift >= sizeof size * 8) + return false; + + off_t divided = size >> shift; + if (divided >= 10) { + out.assign (std::to_wstring (divided)).append (1, suffix); + return true; + } else if (divided > 0) { + unsigned times_ten = size / double (off_t (1) << shift) * 10.0; + out.assign ({L'0' + wchar_t (times_ten / 10), L'.', + L'0' + wchar_t (times_ten % 10), suffix}); + return true; + } + return false; +} + fun make_entry (const struct dirent *f) -> entry { entry e; e.filename = f->d_name; e.info.st_mode = DTTOIF (f->d_type); auto &info = e.info; - // TODO: benchmark just readdir() vs. lstat(), also on dead mounts; - // it might make sense to stat asynchronously in threads - // http://lkml.iu.edu/hypermail//linux/kernel/0804.3/1616.html + // io_uring is only at most about 50% faster, though it might help with + // slowly statting devices, at a major complexity cost. if (lstat (f->d_name, &info)) { e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode), L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0); @@ -621,38 +692,41 @@ fun make_entry (const struct dirent *f) -> entry { } auto mode = decode_mode (info.st_mode); - // This is a Linux-only extension + // We're using a laughably small subset of libacl: this translates to + // two lgetxattr() calls, the results of which are compared with + // specific architecture-dependent constants. Linux-only. if (acl_extended_file_nofollow (f->d_name) > 0) mode += L"+"; e.cols[entry::MODES] = apply_attrs (mode, 0); auto usr = g.unames.find (info.st_uid); e.cols[entry::USER] = (usr != g.unames.end ()) - ? apply_attrs (to_wide (usr->second), 0) + ? apply_attrs (usr->second, 0) : apply_attrs (to_wstring (info.st_uid), 0); auto grp = g.gnames.find (info.st_gid); e.cols[entry::GROUP] = (grp != g.gnames.end ()) - ? apply_attrs (to_wide (grp->second), 0) + ? apply_attrs (grp->second, 0) : apply_attrs (to_wstring (info.st_gid), 0); - auto size = to_wstring (info.st_size); - if (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T"; - else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G"; - else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M"; - else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K"; + std::wstring size; + if (!suffixize (info.st_size, 40, L'T', size) && + !suffixize (info.st_size, 30, L'G', size) && + !suffixize (info.st_size, 20, L'M', size) && + !suffixize (info.st_size, 10, L'K', size)) + size = to_wstring (info.st_size); e.cols[entry::SIZE] = apply_attrs (size, 0); - char buf[32] = ""; + wchar_t buf[32] = L""; auto tm = localtime (&info.st_mtime); - strftime (buf, sizeof buf, - (tm->tm_year == g.now.tm_year) ? "%b %e %H:%M" : "%b %e %Y", tm); - e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0); + wcsftime (buf, sizeof buf / sizeof *buf, + (tm->tm_year == g.now.tm_year) ? L"%b %e %H:%M" : L"%b %e %Y", tm); + e.cols[entry::MTIME] = apply_attrs (buf, 0); auto &fn = e.cols[entry::FILENAME] = apply_attrs (to_wide (e.filename), ls_format (e, false)); if (!e.target_path.empty ()) { - fn.append (apply_attrs (to_wide (" -> "), 0)); + fn.append (apply_attrs (L" -> ", 0)); fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true))); } return e; @@ -662,7 +736,7 @@ fun inline visible_lines () -> int { return max (0, LINES - 2); } fun update () { int start_column = g.full_view ? 0 : entry::FILENAME; - static int alignment[entry::COLUMNS] = { -1, -1, -1, 1, 1, -1 }; + static int alignment[entry::COLUMNS] = {-1, -1, -1, 1, 1, -1}; erase (); int available = visible_lines (); @@ -713,21 +787,35 @@ fun update () { 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)); + auto prompt = apply_attrs (wstring (g.editor) + L": ", 0), + line = apply_attrs (g.editor_line, 0), + info = apply_attrs (g.editor_info, g.attrs[g.AT_INFO]); + + auto info_width = compute_width (info); + if (print (prompt + line, COLS - 1) < COLS - info_width) { + move (LINES - 1, COLS - info_width); + print (info, info_width); + } + + auto start = sanitize (prompt + line.substr (0, g.editor_cursor)); + move (LINES - 1, compute_width (start)); curs_set (1); } else if (!g.message.empty ()) { move (LINES - 1, 0); print (apply_attrs (g.message, 0), COLS); + } else if (!g.cmdline.empty ()) { + move (LINES - 1, 0); + print (g.cmdline, COLS); } refresh (); } fun operator< (const entry &e1, const entry &e2) -> bool { - auto t1 = make_tuple (e1.filename != "..", + static string dotdot {".."}; + auto t1 = make_tuple (e1.filename != dotdot, !S_ISDIR (e1.info.st_mode) && !S_ISDIR (e1.target_info.st_mode)); - auto t2 = make_tuple (e2.filename != "..", + auto t2 = make_tuple (e2.filename != dotdot, !S_ISDIR (e2.info.st_mode) && !S_ISDIR (e2.target_info.st_mode)); if (t1 != t2) return t1 < t2; @@ -759,24 +847,57 @@ fun operator< (const entry &e1, const entry &e2) -> bool { return a.filename < b.filename; } -fun reload (const string &old_cwd) { - g.unames.clear(); +fun at_cursor () -> const entry & { + static entry invalid; + return g.cursor >= int (g.entries.size ()) ? invalid : g.entries[g.cursor]; +} + +fun focus (const string &anchor) { + if (!anchor.empty ()) { + for (size_t i = 0; i < g.entries.size (); i++) + if (g.entries[i].filename == anchor) + g.cursor = i; + } +} + +fun resort (const string anchor = at_cursor ().filename) { + sort (begin (g.entries), end (g.entries)); + focus (anchor); +} + +fun show_message (const string &message, int ttl = 30) { + g.message = to_wide (message); + g.message_ttl = ttl; +} + +fun reload (bool keep_anchor) { + g.unames.clear (); while (auto *ent = getpwent ()) - g.unames.emplace (ent->pw_uid, ent->pw_name); - endpwent(); + g.unames.emplace (ent->pw_uid, to_wide (ent->pw_name)); + endpwent (); - g.gnames.clear(); + g.gnames.clear (); while (auto *ent = getgrent ()) - g.gnames.emplace (ent->gr_gid, ent->gr_name); - endgrent(); + g.gnames.emplace (ent->gr_gid, to_wide (ent->gr_name)); + endgrent (); string anchor; - if (!g.entries.empty ()) - anchor = g.entries[g.cursor].filename; + if (keep_anchor) + anchor = at_cursor ().filename; auto now = time (NULL); g.now = *localtime (&now); auto dir = opendir ("."); g.entries.clear (); + if (!dir) { + show_message (strerror (errno)); + if (g.cwd != "/") { + struct dirent f = {}; + strncpy (f.d_name, "..", sizeof f.d_name); + f.d_type = DT_DIR; + g.entries.push_back (make_entry (&f)); + } + goto readfail; + } while (auto f = readdir (dir)) { string name = f->d_name; // Two dots are for navigation but this ain't as useful @@ -786,22 +907,19 @@ fun reload (const string &old_cwd) { g.entries.push_back (make_entry (f)); } closedir (dir); - sort (begin (g.entries), end (g.entries)); - g.out_of_date = false; - if (g.cwd == old_cwd && !anchor.empty ()) { - for (size_t i = 0; i < g.entries.size (); i++) - if (g.entries[i].filename == anchor) - g.cursor = i; - } +readfail: + g.out_of_date = false; for (int col = 0; col < entry::COLUMNS; col++) { auto &longest = g.max_widths[col] = 0; for (const auto &entry : g.entries) longest = max (longest, compute_width (entry.cols[col])); } - g.cursor = min (g.cursor, int (g.entries.size ()) - 1); - g.offset = min (g.offset, int (g.entries.size ()) - 1); + resort (anchor); + + g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1)); + g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1)); if (g.inotify_wd != -1) inotify_rm_watch (g.inotify_fd, g.inotify_wd); @@ -811,14 +929,22 @@ fun reload (const string &old_cwd) { (IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN)); } -fun show_message (const string &message, int ttl = 30) { - g.message = to_wide (message); - g.message_ttl = ttl; -} +fun run_program (initializer_list<const char *> list, const string &filename) { + auto args = (!filename.empty() && filename.front() == '-' ? " -- " : " ") + + shell_escape (filename); + if (g.ext_helpers) { + // XXX: this doesn't try them all out, + // though it shouldn't make any noticeable difference + const char *found = nullptr; + for (auto program : list) + if ((found = program)) + break; + g.ext_helper.assign (found).append (args); + g.quitting = true; + return; + } -fun run_program (initializer_list<const char*> list, const string &filename) { endwin (); - switch (pid_t child = fork ()) { int status; case -1: @@ -828,9 +954,9 @@ fun run_program (initializer_list<const char*> list, const string &filename) { setpgid (0, 0); tcsetpgrp (STDOUT_FILENO, getpgid (0)); - for (auto pager : list) - if (pager) execl ("/bin/sh", "/bin/sh", "-c", (string (pager) - + " " + shell_escape (filename)).c_str (), NULL); + for (auto program : list) + if (program) execl ("/bin/sh", "/bin/sh", "-c", + (program + args).c_str (), NULL); _exit (EXIT_FAILURE); default: // ...and make sure of it in the parent as well @@ -839,8 +965,14 @@ fun run_program (initializer_list<const char*> list, const string &filename) { // We don't provide job control--don't let us hang after ^Z while (waitpid (child, &status, WUNTRACED) > -1 && WIFSTOPPED (status)) if (WSTOPSIG (status) == SIGTSTP) - kill (child, SIGCONT); + kill (-child, SIGCONT); tcsetpgrp (STDOUT_FILENO, getpgid (0)); + + if (WIFEXITED (status) && WEXITSTATUS (status)) { + printf ("Helper returned non-zero exit status %d. " + "Press Enter to continue.\n", WEXITSTATUS (status)); + string dummy; getline (cin, dummy); + } } refresh (); @@ -848,7 +980,9 @@ fun run_program (initializer_list<const char*> list, const string &filename) { } fun view (const string &filename) { - run_program ({(const char *) getenv ("PAGER"), "pager", "cat"}, filename); + // XXX: we cannot realistically detect that the pager hasn't made a pause + // at the end of the file, so we can't ensure all contents have been seen + run_program ({(const char *) getenv ("PAGER"), "less", "cat"}, filename); } fun edit (const string &filename) { @@ -872,7 +1006,7 @@ fun run_pager (FILE *contents) { dup2 (fileno (contents), STDIN_FILENO); // Behaviour copies man-db's man(1), similar to POSIX man(1) - for (auto pager : {(const char *) getenv ("PAGER"), "pager", "cat"}) + for (auto pager : {(const char *) getenv ("PAGER"), "less", "cat"}) if (pager) execl ("/bin/sh", "/bin/sh", "-c", pager, NULL); _exit (EXIT_FAILURE); default: @@ -893,8 +1027,8 @@ fun encode_key (wint_t key) -> string { wchar_t bare = key & ~ALT; if (g.key_to_name.count (bare)) encoded.append (capitalize (g.key_to_name.at (bare))); - else if (bare < 32) - encoded.append ("C-").append ({char (tolower (bare + 64))}); + else if (bare < 32 || bare == 0x7f) + encoded.append ("C-").append ({char (tolower ((bare + 64) & 0x7f))}); else encoded.append (to_mb ({bare})); return encoded; @@ -908,11 +1042,13 @@ fun show_help () { for (const auto &kv : g_binding_contexts) { fprintf (contents, "%s\n", underline (capitalize (kv.first + " key bindings")).c_str ()); - for (const auto &kv : *kv.second) { - auto key = encode_key (kv.first); - key.append (max (0, 10 - compute_width (to_wide (key))), ' '); - fprintf (contents, "%s %s\n", - key.c_str (), g.action_names[kv.second].c_str ()); + map<action, string> agg; + for (const auto &kv : *kv.second) + agg[kv.second] += encode_key (kv.first) + " "; + for (const auto &kv : agg) { + auto action = g.action_names[kv.first]; + action.append (max (0, 20 - int (action.length ())), ' '); + fprintf (contents, "%s %s\n", action.c_str (), kv.second.c_str ()); } fprintf (contents, "\n"); } @@ -920,11 +1056,40 @@ fun show_help () { fclose (contents); } -fun search (const wstring &needle) { - int best = g.cursor, best_n = 0; - for (int i = 0; i < int (g.entries.size ()); i++) { - auto o = (i + g.cursor) % g.entries.size (); - int n = prefix_length (to_wide (g.entries[o].filename), needle); +fun match (const wstring &needle, int push) -> int { + string pattern = to_mb (needle) + "*"; + bool jump_to_first = push || fnmatch (pattern.c_str (), + g.entries[g.cursor].filename.c_str (), 0) == FNM_NOMATCH; + int best = g.cursor, matches = 0, step = push + !push; + for (int i = 0, count = g.entries.size (); i < count; i++) { + int o = (g.cursor + (count + i * step) + (count + push)) % count; + if (!fnmatch (pattern.c_str (), g.entries[o].filename.c_str (), 0) + && !matches++ && jump_to_first) + best = o; + } + g.cursor = best; + return matches; +} + +fun match_interactive (int push) { + int matches = match (g.editor_line, push); + if (g.editor_line.empty ()) + g.editor_info.clear (); + else if (matches == 0) + g.editor_info = L"(no match)"; + else if (matches == 1) + g.editor_info = L"(1 match)"; + else + g.editor_info = L"(" + to_wstring (matches) + L" matches)"; +} + +/// Stays on the current item unless there are better matches +fun lookup (const wstring &needle) { + int best = g.cursor; + size_t best_n = 0; + for (int i = 0, count = g.entries.size (); i < count; i++) { + int o = (g.cursor + i) % count; + size_t n = prefix_length (to_wide (g.entries[o].filename), needle); if (n > best_n) { best = o; best_n = n; @@ -934,16 +1099,16 @@ fun search (const wstring &needle) { } fun fix_cursor_and_offset () { - g.cursor = max (g.cursor, 0); g.cursor = min (g.cursor, int (g.entries.size ()) - 1); + g.cursor = max (g.cursor, 0); // Decrease the offset when more items can suddenly fit int pushable = visible_lines () - (int (g.entries.size ()) - g.offset); g.offset -= max (pushable, 0); // Make sure the cursor is visible - g.offset = max (g.offset, 0); g.offset = min (g.offset, int (g.entries.size ()) - 1); + g.offset = max (g.offset, 0); if (g.offset > g.cursor) g.offset = g.cursor; @@ -957,7 +1122,19 @@ fun is_ancestor_dir (const string &ancestor, const string &of) -> bool { return of[ancestor.length ()] == '/' || (ancestor == "/" && ancestor != of); } -fun pop_levels () { +/// If `path` is equal to the `current` directory, or lies underneath it, +/// return it as a relative path +fun relativize (string current, const string &path) -> string { + if (current == path) + return "."; + if (current.back () != '/') + current += '/'; + if (!strncmp (current.c_str (), path.c_str (), current.length ())) + return path.substr (current.length ()); + return path; +} + +fun pop_levels (const string &old_cwd) { string anchor; auto i = g.levels.rbegin (); while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) { if (i->path == g.cwd) { @@ -968,9 +1145,16 @@ fun pop_levels () { i++; g.levels.pop_back (); } + + // Don't pick up bullshit from foreign history entries, especially for / + if (is_ancestor_dir (g.cwd, old_cwd)) { + auto subpath = relativize (g.cwd, old_cwd); + anchor = subpath.substr (0, subpath.find ('/')); + } + fix_cursor_and_offset (); - if (!anchor.empty () && g.entries[g.cursor].filename != anchor) - search (to_wide (anchor)); + if (!anchor.empty () && at_cursor ().filename != anchor) + lookup (to_wide (anchor)); } fun explode_path (const string &path, vector<string> &out) { @@ -1000,18 +1184,6 @@ fun absolutize (const string &abs_base, const string &path) -> string { return abs_base + "/" + path; } -/// If `path` is equal to the `current` directory, or lies underneath it, -/// return it as a relative path -fun relativize (string current, const string &path) -> string { - if (current == path) - return "."; - if (current.back () != '/') - current += '/'; - if (!strncmp (current.c_str (), path.c_str (), current.length ())) - return path.substr (current.length ()); - return path; -} - // Roughly follows the POSIX description of `cd -L` because of symlinks. // HOME and CDPATH handling is ommitted. fun change_dir (const string &path) { @@ -1040,7 +1212,7 @@ fun change_dir (const string &path) { beep (); return; } - if (!out.back().empty ()) + if (!out.back ().empty ()) out.pop_back (); } else if (in[i] != "." && (!in[i].empty () || i < startempty)) { out.push_back (in[i]); @@ -1053,23 +1225,27 @@ fun change_dir (const string &path) { return; } - auto old_cwd = g.cwd; - level last {g.offset, g.cursor, old_cwd, g.entries[g.cursor].filename}; + level last {g.offset, g.cursor, g.cwd, at_cursor ().filename}; g.cwd = full_path; - reload (old_cwd); + bool same_path = last.path == g.cwd; + reload (same_path); - if (is_ancestor_dir (last.path, g.cwd)) { - g.levels.push_back (last); + if (!same_path) { g.offset = g.cursor = 0; - } else { - pop_levels (); + if (is_ancestor_dir (last.path, g.cwd)) + g.levels.push_back (last); + else + pop_levels (last.path); } } // Roughly follows the POSIX description of the PWD environment variable fun initial_cwd () -> string { - char cwd[4096] = ""; getcwd (cwd, sizeof cwd); - const char *pwd = getenv ("PWD"); + char cwd[4096] = ""; const char *pwd = getenv ("PWD"); + if (!getcwd (cwd, sizeof cwd)) { + show_message (strerror (errno)); + return pwd; + } if (!pwd || pwd[0] != '/' || strlen (pwd) >= PATH_MAX) return cwd; @@ -1100,32 +1276,111 @@ fun choose (const entry &entry) { } } +// Move the cursor in `diff` direction and look for non-combining characters +fun move_towards_spacing (int diff) -> bool { + g.editor_cursor += diff; + return g.editor_cursor <= 0 || + g.editor_cursor >= int (g.editor_line.length ()) || + wcwidth (g.editor_line.at (g.editor_cursor)); +} + fun handle_editor (wint_t c) { - auto i = g_input_actions.find (c); - switch (i == g_input_actions.end () ? ACTION_NONE : i->second) { + auto action = ACTION_NONE; + if (g.editor_inserting) { + (void) halfdelay (1); + g.editor_inserting = false; + } else { + auto i = g_input_actions.find (c); + if (i != g_input_actions.end ()) + action = i->second; + + auto m = g_binding_contexts.find (to_mb (g.editor)); + if (m != g_binding_contexts.end () && + (i = m->second->find (c)) != m->second->end ()) + action = i->second; + } + + auto original = g.editor_line; + switch (action) { case ACTION_INPUT_CONFIRM: - if (g.editor_on_confirm) - g.editor_on_confirm (); + if (auto handler = g.editor_on[action]) + handler (); // Fall-through case ACTION_INPUT_ABORT: - g.editor_line.clear (); g.editor = 0; + g.editor_info.clear (); + g.editor_line.clear (); + g.editor_cursor = 0; + g.editor_inserting = false; g.editor_on_change = nullptr; - g.editor_on_confirm = nullptr; + g.editor_on.clear (); + return; + case ACTION_INPUT_BEGINNING: + g.editor_cursor = 0; + break; + case ACTION_INPUT_END: + g.editor_cursor = g.editor_line.length (); + break; + case ACTION_INPUT_BACKWARD: + while (g.editor_cursor > 0 && + !move_towards_spacing (-1)) + ; + break; + case ACTION_INPUT_FORWARD: + while (g.editor_cursor < int (g.editor_line.length ()) && + !move_towards_spacing (+1)) + ; break; case ACTION_INPUT_B_DELETE: - if (!g.editor_line.empty ()) - g.editor_line.erase (g.editor_line.length () - 1); + while (g.editor_cursor > 0) { + auto finished = move_towards_spacing (-1); + g.editor_line.erase (g.editor_cursor, 1); + if (finished) + break; + } + break; + case ACTION_INPUT_DELETE: + while (g.editor_cursor < int (g.editor_line.length ())) { + g.editor_line.erase (g.editor_cursor, 1); + if (move_towards_spacing (0)) + break; + } + break; + case ACTION_INPUT_B_KILL_WORD: + { + int i = g.editor_cursor; + while (i && g.editor_line[--i] == L' '); + while (i-- && g.editor_line[i] != L' '); + i++; + + g.editor_line.erase (i, g.editor_cursor - i); + g.editor_cursor = i; + break; + } + case ACTION_INPUT_B_KILL_LINE: + g.editor_line.erase (0, g.editor_cursor); + g.editor_cursor = 0; + break; + case ACTION_INPUT_KILL_LINE: + g.editor_line.erase (g.editor_cursor); + break; + case ACTION_INPUT_QUOTED_INSERT: + (void) raw (); + g.editor_inserting = true; break; default: - if (c & (ALT | SYM)) { - beep (); + if (auto handler = g.editor_on[action]) { + handler (); + } else if (c & (ALT | SYM)) { + if (c != KEY (RESIZE)) + beep (); } else { - g.editor_line += c; - if (g.editor_on_change) - g.editor_on_change (); + g.editor_line.insert (g.editor_cursor, 1, c); + g.editor_cursor++; } } + if (g.editor_on_change && g.editor_line != original) + g.editor_on_change (); } fun handle (wint_t c) -> bool { @@ -1138,10 +1393,15 @@ fun handle (wint_t c) -> bool { c = WEOF; } - const auto ¤t = g.entries[g.cursor]; + const auto ¤t = at_cursor (); + bool is_directory = + S_ISDIR (current.info.st_mode) || + S_ISDIR (current.target_info.st_mode); + auto i = g_normal_actions.find (c); switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) { case ACTION_CHOOSE_FULL: + // FIXME: in the root directory, this inserts //item g.chosen = g.cwd + "/" + current.filename; g.no_chdir = true; g.quitting = true; @@ -1150,7 +1410,8 @@ fun handle (wint_t c) -> bool { choose (current); break; case ACTION_VIEW: - view (current.filename); + // Mimic mc, it does not seem sensible to page directories + (is_directory ? change_dir : view) (current.filename); break; case ACTION_EDIT: edit (current.filename); @@ -1168,12 +1429,12 @@ fun handle (wint_t c) -> bool { case ACTION_SORT_LEFT: g.sort_column = (g.sort_column + entry::COLUMNS - 1) % entry::COLUMNS; g.sort_flash_ttl = 2; - reload (g.cwd); + resort (); break; case ACTION_SORT_RIGHT: g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS; g.sort_flash_ttl = 2; - reload (g.cwd); + resort (); break; case ACTION_UP: @@ -1212,11 +1473,14 @@ fun handle (wint_t c) -> bool { case ACTION_SCROLL_UP: g.offset--; break; + case ACTION_CENTER: + g.offset = g.cursor - (visible_lines () - 1) / 2; + break; case ACTION_CHDIR: g.editor = L"chdir"; - g.editor_on_confirm = [] { - change_dir (to_mb (g.editor_line)); + g.editor_on[ACTION_INPUT_CONFIRM] = [] { + change_dir (untilde (to_mb (g.editor_line))); }; break; case ACTION_PARENT: @@ -1226,30 +1490,37 @@ fun handle (wint_t c) -> bool { change_dir (g.start_dir); break; case ACTION_GO_HOME: - if (const auto *home = getenv ("HOME")) - change_dir (home); - else if (const auto *pw = getpwuid (getuid ())) - change_dir (pw->pw_dir); + change_dir (untilde ("~")); break; case ACTION_SEARCH: g.editor = L"search"; - g.editor_on_change = [] { - search (g.editor_line); - }; - g.editor_on_confirm = [] { - choose (g.entries[g.cursor]); - }; + g.editor_on_change = [] { match_interactive (0); }; + g.editor_on[ACTION_UP] = [] { match_interactive (-1); }; + g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); }; + g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); }; break; case ACTION_RENAME_PREFILL: g.editor_line = to_wide (current.filename); + g.editor_cursor = g.editor_line.length (); // Fall-through case ACTION_RENAME: g.editor = L"rename"; - g.editor_on_confirm = [] { + g.editor_on[ACTION_INPUT_CONFIRM] = [] { + auto mb = to_mb (g.editor_line); + if (rename (at_cursor ().filename.c_str (), mb.c_str ())) + show_message (strerror (errno)); + reload (true); + }; + break; + case ACTION_MKDIR: + g.editor = L"mkdir"; + g.editor_on[ACTION_INPUT_CONFIRM] = [] { auto mb = to_mb (g.editor_line); - rename (g.entries[g.cursor].filename.c_str (), mb.c_str ()); - reload (g.cwd); + if (mkdir (mb.c_str (), 0777)) + show_message (strerror (errno)); + reload (true); + focus (mb); }; break; @@ -1258,17 +1529,17 @@ fun handle (wint_t c) -> bool { break; case ACTION_REVERSE_SORT: g.reverse_sort = !g.reverse_sort; - reload (g.cwd); + resort (); break; case ACTION_SHOW_HIDDEN: g.show_hidden = !g.show_hidden; - reload (g.cwd); + reload (true); break; case ACTION_REDRAW: clear (); break; case ACTION_RELOAD: - reload (g.cwd); + reload (true); break; default: if (c != KEY (RESIZE) && c != WEOF) @@ -1295,6 +1566,20 @@ fun inotify_check () { update (); } +fun load_cmdline (int argc, char *argv[]) { + if (argc < 3) + return; + + wstring line = to_wide (argv[1]); int cursor = atoi (argv[2]); + if (line.empty () || cursor < 0 || cursor > (int) line.length ()) + return; + + std::replace_if (begin (line), end (line), iswspace, L' '); + g.cmdline = apply_attrs (line += L' ', g.attrs[g.AT_CMDLINE]); + // It is tempting to touch the cchar_t directly, though let's rather not + g.cmdline[cursor] = cchar (g.attrs[g.AT_CMDLINE] ^ A_REVERSE, line[cursor]); +} + fun decode_ansi_sgr (const vector<string> &v) -> chtype { vector<int> args; for (const auto &arg : v) { @@ -1341,8 +1626,8 @@ fun load_ls_colors (vector<string> colors) { 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")) + if (key != g_ls_colors[LS_SYMLINK] || + !(g.ls_symlink_as_target = value == "target")) attrs[key] = decode_ansi_sgr (split (value, ";")); } for (int i = 0; i < LS_COUNT; i++) { @@ -1415,16 +1700,16 @@ fun parse_key (const string &key_name) -> wint_t { c |= ALT; p += 2; } - if (!strncmp (p, "C-", 2)) { + if (g.name_to_key.count (p)) { + return c | g.name_to_key.at (p); + } else if (!strncmp (p, "C-", 2)) { p += 2; - if (*p < 32) { + if (*p < '?' || *p > '~') { cerr << "bindings: invalid combination: " << key_name << endl; return WEOF; } - c |= CTRL *p; + c |= CTRL (*p); p += 1; - } else if (g.name_to_key.count (p)) { - return c | g.name_to_key.at (p); } else { wchar_t w; mbstate_t mb {}; auto len = strlen (p) + 1, res = mbrtowc (&w, p, len, &mb); @@ -1453,7 +1738,9 @@ fun learn_named_key (const string &name, wint_t key) { fun load_bindings () { learn_named_key ("space", ' '); learn_named_key ("escape", 0x1b); - for (int kc = KEY_MIN; kc < KEY_MAX; kc++) { + + int kc = 0; + for (kc = KEY_MIN; kc <= KEY_MAX; kc++) { const char *name = keyname (kc); if (!name) continue; @@ -1467,10 +1754,6 @@ fun load_bindings () { learn_named_key (filtered, SYM | kc); } - auto config = xdg_config_find ("bindings"); - if (!config) - return; - // Stringization in the preprocessor is a bit limited, we want lisp-case map<string, action> actions; int a = 0; @@ -1482,16 +1765,27 @@ fun load_bindings () { actions[name] = action (a++); } + auto config = xdg_config_find ("bindings"); + if (!config) + return; + vector<string> tokens; while (parse_line (*config, tokens)) { if (tokens.empty ()) continue; if (tokens.size () < 3) { - cerr << "bindings: expected: context binding action"; + cerr << "bindings: expected: define name key-sequence" + " | context binding action"; continue; } auto context = tokens[0], key_name = tokens[1], action = tokens[2]; + if (context == "define") { + // We haven't run initscr() yet, so define_key() would fail here + learn_named_key (key_name, SYM | (g.custom_keys[action] = ++kc)); + continue; + } + auto m = g_binding_contexts.find (context); if (m == g_binding_contexts.end ()) { cerr << "bindings: invalid context: " << context << endl; @@ -1534,6 +1828,8 @@ fun load_config () { g.reverse_sort = tokens.at (1) == "1"; else if (tokens.front () == "show-hidden" && tokens.size () > 1) g.show_hidden = tokens.at (1) == "1"; + else if (tokens.front () == "ext-helpers" && tokens.size () > 1) + g.ext_helpers = tokens.at (1) == "1"; else if (tokens.front () == "sort-column" && tokens.size () > 1) g.sort_column = stoi (tokens.at (1)); else if (tokens.front () == "history") @@ -1550,6 +1846,7 @@ fun save_config () { write_line (*config, {"gravity", g.gravity ? "1" : "0"}); write_line (*config, {"reverse-sort", g.reverse_sort ? "1" : "0"}); write_line (*config, {"show-hidden", g.show_hidden ? "1" : "0"}); + write_line (*config, {"ext-helpers", g.ext_helpers ? "1" : "0"}); write_line (*config, {"sort-column", to_string (g.sort_column)}); @@ -1563,14 +1860,17 @@ fun save_config () { 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}); + at_cursor ().filename}); } int main (int argc, char *argv[]) { - (void) argc; - (void) argv; + if (argc == 2 && string (argv[1]) == "--version") { + cout << PROJECT_NAME << " " << PROJECT_VERSION << endl; + return 0; + } - // That bitch zle closes stdin before exec without redirection + // zsh before 5.4 may close stdin before exec without redirection, + // since then it redirects stdin to /dev/null (void) close (STDIN_FILENO); if (open ("/dev/tty", O_RDWR)) { cerr << "cannot open tty" << endl; @@ -1597,13 +1897,23 @@ int main (int argc, char *argv[]) { cerr << "cannot initialize screen" << endl; return 1; } + for (const auto &definition_kc : g.custom_keys) + define_key (definition_kc.first.c_str (), definition_kc.second); load_colors (); + load_cmdline (argc, argv); g.start_dir = g.cwd = initial_cwd (); - reload (g.cwd); - pop_levels (); + reload (false); + pop_levels (g.cwd); update (); + // Cunt, now I need to reïmplement all signal handling +#if NCURSES_VERSION_PATCH < 20210821 + // This gets applied along with the following halfdelay() + cur_term->Nttyb.c_cc[VSTOP] = + cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE; +#endif + // Invoking keypad() earlier would make ncurses flush its output buffer, // which would worsen start-up flickering if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) { @@ -1632,11 +1942,13 @@ int main (int argc, char *argv[]) { // We can't portably create a standard stream from an FD, so modify the FD dup2 (output_fd, STDOUT_FILENO); + // TODO: avoid printing any of this unless the SDN envvar is set if (g.cwd != g.start_dir && !g.no_chdir) cout << "local cd=" << shell_escape (g.cwd) << endl; else cout << "local cd=" << endl; cout << "local insert=" << shell_escape (g.chosen) << endl; + cout << "local helper=" << shell_escape (g.ext_helper) << endl; return 0; } |