aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt9
-rw-r--r--LICENSE2
-rw-r--r--README.adoc6
-rw-r--r--sdn.cpp136
4 files changed, 107 insertions, 46 deletions
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/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.cpp b/sdn.cpp
index 6017c51..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.
@@ -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: