aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format14
-rw-r--r--CMakeLists.txt9
-rw-r--r--LICENSE2
-rw-r--r--Makefile10
-rw-r--r--README.adoc6
-rwxr-xr-xsdn-install4
-rw-r--r--sdn.cpp321
7 files changed, 234 insertions, 132 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/CMakeLists.txt b/CMakeLists.txt
index 0bef602..50ebf3b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
# target_compile_features has been introduced in that version
-cmake_minimum_required (VERSION 3.1)
+cmake_minimum_required (VERSION 3.1...3.27)
project (sdn VERSION 0.1 LANGUAGES CXX)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
@@ -7,13 +7,6 @@ if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
"${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic")
endif ()
-# Since we use a language with slow compilers, let's at least use a fast linker
-execute_process (COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=gold -Wl,--version
- ERROR_QUIET OUTPUT_VARIABLE ld_version)
-if ("${ld_version}" MATCHES "GNU gold")
- set (CMAKE_EXE_LINKER_FLAGS "-fuse-ld=gold ${CMAKE_EXE_LINKER_FLAGS}")
-endif ()
-
find_package (PkgConfig REQUIRED)
pkg_check_modules (NCURSESW QUIET ncursesw)
diff --git a/LICENSE b/LICENSE
index 5e342a0..7511f3e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-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.
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/README.adoc b/README.adoc
index 6e8e363..28ff6c1 100644
--- a/README.adoc
+++ b/README.adoc
@@ -18,8 +18,10 @@ image::sdn.png[align="center"]
Packages
--------
-Regular releases are sporadic. git master should be stable enough. You can get
-a package with the latest development version from Archlinux's AUR.
+Regular releases are sporadic. git master should be stable enough.
+You can get a package with the latest development version using Arch Linux's
+https://aur.archlinux.org/packages/sdn-git[AUR],
+or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Building
--------
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..5a7c14b 100644
--- a/sdn.cpp
+++ b/sdn.cpp
@@ -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.
@@ -18,38 +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 <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 +165,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 +252,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 +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;
@@ -318,9 +324,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;
}
@@ -405,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,
@@ -437,9 +444,10 @@ 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},
+ {'/', 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},
@@ -452,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},
@@ -486,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); });
}
@@ -538,7 +547,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 +565,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 +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;
@@ -632,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);
@@ -666,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;
@@ -707,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 ();
@@ -783,9 +812,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 +852,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 +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
@@ -849,20 +907,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,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) {
+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;
}
@@ -903,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
@@ -1004,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)
@@ -1032,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);
@@ -1068,7 +1134,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) {
@@ -1088,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) {
@@ -1146,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]);
@@ -1213,9 +1279,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 +1295,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 +1313,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 +1322,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:
@@ -1281,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;
@@ -1296,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++;
@@ -1352,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 (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:
@@ -1396,10 +1473,13 @@ 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 = [] {
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
change_dir (untilde (to_mb (g.editor_line)));
};
break;
@@ -1415,10 +1495,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 = [] { 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);
@@ -1426,7 +1506,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 +1515,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 +1529,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 +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++) {
@@ -1825,6 +1907,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) {