Newer
Older
const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
const ui = @import("ui.zig");
const c = @cImport(@cInclude("time.h"));
usingnamespace @import("util.zig");
// Currently opened directory and its parents.
var dir_parents = model.Parents{};
// 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;
// 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_parents.top()), self.*) catch {};
}
// Should be called after dir_parents or dir_items has changed, will load the last saved view and find the proper cursor_idx.
fn load(self: *@This()) void {
if (opened_dir_views.get(@ptrToInt(dir_parents.top()))) |v| self.* = v
else self.* = @This(){};
for (dir_items.items) |e, i| {
if (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.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.items else 0;
const bi = if (b.dir()) |d| d.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);
}
// 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);
}
// 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
pub fn loadDir() !void {
dir_items.shrinkRetainingCapacity(0);
dir_max_size = 1;
dir_max_blocks = 1;
if (dir_parents.top() != model.root)
try dir_items.append(null);
var it = dir_parents.top().sub;
while (it) |e| {
if (e.blocks > dir_max_blocks) dir_max_blocks = e.blocks;
if (e.size > dir_max_size) dir_max_size = e.size;
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();
}
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 graph(self: *Self) void {
if (main.config.show_graph == .off) return;
const bar_size = std.math.max(ui.cols/7, 10);
defer self.col += switch (main.config.show_graph) {
.off => unreachable,
.graph => bar_size + 3,
.percent => 9,
.both => bar_size + 10,
};
const item = self.item orelse return;
ui.move(self.row, self.col);
self.bg.fg(.default);
ui.addch('[');
if (main.config.show_graph == .both or main.config.show_graph == .percent) {
self.bg.fg(.num);
ui.addprint("{d:>5.1}", .{ 100*
if (main.config.show_blocks) @intToFloat(f32, item.blocks) / @intToFloat(f32, std.math.max(1, dir_parents.top().entry.blocks))
else @intToFloat(f32, item.size) / @intToFloat(f32, std.math.max(1, dir_parents.top().entry.size))
});
self.bg.fg(.default);
ui.addch('%');
}
if (main.config.show_graph == .both) ui.addch(' ');
if (main.config.show_graph == .both or main.config.show_graph == .graph) {
const perblock = std.math.divFloor(u64, if (main.config.show_blocks) dir_max_blocks else dir_max_size, bar_size) catch unreachable;
const num = if (main.config.show_blocks) item.blocks else item.size;
var i: u32 = 0;
self.bg.fg(.graph);
while (i < bar_size) : (i += 1) {
siz = saturateAdd(siz, perblock);
ui.addch(if (siz <= num) '#' else ' ');
}
}
self.bg.fg(.default);
ui.addch(']');
}
fn items(self: *Self) void {
if (!main.config.show_items) 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)
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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) 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_parents.top().entry.ext();
if (ext) |e| {
const t = castClamp(c.time_t, e.mtime);
var buf: [32:0]u8 = undefined;
const len = c.strftime(&buf, buf.len, "%Y-%m-%d %H:%M:%S %z", c.localtime(&t));
if (len > 0) {
self.bg.fg(.num);
ui.addstr(buf[0..len:0]);
} else
ui.addstr(" invalid mtime");
} else
ui.addstr(" no mtime");
}
fn name(self: *Self) !void {
ui.move(self.row, self.col);
self.bg.fg(if (i.etype == .dir) .dir else .default);
ui.addch(if (i.etype == .dir) '/' else ' ');
ui.addstr(try ui.shorten(try ui.toUtf8(i.name()), saturateSub(ui.cols, self.col + 1)));
} else {
self.bg.fg(.dir);
}
fn draw(self: *Self) !void {
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();
try self.name();
}
};
var need_confirm_quit = false;
fn drawQuit() 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(')');
}
pub fn draw() !void {
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");
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.style(.dir);
var pathbuf = std.ArrayList(u8).init(main.allocator);
try dir_parents.path(pathbuf.writer());
ui.addstr(try ui.shorten(try ui.toUtf8(try arrayListBufZ(&pathbuf)), saturateSub(ui.cols, 5)));
pathbuf.deinit();
ui.style(.default);
ui.addch(' ');
const numrows = saturateSub(ui.rows, 3);
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, blocksToSize(dir_parents.top().entry.blocks));
ui.style(if (main.config.show_blocks) .hd else .bold_hd);
ui.addstr(" Apparent size: ");
ui.addsize(.hd, dir_parents.top().entry.size);
ui.addstr(" Items: ");
ui.addnum(.hd, dir_parents.top().items);
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;
sortDir();
}
pub fn key(ch: i32) !void {
if (need_confirm_quit) {
switch (ch) {
'y', 'Y' => if (need_confirm_quit) ui.quit(),
else => need_confirm_quit = false,
}
return;
}
defer current_view.save();
'q' => if (main.config.confirm_quit) { need_confirm_quit = true; } else ui.quit(),
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
// Selection
'j', ui.c.KEY_DOWN => {
if (cursor_idx+1 < dir_items.items.len) cursor_idx += 1;
},
'k', ui.c.KEY_UP => {
if (cursor_idx > 0) cursor_idx -= 1;
},
ui.c.KEY_HOME => cursor_idx = 0,
ui.c.KEY_END, ui.c.KEY_LL => cursor_idx = saturateSub(dir_items.items.len, 1),
ui.c.KEY_PPAGE => cursor_idx = saturateSub(cursor_idx, saturateSub(ui.rows, 3)),
ui.c.KEY_NPAGE => cursor_idx = std.math.min(saturateSub(dir_items.items.len, 1), cursor_idx + saturateSub(ui.rows, 3)),
// 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;
try loadDir();
},
't' => {
main.config.sort_dirsfirst = !main.config.sort_dirsfirst;
sortDir();
},
'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;
sortDir();
}
if (!main.config.show_blocks and main.config.sort_col == .blocks) {
main.config.sort_col = .size;
sortDir();
}
},
// Navigation
10, 'l', ui.c.KEY_RIGHT => {
if (dir_items.items.len == 0) {
} else if (dir_items.items[cursor_idx]) |e| {
if (e.dir()) |d| {
try dir_parents.push(d);
try loadDir();
}
} else if (dir_parents.top() != model.root) {
dir_parents.pop();
try loadDir();
}
},
'h', '<', ui.c.KEY_BACKSPACE, ui.c.KEY_LEFT => {
if (dir_parents.top() != model.root) {
dir_parents.pop();
try loadDir();
}
},
// 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' => main.config.show_graph = switch (main.config.show_graph) {
.off => .graph,
.graph => .percent,
.percent => .both,
.both => .off,
},