diff --git a/README.md b/README.md index 18da30bea5da90cff4092cb244b350726d0c2725..a3a64431b747d0cb0c44921dc95eb581cdb85cf2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ backported to the C version, depending on how viable a proper Zig release is. Missing features: - File import -- Most directory listing settings - Lots of informational UI windows - Directory refresh - File deletion diff --git a/src/browser.zig b/src/browser.zig index 25db5e81de7d8e8d7f9b811f13d254b28b647c0c..4f450f8cb4a25113920db6dc08613309e4b5bf27 100644 --- a/src/browser.zig +++ b/src/browser.zig @@ -2,6 +2,7 @@ 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. @@ -11,6 +12,9 @@ var dir_parents = model.Parents{}; // (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; @@ -97,7 +101,7 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool { 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); + else std.mem.lessThan(u8, bn, an); } // Should be called when: @@ -118,10 +122,15 @@ fn sortDir() void { // - 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 { @@ -143,7 +152,7 @@ const Row = struct { const Self = @This(); - fn flag(self: *Self) !void { + fn flag(self: *Self) void { defer self.col += 2; const item = self.item orelse return; const ch: u7 = ch: { @@ -165,7 +174,7 @@ const Row = struct { ui.addch(ch); } - fn size(self: *Self) !void { + 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); @@ -173,14 +182,99 @@ const Row = struct { // TODO: shared sizes } - fn name(self: *Self) !void { + 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.divCeil(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) ui.addch(if (i*perblock <= num) '#' else ' '); + } + self.bg.fg(.default); + ui.addch(']'); + } + + fn items(self: *Self) void { + if (!main.config.show_items) return; + defer self.col += 7; + const d = if (self.item) |d| d.dir() orelse return else return; + const n = d.total_items; + ui.move(self.row, self.col); + self.bg.fg(.num); + if (n < 1000) + ui.addprint(" {d:>4}", .{n}) + else if (n < 100_000) + ui.addprint("{d:>6.3}", .{ @intToFloat(f32, n) / 1000 }) + 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); if (self.item) |i| { + 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 + } else { + self.bg.fg(.dir); ui.addstr("/.."); + } } fn draw(self: *Self) !void { @@ -189,8 +283,11 @@ const Row = struct { ui.move(self.row, 0); ui.hline(' ', ui.cols); } - try self.flag(); - try self.size(); + self.flag(); + self.size(); + self.graph(); + self.items(); + self.mtime(); try self.name(); } }; @@ -232,7 +329,14 @@ pub fn draw() !void { ui.hline('-', ui.cols); ui.move(1,3); ui.addch(' '); - ui.addstr(try ui.shorten(try ui.toUtf8(model.root.entry.name()), saturateSub(ui.cols, 5))); + 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); @@ -240,6 +344,7 @@ pub fn draw() !void { if (cursor_idx >= current_view.top + numrows) current_view.top = cursor_idx - numrows + 1; var i: u32 = 0; + var sel_row: u32 = 0; while (i < numrows) : (i += 1) { if (i+current_view.top >= dir_items.items.len) break; var row = Row{ @@ -247,6 +352,7 @@ pub fn draw() !void { .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; try row.draw(); } @@ -262,6 +368,7 @@ pub fn draw() !void { ui.addnum(.hd, dir_parents.top().total_items); if (need_confirm_quit) drawQuit(); + if (sel_row > 0) ui.move(sel_row, 0); } fn sortToggle(col: main.SortCol, default_order: main.SortOrder) void { @@ -343,6 +450,16 @@ pub fn key(ch: i32) !void { } }, + // 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, + }, + else => {} } } diff --git a/src/main.zig b/src/main.zig index b12d31e8589db6e39643c5e00f5336285a60b3fa..b34a59236c6e4c4f46884bb7145ddcde552f687f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,4 @@ -pub const program_version = "2.0"; +pub const program_version = "2.0-dev"; const std = @import("std"); const model = @import("model.zig"); @@ -29,6 +29,9 @@ pub const Config = struct { show_hidden: bool = true, show_blocks: bool = true, + show_items: bool = false, + show_mtime: bool = false, + show_graph: enum { off, graph, percent, both } = .graph, sort_col: SortCol = .blocks, sort_order: SortOrder = .desc, sort_dirsfirst: bool = false, @@ -267,12 +270,16 @@ pub fn handleEvent(block: bool, force_draw: bool) !void { return; } - var ch = ui.getch(block); - if (ch == 0) return; - if (ch == -1) return handleEvent(block, true); - switch (state) { - .scan => try scan.key(ch), - .browse => try browser.key(ch), + var firstblock = block; + while (true) { + var ch = ui.getch(firstblock); + if (ch == 0) return; + if (ch == -1) return handleEvent(firstblock, true); + switch (state) { + .scan => try scan.key(ch), + .browse => try browser.key(ch), + } + firstblock = false; } } diff --git a/src/model.zig b/src/model.zig index 48340dd4b86c13083a39519da3ab93a6187c8833..c5ea1ceab0d4ad122ca48daa04cf3ce418295728 100644 --- a/src/model.zig +++ b/src/model.zig @@ -106,11 +106,14 @@ pub const Entry = packed struct { // Means we should count it for other-dev parent dirs, too. var new_hl = false; - // TODO: Saturating add/substract var it = parents.iter(); while(it.next()) |p| { var add_total = false; + if (self.ext()) |e| + if (p.entry.ext()) |pe| + if (e.mtime > pe.mtime) { pe.mtime = e.mtime; }; + // Hardlink in a subdirectory with a different device, only count it the first time. if (self.link() != null and dev != p.dev) { add_total = new_hl; @@ -204,6 +207,12 @@ pub const Ext = packed struct { mode: u16, }; +comptime { + std.debug.assert(@bitOffsetOf(Dir, "name") % 8 == 0); + std.debug.assert(@bitOffsetOf(Link, "name") % 8 == 0); + std.debug.assert(@bitOffsetOf(File, "name") % 8 == 0); +} + // Hardlink handling: // @@ -350,11 +359,6 @@ pub const Parents = struct { } }; -test "name offsets" { - std.testing.expectEqual(@bitOffsetOf(Dir, "name") % 8, 0); - std.testing.expectEqual(@bitOffsetOf(Link, "name") % 8, 0); - std.testing.expectEqual(@bitOffsetOf(File, "name") % 8, 0); -} test "entry" { var e = Entry.create(.file, false, "hello") catch unreachable; diff --git a/src/scan.zig b/src/scan.zig index 96978ed66f35c6b76089389a0b29ca07d5b39efa..0af71a78ef5d238d9071ae5563e08ceadcc47a29 100644 --- a/src/scan.zig +++ b/src/scan.zig @@ -19,26 +19,6 @@ const Stat = struct { symlink: bool, ext: model.Ext, - // Cast any integer type to the target type, clamping the value to the supported maximum if necessary. - fn castClamp(comptime T: type, x: anytype) T { - // (adapted from std.math.cast) - if (std.math.maxInt(@TypeOf(x)) > std.math.maxInt(T) and x > std.math.maxInt(T)) { - return std.math.maxInt(T); - } else if (std.math.minInt(@TypeOf(x)) < std.math.minInt(T) and x < std.math.minInt(T)) { - return std.math.minInt(T); - } else { - return @intCast(T, x); - } - } - - // Cast any integer type to the target type, truncating if necessary. - fn castTruncate(comptime T: type, x: anytype) T { - const Ti = @typeInfo(T).Int; - const Xi = @typeInfo(@TypeOf(x)).Int; - const nx = if (Xi.signedness != Ti.signedness) @bitCast(std.meta.Int(Ti.signedness, Xi.bits), x) else x; - return if (Xi.bits > Ti.bits) @truncate(T, nx) else nx; - } - fn clamp(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type { return castClamp(std.meta.fieldInfo(T, field).field_type, x); } @@ -176,9 +156,7 @@ const Context = struct { } fn pathZ(self: *Self) [:0]const u8 { - self.path.append(0) catch unreachable; - defer self.path.items.len -= 1; - return self.path.items[0..self.path.items.len-1:0]; + return arrayListBufZ(&self.path) catch unreachable; } // Set a flag to indicate that there was an error listing file entries in the current directory. @@ -266,6 +244,13 @@ const Context = struct { if (self.parents) |p| p.pop(); if (self.wr) |w| try w.writeByte(']'); } + + fn deinit(self: *Self) void { + if (self.last_error) |p| main.allocator.free(p); + if (self.parents) |p| p.deinit(); + self.path.deinit(); + self.path_indices.deinit(); + } }; // Context that is currently being used for scanning. @@ -360,8 +345,10 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) (std.fs.File.Writer.Err pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void { const full_path = std.fs.realpathAlloc(main.allocator, path) catch path; + defer main.allocator.free(full_path); var ctx = Context{}; + defer ctx.deinit(); try ctx.pushPath(full_path); active_context = &ctx; defer active_context = null; @@ -388,7 +375,8 @@ pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void { if (model.root.entry.ext()) |ext| ext.* = ctx.stat.ext; } - const dir = try std.fs.cwd().openDirZ(ctx.pathZ(), .{ .access_sub_paths = true, .iterate = true }); + var dir = try std.fs.cwd().openDirZ(ctx.pathZ(), .{ .access_sub_paths = true, .iterate = true }); + defer dir.close(); try scanDir(&ctx, dir, ctx.stat.dev); if (out != null) { try ctx.leaveDir(); diff --git a/src/ui.zig b/src/ui.zig index d8721883ea53a5850c0d46bab9d5a7153177f4c2..c56e7be5f38a83561e7c85c9c4303aacdc86ed1a 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -65,7 +65,7 @@ pub fn toUtf8(in: [:0]const u8) ![:0]const u8 { try to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]}); i += 1; } - return try to_utf8_buf.toOwnedSliceSentinel(0); + return try arrayListBufZ(&to_utf8_buf); } var shorten_buf = std.ArrayList(u8).init(main.allocator); @@ -115,7 +115,7 @@ pub fn shorten(in: [:0]const u8, max_width: u32) ![:0] const u8 { break; } } - return try shorten_buf.toOwnedSliceSentinel(0); + return try arrayListBufZ(&shorten_buf); } fn shortenTest(in: [:0]const u8, max_width: u32, out: [:0]const u8) void { @@ -325,6 +325,13 @@ pub fn addstr(s: [:0]const u8) void { _ = c.addstr(s); } +// Not to be used for strings that may end up >256 bytes. +pub fn addprint(comptime fmt: []const u8, args: anytype) void { + var buf: [256:0]u8 = undefined; + const s = std.fmt.bufPrintZ(&buf, fmt, args) catch unreachable; + addstr(s); +} + pub fn addch(ch: c.chtype) void { _ = c.addch(ch); } diff --git a/src/util.zig b/src/util.zig index 3b6986ad8666b362fbe7525c84af0484c7a619d3..9b73de57351232738841fb262e43f41415548464 100644 --- a/src/util.zig +++ b/src/util.zig @@ -10,7 +10,36 @@ pub fn saturateSub(a: anytype, b: @TypeOf(a)) @TypeOf(a) { return std.math.sub(@TypeOf(a), a, b) catch std.math.minInt(@TypeOf(a)); } +// Cast any integer type to the target type, clamping the value to the supported maximum if necessary. +pub fn castClamp(comptime T: type, x: anytype) T { + // (adapted from std.math.cast) + if (std.math.maxInt(@TypeOf(x)) > std.math.maxInt(T) and x > std.math.maxInt(T)) { + return std.math.maxInt(T); + } else if (std.math.minInt(@TypeOf(x)) < std.math.minInt(T) and x < std.math.minInt(T)) { + return std.math.minInt(T); + } else { + return @intCast(T, x); + } +} + +// Cast any integer type to the target type, truncating if necessary. +pub fn castTruncate(comptime T: type, x: anytype) T { + const Ti = @typeInfo(T).Int; + const Xi = @typeInfo(@TypeOf(x)).Int; + const nx = if (Xi.signedness != Ti.signedness) @bitCast(std.meta.Int(Ti.signedness, Xi.bits), x) else x; + return if (Xi.bits > Ti.bits) @truncate(T, nx) else nx; +} + // Multiplies by 512, saturating. pub fn blocksToSize(b: u64) u64 { return if (b & 0xFF80000000000000 > 0) std.math.maxInt(u64) else b << 9; } + +// Ensure the given arraylist buffer gets zero-terminated and returns a slice +// into the buffer. The returned buffer is invalidated whenever the arraylist +// is freed or written to. +pub fn arrayListBufZ(buf: *std.ArrayList(u8)) ![:0]const u8 { + try buf.append(0); + defer buf.items.len -= 1; + return buf.items[0..buf.items.len-1:0]; +}