Add an action to compute directory sizes
The binding, modelled after Midnight Commander, aliases to C-Space. Not saving this to the state file.
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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--;
|
||||
|
||||
Reference in New Issue
Block a user