|
|
|
@@ -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) {
|
|
|
|
|