From 2ac93c0e0eb1519028f488e81e4435280004d6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Fri, 24 Jul 2026 12:39:47 +0200 Subject: [PATCH] Allow unpacking multiple levels of directories To get an overview despite hierarchies. --- NEWS | 3 + sdn.cpp | 169 ++++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 137 insertions(+), 35 deletions(-) diff --git a/NEWS b/NEWS index aaada7c..c37a545 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ Unreleased * Added a configurable status bar template, also replacing the "cwd" attribute. + * Added ]/[/}/{ bindings to unpack multiple levels of subdirectories + into the listing as relative paths (subdir/file). + * Added C-b and C-f bindings for page up and page down navigation in normal mode. The respective actions now move by visible lines rather than by the full height of the terminal. diff --git a/sdn.cpp b/sdn.cpp index 690f8d7..3779734 100644 --- a/sdn.cpp +++ b/sdn.cpp @@ -472,7 +472,8 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode 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(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) \ + XX(PACK) XX(UNPACK) XX(PACK_ALL) XX(UNPACK_ALL) XX(REDRAW) XX(RELOAD) \ XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) XX(INPUT_DELETE) \ XX(INPUT_B_KILL_WORD) XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) \ XX(INPUT_QUOTED_INSERT) \ @@ -515,6 +516,8 @@ static map g_normal_actions { {KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR}, {ALT | 't', ACTION_TOGGLE_FULL}, {'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN}, + {']', ACTION_UNPACK}, {'[', ACTION_PACK}, + {'}', ACTION_UNPACK_ALL}, {'{', ACTION_PACK_ALL}, {CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD}, }; static map g_input_actions { @@ -593,6 +596,7 @@ static struct { bool reverse_sort; ///< Reverse sort bool show_hidden; ///< Show hidden files bool ext_helpers; ///< Launch helpers externally + int unpack_depth; ///< Nested listing depth (-1 = all) int max_widths[entry::COLUMNS]; ///< Column widths int sort_column = entry::FILENAME; ///< Sorting column int sort_flash_ttl; ///< Sorting column flash TTL @@ -701,8 +705,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype { if (x != g.ls_colors.end ()) format = x->second; - auto dot = name.find_last_of ('.'); - if (dot != string::npos && type == LS_FILE) { + auto slash = name.rfind ('/'); + auto base = slash == string::npos ? 0 : slash + 1; + auto dot = name.rfind ('.'); + if (dot != string::npos && dot >= base && type == LS_FILE) { const auto x = g.ls_exts.find (name.substr (++dot)); if (x != g.ls_exts.end ()) format = x->second; @@ -729,15 +735,15 @@ fun suffixize (off_t size, unsigned shift, wchar_t suffix, std::wstring &out) return false; } -fun make_entry (const struct dirent *f) -> entry { +fun make_entry (const string &path, unsigned char d_type) -> entry { entry e; - e.filename = f->d_name; - e.info.st_mode = DTTOIF (f->d_type); + e.filename = path; + e.info.st_mode = DTTOIF (d_type); auto &info = e.info; // 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)) { + if (lstat (path.c_str (), &info)) { e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode), L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0); @@ -751,13 +757,20 @@ fun make_entry (const struct dirent *f) -> entry { if (S_ISLNK (info.st_mode)) { char buf[PATH_MAX] = {}; - auto len = readlink (f->d_name, buf, sizeof buf); + auto len = readlink (path.c_str (), buf, sizeof buf); if (len < 0 || size_t (len) >= sizeof buf) { e.target_path = "?"; } else { e.target_path = buf; + + // Resolve relative targets against the symlink's directory + string target = buf; + if (target[0] != '/') { + if (auto slash = path.rfind ('/'); slash != string::npos) + target = path.substr (0, slash + 1) + target; + } // If a symlink links to another symlink, we follow all the way - (void) stat (buf, &e.target_info); + (void) stat (target.c_str (), &e.target_info); } } @@ -766,7 +779,7 @@ fun make_entry (const struct dirent *f) -> entry { // 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) + if (acl_extended_file_nofollow (path.c_str ()) > 0) mode += L"+"; #endif e.cols[entry::MODES] = apply_attrs (mode, 0); @@ -869,6 +882,10 @@ fun update () { } auto bar = format_status (g.status); + if (g.unpack_depth < 0) + bar += apply_attrs (L" [/*]", 0); + else if (g.unpack_depth > 0) + bar += apply_attrs (L" [/" + to_wstring (g.unpack_depth) + L"]", 0); if (!g.show_hidden) bar += apply_attrs (L" (hidden)", 0); if (g.out_of_date) @@ -978,8 +995,35 @@ fun focus (const string &anchor) { } } +fun resort_emit (map> &children, const string &parent) + -> void { + auto &kids = children[parent]; + sort (begin (kids), end (kids)); + for (auto &e : kids) { + auto name = e.filename; + g.entries.push_back (std::move (e)); + if (children.count (name)) + resort_emit (children, name); + } +} + +fun resort_hierarchical () { + map> children; + for (auto &e : g.entries) { + if (auto slash = e.filename.rfind ('/'); slash == string::npos) + children[""].push_back (std::move (e)); + else + children[e.filename.substr (0, slash)].push_back (std::move (e)); + } + g.entries.clear (); + resort_emit (children, ""); +} + fun resort (const string anchor = at_cursor ().filename) { - sort (begin (g.entries), end (g.entries)); + if (g.unpack_depth) + resort_hierarchical (); + else + sort (begin (g.entries), end (g.entries)); focus (anchor); } @@ -997,6 +1041,47 @@ fun filter_selection (const set &selection) { return reselection; } +fun collect_subentries (const string &prefix, int depth) { + auto dir = opendir (prefix.c_str ()); + if (!dir) + return; + + while (auto f = readdir (dir)) { + string name = f->d_name; + if (name == "." || name == ".." || (name[0] == '.' && !g.show_hidden)) + continue; + + string path = prefix + "/" + name; + g.entries.push_back (make_entry (path, f->d_type)); + if (depth && S_ISDIR (g.entries.back ().info.st_mode)) + collect_subentries (path, depth - (depth > 0)); + } + closedir (dir); +} + +fun collect_entries (int depth) { + auto dir = opendir ("."); + if (!dir) { + show_message (strerror (errno)); + if (g.cwd != "/") + g.entries.push_back (make_entry ("..", DT_DIR)); + return; + } + while (auto f = readdir (dir)) { + string name = f->d_name; + // Two dots are for navigation but this ain't as useful + if (name == ".") + continue; + if (name == ".." ? g.cwd == "/" : (name[0] == '.' && !g.show_hidden)) + continue; + + g.entries.push_back (make_entry (name, f->d_type)); + if (depth && name != ".." && S_ISDIR (g.entries.back ().info.st_mode)) + collect_subentries (name, depth - (depth > 0)); + } + closedir (dir); +} + fun reload (bool keep_anchor) { g.unames.clear (); while (auto *ent = getpwent ()) @@ -1013,31 +1098,11 @@ fun reload (bool keep_anchor) { anchor = at_cursor ().filename; 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 - if (name == ".") - continue; - if (name == ".." ? g.cwd != "/" : (name[0] != '.' || g.show_hidden)) - g.entries.push_back (make_entry (f)); - } - closedir (dir); + collect_entries (g.unpack_depth); g.selection = filter_selection (g.selection); -readfail: g.out_of_date = false; for (int col = 0; col < entry::COLUMNS; col++) { auto &longest = g.max_widths[col] = 0; @@ -1250,8 +1315,10 @@ fun select_matches (bool dotdot) -> set { for (const auto &e : g.entries) { if (!dotdot && e.filename == "..") continue; - if (!fnmatch (to_mb (g.editor_line).c_str (), - e.filename.c_str (), FNM_PATHNAME)) + auto base = e.filename.c_str (); + if (auto slash = e.filename.rfind ('/'); slash != string::npos) + base += slash + 1; + if (!fnmatch (to_mb (g.editor_line).c_str (), base, FNM_PATHNAME)) matches.insert (e.filename); } return matches; @@ -1407,8 +1474,10 @@ fun change_dir (const string &path) { level last {g.offset, g.cursor, g.cwd, at_cursor ().filename, g.selection}; g.cwd = full_path; bool same_path = last.path == g.cwd; - if (!same_path) + if (!same_path) { + g.unpack_depth = 0; g.selection.clear (); + } reload (same_path); @@ -1769,6 +1838,30 @@ fun handle (Key k) -> bool { g.show_hidden = !g.show_hidden; reload (true); break; + case ACTION_PACK: + if (g.unpack_depth == 0) + break; + g.unpack_depth = max (0, g.unpack_depth - 1); + reload (true); + break; + case ACTION_UNPACK: + if (g.unpack_depth < 0) + break; + g.unpack_depth++; + reload (true); + break; + case ACTION_PACK_ALL: + if (!g.unpack_depth) + break; + g.unpack_depth = 0; + reload (true); + break; + case ACTION_UNPACK_ALL: + if (g.unpack_depth < 0) + break; + g.unpack_depth = -1; + reload (true); + break; case ACTION_REDRAW: clear (); break; @@ -2082,11 +2175,16 @@ fun load_config () { g.ext_helpers = tokens.at (1) == "1"; else if (tokens.front () == "sort-column" && tokens.size () > 1) g.sort_column = stoi (tokens.at (1)); + else if (tokens.front () == "unpack-depth" && tokens.size () > 1) + g.unpack_depth = stoi (tokens.at (1)); else if (tokens.front () == "status" && tokens.size () > 1) g.status = to_wide (tokens.at (1)); else if (tokens.front () == "history") load_history_level (tokens); } + + // FIXME: we don't reset unpack_depth when the last history level + // doesn't match the start directory. } fun save_config () { @@ -2101,6 +2199,7 @@ fun save_config () { write_line (*config, {"ext-helpers", g.ext_helpers ? "1" : "0"}); write_line (*config, {"sort-column", to_string (g.sort_column)}); + write_line (*config, {"unpack-depth", to_string (g.unpack_depth)}); write_line (*config, {"status", to_mb (g.status)}); char hostname[256];