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.
This commit is contained in:
Yorhel 2021-05-06 19:15:47 +02:00
parent a54c10bffb
commit 27cb599e22
6 changed files with 296 additions and 35 deletions

View file

@ -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);
}

View file

@ -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();

View file

@ -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" {

View file

@ -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;

View file

@ -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));
}

16
src/util.zig Normal file
View file

@ -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;
}