diff options
Diffstat (limited to 'sdn.cpp')
-rw-r--r-- | sdn.cpp | 136 |
1 files changed, 101 insertions, 35 deletions
@@ -1,7 +1,7 @@ // // sdn: simple directory navigator // -// Copyright (c) 2017 - 2021, 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. @@ -34,6 +34,7 @@ #include <dirent.h> #include <fcntl.h> +#include <fnmatch.h> #include <grp.h> #include <libgen.h> #include <pwd.h> @@ -410,12 +411,13 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode #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(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_DELETE) \ - XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) XX(INPUT_QUOTED_INSERT) \ + 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, @@ -442,6 +444,7 @@ static map<wint_t, action> g_normal_actions { {'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}, + {'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}, @@ -457,7 +460,8 @@ static map<wint_t, action> g_input_actions { // 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 ('D'), ACTION_INPUT_DELETE}, {CTRL ('U'), ACTION_INPUT_B_KILL_LINE}, + {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}, @@ -491,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); }); } @@ -636,6 +640,25 @@ 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; @@ -686,11 +709,12 @@ fun make_entry (const struct dirent *f) -> entry { ? 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); wchar_t buf[32] = L""; @@ -841,6 +865,11 @@ fun resort (const string anchor = at_cursor ().filename) { 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 ()) @@ -859,6 +888,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 @@ -869,6 +908,7 @@ fun reload (bool keep_anchor) { } closedir (dir); +readfail: g.out_of_date = false; for (int col = 0; col < entry::COLUMNS; col++) { auto &longest = g.max_widths[col] = 0; @@ -889,20 +929,17 @@ 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) { + 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 + // 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 = found + (" " + shell_escape (filename)); + g.ext_helper.assign (found).append (args); g.quitting = true; return; } @@ -918,8 +955,8 @@ fun run_program (initializer_list<const char *> list, const string &filename) { tcsetpgrp (STDOUT_FILENO, getpgid (0)); for (auto program : list) - if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program) - + " " + shell_escape (filename)).c_str (), NULL); + 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 @@ -1019,24 +1056,23 @@ fun show_help () { fclose (contents); } -/// Stays on the current match when there are no better ones, unless it's pushed -fun search (const wstring &needle, int push) -> int { - int best = g.cursor, best_n = 0, matches = 0, step = push != 0 ? push : 1; +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; - size_t n = prefix_length (to_wide (g.entries[o].filename), needle); - matches += n == needle.size (); - if (n > (size_t) best_n) { + if (!fnmatch (pattern.c_str (), g.entries[o].filename.c_str (), 0) + && !matches++ && jump_to_first) best = o; - best_n = n; - } } g.cursor = best; return matches; } -fun search_interactive (int push) { - int matches = search (g.editor_line, push); +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) @@ -1047,6 +1083,21 @@ fun search_interactive (int push) { 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; + } + } + g.cursor = best; +} + fun fix_cursor_and_offset () { g.cursor = min (g.cursor, int (g.entries.size ()) - 1); g.cursor = max (g.cursor, 0); @@ -1103,7 +1154,7 @@ fun pop_levels (const string &old_cwd) { fix_cursor_and_offset (); if (!anchor.empty () && at_cursor ().filename != anchor) - search (to_wide (anchor), 0); + lookup (to_wide (anchor)); } fun explode_path (const string &path, vector<string> &out) { @@ -1295,6 +1346,17 @@ fun handle_editor (wint_t c) { 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; @@ -1310,7 +1372,8 @@ fun handle_editor (wint_t c) { if (auto handler = g.editor_on[action]) { handler (); } else if (c & (ALT | SYM)) { - beep (); + if (c != KEY (RESIZE)) + beep (); } else { g.editor_line.insert (g.editor_cursor, 1, c); g.editor_cursor++; @@ -1410,6 +1473,9 @@ 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"; @@ -1429,9 +1495,9 @@ 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_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: |