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;
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_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.isDirectory() != b.isDirectory())
return a.isDirectory();
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
dir_items.shrinkRetainingCapacity(0);
dir_max_size = 1;
dir_max_blocks = 1;
dir_has_shared = false;
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;
const shown = main.config.show_hidden or blk: {
const excl = if (e.file()) |f| f.excluded else false;
const name = e.name();
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; };
}
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 {
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) blocksToSize(item.blocks) else item.size;
var shr = if (item.dir()) |d| (if (main.config.show_blocks) blocksToSize(d.shared_blocks) else d.shared_size) else 0;
if (main.config.show_shared == .unique) shr = saturateSub(siz, shr);
ui.addsize(self.bg, siz);
if (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 == .off or self.col + 20 > ui.cols) 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 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_parents.top().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.etype == .dir) .dir else .default);
ui.addstr(ui.shorten(ui.toUtf8(i.name()), saturateSub(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();
var state: enum { main, quit, info } = .main;
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 {
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
const Tab = enum { info, links };
var tab: Tab = .info;
var entry: ?*model.Entry = null;
var links: ?model.LinkPaths = null;
var links_top: usize = 0;
var links_idx: usize = 0;
// 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) {
links = model.LinkPaths.find(&dir_parents, e.?.link().?);
for (links.?.paths.items) |n,i| {
if (&n.node.entry == e) {
links_idx = i;
}
}
}
}
fn drawLinks(box: ui.Box, row: *u32, rows: u32, cols: u32) void {
var pathbuf = std.ArrayList(u8).init(main.allocator);
const numrows = saturateSub(rows, 4);
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.?.paths.items.len) break;
const e = links.?.paths.items[i+links_top];
ui.style(if (i+links_top == links_idx) .sel else .default);
box.move(row.*, 2);
ui.addch(if (&e.node.entry == entry) '*' else ' ');
pathbuf.shrinkRetainingCapacity(0);
e.fmtPath(false, &pathbuf);
ui.addstr(ui.shorten(ui.toUtf8(arrayListBufZ(&pathbuf)), saturateSub(cols, 5)));
row.* += 1;
}
ui.style(.default);
box.move(rows-2, 4);
ui.addprint("{:>3}/{}", .{ links_idx+1, links.?.paths.items.len });
pathbuf.deinit();
}
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: ", saturateSub(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.notreg else false) "Other" else "File");
}
ui.style(.bold);
ui.addstr("Last modified: ");
ui.addts(.default, ext.mtime);
drawSize(box, row, " Disk usage: ", blocksToSize(e.blocks), if (e.dir()) |d| 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.style(.bold);
ui.addstr(" Link count: ");
ui.addnum(.default, 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);
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
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.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");
}
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
fn keyInput(ch: i32) bool {
if (entry.?.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.?.paths.items.len, 5))
return true;
if (ch == 10) { // Enter - go to selected entry
// XXX: This jump can be a little bit jarring as, usually,
// browsing to parent directory will cause the previously
// opened dir to be selected. This jump doesn't update the View
// state of parent dirs, so that won't be the case anymore.
const p = links.?.paths.items[links_idx];
dir_parents.stack.shrinkRetainingCapacity(0);
dir_parents.stack.appendSlice(p.path.stack.items) catch unreachable;
loadDir();
for (dir_items.items) |e, i| {
if (e == &p.node.entry)
cursor_idx = i;
}
set(null, .info);
}
}
if (keyInputSelection(ch, &cursor_idx, dir_items.items.len, saturateSub(ui.rows, 3))) {
set(dir_items.items[cursor_idx], .info);
return true;
'i', 'q' => set(null, .info),
else => return false,
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.imported) {
ui.move(0, saturateSub(ui.cols, 10));
ui.addstr("[imported]");
} else if (main.config.read_only) {
ui.move(0, saturateSub(ui.cols, 10));
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_parents.fmtPath(true, &pathbuf);
ui.addstr(ui.shorten(ui.toUtf8(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);
switch (state) {
.main => {},
.quit => quit.draw(),
.info => info.draw(),
}
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();
}
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_HOME => idx.* = 0,
ui.c.KEY_END, ui.c.KEY_LL => idx.* = saturateSub(len, 1),
ui.c.KEY_PPAGE => idx.* = saturateSub(idx.*, page),
ui.c.KEY_NPAGE => idx.* = std.math.min(saturateSub(len, 1), idx.* + page),
else => return false,
}
return true;
}
pub fn keyInput(ch: i32) void {
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' => info.set(dir_items.items[cursor_idx], .info),
'r' => {
if (main.config.imported) {
// TODO: Display message
} else {
main.state = .refresh;
scan.setupRefresh(dir_parents.copy());
}
},
// 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;
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| {
} else if (!dir_parents.isRoot()) {
}
},
'h', '<', ui.c.KEY_BACKSPACE, ui.c.KEY_LEFT => {
// 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,
},
// TODO: This key binding is not final! I'd rather add a menu selection thing for advanced settings rather than risk running out of more keys.
'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, saturateSub(ui.rows, 3)),