aboutsummaryrefslogtreecommitdiff
path: root/sdn.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'sdn.cpp')
-rw-r--r--sdn.cpp716
1 files changed, 514 insertions, 202 deletions
diff --git a/sdn.cpp b/sdn.cpp
index 2de9f4e..5a7c14b 100644
--- a/sdn.cpp
+++ b/sdn.cpp
@@ -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 &current = g.entries[g.cursor];
+ const auto &current = 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;
}