diff --git a/src/browser.zig b/src/browser.zig index 2fffae289001b2befc718c265b202a624d6923f2..0c8038dbd712a31cfafaed15433a87523c18e300 100644 --- a/src/browser.zig +++ b/src/browser.zig @@ -11,6 +11,9 @@ var dir_items = std.ArrayList(?*model.Entry).init(main.allocator); // Currently opened directory and its parents. var dir_parents = model.Parents{}; +var cursor_idx: usize = 0; +var window_top: usize = 0; + 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; } @@ -93,6 +96,8 @@ pub fn open(dir: model.Parents) !void { dir_parents = dir; try loadDir(); + window_top = 0; + cursor_idx = 0; // TODO: Load view & cursor position if we've opened this dir before. } @@ -145,6 +150,11 @@ const Row = struct { } fn draw(self: *Self) !void { + if (self.bg == .sel) { + self.bg.fg(.default); + ui.move(self.row, 0); + ui.hline(' ', ui.cols); + } try self.flag(); try self.size(); try self.name(); @@ -175,10 +185,18 @@ pub fn draw() !void { ui.addstr(try ui.shorten(try ui.toUtf8(model.root.entry.name()), saturateSub(ui.cols, 5))); ui.addch(' '); + const numrows = saturateSub(ui.rows, 3); + if (cursor_idx < window_top) window_top = cursor_idx; + if (cursor_idx >= window_top + numrows) window_top = cursor_idx - numrows + 1; + 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] }; + while (i < numrows) : (i += 1) { + if (i+window_top >= dir_items.items.len) break; + var row = Row{ + .row = i+2, + .item = dir_items.items[i+window_top], + .bg = if (i+window_top == cursor_idx) .sel else .default, + }; try row.draw(); } @@ -193,3 +211,56 @@ pub fn draw() !void { ui.addstr(" Items: "); ui.addnum(.hd, dir_parents.top().total_items); } + +fn sortToggle(col: main.SortCol, default_order: main.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 { + switch (ch) { + 'q' => main.state = .quit, + + // 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(); + } + }, + + else => {} + } +} diff --git a/src/main.zig b/src/main.zig index 04d299121d8fdde3bc8509e9ff60be759a06a17a..bb3a3b759f734629f7b6ecbfcedfb8fd76d008b6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,9 @@ const c = @cImport(@cInclude("locale.h")); pub const allocator = std.heap.c_allocator; +pub const SortCol = enum { name, blocks, size, items, mtime }; +pub const SortOrder = enum { asc, desc }; + pub const Config = struct { same_fs: bool = true, extended: bool = false, @@ -17,7 +20,7 @@ pub const Config = struct { exclude_kernfs: bool = false, exclude_patterns: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(allocator), - update_delay: u32 = 100, + update_delay: u64 = 100*std.time.ns_per_ms, si: bool = false, nc_tty: bool = false, ui_color: enum { off, dark } = .off, @@ -25,8 +28,8 @@ pub const Config = struct { show_hidden: bool = true, show_blocks: bool = true, - sort_col: enum { name, blocks, size, items, mtime } = .blocks, - sort_order: enum { asc, desc } = .desc, + sort_col: SortCol = .blocks, + sort_order: SortOrder = .desc, sort_dirsfirst: bool = false, read_only: bool = false, @@ -36,6 +39,8 @@ pub const Config = struct { pub var config = Config{}; +pub var state: enum { browse, quit } = .browse; + // Simple generic argument parser, supports getopt_long() style arguments. // T can be any type that has a 'fn next(T) ?[:0]const u8' method, e.g.: // var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init()); @@ -112,46 +117,6 @@ fn Args(T: anytype) type { }; } - -// For debugging -fn writeTree(out: anytype, e: *model.Entry, indent: u32) @TypeOf(out).Error!void { - var i: u32 = 0; - while (i<indent) { - try out.writeByte(' '); - i += 1; - } - try out.print("{s} blocks={d} size={d}", .{ e.name(), e.blocks, e.size }); - - if (e.dir()) |d| { - try out.print(" blocks={d}-{d} size={d}-{d} items={d}-{d} dev={x}", .{ - d.total_blocks, d.shared_blocks, - d.total_size, d.shared_size, - d.total_items, d.shared_items, d.dev - }); - if (d.err) try out.writeAll(" err"); - if (d.suberr) try out.writeAll(" suberr"); - } else if (e.file()) |f| { - if (f.err) try out.writeAll(" err"); - if (f.excluded) try out.writeAll(" excluded"); - if (f.other_fs) try out.writeAll(" other_fs"); - if (f.kernfs) try out.writeAll(" kernfs"); - if (f.notreg) try out.writeAll(" notreg"); - } else if (e.link()) |l| { - try out.print(" ino={x} nlinks={d}", .{ l.ino, l.nlink }); - } - if (e.ext()) |ext| - try out.print(" mtime={d} uid={d} gid={d} mode={o}", .{ ext.mtime, ext.uid, ext.gid, ext.mode }); - - try out.writeByte('\n'); - if (e.dir()) |d| { - var s = d.sub; - while (s) |sub| { - try writeTree(out, sub, indent+4); - s = sub.next; - } - } -} - fn version() noreturn { std.io.getStdOut().writer().writeAll("ncdu " ++ program_version ++ "\n") catch {}; std.process.exit(0); @@ -218,7 +183,7 @@ pub fn main() anyerror!void { } if (opt.is("-h") or opt.is("-?") or opt.is("--help")) help() else if(opt.is("-v") or opt.is("-V") or opt.is("--version")) version() - else if(opt.is("-q")) config.update_delay = 2000 + else if(opt.is("-q")) config.update_delay = 2*std.time.ns_per_s else if(opt.is("-x")) config.same_fs = true else if(opt.is("-e")) config.extended = true else if(opt.is("-r") and config.read_only) config.can_shell = false @@ -244,19 +209,35 @@ pub fn main() anyerror!void { if (std.builtin.os.tag != .linux and config.exclude_kernfs) ui.die("The --exclude-kernfs tag is currently only supported on Linux.\n", .{}); + event_delay_timer = try std.time.Timer.start(); + try scan.scanRoot(scan_dir orelse "."); try browser.open(model.Parents{}); ui.init(); defer ui.deinit(); - try browser.draw(); + // TODO: Handle OOM errors + // TODO: Confirm quit + while (state != .quit) try handleEvent(true, false); +} + +var event_delay_timer: std.time.Timer = undefined; - _ = ui.c.getch(); +// Draw the screen and handle the next input event. +// In non-blocking mode, screen drawing is rate-limited to keep this function fast. +pub fn handleEvent(block: bool, force_draw: bool) !void { + if (block or force_draw or event_delay_timer.read() > config.update_delay) { + _ = ui.c.erase(); + try browser.draw(); + _ = ui.c.refresh(); + event_delay_timer.reset(); + } - //var out = std.io.bufferedWriter(std.io.getStdOut().writer()); - //try writeTree(out.writer(), &model.root.entry, 0); - //try out.flush(); + var ch = ui.getch(block); + if (ch == 0) return; + if (ch == -1) return handleEvent(block, true); + try browser.key(ch); } diff --git a/src/ui.zig b/src/ui.zig index a685624b082f8ab94e3ae355ce7d57df41283ae3..02f7e6a0c5c68caf13e6cc97b371836e975ecd4f 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -279,6 +279,7 @@ pub fn init() void { updateSize(); _ = c.cbreak(); _ = c.noecho(); + _ = c.nonl(); _ = c.curs_set(0); _ = c.keypad(c.stdscr, true); @@ -376,3 +377,29 @@ pub fn addnum(bg: Bg, v: u64) void { pub fn hline(ch: c.chtype, len: u32) void { _ = c.hline(ch, @intCast(i32, len)); } + +// Returns 0 if no key was pressed in non-blocking mode. +// Returns -1 if it was KEY_RESIZE, requiring a redraw of the screen. +pub fn getch(block: bool) i32 { + _ = c.nodelay(c.stdscr, !block); + // getch() has a bad tendency to not set a sensible errno when it returns ERR. + // In non-blocking mode, we can only assume that ERR means "no input yet". + // In blocking mode, give it 100 tries with a 10ms delay in between, + // then just give up and die to avoid an infinite loop and unresponsive program. + var attempts: u8 = 0; + while (attempts < 100) : (attempts += 1) { + var ch = c.getch(); + if (ch == c.KEY_RESIZE) { + updateSize(); + return -1; + } + if (ch == c.ERR) { + if (!block) return 0; + std.os.nanosleep(0, 10*std.time.ns_per_ms); + continue; + } + return ch; + } + die("Error reading keyboard input, assuming TTY has been lost.\n(Potentially nonsensical error message: {s})\n", + .{ c.strerror(std.c.getErrno(-1)) }); +}