Skip to content
Snippets Groups Projects
Commit 27cb599e authored by Yorhel's avatar Yorhel
Browse files

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.
parent a54c10bf
No related branches found
No related tags found
No related merge requests found
......@@ -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);
}
......@@ -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();
......
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" {
......
......@@ -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;
......
......@@ -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));
}
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;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment