Newer
Older
// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
const delete = @import("delete.zig");
const ui = @import("ui.zig");
const c = @cImport(@cInclude("time.h"));
// Currently opened directory.
pub var dir_parent: *model.Dir = undefined;
// Sorted list of all items in the currently opened directory.
// (first item may be null to indicate the "parent directory" item)
var dir_items = std.ArrayList(?*model.Entry).init(main.allocator);
var dir_max_blocks: u64 = 0;
var dir_max_size: u64 = 0;
var dir_has_shared: bool = false;
// Index into dir_items that is currently selected.
var cursor_idx: usize = 0;
const View = struct {
// Index into dir_items, indicates which entry is displayed at the top of the view.
// This is merely a suggestion, it will be adjusted upon drawing if it's
// out of bounds or if the cursor is not otherwise visible.
top: usize = 0,
// The hash(name) of the selected entry (cursor), this is used to derive
// cursor_idx after sorting or changing directory.
// (collisions may cause the wrong entry to be selected, but dealing with
// string allocations sucks and I expect collisions to be rare enough)
cursor_hash: u64 = 0,
fn hashEntry(entry: ?*model.Entry) u64 {
return if (entry) |e| std.hash.Wyhash.hash(0, e.name()) else 0;
}
// Update cursor_hash and save the current view to the hash table.
fn save(self: *@This()) void {
self.cursor_hash = if (dir_items.items.len == 0) 0
else hashEntry(dir_items.items[cursor_idx]);
opened_dir_views.put(@ptrToInt(dir_parent), self.*) catch {};
// Should be called after dir_parent or dir_items has changed, will load the last saved view and find the proper cursor_idx.
fn load(self: *@This(), sel: ?*const model.Entry) void {
if (opened_dir_views.get(@ptrToInt(dir_parent))) |v| self.* = v
else self.* = @This(){};
for (dir_items.items) |e, i| {
if (if (sel != null) e == sel else self.cursor_hash == hashEntry(e)) {
cursor_idx = i;
break;
}
}
}
};
var current_view = View{};
// Directories the user has browsed to before, and which item was last selected.
// The key is the @ptrToInt() of the opened *Dir; An int because the pointer
// itself may have gone stale after deletion or refreshing. They're only for
// lookups, not dereferencing.
var opened_dir_views = std.AutoHashMap(usize, View).init(main.allocator);
fn sortIntLt(a: anytype, b: @TypeOf(a)) ?bool {
return if (a == b) null else if (main.config.sort_order == .asc) a < b else a > b;
}
fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
const a = ap.?;
const b = bp.?;
if (main.config.sort_dirsfirst and a.isDirectory() != b.isDirectory())
return a.isDirectory();
switch (main.config.sort_col) {
.name => {}, // name sorting is the fallback
.blocks => {
if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
if (sortIntLt(a.size, b.size)) |r| return r;
},
.size => {
if (sortIntLt(a.size, b.size)) |r| return r;
if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
const ai = if (a.dir()) |d| d.items else 0;
const bi = if (b.dir()) |d| d.items else 0;
if (sortIntLt(ai, bi)) |r| return r;
if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
if (sortIntLt(a.size, b.size)) |r| return r;
},
.mtime => {
if (!a.pack.isext or !b.pack.isext) return a.pack.isext;
if (sortIntLt(a.ext().?.mtime, b.ext().?.mtime)) |r| return r;
},
}
const an = a.name();
const bn = b.name();
return if (main.config.sort_order == .asc) util.strnatcmp(an, bn) == .lt
else util.strnatcmp(bn, an) == .lt;
}
// Should be called when:
// - config.sort_* changes
// - dir_items changes (i.e. from loadDir())
// - files in this dir have changed in a way that affects their ordering
fn sortDir(next_sel: ?*const model.Entry) void {
// No need to sort the first item if that's the parent dir reference,
// excluding that allows sortLt() to ignore null values.
const lst = dir_items.items[(if (dir_items.items.len > 0 and dir_items.items[0] == null) @as(usize, 1) else 0)..];
std.sort.sort(?*model.Entry, lst, @as(void, undefined), sortLt);
}
// Must be called when:
// - dir_parent changes (i.e. we change directory)
// - config.show_hidden changes
// - files in this dir have been added or removed
pub fn loadDir(next_sel: ?*const model.Entry) void {
dir_items.shrinkRetainingCapacity(0);
dir_max_size = 1;
dir_max_blocks = 1;
dir_has_shared = false;
if (dir_parent != model.root)
var it = dir_parent.sub;
if (e.pack.blocks > dir_max_blocks) dir_max_blocks = e.pack.blocks;
if (e.size > dir_max_size) dir_max_size = e.size;
const shown = main.config.show_hidden or blk: {
const excl = if (e.file()) |f| f.pack.excluded else false;
break :blk !excl and name[0] != '.' and name[name.len-1] != '~';
};
if (shown) {
dir_items.append(e) catch unreachable;
if (e.dir()) |d| if (d.shared_blocks > 0 or d.shared_size > 0) { dir_has_shared = true; };
}
const Row = struct {
row: u32,
col: u32 = 0,
bg: ui.Bg = .default,
item: ?*model.Entry,
const Self = @This();
fn flag(self: *Self) void {
defer self.col += 2;
const item = self.item orelse return;
const ch: u7 = ch: {
if (item.file()) |f| {
if (f.pack.err) break :ch '!';
if (f.pack.excluded) break :ch '<';
if (f.pack.other_fs) break :ch '>';
if (f.pack.kernfs) break :ch '^';
if (f.pack.notreg) break :ch '@';
if (d.pack.err) break :ch '!';
if (d.pack.suberr) break :ch '.';
if (d.sub == null) break :ch 'e';
} else if (item.link()) |_| break :ch 'H';
return;
};
ui.move(self.row, self.col);
self.bg.fg(.flag);
ui.addch(ch);
}
fn size(self: *Self) void {
var width = if (main.config.si) @as(u32, 9) else 10;
if (dir_has_shared and main.config.show_shared != .off)
width += 2 + width;
defer self.col += width;
const item = self.item orelse return;
const siz = if (main.config.show_blocks) util.blocksToSize(item.pack.blocks) else item.size;
var shr = if (item.dir()) |d| (if (main.config.show_blocks) util.blocksToSize(d.shared_blocks) else d.shared_size) else 0;
if (main.config.show_shared == .unique) shr = siz -| shr;
ui.addsize(self.bg, siz);
if (dir_has_shared and shr > 0 and main.config.show_shared != .off) {
self.bg.fg(.flag);
ui.addstr(if (main.config.show_shared == .unique) " U " else " S ");
ui.addsize(self.bg, shr);
}
fn graph(self: *Self) void {
if ((!main.config.show_graph and !main.config.show_percent) or self.col + 20 > ui.cols) return;
const bar_size = std.math.max(ui.cols/7, 10);
defer self.col += 3
+ (if (main.config.show_graph) bar_size else 0)
+ (if (main.config.show_percent) @as(u32, 6) else 0)
+ (if (main.config.show_graph and main.config.show_percent) @as(u32, 1) else 0);
const item = self.item orelse return;
ui.move(self.row, self.col);
self.bg.fg(.default);
if (main.config.show_blocks) @intToFloat(f32, item.pack.blocks) / @intToFloat(f32, std.math.max(1, dir_parent.entry.pack.blocks))
else @intToFloat(f32, item.size) / @intToFloat(f32, std.math.max(1, dir_parent.entry.size))
});
self.bg.fg(.default);
ui.addch('%');
}
if (main.config.show_graph and main.config.show_percent) ui.addch(' ');
if (main.config.show_graph) {
var max = if (main.config.show_blocks) dir_max_blocks else dir_max_size;
var num = if (main.config.show_blocks) item.pack.blocks else item.size;
if (max < bar_size) {
max *= bar_size;
num *= bar_size;
}
const perblock = std.math.divFloor(u64, max, bar_size) catch unreachable;
var i: u32 = 0;
self.bg.fg(.graph);
while (i < bar_size) : (i += 1) {
const frac = std.math.min(@as(usize, 8), (num *| 8) / perblock);
ui.addstr(switch (main.config.graph_style) {
.hash => ([_][:0]const u8{ " ", " ", " ", " ", " ", " ", " ", " ", "#" })[frac],
.half => ([_][:0]const u8{ " ", " ", " ", " ", "▌", "▌", "▌", "▌", "█" })[frac],
.eighth => ([_][:0]const u8{ " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█" })[frac],
}
self.bg.fg(.default);
ui.addch(']');
}
fn items(self: *Self) void {
if (!main.config.show_items or self.col + 10 > ui.cols) return;
defer self.col += 7;
const n = (if (self.item) |d| d.dir() orelse return else return).items;
ui.move(self.row, self.col);
self.bg.fg(.num);
if (n < 1000)
ui.addprint(" {d:>4}", .{n})
else if (n < 10_000) {
ui.addch(' ');
ui.addnum(self.bg, n);
} else if (n < 100_000)
ui.addnum(self.bg, n)
else if (n < 1000_000) {
ui.addprint("{d:>5.1}", .{ @intToFloat(f32, n) / 1000 });
self.bg.fg(.default);
ui.addch('k');
} else if (n < 1000_000_000) {
ui.addprint("{d:>5.1}", .{ @intToFloat(f32, n) / 1000_000 });
self.bg.fg(.default);
ui.addch('M');
} else {
self.bg.fg(.default);
ui.addstr(" > ");
self.bg.fg(.num);
ui.addch('1');
self.bg.fg(.default);
ui.addch('G');
}
}
fn mtime(self: *Self) void {
if (!main.config.show_mtime or self.col + 37 > ui.cols) return;
defer self.col += 27;
ui.move(self.row, self.col+1);
const ext = (if (self.item) |e| e.ext() else @as(?*model.Ext, null)) orelse dir_parent.entry.ext();
if (ext) |e| ui.addts(self.bg, e.mtime)
else ui.addstr(" no mtime");
ui.move(self.row, self.col);
self.bg.fg(if (i.pack.etype == .dir) .dir else .default);
ui.addstr(ui.shorten(ui.toUtf8(i.name()), ui.cols -| self.col -| 1));
} else {
self.bg.fg(.dir);
if (self.bg == .sel) {
self.bg.fg(.default);
ui.move(self.row, 0);
ui.hline(' ', ui.cols);
}
self.flag();
self.size();
self.graph();
self.items();
self.mtime();
const quit = struct {
fn draw() void {
const box = ui.Box.create(4, 22, "Confirm quit");
box.move(2, 2);
ui.addstr("Really quit? (");
ui.style(.key);
ui.addch('y');
ui.style(.default);
ui.addch('/');
ui.style(.key);
ui.addch('N');
ui.style(.default);
ui.addch(')');
}
fn keyInput(ch: i32) void {
switch (ch) {
'y', 'Y' => ui.quit(),
else => state = .main,
}
}
};
const info = struct {
const Tab = enum { info, links };
var tab: Tab = .info;
var entry: ?*model.Entry = null;
var links: ?std.ArrayList(*model.Link) = null;
var links_top: usize = 0;
var links_idx: usize = 0;
fn lt(_: void, a: *model.Link, b: *model.Link) bool {
var pa = a.path(false);
var pb = b.path(false);
defer main.allocator.free(pa);
defer main.allocator.free(pb);
return std.mem.lessThan(u8, pa, pb);
}
// Set the displayed entry to the currently selected item and open the tab.
fn set(e: ?*model.Entry, t: Tab) void {
if (e != entry) {
if (links) |*l| l.deinit();
links = null;
links_top = 0;
links_idx = 0;
}
entry = e;
if (e == null) {
state = .main;
return;
}
state = .info;
tab = t;
if (tab == .links and links == null) {
var list = std.ArrayList(*model.Link).init(main.allocator);
var l = e.?.link().?;
while (true) {
list.append(l) catch unreachable;
l = l.next;
if (&l.entry == e)
break;
// TODO: Zig's sort() implementation is type-generic and not very
// small. I suspect we can get a good save on our binary size by using
// a smaller or non-generic sort. This doesn't have to be very fast.
std.sort.sort(*model.Link, list.items, @as(void, undefined), lt);
for (list.items) |n,i| if (&n.entry == e) { links_idx = i; };
links = list;
}
}
fn drawLinks(box: ui.Box, row: *u32, rows: u32, cols: u32) void {
if (links_idx < links_top) links_top = links_idx;
if (links_idx >= links_top + numrows) links_top = links_idx - numrows + 1;
var i: u32 = 0;
while (i < numrows) : (i += 1) {
if (i + links_top >= links.?.items.len) break;
const e = links.?.items[i+links_top];
ui.style(if (i+links_top == links_idx) .sel else .default);
box.move(row.*, 2);
ui.addch(if (&e.entry == entry) '*' else ' ');
const path = e.path(false);
defer main.allocator.free(path);
ui.addstr(ui.shorten(ui.toUtf8(path), cols -| 5));
row.* += 1;
}
ui.style(.default);
box.move(rows-2, 4);
ui.addprint("{:>3}/{}", .{ links_idx+1, links.?.items.len });
fn drawSizeRow(box: ui.Box, row: *u32, label: [:0]const u8, size: u64) void {
box.move(row.*, 3);
ui.addstr(label);
ui.addsize(.default, size);
ui.addstr(" (");
ui.addnum(.default, size);
ui.addch(')');
row.* += 1;
}
fn drawSize(box: ui.Box, row: *u32, label: [:0]const u8, size: u64, shared: u64) void {
ui.style(.bold);
drawSizeRow(box, row, label, size);
if (shared > 0) {
ui.style(.default);
drawSizeRow(box, row, " > shared: ", shared);
drawSizeRow(box, row, " > unique: ", size -| shared);
fn drawInfo(box: ui.Box, row: *u32, cols: u32, e: *model.Entry) void {
ui.style(.bold);
ui.addstr("Name: ");
ui.style(.default);
ui.addstr(ui.shorten(ui.toUtf8(e.name()), cols-11));
ui.style(.bold);
if (e.ext()) |ext| {
ui.addstr("Mode: ");
ui.style(.default);
ui.addmode(ext.mode);
var buf: [32]u8 = undefined;
ui.style(.bold);
ui.addstr(" UID: ");
ui.style(.default);
ui.addstr(std.fmt.bufPrintZ(&buf, "{d:<6}", .{ ext.uid }) catch unreachable);
ui.style(.bold);
ui.addstr(" GID: ");
ui.style(.default);
ui.addstr(std.fmt.bufPrintZ(&buf, "{d:<6}", .{ ext.gid }) catch unreachable);
} else {
ui.addstr("Type: ");
ui.style(.default);
ui.addstr(if (e.isDirectory()) "Directory" else if (if (e.file()) |f| f.pack.notreg else false) "Other" else "File");
ui.style(.bold);
ui.addstr("Last modified: ");
ui.addts(.default, ext.mtime);
drawSize(box, row, " Disk usage: ", util.blocksToSize(e.pack.blocks), if (e.dir()) |d| util.blocksToSize(d.shared_blocks) else 0);
drawSize(box, row, "Apparent size: ", e.size, if (e.dir()) |d| d.shared_size else 0);
ui.style(.bold);
ui.addstr(" Sub items: ");
ui.addnum(.default, d.items);
}
// Number of links + inode (dev?)
if (e.link()) |l| {
ui.addnum(.default, model.inodes.map.get(l).?.nlink);
ui.style(.bold);
ui.addstr(" Inode: ");
ui.style(.default);
var buf: [32]u8 = undefined;
ui.addstr(std.fmt.bufPrintZ(&buf, "{}", .{ l.ino }) catch unreachable);
row.* += 1;
}
}
fn draw() void {
const e = dir_items.items[cursor_idx].?;
// XXX: The dynamic height is a bit jarring, especially when that
// causes the same lines of information to be placed on different rows
// for each item. Think it's better to have a dynamic height based on
// terminal size and scroll if the content doesn't fit.
const rows = 5 // border + padding + close message
+ if (tab == .links) 8 else
4 // name + type + disk usage + apparent size
+ (if (e.ext() != null) @as(u32, 1) else 0) // last modified
+ (if (e.link() != null) @as(u32, 1) else 0) // link count
+ (if (e.dir()) |d| 1 // sub items
+ (if (d.shared_size > 0) @as(u32, 2) else 0)
+ (if (d.shared_blocks > 0) @as(u32, 2) else 0)
else 0);
const cols = 60; // TODO: dynamic width?
const box = ui.Box.create(rows, cols, "Item info");
var row: u32 = 2;
// Tabs
if (e.pack.etype == .link) {
box.tab(cols-19, tab == .info, 1, "Info");
box.tab(cols-10, tab == .links, 2, "Links");
}
switch (tab) {
.info => drawInfo(box, &row, cols, e),
.links => drawLinks(box, &row, rows, cols),
ui.style(.default);
ui.addstr("Press ");
ui.style(.key);
ui.addch('i');
ui.style(.default);
ui.addstr(" to close this window");
}
if (entry.?.pack.etype == .link) {
switch (ch) {
'1', 'h', ui.c.KEY_LEFT => { set(entry, .info); return true; },
'2', 'l', ui.c.KEY_RIGHT => { set(entry, .links); return true; },
else => {},
}
}
if (tab == .links) {
if (keyInputSelection(ch, &links_idx, links.?.items.len, 5))
return true;
if (ch == 10) { // Enter - go to selected entry
const l = links.?.items[links_idx];
dir_parent = l.parent;
loadDir(&l.entry);
if (keyInputSelection(ch, &cursor_idx, dir_items.items.len, ui.rows -| 3)) {
set(dir_items.items[cursor_idx], .info);
return true;
'i', 'q' => set(null, .info),
else => return false,
const help = struct {
const keys = [_][:0]const u8{
"up, k", "Move cursor up",
"down, j", "Move cursor down",
"right/enter", "Open selected directory",
"left, <, h", "Open parent directory",
"n", "Sort by name (ascending/descending)",
"s", "Sort by size (ascending/descending)",
"C", "Sort by items (ascending/descending)",
"M", "Sort by mtime (-e flag)",
"d", "Delete selected file or directory",
"t", "Toggle dirs before files when sorting",
"g", "Show percentage and/or graph",
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
"a", "Toggle between apparent size and disk usage",
"c", "Toggle display of child item counts",
"m", "Toggle display of latest mtime (-e flag)",
"e", "Show/hide hidden or excluded files",
"i", "Show information about selected item",
"r", "Recalculate the current directory",
"b", "Spawn shell in current directory",
"q", "Quit ncdu"
};
const keylines = 10;
const flags = [_][:0]const u8{
"!", "An error occurred while reading this directory",
".", "An error occurred while reading a subdirectory",
"<", "File or directory is excluded from the statistics",
"e", "Empty directory",
">", "Directory was on another filesystem",
"@", "This is not a file nor a dir (symlink, socket, ...)",
"^", "Excluded Linux pseudo-filesystem",
"H", "Same file was already counted (hard link)",
};
// It's kinda ugly, but for nostalgia's sake...
const logo = [_]u29{
0b11111100111110000001100110011,
0b11001100110000000001100110011,
0b11001100110000011111100110011,
0b11001100110000011001100110011,
0b11001100111110011111100111111,
};
var tab: enum { keys, flags, about } = .keys;
var offset: u32 = 0;
fn drawKeys(box: ui.Box) void {
var line: u32 = 1;
var i = offset*2;
while (i < (offset + keylines)*2) : (i += 2) {
line += 1;
box.move(line, 13 - @intCast(u32, keys[i].len));
ui.style(.key);
ui.addstr(keys[i]);
ui.style(.default);
ui.addch(' ');
ui.addstr(keys[i+1]);
}
if (offset < keys.len/2-keylines) {
box.move(12, 25);
ui.addstr("-- more --");
}
}
fn drawFlags(box: ui.Box) void {
box.move(2, 3);
ui.style(.bold);
ui.addstr("X [size] [graph] [file or directory]");
box.move(3, 4);
ui.style(.default);
ui.addstr("The X is only present in the following cases:");
var i: u32 = 0;
while (i < flags.len) : (i += 2) {
box.move(i/2+5, 4);
ui.style(.flag);
ui.addstr(flags[i]);
ui.style(.default);
ui.addch(' ');
ui.addstr(flags[i+1]);
}
}
fn drawAbout(box: ui.Box) void {
for (logo) |s, n| {
box.move(@intCast(u32, n)+3, 12);
var i: u5 = 28;
while (true) {
ui.style(if (s & (@as(u29,1)<<i) > 0) .sel else .default);
ui.addch(' ');
if (i == 0)
break;
i -= 1;
}
}
ui.style(.default);
box.move(3, 43); ui.addstr("NCurses");
box.move(4, 43); ui.addstr("Disk");
box.move(5, 43); ui.addstr("Usage");
ui.style(.num);
box.move(7, 43); ui.addstr(main.program_version);
ui.style(.default);
box.move(9, 9); ui.addstr("Written by Yoran Heling <projects@yorhel.nl>");
box.move(10,16); ui.addstr("https://dev.yorhel.nl/ncdu");
}
fn draw() void {
const box = ui.Box.create(15, 60, "ncdu help");
box.tab(30, tab == .keys, 1, "Keys");
box.tab(39, tab == .flags, 2, "Format");
box.tab(50, tab == .about, 3, "About");
box.move(13, 42);
ui.addstr("Press ");
ui.style(.key);
ui.addch('q');
ui.style(.default);
ui.addstr(" to close");
switch (tab) {
.keys => drawKeys(box),
.flags => drawFlags(box),
.about => drawAbout(box),
}
}
fn keyInput(ch: i32) void {
const ctab = tab;
defer if (ctab != tab or state != .help) { offset = 0; };
switch (ch) {
'1' => tab = .keys,
'2' => tab = .flags,
'3' => tab = .about,
'h', ui.c.KEY_LEFT => tab = if (tab == .about) .flags else .keys,
'l', ui.c.KEY_RIGHT => tab = if (tab == .keys) .flags else .about,
'j', ' ', ui.c.KEY_DOWN, ui.c.KEY_NPAGE => {
const max = switch (tab) {
.keys => keys.len/2 - keylines,
else => @as(u32, 0),
};
if (offset < max)
offset += 1;
},
'k', ui.c.KEY_UP, ui.c.KEY_PPAGE => { if (offset > 0) offset -= 1; },
else => state = .main,
}
}
};
ui.move(0,0);
ui.hline(' ', ui.cols);
ui.move(0,0);
ui.addstr("ncdu " ++ main.program_version ++ " ~ Use the arrow keys to navigate, press ");
ui.addch('?');
ui.addstr(" for help");
ui.addstr("[readonly]");
}
ui.style(.default);
ui.move(1,0);
ui.hline('-', ui.cols);
ui.move(1,3);
ui.addch(' ');
ui.style(.dir);
var pathbuf = std.ArrayList(u8).init(main.allocator);
dir_parent.fmtPath(true, &pathbuf);
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&pathbuf)), ui.cols -| 5));
pathbuf.deinit();
ui.style(.default);
ui.addch(' ');
if (cursor_idx < current_view.top) current_view.top = cursor_idx;
if (cursor_idx >= current_view.top + numrows) current_view.top = cursor_idx - numrows + 1;
var sel_row: u32 = 0;
while (i < numrows) : (i += 1) {
if (i+current_view.top >= dir_items.items.len) break;
var row = Row{
.row = i+2,
.item = dir_items.items[i+current_view.top],
.bg = if (i+current_view.top == cursor_idx) .sel else .default,
if (row.bg == .sel) sel_row = i+2;
ui.move(ui.rows-1, 0);
ui.hline(' ', ui.cols);
ui.move(ui.rows-1, 1);
ui.style(if (main.config.show_blocks) .bold_hd else .hd);
ui.addstr("Total disk usage: ");
ui.addsize(.hd, util.blocksToSize(dir_parent.entry.pack.blocks));
ui.style(if (main.config.show_blocks) .hd else .bold_hd);
ui.addstr(" Apparent size: ");
ui.addsize(.hd, dir_parent.entry.size);
ui.addnum(.hd, dir_parent.items);
switch (state) {
.main => {},
.quit => quit.draw(),
if (message) |m| {
const box = ui.Box.create(6, 60, "Message");
box.move(2, 2);
ui.addstr(m);
if (sel_row > 0) ui.move(sel_row, 0);
fn sortToggle(col: main.config.SortCol, default_order: main.config.SortOrder) void {
if (main.config.sort_col != col) main.config.sort_order = default_order
else if (main.config.sort_order == .asc) main.config.sort_order = .desc
else main.config.sort_order = .asc;
main.config.sort_col = col;
fn keyInputSelection(ch: i32, idx: *usize, len: usize, page: u32) bool {
switch (ch) {
'j', ui.c.KEY_DOWN => {
},
'k', ui.c.KEY_UP => {
ui.c.KEY_END, ui.c.KEY_LL => idx.* = len -| 1,
ui.c.KEY_PPAGE => idx.* = idx.* -| page,
ui.c.KEY_NPAGE => idx.* = std.math.min(len -| 1, idx.* + page),
else => return false,
}
return true;
}
pub fn keyInput(ch: i32) void {
if (message != null) {
message = null;
return;
}
switch (state) {
.main => {}, // fallthrough
.quit => return quit.keyInput(ch),
.info => if (info.keyInput(ch)) return,
}
switch (ch) {
'q' => if (main.config.confirm_quit) { state = .quit; } else ui.quit(),
'i' => if (dir_items.items.len > 0) info.set(dir_items.items[cursor_idx], .info),
message = "Directory refresh feature disabled."
scan.setupRefresh(dir_parent);
message = "Shell feature disabled."
'd' => {
if (dir_items.items.len == 0) {
message = "Deletion feature disabled."
else if (dir_items.items[cursor_idx]) |e| {
main.state = .delete;
const next =
if (cursor_idx+1 < dir_items.items.len) dir_items.items[cursor_idx+1]
else if (cursor_idx == 0) null
else dir_items.items[cursor_idx-1];
delete.setup(dir_parent, e, next);
// Sort & filter settings
'n' => sortToggle(.name, .asc),
's' => sortToggle(if (main.config.show_blocks) .blocks else .size, .desc),
'C' => sortToggle(.items, .desc),
'M' => if (main.config.extended) sortToggle(.mtime, .desc),
'e' => {
main.config.show_hidden = !main.config.show_hidden;
},
't' => {
main.config.sort_dirsfirst = !main.config.sort_dirsfirst;
},
'a' => {
main.config.show_blocks = !main.config.show_blocks;
if (main.config.show_blocks and main.config.sort_col == .size) {
main.config.sort_col = .blocks;
}
if (!main.config.show_blocks and main.config.sort_col == .blocks) {
main.config.sort_col = .size;
// Navigation
10, 'l', ui.c.KEY_RIGHT => {
if (dir_items.items.len == 0) {
} else if (dir_items.items[cursor_idx]) |e| {
dir_parent = d;
} else if (dir_parent.parent) |p| {
dir_parent = p;
}
},
'h', '<', ui.c.KEY_BACKSPACE, ui.c.KEY_LEFT => {
if (dir_parent.parent) |p| {
const e = dir_parent;
dir_parent = p;
// Display settings
'c' => main.config.show_items = !main.config.show_items,
'm' => if (main.config.extended) { main.config.show_mtime = !main.config.show_mtime; },
'g' => {
if (!main.config.show_graph and !main.config.show_percent) { main.config.show_graph = true; main.config.show_percent = false; }
else if ( main.config.show_graph and !main.config.show_percent) { main.config.show_graph = false; main.config.show_percent = true; }
else if (!main.config.show_graph and main.config.show_percent) { main.config.show_graph = true; main.config.show_percent = true; }
else if ( main.config.show_graph and main.config.show_percent) { main.config.show_graph = false; main.config.show_percent = false; }
'u' => main.config.show_shared = switch (main.config.show_shared) {
.off => .shared,
.shared => .unique,
.unique => .off,
},
else => _ = keyInputSelection(ch, &cursor_idx, dir_items.items.len, ui.rows -| 3),