summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2021-05-06 19:15:47 +0200
committerYorhel <git@yorhel.nl>2021-05-06 19:20:55 +0200
commit27cb599e22ed4e592375e7f6d0e14ee6181d36fe (patch)
treebf489f4345027cf20dd3e116a4f483e89e00469e /src
parenta54c10bffb56367bd5d2dcc7a9c1c4ef06e84cbb (diff)
More UI stuff + shave off 16 bytes from model.Dir
I initially wanted to keep a directory's block count and size as a separate field so that exporting an in-memory tree to a JSON dump would be easier to do, but that doesn't seem like a common operation to optimize for. We'll probably need the algorithms to subtract sub-items from directory counts anyway, so such an export can still be implemented, albeit slower.
Diffstat (limited to 'src')
-rw-r--r--src/browser.zig170
-rw-r--r--src/main.zig8
-rw-r--r--src/model.zig41
-rw-r--r--src/scan.zig8
-rw-r--r--src/ui.zig88
-rw-r--r--src/util.zig16
6 files changed, 296 insertions, 35 deletions
diff --git a/src/browser.zig b/src/browser.zig
index 9024f75..2fffae2 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -2,6 +2,154 @@ const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
const ui = @import("ui.zig");
+usingnamespace @import("util.zig");
+
+// 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);
+
+// Currently opened directory and its parents.
+var dir_parents = model.Parents{};
+
+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.etype == .dir) != (b.etype == .dir))
+ return a.etype == .dir;
+
+ switch (main.config.sort_col) {
+ .name => {}, // name sorting is the fallback
+ .blocks => {
+ if (sortIntLt(a.blocks, b.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.blocks, b.blocks)) |r| return r;
+ },
+ .items => {
+ const ai = if (a.dir()) |d| d.total_items else 0;
+ const bi = if (b.dir()) |d| d.total_items else 0;
+ if (sortIntLt(ai, bi)) |r| return r;
+ if (sortIntLt(a.blocks, b.blocks)) |r| return r;
+ if (sortIntLt(a.size, b.size)) |r| return r;
+ },
+ .mtime => {
+ if (!a.isext or !b.isext) return a.isext;
+ if (sortIntLt(a.ext().?.mtime, b.ext().?.mtime)) |r| return r;
+ },
+ }
+
+ // TODO: Unicode-aware sorting might be nice (and slow)
+ const an = a.name();
+ const bn = b.name();
+ return if (main.config.sort_order == .asc) std.mem.lessThan(u8, an, bn)
+ else std.mem.lessThan(u8, bn, an) or std.mem.eql(u8, an, bn);
+}
+
+// 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() 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);
+ // TODO: Fixup selected item index
+}
+
+// Must be called when:
+// - dir_parents changes (i.e. we change directory)
+// - config.show_hidden changes
+// - files in this dir have been added or removed
+fn loadDir() !void {
+ dir_items.shrinkRetainingCapacity(0);
+ if (dir_parents.top() != model.root)
+ try dir_items.append(null);
+ var it = dir_parents.top().sub;
+ while (it) |e| {
+ if (main.config.show_hidden) // fast path
+ try dir_items.append(e)
+ else {
+ const excl = if (e.file()) |f| f.excluded else false;
+ const name = e.name();
+ if (!excl and name[0] != '.' and name[name.len-1] != '~')
+ try dir_items.append(e);
+ }
+ it = e.next;
+ }
+ sortDir();
+}
+
+// Open the given dir for browsing; takes ownership of the Parents struct.
+pub fn open(dir: model.Parents) !void {
+ dir_parents.deinit();
+ dir_parents = dir;
+ try loadDir();
+
+ // TODO: Load view & cursor position if we've opened this dir before.
+}
+
+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.err) break :ch '!';
+ if (f.excluded) break :ch '<';
+ if (f.other_fs) break :ch '>';
+ if (f.kernfs) break :ch '^';
+ if (f.notreg) break :ch '@';
+ } else if (item.dir()) |d| {
+ if (d.err) break :ch '!';
+ if (d.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 {
+ defer self.col += if (main.config.si) @as(u32, 9) else 10;
+ const item = self.item orelse return;
+ ui.move(self.row, self.col);
+ ui.addsize(self.bg, if (main.config.show_blocks) blocksToSize(item.blocks) else item.size);
+ // TODO: shared sizes
+ }
+
+ fn name(self: *Self) !void {
+ ui.move(self.row, self.col);
+ self.bg.fg(.default);
+ if (self.item) |i| {
+ ui.addch(if (i.etype == .dir) '/' else ' ');
+ ui.addstr(try ui.shorten(try ui.toUtf8(i.name()), saturateSub(ui.cols, saturateSub(self.col, 1))));
+ } else
+ ui.addstr("/..");
+ }
+
+ fn draw(self: *Self) !void {
+ try self.flag();
+ try self.size();
+ try self.name();
+ }
+};
pub fn draw() !void {
ui.style(.hd);
@@ -13,19 +161,35 @@ pub fn draw() !void {
ui.addch('?');
ui.style(.hd);
ui.addstr(" for help");
- // TODO: [imported]/[readonly] indicators
+ if (main.config.read_only) {
+ ui.move(0, saturateSub(ui.cols, 10));
+ ui.addstr("[readonly]");
+ }
+ // TODO: [imported] indicator
ui.style(.default);
ui.move(1,0);
ui.hline('-', ui.cols);
ui.move(1,3);
ui.addch(' ');
- ui.addstr(try ui.shorten(try ui.toUtf8(model.root.entry.name()), std.math.sub(u32, ui.cols, 5) catch 4));
+ ui.addstr(try ui.shorten(try ui.toUtf8(model.root.entry.name()), saturateSub(ui.cols, 5)));
ui.addch(' ');
+ var i: u32 = 0;
+ while (i < saturateSub(ui.rows, 3)) : (i += 1) {
+ if (i >= dir_items.items.len) break;
+ var row = Row{ .row = i+2, .item = dir_items.items[i] };
+ try row.draw();
+ }
+
ui.style(.hd);
ui.move(ui.rows-1, 0);
ui.hline(' ', ui.cols);
ui.move(ui.rows-1, 1);
- ui.addstr("No items to display.");
+ ui.addstr("Total disk usage: ");
+ ui.addsize(.hd, blocksToSize(dir_parents.top().entry.blocks));
+ ui.addstr(" Apparent size: ");
+ ui.addsize(.hd, dir_parents.top().entry.size);
+ ui.addstr(" Items: ");
+ ui.addnum(.hd, dir_parents.top().total_items);
}
diff --git a/src/main.zig b/src/main.zig
index cb6043c..04d2991 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -23,6 +23,12 @@ pub const Config = struct {
ui_color: enum { off, dark } = .off,
thousands_sep: []const u8 = ".",
+ show_hidden: bool = true,
+ show_blocks: bool = true,
+ sort_col: enum { name, blocks, size, items, mtime } = .blocks,
+ sort_order: enum { asc, desc } = .desc,
+ sort_dirsfirst: bool = false,
+
read_only: bool = false,
can_shell: bool = true,
confirm_quit: bool = false,
@@ -239,9 +245,11 @@ pub fn main() anyerror!void {
ui.die("The --exclude-kernfs tag is currently only supported on Linux.\n", .{});
try scan.scanRoot(scan_dir orelse ".");
+ try browser.open(model.Parents{});
ui.init();
defer ui.deinit();
+
try browser.draw();
_ = ui.c.getch();
diff --git a/src/model.zig b/src/model.zig
index 34e3aa9..48340dd 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -1,24 +1,15 @@
const std = @import("std");
const main = @import("main.zig");
+usingnamespace @import("util.zig");
// While an arena allocator is optimimal for almost all scenarios in which ncdu
// is used, it doesn't allow for re-using deleted nodes after doing a delete or
// refresh operation, so a long-running ncdu session with regular refreshes
// will leak memory, but I'd say that's worth the efficiency gains.
-// (TODO: Measure, though. Might as well use a general purpose allocator if the
-// memory overhead turns out to be insignificant.)
+// TODO: Can still implement a simple bucketed free list on top of this arena
+// allocator to reuse nodes, if necessary.
var allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator);
-fn saturateAdd(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
- std.debug.assert(@typeInfo(@TypeOf(a)).Int.signedness == .unsigned);
- return std.math.add(@TypeOf(a), a, b) catch std.math.maxInt(@TypeOf(a));
-}
-
-fn saturateSub(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
- std.debug.assert(@typeInfo(@TypeOf(a)).Int.signedness == .unsigned);
- return std.math.sub(@TypeOf(a), a, b) catch std.math.minInt(@TypeOf(a));
-}
-
pub const EType = packed enum(u2) { dir, link, file };
// Memory layout:
@@ -57,7 +48,7 @@ pub const Entry = packed struct {
return if (self.etype == .file) @ptrCast(*File, self) else null;
}
- fn name_offset(etype: EType) usize {
+ fn nameOffset(etype: EType) usize {
return switch (etype) {
.dir => @byteOffsetOf(Dir, "name"),
.link => @byteOffsetOf(Link, "name"),
@@ -66,25 +57,25 @@ pub const Entry = packed struct {
}
pub fn name(self: *const Self) [:0]const u8 {
- const ptr = @intToPtr([*:0]u8, @ptrToInt(self) + name_offset(self.etype));
+ const ptr = @intToPtr([*:0]u8, @ptrToInt(self) + nameOffset(self.etype));
return ptr[0..std.mem.lenZ(ptr) :0];
}
pub fn ext(self: *Self) ?*Ext {
if (!self.isext) return null;
const n = self.name();
- return @intToPtr(*Ext, std.mem.alignForward(@ptrToInt(self) + name_offset(self.etype) + n.len + 1, @alignOf(Ext)));
+ return @intToPtr(*Ext, std.mem.alignForward(@ptrToInt(self) + nameOffset(self.etype) + n.len + 1, @alignOf(Ext)));
}
pub fn create(etype: EType, isext: bool, ename: []const u8) !*Entry {
- const base_size = name_offset(etype) + ename.len + 1;
+ const base_size = nameOffset(etype) + ename.len + 1;
const size = (if (isext) std.mem.alignForward(base_size, @alignOf(Ext))+@sizeOf(Ext) else base_size);
var ptr = try allocator.allocator.allocWithOptions(u8, size, @alignOf(Entry), null);
std.mem.set(u8, ptr, 0); // kind of ugly, but does the trick
var e = @ptrCast(*Entry, ptr);
e.etype = etype;
e.isext = isext;
- var name_ptr = @intToPtr([*]u8, @ptrToInt(e) + name_offset(etype));
+ var name_ptr = @intToPtr([*]u8, @ptrToInt(e) + nameOffset(etype));
std.mem.copy(u8, name_ptr[0..ename.len], ename);
//std.debug.warn("{any}\n", .{ @ptrCast([*]u8, e)[0..size] });
return e;
@@ -145,8 +136,8 @@ pub const Entry = packed struct {
add_total = true;
}
if(add_total) {
- p.total_size = saturateAdd(p.total_size, self.size);
- p.total_blocks = saturateAdd(p.total_blocks, self.blocks);
+ p.entry.size = saturateAdd(p.entry.size, self.size);
+ p.entry.blocks = saturateAdd(p.entry.blocks, self.blocks);
p.total_items = saturateAdd(p.total_items, 1);
}
}
@@ -160,17 +151,15 @@ pub const Dir = packed struct {
sub: ?*Entry,
- // total_*: Total size of all unique files + dirs. Non-shared hardlinks are counted only once.
+ // entry.{blocks,size}: Total size of all unique files + dirs. Non-shared hardlinks are counted only once.
// (i.e. the space you'll need if you created a filesystem with only this dir)
// shared_*: Unique hardlinks that still have references outside of this directory.
// (i.e. the space you won't reclaim by deleting this dir)
- // (space reclaimed by deleting a dir =~ total_ - shared_)
- total_blocks: u64,
+ // (space reclaimed by deleting a dir =~ entry. - shared_)
shared_blocks: u64,
- total_size: u64,
shared_size: u64,
- total_items: u32,
shared_items: u32,
+ total_items: u32,
// TODO: ncdu1 only keeps track of a total item count including duplicate hardlinks.
// That number seems useful, too. Include it somehow?
@@ -355,6 +344,10 @@ pub const Parents = struct {
i += 1;
}
}
+
+ pub fn deinit(self: *Self) void {
+ self.stack.deinit();
+ }
};
test "name offsets" {
diff --git a/src/scan.zig b/src/scan.zig
index ee673d8..d640150 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -217,13 +217,7 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
var e = try model.Entry.create(etype, main.config.extended, entry.name);
e.blocks = stat.blocks;
e.size = stat.size;
- if (e.dir()) |d| {
- d.dev = try model.getDevId(stat.dev);
- // The dir entry itself also counts.
- d.total_blocks = stat.blocks;
- d.total_size = stat.size;
- d.total_items = 1;
- }
+ if (e.dir()) |d| d.dev = try model.getDevId(stat.dev);
if (e.file()) |f| f.notreg = !stat.dir and !stat.reg;
if (e.link()) |l| {
l.ino = stat.ino;
diff --git a/src/ui.zig b/src/ui.zig
index 14608bd..a685624 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -209,7 +209,7 @@ const styles = [_]StyleDef{
.dark = .{ .fg = c.COLOR_MAGENTA, .bg = c.COLOR_GREEN, .attr = 0 } },
};
-const Style = lbl: {
+pub const Style = lbl: {
var fields: [styles.len]std.builtin.TypeInfo.EnumField = undefined;
var decls = [_]std.builtin.TypeInfo.Declaration{};
inline for (styles) |s, i| {
@@ -229,6 +229,35 @@ const Style = lbl: {
});
};
+const ui = @This();
+
+pub const Bg = enum {
+ default, hd, sel,
+
+ // Set the style to the selected bg combined with the given fg.
+ pub fn fg(self: @This(), s: Style) void {
+ ui.style(switch (self) {
+ .default => s,
+ .hd =>
+ switch (s) {
+ .default => Style.hd,
+ .key => Style.key_hd,
+ .num => Style.num_hd,
+ else => unreachable,
+ },
+ .sel =>
+ switch (s) {
+ .default => Style.sel,
+ .num => Style.num_sel,
+ .dir => Style.dir_sel,
+ .flag => Style.flag_sel,
+ .graph => Style.graph_sel,
+ else => unreachable,
+ }
+ });
+ }
+};
+
fn updateSize() void {
// getmax[yx] macros are marked as "legacy", but Zig can't deal with the "proper" getmaxyx macro.
rows = @intCast(u32, c.getmaxy(c.stdscr));
@@ -287,6 +316,63 @@ pub fn addch(ch: c.chtype) void {
_ = c.addch(ch);
}
+// Print a human-readable size string, formatted into the given bavkground.
+// Takes 8 columns in SI mode, 9 otherwise.
+// "###.# XB"
+// "###.# XiB"
+pub fn addsize(bg: Bg, v: u64) void {
+ var f = @intToFloat(f32, v);
+ var unit: [:0]const u8 = undefined;
+ if (main.config.si) {
+ if(f < 1000.0) { unit = " B"; }
+ else if(f < 1e6) { unit = " KB"; f /= 1e3; }
+ else if(f < 1e9) { unit = " MB"; f /= 1e6; }
+ else if(f < 1e12) { unit = " GB"; f /= 1e9; }
+ else if(f < 1e15) { unit = " TB"; f /= 1e12; }
+ else if(f < 1e18) { unit = " PB"; f /= 1e15; }
+ else { unit = " EB"; f /= 1e18; }
+ }
+ else {
+ if(f < 1000.0) { unit = " B"; }
+ else if(f < 1023e3) { unit = " KiB"; f /= 1024.0; }
+ else if(f < 1023e6) { unit = " MiB"; f /= 1048576.0; }
+ else if(f < 1023e9) { unit = " GiB"; f /= 1073741824.0; }
+ else if(f < 1023e12) { unit = " TiB"; f /= 1099511627776.0; }
+ else if(f < 1023e15) { unit = " PiB"; f /= 1125899906842624.0; }
+ else { unit = " EiB"; f /= 1152921504606846976.0; }
+ }
+ var buf: [8:0]u8 = undefined;
+ _ = std.fmt.bufPrintZ(&buf, "{d:>5.1}", .{f}) catch unreachable;
+ bg.fg(.num);
+ addstr(&buf);
+ bg.fg(.default);
+ addstr(unit);
+}
+
+// Print a full decimal number with thousand separators.
+// Max: 18,446,744,073,709,551,615 -> 26 columns
+// (Assuming thousands_sep takes a single column)
+pub fn addnum(bg: Bg, v: u64) void {
+ var buf: [32]u8 = undefined;
+ const s = std.fmt.bufPrint(&buf, "{d}", .{v}) catch unreachable;
+ var f: [64:0]u8 = undefined;
+ var i: usize = 0;
+ for (s) |digit, n| {
+ if (n != 0 and (s.len - n) % 3 == 0) {
+ for (main.config.thousands_sep) |ch| {
+ f[i] = ch;
+ i += 1;
+ }
+ }
+ f[i] = digit;
+ i += 1;
+ }
+ f[i] = 0;
+ bg.fg(.num);
+ addstr(&f);
+ bg.fg(.default);
+}
+
pub fn hline(ch: c.chtype, len: u32) void {
_ = c.hline(ch, @intCast(i32, len));
}
diff --git a/src/util.zig b/src/util.zig
new file mode 100644
index 0000000..3b6986a
--- /dev/null
+++ b/src/util.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+
+pub fn saturateAdd(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
+ std.debug.assert(@typeInfo(@TypeOf(a)).Int.signedness == .unsigned);
+ return std.math.add(@TypeOf(a), a, b) catch std.math.maxInt(@TypeOf(a));
+}
+
+pub fn saturateSub(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
+ std.debug.assert(@typeInfo(@TypeOf(a)).Int.signedness == .unsigned);
+ return std.math.sub(@TypeOf(a), a, b) catch std.math.minInt(@TypeOf(a));
+}
+
+// Multiplies by 512, saturating.
+pub fn blocksToSize(b: u64) u64 {
+ return if (b & 0xFF80000000000000 > 0) std.math.maxInt(u64) else b << 9;
+}