Compare commits

..

18 Commits

Author SHA1 Message Date
272ee62ad8 Make a release
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-12-21 09:10:54 +01:00
a85426541a sdn-install: improve macOS installation
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Login shells have a dash at the beginning of their first argument.
2024-12-21 08:16:23 +01:00
c9b003735d Fix OpenBSD build
All checks were successful
Arch Linux AUR Success
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-12-21 07:59:44 +01:00
52a28f01c8 Add support for BSD derivatives, fix macOS
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-21 07:37:06 +01:00
3607757554 Improve filename passing
All checks were successful
Alpine 3.19 Success
Arch Linux AUR Success
c9662f1 forgot about internal helpers.

Moreover, it is annoying to see the -- in shell history
for every single external helper call.
2024-04-17 02:00:10 +02:00
6eb216a40a Remove linker preference from CMakeLists.txt
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
ld.gold doesn't understand all options that ld.bfd does.

In particular, this broke AUR builds with the current makepkg.conf.
2024-04-08 01:49:55 +02:00
9ce6f47716 Use more precise filesizes
Some checks failed
Arch Linux AUR Scripts failed
The behaviour differs from GNU `ls -lh` in that we use binary units,
meaning we get 1023 before 1.0K rather than 999 before 1.0K,
which is nonetheless still four characters wide.
2024-04-07 16:00:21 +02:00
c9662f1a7b Fix passing filenames starting with -
We don't want to pass them as program options.
2024-01-20 08:33:17 +01:00
9ddeb03652 CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:11:11 +02:00
acb187c6b1 README.adoc: update package information 2023-07-01 21:58:29 +02:00
9427df62e7 Fix code formatting, bump copyright years 2023-06-12 14:00:58 +02:00
4d6999c415 Do not beep on window resizes 2023-06-11 21:11:20 +02:00
30ed61fdd2 Implement ^W in the editor 2023-06-11 21:05:55 +02:00
2df916c9b3 Support wildcards in interactive search
The previous search for the longest match is functionally
duplicated by typing individual characters on the input line,
and wildcards can be escaped, so there shouldn't be regressions
in terms of capability.
2022-09-03 12:17:02 +02:00
24401825b4 Add a missing case break 2022-01-08 11:23:36 +01:00
2bfb490798 Add and bind an action to center the cursor
"z" stands for VIM's "zz".
2022-01-08 11:17:24 +01:00
338d00d605 Do not crash on opendir() failures
Show an error message, and a way out.
2021-11-09 07:52:48 +01:00
015652e379 Fix build with recent ncurses
Easily gained, easily lost.  No more ^S binding.
2021-11-09 07:45:20 +01:00
6 changed files with 202 additions and 77 deletions

View File

@@ -1,25 +1,24 @@
# target_compile_features has been introduced in that version # 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) project (sdn VERSION 1.0 LANGUAGES CXX)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU") if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
set (CMAKE_CXX_FLAGS set (CMAKE_CXX_FLAGS
"${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic") "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic")
endif () 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) find_package (PkgConfig REQUIRED)
pkg_check_modules (NCURSESW QUIET ncursesw) pkg_check_modules (ACL libacl)
pkg_check_modules (NCURSESW ncursesw)
if (NOT NCURSESW_FOUND)
find_library (NCURSESW_LIBRARIES NAMES ncursesw)
find_path (NCURSESW_INCLUDE_DIRS ncurses.h PATH_SUFFIXES ncurses)
endif ()
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp) add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS}) target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS})
target_link_libraries (${PROJECT_NAME} PUBLIC ${NCURSESW_LIBRARIES} acl) target_link_libraries (${PROJECT_NAME}
PUBLIC ${NCURSESW_LIBRARIES} ${ACL_LIBRARIES})
target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14) target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14)
target_compile_definitions (${PROJECT_NAME} PUBLIC target_compile_definitions (${PROJECT_NAME} PUBLIC
-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\") -DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\")

View File

@@ -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 Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. purpose with or without fee is hereby granted.

4
NEWS Normal file
View File

@@ -0,0 +1,4 @@
1.0.0 (2024-12-21)
* Initial release

View File

@@ -11,20 +11,22 @@ commands. It enables you to:
can be simply forwarded if it is to be edited. What's more, it will always can be simply forwarded if it is to be edited. What's more, it will always
be obvious whether the navigator is running. be obvious whether the navigator is running.
The only supported platform is Linux. I wanted to try a different, simpler 'sdn' runs on Linux and all BSD derivatives. I wanted to try a different,
approach here, and the end result is very friendly to tinkering. simpler approach here, and the end result is very friendly to tinkering.
image::sdn.png[align="center"] image::sdn.png[align="center"]
Packages Packages
-------- --------
Regular releases are sporadic. git master should be stable enough. You can get Regular releases are sporadic. git master should be stable enough.
a package with the latest development version from Archlinux's AUR. 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 Building
-------- --------
Build dependencies: CMake and/or make, a C++14 compiler, pkg-config + Build dependencies: CMake and/or make, a C++14 compiler, pkg-config +
Runtime dependencies: ncursesw, libacl Runtime dependencies: ncursesw, libacl (on Linux)
// Working around libasciidoc's missing support for escaping it like \++ // Working around libasciidoc's missing support for escaping it like \++
Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn' Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn'

View File

@@ -125,7 +125,7 @@ done
# Figure out the shell to integrate with # Figure out the shell to integrate with
login=$(basename "$SHELL") login=$(basename "$SHELL")
actual=$(ps -p $$ -o ppid= | xargs ps -o comm= -p) actual=$(ps -p $$ -o ppid= | xargs ps -o comm= -p | sed 's/^-//')
if [ -z "$shell" ] if [ -z "$shell" ]
then then
if [ "$login" != "$actual" ] if [ "$login" != "$actual" ]

236
sdn.cpp
View File

@@ -1,7 +1,7 @@
// //
// sdn: simple directory navigator // 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 // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // purpose with or without fee is hereby granted.
@@ -34,31 +34,37 @@
#include <dirent.h> #include <dirent.h>
#include <fcntl.h> #include <fcntl.h>
#include <fnmatch.h>
#include <grp.h> #include <grp.h>
#include <libgen.h> #include <libgen.h>
#include <pwd.h> #include <pwd.h>
#include <signal.h> #include <signal.h>
#include <sys/acl.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/wait.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#include <acl/libacl.h> #ifdef __linux__
#include <ncurses.h>
#include <sys/inotify.h> #include <sys/inotify.h>
#include <sys/types.h> // ACL information is not important enough to be ported
#include <sys/wait.h> #include <acl/libacl.h>
#include <sys/acl.h>
#include <sys/xattr.h> #include <sys/xattr.h>
#else
#include <sys/event.h>
#endif
#include <ncurses.h>
// To implement cbreak() with disabled ^S that gets reënabled on endwin() // To implement cbreak() with disabled ^S that gets reënabled on endwin()
#define NCURSES_INTERNALS #define NCURSES_INTERNALS
#include <term.h> #include <term.h>
#undef CTRL // term.h -> termios.h -> sys/ttydefaults.h, too simplistic #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__ #ifndef __STDC_ISO_10646__
#error Unicode required for wchar_t // Unicode is complex enough already and we might make assumptions,
// though macOS doesn't define this despite using UCS-4,
// and we won't build on Windows that seems to be the only one to use UTF-16.
#endif #endif
// Trailing return types make C++ syntax suck considerably less // Trailing return types make C++ syntax suck considerably less
@@ -302,7 +308,21 @@ fun xdg_config_write (const string &suffix) -> unique_ptr<fstream> {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
using ncstring = basic_string<cchar_t>; // This should be basic_string, however that crashes on macOS
using ncstring = vector<cchar_t>;
fun operator+ (const ncstring &lhs, const ncstring &rhs) -> ncstring {
ncstring result;
result.reserve (lhs.size () + rhs.size ());
result.insert (result.end (), lhs.begin (), lhs.end ());
result.insert (result.end (), rhs.begin (), rhs.end ());
return result;
}
fun operator+= (ncstring &lhs, const ncstring &rhs) -> ncstring & {
lhs.insert (lhs.end (), rhs.begin (), rhs.end ());
return lhs;
}
fun cchar (chtype attrs, wchar_t c) -> cchar_t { fun cchar (chtype attrs, wchar_t c) -> cchar_t {
cchar_t ch {}; wchar_t ws[] = {c, 0}; cchar_t ch {}; wchar_t ws[] = {c, 0};
@@ -410,12 +430,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) \ #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(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(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(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \ XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \ 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_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) XX(INPUT_BACKWARD) XX(INPUT_FORWARD) XX(INPUT_BEGINNING) XX(INPUT_END)
#define XX(name) ACTION_ ## name, #define XX(name) ACTION_ ## name,
@@ -442,6 +463,7 @@ static map<wint_t, action> g_normal_actions {
{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW}, {'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT}, {KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
{CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN}, {CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
{'z', ACTION_CENTER},
{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT}, {'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME}, {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH}, {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
@@ -457,7 +479,8 @@ static map<wint_t, action> g_input_actions {
// Sometimes terminfo is wrong, we need to accept both of these // Sometimes terminfo is wrong, we need to accept both of these
{L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE}, {L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE},
{KEY (BACKSPACE), ACTION_INPUT_B_DELETE}, {KEY (DC), ACTION_INPUT_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 ('K'), ACTION_INPUT_KILL_LINE},
{CTRL ('V'), ACTION_INPUT_QUOTED_INSERT}, {CTRL ('V'), ACTION_INPUT_QUOTED_INSERT},
{CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD}, {CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD},
@@ -491,7 +514,7 @@ static const char *g_ls_colors[] = {LS(XX)};
struct stringcaseless { struct stringcaseless {
bool operator () (const string &a, const string &b) const { 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), return lexicographical_compare (begin (a), end (a), begin (b), end (b),
[&](char m, char n) { return tolower (m, c) < tolower (n, c); }); [&](char m, char n) { return tolower (m, c) < tolower (n, c); });
} }
@@ -534,7 +557,7 @@ static struct {
bool no_chdir; ///< Do not tell the shell to chdir bool no_chdir; ///< Do not tell the shell to chdir
bool quitting; ///< Whether we should quit already bool quitting; ///< Whether we should quit already
int inotify_fd, inotify_wd = -1; ///< File watch int watch_fd, watch_wd = -1; ///< File watch (inotify/kqueue)
bool out_of_date; ///< Entries may be out of date bool out_of_date; ///< Entries may be out of date
const wchar_t *editor; ///< Prompt string for editing const wchar_t *editor; ///< Prompt string for editing
@@ -593,8 +616,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
set (LS_MULTIHARDLINK); set (LS_MULTIHARDLINK);
if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
set (LS_EXECUTABLE); set (LS_EXECUTABLE);
#ifdef __linux__
if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0) if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
set (LS_CAPABILITY); set (LS_CAPABILITY);
#endif
if ((info.st_mode & S_ISGID)) if ((info.st_mode & S_ISGID))
set (LS_SETGID); set (LS_SETGID);
if ((info.st_mode & S_ISUID)) if ((info.st_mode & S_ISUID))
@@ -636,6 +661,25 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
return format; 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 { fun make_entry (const struct dirent *f) -> entry {
entry e; entry e;
e.filename = f->d_name; e.filename = f->d_name;
@@ -669,11 +713,13 @@ fun make_entry (const struct dirent *f) -> entry {
} }
auto mode = decode_mode (info.st_mode); auto mode = decode_mode (info.st_mode);
#ifdef __linux__
// We're using a laughably small subset of libacl: this translates to // We're using a laughably small subset of libacl: this translates to
// two lgetxattr() calls, the results of which are compared with // two lgetxattr() calls, the results of which are compared with
// specific architecture-dependent constants. Linux-only. // specific architecture-dependent constants. Linux-only.
if (acl_extended_file_nofollow (f->d_name) > 0) if (acl_extended_file_nofollow (f->d_name) > 0)
mode += L"+"; mode += L"+";
#endif
e.cols[entry::MODES] = apply_attrs (mode, 0); e.cols[entry::MODES] = apply_attrs (mode, 0);
auto usr = g.unames.find (info.st_uid); auto usr = g.unames.find (info.st_uid);
@@ -686,11 +732,12 @@ fun make_entry (const struct dirent *f) -> entry {
? apply_attrs (grp->second, 0) ? apply_attrs (grp->second, 0)
: apply_attrs (to_wstring (info.st_gid), 0); : apply_attrs (to_wstring (info.st_gid), 0);
auto size = to_wstring (info.st_size); std::wstring size;
if (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T"; if (!suffixize (info.st_size, 40, L'T', size) &&
else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G"; !suffixize (info.st_size, 30, L'G', size) &&
else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M"; !suffixize (info.st_size, 20, L'M', size) &&
else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K"; !suffixize (info.st_size, 10, L'K', size))
size = to_wstring (info.st_size);
e.cols[entry::SIZE] = apply_attrs (size, 0); e.cols[entry::SIZE] = apply_attrs (size, 0);
wchar_t buf[32] = L""; wchar_t buf[32] = L"";
@@ -702,8 +749,8 @@ fun make_entry (const struct dirent *f) -> entry {
auto &fn = e.cols[entry::FILENAME] = auto &fn = e.cols[entry::FILENAME] =
apply_attrs (to_wide (e.filename), ls_format (e, false)); apply_attrs (to_wide (e.filename), ls_format (e, false));
if (!e.target_path.empty ()) { if (!e.target_path.empty ()) {
fn.append (apply_attrs (L" -> ", 0)); fn += apply_attrs (L" -> ", 0);
fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true))); fn += apply_attrs (to_wide (e.target_path), ls_format (e, true));
} }
return e; return e;
} }
@@ -773,8 +820,8 @@ fun update () {
print (info, info_width); print (info, info_width);
} }
auto start = sanitize (prompt + line.substr (0, g.editor_cursor)); line.resize (g.editor_cursor);
move (LINES - 1, compute_width (start)); move (LINES - 1, compute_width (sanitize (prompt + line)));
curs_set (1); curs_set (1);
} else if (!g.message.empty ()) { } else if (!g.message.empty ()) {
move (LINES - 1, 0); move (LINES - 1, 0);
@@ -841,6 +888,11 @@ fun resort (const string anchor = at_cursor ().filename) {
focus (anchor); 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) { fun reload (bool keep_anchor) {
g.unames.clear (); g.unames.clear ();
while (auto *ent = getpwent ()) while (auto *ent = getpwent ())
@@ -859,6 +911,16 @@ fun reload (bool keep_anchor) {
auto now = time (NULL); g.now = *localtime (&now); auto now = time (NULL); g.now = *localtime (&now);
auto dir = opendir ("."); auto dir = opendir (".");
g.entries.clear (); 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)) { while (auto f = readdir (dir)) {
string name = f->d_name; string name = f->d_name;
// Two dots are for navigation but this ain't as useful // Two dots are for navigation but this ain't as useful
@@ -869,6 +931,7 @@ fun reload (bool keep_anchor) {
} }
closedir (dir); closedir (dir);
readfail:
g.out_of_date = false; g.out_of_date = false;
for (int col = 0; col < entry::COLUMNS; col++) { for (int col = 0; col < entry::COLUMNS; col++) {
auto &longest = g.max_widths[col] = 0; auto &longest = g.max_widths[col] = 0;
@@ -881,28 +944,38 @@ fun reload (bool keep_anchor) {
g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1)); g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1));
g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1)); g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1));
if (g.inotify_wd != -1) #ifdef __linux__
inotify_rm_watch (g.inotify_fd, g.inotify_wd); if (g.watch_wd != -1)
inotify_rm_watch (g.watch_fd, g.watch_wd);
// We don't show atime, so access and open are merely spam // We don't show atime, so access and open are merely spam
g.inotify_wd = inotify_add_watch (g.inotify_fd, ".", g.watch_wd = inotify_add_watch (g.watch_fd, ".",
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN)); (IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
} #else
if (g.watch_wd != -1)
close (g.watch_wd);
fun show_message (const string &message, int ttl = 30) { if ((g.watch_wd = open (".", O_RDONLY | O_DIRECTORY | O_CLOEXEC)) >= 0) {
g.message = to_wide (message); // At least the macOS kqueue doesn't report anything too specific
g.message_ttl = ttl; struct kevent ev {};
EV_SET (&ev, g.watch_wd, EVFILT_VNODE, EV_ADD | EV_CLEAR,
NOTE_WRITE | NOTE_LINK, 0, nullptr);
(void) kevent (g.watch_fd, &ev, 1, nullptr, 0, nullptr);
}
#endif
} }
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) { if (g.ext_helpers) {
// XXX: this doesn't try them all out, though it shouldn't make any // XXX: this doesn't try them all out,
// noticeable difference // though it shouldn't make any noticeable difference
const char *found = nullptr; const char *found = nullptr;
for (auto program : list) for (auto program : list)
if ((found = program)) if ((found = program))
break; break;
g.ext_helper = found + (" " + shell_escape (filename)); g.ext_helper.assign (found).append (args);
g.quitting = true; g.quitting = true;
return; return;
} }
@@ -918,8 +991,8 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
tcsetpgrp (STDOUT_FILENO, getpgid (0)); tcsetpgrp (STDOUT_FILENO, getpgid (0));
for (auto program : list) for (auto program : list)
if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program) if (program) execl ("/bin/sh", "/bin/sh", "-c",
+ " " + shell_escape (filename)).c_str (), NULL); (program + args).c_str (), NULL);
_exit (EXIT_FAILURE); _exit (EXIT_FAILURE);
default: default:
// ...and make sure of it in the parent as well // ...and make sure of it in the parent as well
@@ -1019,24 +1092,23 @@ fun show_help () {
fclose (contents); fclose (contents);
} }
/// Stays on the current match when there are no better ones, unless it's pushed fun match (const wstring &needle, int push) -> int {
fun search (const wstring &needle, int push) -> int { string pattern = to_mb (needle) + "*";
int best = g.cursor, best_n = 0, matches = 0, step = push != 0 ? push : 1; 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++) { for (int i = 0, count = g.entries.size (); i < count; i++) {
int o = (g.cursor + (count + i * step) + (count + push)) % count; int o = (g.cursor + (count + i * step) + (count + push)) % count;
size_t n = prefix_length (to_wide (g.entries[o].filename), needle); if (!fnmatch (pattern.c_str (), g.entries[o].filename.c_str (), 0)
matches += n == needle.size (); && !matches++ && jump_to_first)
if (n > (size_t) best_n) {
best = o; best = o;
best_n = n;
}
} }
g.cursor = best; g.cursor = best;
return matches; return matches;
} }
fun search_interactive (int push) { fun match_interactive (int push) {
int matches = search (g.editor_line, push); int matches = match (g.editor_line, push);
if (g.editor_line.empty ()) if (g.editor_line.empty ())
g.editor_info.clear (); g.editor_info.clear ();
else if (matches == 0) else if (matches == 0)
@@ -1047,6 +1119,21 @@ fun search_interactive (int push) {
g.editor_info = L"(" + to_wstring (matches) + L" matches)"; 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 () { fun fix_cursor_and_offset () {
g.cursor = min (g.cursor, int (g.entries.size ()) - 1); g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
g.cursor = max (g.cursor, 0); g.cursor = max (g.cursor, 0);
@@ -1103,7 +1190,7 @@ fun pop_levels (const string &old_cwd) {
fix_cursor_and_offset (); fix_cursor_and_offset ();
if (!anchor.empty () && at_cursor ().filename != anchor) 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) { fun explode_path (const string &path, vector<string> &out) {
@@ -1295,6 +1382,17 @@ fun handle_editor (wint_t c) {
break; break;
} }
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: case ACTION_INPUT_B_KILL_LINE:
g.editor_line.erase (0, g.editor_cursor); g.editor_line.erase (0, g.editor_cursor);
g.editor_cursor = 0; g.editor_cursor = 0;
@@ -1310,6 +1408,7 @@ fun handle_editor (wint_t c) {
if (auto handler = g.editor_on[action]) { if (auto handler = g.editor_on[action]) {
handler (); handler ();
} else if (c & (ALT | SYM)) { } else if (c & (ALT | SYM)) {
if (c != KEY (RESIZE))
beep (); beep ();
} else { } else {
g.editor_line.insert (g.editor_cursor, 1, c); g.editor_line.insert (g.editor_cursor, 1, c);
@@ -1410,6 +1509,9 @@ fun handle (wint_t c) -> bool {
case ACTION_SCROLL_UP: case ACTION_SCROLL_UP:
g.offset--; g.offset--;
break; break;
case ACTION_CENTER:
g.offset = g.cursor - (visible_lines () - 1) / 2;
break;
case ACTION_CHDIR: case ACTION_CHDIR:
g.editor = L"chdir"; g.editor = L"chdir";
@@ -1429,9 +1531,9 @@ fun handle (wint_t c) -> bool {
case ACTION_SEARCH: case ACTION_SEARCH:
g.editor = L"search"; g.editor = L"search";
g.editor_on_change = [] { search_interactive (0); }; g.editor_on_change = [] { match_interactive (0); };
g.editor_on[ACTION_UP] = [] { search_interactive (-1); }; g.editor_on[ACTION_UP] = [] { match_interactive (-1); };
g.editor_on[ACTION_DOWN] = [] { search_interactive (+1); }; g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); };
g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); }; g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); };
break; break;
case ACTION_RENAME_PREFILL: case ACTION_RENAME_PREFILL:
@@ -1484,19 +1586,27 @@ fun handle (wint_t c) -> bool {
return !g.quitting; return !g.quitting;
} }
fun inotify_check () { fun watch_check () {
// Only provide simple indication that contents might have changed
char buf[4096]; ssize_t len;
bool changed = false; bool changed = false;
while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) { // Only provide simple indication that contents might have changed,
// if only because kqueue can't do any better
#ifdef __linux__
char buf[4096]; ssize_t len;
while ((len = read (g.watch_fd, buf, sizeof buf)) > 0) {
const inotify_event *e; const inotify_event *e;
for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) { for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
e = (const inotify_event *) buf; e = (const inotify_event *) buf;
if (e->wd == g.inotify_wd) if (e->wd == g.watch_wd)
changed = g.out_of_date = true; changed = true;
} }
} }
if (changed) #else
struct kevent ev {};
struct timespec timeout {};
if (kevent (g.watch_fd, nullptr, 0, &ev, 1, &timeout) > 0)
changed = ev.filter == EVFILT_VNODE && (ev.fflags & NOTE_WRITE);
#endif
if ((g.out_of_date = changed))
update (); update ();
} }
@@ -1818,10 +1928,17 @@ int main (int argc, char *argv[]) {
// So that the neither us nor our children stop on tcsetpgrp() // So that the neither us nor our children stop on tcsetpgrp()
signal (SIGTTOU, SIG_IGN); signal (SIGTTOU, SIG_IGN);
if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) { #ifdef __linux__
if ((g.watch_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
cerr << "cannot initialize inotify" << endl; cerr << "cannot initialize inotify" << endl;
return 1; return 1;
} }
#else
if ((g.watch_fd = kqueue ()) < 0) {
cerr << "cannot initialize kqueue" << endl;
return 1;
}
#endif
locale::global (locale ("")); locale::global (locale (""));
load_bindings (); load_bindings ();
@@ -1841,9 +1958,12 @@ int main (int argc, char *argv[]) {
pop_levels (g.cwd); pop_levels (g.cwd);
update (); update ();
// Cunt, now I need to reïmplement all signal handling
#if NCURSES_VERSION_PATCH < 20210821
// This gets applied along with the following halfdelay() // This gets applied along with the following halfdelay()
cur_term->Nttyb.c_cc[VSTOP] = cur_term->Nttyb.c_cc[VSTOP] =
cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE; cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE;
#endif
// Invoking keypad() earlier would make ncurses flush its output buffer, // Invoking keypad() earlier would make ncurses flush its output buffer,
// which would worsen start-up flickering // which would worsen start-up flickering
@@ -1855,7 +1975,7 @@ int main (int argc, char *argv[]) {
wint_t c; wint_t c;
while (!read_key (c) || handle (c)) { while (!read_key (c) || handle (c)) {
inotify_check (); watch_check ();
if (g.sort_flash_ttl && !--g.sort_flash_ttl) if (g.sort_flash_ttl && !--g.sort_flash_ttl)
update (); update ();
if (g.message_ttl && !--g.message_ttl) { if (g.message_ttl && !--g.message_ttl) {