Allow unpacking multiple levels of directories
To get an overview despite hierarchies.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<Key, action> 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<Key, action> 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<string, vector<entry>> &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<string, vector<entry>> 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<string> &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<string> {
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user