aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format14
-rw-r--r--Makefile10
-rwxr-xr-xsdn-install4
-rw-r--r--sdn.cpp212
4 files changed, 146 insertions, 94 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..fa8134e
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,14 @@
+BasedOnStyle: LLVM
+ColumnLimit: 80
+IndentWidth: 4
+TabWidth: 4
+UseTab: ForContinuationAndIndentation
+SpaceAfterCStyleCast: true
+SpaceBeforeParens: Always
+AlignAfterOpenBracket: DontAlign
+AlignEscapedNewlines: DontAlign
+AlignOperands: DontAlign
+AlignConsecutiveMacros: Consecutive
+BreakBeforeTernaryOperators: true
+SpacesBeforeTrailingComments: 2
+WhitespaceSensitiveMacros: ['XX', 'ACTIONS', 'LS']
diff --git a/Makefile b/Makefile
index 57ff1bb..0468a72 100644
--- a/Makefile
+++ b/Makefile
@@ -5,15 +5,15 @@ CPPFLAGS = `sed -ne '/^project (\([^ )]*\) VERSION \([^ )]*\).*/ \
s//-DPROJECT_NAME="\1" -DPROJECT_VERSION="\2"/p' CMakeLists.txt`
sdn: sdn.cpp CMakeLists.txt
- $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o sdn \
+ $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
-lacl `pkg-config --libs --cflags ncursesw`
-static: sdn.cpp CMakeLists.txt
- $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o sdn \
+sdn-static: sdn.cpp CMakeLists.txt
+ $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
-static-libstdc++ \
-Wl,--start-group,-Bstatic \
-lacl `pkg-config --static --libs --cflags ncursesw` \
-Wl,--end-group,-Bdynamic
clean:
- rm -f sdn
+ rm -f sdn sdn-static
-.PHONY: static clean
+.PHONY: clean
diff --git a/sdn-install b/sdn-install
index 93821bf..0e28390 100755
--- a/sdn-install
+++ b/sdn-install
@@ -16,6 +16,9 @@ sdn-navigate () {
# helpers after the terminal has been resized while running sdn
command true
+ # Add to history, see https://www.zsh.org/mla/workers/2020/msg00633.html
+ fc -R =(print -- "$helper")
+
/bin/sh -c "$helper" </dev/tty || break
done
# optionally: zle zle-line-init
@@ -51,6 +54,7 @@ sdn-navigate () {
((SDN_P=SDN_P+${#insert}+1))
}
[[ -z $helper ]] && break
+ history -s -- "$helper"
/bin/sh -c "$helper" || break
done
}
diff --git a/sdn.cpp b/sdn.cpp
index ddcb342..6d0ac40 100644
--- a/sdn.cpp
+++ b/sdn.cpp
@@ -18,38 +18,43 @@
// 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 <grp.h>
#include <libgen.h>
-#include <time.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__
@@ -159,9 +164,9 @@ 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 no-op
static char table[STATES][7] = {
@@ -246,7 +251,7 @@ fun capitalize (const string &s) -> string {
/// Underlining for teletypes (also called overstriking),
/// also imitated in more(1) and less(1)
-fun underline (const string& s) -> string {
+fun underline (const string &s) -> string {
string result;
for (auto c : s)
result.append ({c, 8, '_'});
@@ -268,7 +273,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;
@@ -318,9 +323,9 @@ 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;
}
@@ -439,7 +444,7 @@ static map<wint_t, action> g_normal_actions {
{CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
- {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH},
+ {'/', 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},
@@ -538,7 +543,6 @@ static struct {
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_INFO, AT_CMDLINE, AT_COUNT };
@@ -557,8 +561,8 @@ static struct {
// 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;
@@ -605,8 +609,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;
@@ -638,9 +642,8 @@ fun make_entry (const struct dirent *f) -> entry {
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);
@@ -666,19 +669,21 @@ 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);
@@ -688,16 +693,16 @@ fun make_entry (const struct dirent *f) -> entry {
else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
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;
@@ -707,7 +712,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 ();
@@ -783,9 +788,10 @@ fun update () {
}
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;
@@ -822,16 +828,34 @@ fun at_cursor () -> const entry & {
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();
+ 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 (keep_anchor)
@@ -840,6 +864,16 @@ fun reload (bool keep_anchor) {
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
@@ -849,20 +883,17 @@ fun reload (bool keep_anchor) {
g.entries.push_back (make_entry (f));
}
closedir (dir);
- sort (begin (g.entries), end (g.entries));
- g.out_of_date = false;
- if (!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]));
}
+ 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));
@@ -874,12 +905,7 @@ fun reload (bool keep_anchor) {
(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) {
+fun run_program (initializer_list<const char *> list, const string &filename) {
if (g.ext_helpers) {
// XXX: this doesn't try them all out, though it shouldn't make any
// noticeable difference
@@ -1068,7 +1094,7 @@ fun relativize (string current, const string &path) -> string {
return path;
}
-fun pop_levels (const string& old_cwd) {
+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) {
@@ -1146,7 +1172,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]);
@@ -1213,9 +1239,9 @@ 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));
+ 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) {
@@ -1229,16 +1255,16 @@ fun handle_editor (wint_t c) {
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 ())
+ 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 = 0;
@@ -1247,7 +1273,6 @@ fun handle_editor (wint_t c) {
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:
@@ -1257,13 +1282,13 @@ fun handle_editor (wint_t c) {
g.editor_cursor = g.editor_line.length ();
break;
case ACTION_INPUT_BACKWARD:
- while (g.editor_cursor > 0
- && !move_towards_spacing (-1))
+ 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))
+ while (g.editor_cursor < int (g.editor_line.length ()) &&
+ !move_towards_spacing (+1))
;
break;
case ACTION_INPUT_B_DELETE:
@@ -1352,12 +1377,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 (true);
+ resort ();
break;
case ACTION_SORT_RIGHT:
g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS;
g.sort_flash_ttl = 2;
- reload (true);
+ resort ();
break;
case ACTION_UP:
@@ -1399,7 +1424,7 @@ fun handle (wint_t c) -> bool {
case ACTION_CHDIR:
g.editor = L"chdir";
- g.editor_on_confirm = [] {
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
change_dir (untilde (to_mb (g.editor_line)));
};
break;
@@ -1415,10 +1440,10 @@ fun handle (wint_t c) -> bool {
case ACTION_SEARCH:
g.editor = L"search";
- g.editor_on_change = [] { search_interactive (0); };
- g.editor_on[ACTION_UP] = [] { search_interactive (-1); };
- g.editor_on[ACTION_DOWN] = [] { search_interactive (+1); };
- g.editor_on_confirm = [] { choose (at_cursor ()); };
+ g.editor_on_change = [] { search_interactive (0); };
+ g.editor_on[ACTION_UP] = [] { search_interactive (-1); };
+ g.editor_on[ACTION_DOWN] = [] { search_interactive (+1); };
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); };
break;
case ACTION_RENAME_PREFILL:
g.editor_line = to_wide (current.filename);
@@ -1426,7 +1451,7 @@ fun handle (wint_t c) -> bool {
// 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));
@@ -1435,10 +1460,12 @@ fun handle (wint_t c) -> bool {
break;
case ACTION_MKDIR:
g.editor = L"mkdir";
- g.editor_on_confirm = [] {
- if (mkdir (to_mb (g.editor_line).c_str (), 0777))
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
+ auto mb = to_mb (g.editor_line);
+ if (mkdir (mb.c_str (), 0777))
show_message (strerror (errno));
reload (true);
+ focus (mb);
};
break;
@@ -1447,7 +1474,7 @@ fun handle (wint_t c) -> bool {
break;
case ACTION_REVERSE_SORT:
g.reverse_sort = !g.reverse_sort;
- reload (true);
+ resort ();
break;
case ACTION_SHOW_HIDDEN:
g.show_hidden = !g.show_hidden;
@@ -1544,8 +1571,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++) {
@@ -1825,6 +1852,13 @@ int main (int argc, char *argv[]) {
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) {