Add an action to compute directory sizes
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success

The binding, modelled after Midnight Commander, aliases to C-Space.

Not saving this to the state file.
This commit is contained in:
2026-07-28 21:04:05 +02:00
parent 73093cac74
commit aa4be26520
2 changed files with 115 additions and 14 deletions
+2
View File
@@ -2,6 +2,8 @@ Unreleased
* Added a configurable status bar template, also replacing the "cwd" attribute.
* Added a C-@ binding to compute apparent sizes of selected directories.
* Added ]/[/}/{ bindings to unpack multiple levels of subdirectories
into the listing as relative paths (subdir/file).
+113 -14
View File
@@ -25,6 +25,7 @@
#include <cwchar>
#include <fstream>
#include <iostream>
#include <limits>
#include <locale>
#include <map>
#include <memory>
@@ -460,7 +461,7 @@ 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(ENTER) XX(OPEN) XX(CHOOSE) XX(CHOOSE_FULL) \
XX(VIEW_RAW) XX(VIEW) XX(EDIT) XX(EDIT_RAW) XX(SORT_LEFT) XX(SORT_RIGHT) \
XX(SELECT) XX(DESELECT) XX(SELECT_TOGGLE) XX(SELECT_ABORT) \
XX(SELECT) XX(DESELECT) XX(SELECT_TOGGLE) XX(SELECT_ABORT) XX(DIR_SIZES) \
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(CENTER) \
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
@@ -493,6 +494,7 @@ static map<Key, action> g_normal_actions {
{'+', ACTION_SELECT}, {'-', ACTION_DESELECT},
{CTRL ('T'), ACTION_SELECT_TOGGLE}, {KEY (IC), ACTION_SELECT_TOGGLE},
{27, ACTION_SELECT_ABORT}, {CTRL ('G'), ACTION_SELECT_ABORT},
{CTRL ('@'), ACTION_DIR_SIZES},
{'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
@@ -561,9 +563,16 @@ struct stringcaseless {
}
};
struct longerstringsfirst {
bool operator () (const string &a, const string &b) const {
return make_pair (b.size (), a) < make_pair (a.size (), b);
}
};
struct entry {
string filename, target_path;
struct stat info = {}, target_info = {};
off_t size = -1; ///< Recursively computed directory size
enum { MODES, USER, GROUP, SIZE, MTIME, FILENAME, COLUMNS };
ncstring cols[COLUMNS];
@@ -728,6 +737,16 @@ fun suffixize (off_t size, unsigned shift, wchar_t suffix, std::wstring &out)
return false;
}
fun format_size (off_t value, bool dirsize) -> ncstring {
std::wstring size;
if (!suffixize (value, 40, L'T', size) &&
!suffixize (value, 30, L'G', size) &&
!suffixize (value, 20, L'M', size) &&
!suffixize (value, 10, L'K', size))
size = to_wstring (value);
return apply_attrs (size, dirsize ? A_BOLD : 0);
}
fun make_entry (const string &path, unsigned char d_type) -> entry {
entry e;
e.filename = path;
@@ -787,13 +806,7 @@ fun make_entry (const string &path, unsigned char d_type) -> entry {
? apply_attrs (grp->second, 0)
: apply_attrs (to_wstring (info.st_gid), 0);
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);
e.cols[entry::SIZE] = format_size (info.st_size, false);
wchar_t buf[32] = L"";
auto tm = localtime (&info.st_mtime);
@@ -923,8 +936,11 @@ fun update () {
} else if (!g.selection.empty ()) {
uint64_t size = 0;
for (const auto &e : g.entries)
if (g.selection.count (e.filename)
&& S_ISREG (e.info.st_mode) && e.info.st_size > 0)
if (!g.selection.count (e.filename))
continue;
else if (e.size >= 0)
size += e.size;
else if (S_ISREG (e.info.st_mode) && e.info.st_size > 0)
size += e.info.st_size;
wostringstream status;
@@ -964,9 +980,13 @@ fun operator< (const entry &e1, const entry &e2) -> bool {
return a.info.st_gid < b.info.st_gid;
break;
case entry::SIZE:
if (a.info.st_size != b.info.st_size)
return a.info.st_size < b.info.st_size;
{
auto size_a = a.size >= 0 ? a.size : a.info.st_size;
auto size_b = b.size >= 0 ? b.size : b.info.st_size;
if (size_a != size_b)
return size_a < size_b;
break;
}
case entry::MTIME:
if (a.info.st_mtime != b.info.st_mtime)
return a.info.st_mtime < b.info.st_mtime;
@@ -1112,9 +1132,10 @@ fun reload (bool keep_anchor) {
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 read-only open/close are merely spam
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 | IN_CLOSE_NOWRITE));
#elif !defined __CYGWIN__
if (g.watch_wd != -1)
close (g.watch_wd);
@@ -1129,6 +1150,81 @@ fun reload (bool keep_anchor) {
#endif
}
// This feature interacts a bit oddly with unpacking.
// At least let's use a dynamic algorithm of starting depth-first
// and reusing these values higher up.
using size_map = map<string, off_t, longerstringsfirst>;
fun apparent_size (const string &path, const size_map &cache) -> off_t {
if (auto i = cache.find (path); i != cache.end () && i->second >= 0)
return i->second;
struct stat info {};
if (lstat (path.c_str (), &info))
return -1;
if (!S_ISDIR (info.st_mode))
return info.st_size;
auto dir = opendir (path.c_str ());
if (!dir)
return -1;
off_t total = 0;
while (auto f = readdir (dir)) {
string name = f->d_name;
if (name == "." || name == "..")
continue;
// The most likely error is a lack of privileges.
if (off_t child = apparent_size (path + "/" + name, cache); child >= 0)
total += min (child, numeric_limits<off_t>::max () - total);
}
closedir (dir);
return total;
}
fun compute_dir_sizes () {
show_message ("Computing directory sizes", 0);
update ();
size_map sizes;
if (!g.selection.empty ()) {
for (const auto &e : g.entries)
if (S_ISDIR (e.info.st_mode) && g.selection.count (e.filename))
sizes.emplace (e.filename, -1);
} else {
// Midnight Commander cannot place .. in the selection,
// but under the cursor it counts all direct subdirectories.
const auto &cursor = at_cursor ();
if (cursor.filename == "..") {
for (const auto &e : g.entries)
if (S_ISDIR (e.info.st_mode))
sizes.emplace (e.filename, -1);
} else if (!cursor.filename.empty ()) {
sizes.emplace (cursor.filename, -1);
}
}
sizes.erase ("..");
errno = 0;
for (auto &[path, size] : sizes)
size = apparent_size (path, sizes);
if (errno)
show_message (strerror (errno));
else
g.message.clear ();
auto &longest = g.max_widths[entry::SIZE] = 0;
for (auto &e : g.entries) {
auto size = sizes.find (e.filename);
if (size != sizes.end () && size->second >= 0)
e.cols[entry::SIZE] = format_size ((e.size = size->second), true);
longest = max (longest, compute_width (e.cols[entry::SIZE]));
}
if (g.sort_column == entry::SIZE)
resort ();
}
fun run_program (initializer_list<const char *> list, const string &filename) {
auto args = (!filename.empty () && filename.front () == '-' ? " -- " : " ")
+ shell_escape (filename);
@@ -1735,6 +1831,9 @@ fun handle (Key k) -> bool {
case ACTION_SELECT_ABORT:
g.selection.clear ();
break;
case ACTION_DIR_SIZES:
compute_dir_sizes ();
break;
case ACTION_UP:
g.cursor--;