diff options
author | Yorhel <git@yorhel.nl> | 2021-06-11 13:05:39 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2021-06-11 13:12:00 +0200 |
commit | 618972b82bfb08c30b21366491494b79f1e2ad37 (patch) | |
tree | d9e0a2a7264b2de024e11b9b1dcf011c39584976 | |
parent | d910ed8b9f2442db3453b59b490c7c528483b21a (diff) |
Add item info window
Doesn't display the item's path anymore (seems rather redundant) but
adds a few more other fields.
-rw-r--r-- | README.md | 15 | ||||
-rw-r--r-- | src/browser.zig | 226 | ||||
-rw-r--r-- | src/model.zig | 6 | ||||
-rw-r--r-- | src/ui.zig | 38 |
4 files changed, 240 insertions, 45 deletions
@@ -28,7 +28,8 @@ backported to the C version, depending on how viable a proper Zig release is. Missing features: -- Lots of informational UI windows +- Listing paths for the same hard link +- Help window - Directory refresh - File deletion - Opening a shell @@ -70,6 +71,18 @@ Aside from this implementation being unfinished: - Not nearly as well tested. - Directories that could not be opened are displayed as files. +### Minor UI differences + +Not sure if these count as improvements or regressions, so I'll just list these +separately: + +- Some columns in the file browser are hidden automatically if the terminal is + not wide enough to display them. +- Browsing keys other than changing the currently selected item don't work + anymore while the info window is being displayed. +- The file's path is not displayed in the item window anymore (it's redundant). +- The item window's height is dynamic based on its contents. + ## Requirements - Latest Zig compiler diff --git a/src/browser.zig b/src/browser.zig index e5de3a7..e5f1f84 100644 --- a/src/browser.zig +++ b/src/browser.zig @@ -72,8 +72,8 @@ 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; + if (main.config.sort_dirsfirst and a.isDirectory() != b.isDirectory()) + return a.isDirectory(); switch (main.config.sort_col) { .name => {}, // name sorting is the fallback @@ -272,24 +272,15 @@ const Row = struct { 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"); + if (ext) |e| ui.addts(self.bg, e.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.addch(if (i.isDirectory()) '/' else ' '); ui.addstr(ui.shorten(ui.toUtf8(i.name()), saturateSub(ui.cols, self.col + 1))); } else { self.bg.fg(.dir); @@ -312,21 +303,160 @@ const Row = struct { } }; -var need_confirm_quit = false; +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 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(')'); -} + fn keyInput(ch: i32) void { + switch (ch) { + 'y', 'Y' => ui.quit(), + else => state = .main, + } + } +}; + +const info = struct { + // TODO: List of paths for the same hardlink. + + fn drawSizeRow(box: *const 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: *const 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 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. Not really sure how to handle yet. + const rows = 5 // border + padding + close message + + 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; + + // Name + box.move(row, 3); + ui.style(.bold); + ui.addstr("Name: "); + ui.style(.default); + ui.addstr(ui.shorten(ui.toUtf8(e.name()), cols-11)); + row += 1; + + // Type / Mode+UID+GID + box.move(row, 3); + 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"); + } + row += 1; + + // Last modified + if (e.ext()) |ext| { + box.move(row, 3); + ui.style(.bold); + ui.addstr("Last modified: "); + ui.addts(.default, ext.mtime); + row += 1; + } + + // Disk usage & Apparent size + 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); + + // Number of items + if (e.dir()) |d| { + box.move(row, 3); + ui.style(.bold); + ui.addstr(" Sub items: "); + ui.addnum(.default, d.items); + row += 1; + } + + // Number of links + inode (dev?) + if (e.link()) |l| { + box.move(row, 3); + ui.style(.bold); + ui.addstr(" Link count: "); + ui.addnum(.default, l.nlink); + box.move(row, 23); + ui.style(.bold); + ui.addstr(" Inode: "); + ui.style(.default); + var buf: [32]u8 = undefined; + ui.addstr(std.fmt.bufPrintZ(&buf, "{}", .{ l.ino }) catch unreachable); + row += 1; + } + + // "Press i to close this window" + box.move(row+1, cols-30); + ui.style(.default); + ui.addstr("Press "); + ui.style(.key); + ui.addch('i'); + ui.style(.default); + ui.addstr(" to close this window"); + } + + fn keyInput(ch: i32) void { + if (keyInputSelection(ch)) { + if (dir_items.items[cursor_idx] == null) state = .main; + return; + } + switch (ch) { + 'i', 'q' => state = .main, + else => {}, + } + } +}; pub fn draw() void { ui.style(.hd); @@ -391,7 +521,11 @@ pub fn draw() void { ui.addstr(" Items: "); ui.addnum(.hd, dir_parents.top().items); - if (need_confirm_quit) drawQuit(); + switch (state) { + .main => {}, + .quit => quit.draw(), + .info => info.draw(), + } if (sel_row > 0) ui.move(sel_row, 0); } @@ -403,21 +537,8 @@ fn sortToggle(col: main.config.SortCol, default_order: main.config.SortOrder) vo sortDir(); } -pub fn keyInput(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(); - +fn keyInputSelection(ch: i32) bool { switch (ch) { - 'q' => if (main.config.confirm_quit) { need_confirm_quit = true; } else ui.quit(), - - // Selection 'j', ui.c.KEY_DOWN => { if (cursor_idx+1 < dir_items.items.len) cursor_idx += 1; }, @@ -428,6 +549,23 @@ pub fn keyInput(ch: i32) void { 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)), + else => return false, + } + return true; +} + +pub fn keyInput(ch: i32) void { + switch (state) { + .main => {}, // fallthrough + .quit => return quit.keyInput(ch), + .info => return info.keyInput(ch), + } + + defer current_view.save(); + + switch (ch) { + 'q' => if (main.config.confirm_quit) { state = .quit; } else ui.quit(), + 'i' => if (dir_items.items[cursor_idx] != null) { state = .info; }, // Sort & filter settings 'n' => sortToggle(.name, .asc), @@ -490,6 +628,6 @@ pub fn keyInput(ch: i32) void { .unique => .off, }, - else => {} + else => _ = keyInputSelection(ch), } } diff --git a/src/model.zig b/src/model.zig index ef2bd8e..26ca984 100644 --- a/src/model.zig +++ b/src/model.zig @@ -49,6 +49,12 @@ pub const Entry = packed struct { return if (self.etype == .file) @ptrCast(*File, self) else null; } + // Whether this entry should be displayed as a "directory". + // Some dirs are actually represented in this data model as a File for efficiency. + pub fn isDirectory(self: *Self) bool { + return if (self.file()) |f| f.other_fs or f.kernfs else self.etype == .dir; + } + fn nameOffset(etype: EType) usize { return switch (etype) { .dir => @byteOffsetOf(Dir, "name"), @@ -8,6 +8,7 @@ pub const c = @cImport({ @cInclude("stdio.h"); @cInclude("string.h"); @cInclude("curses.h"); + @cInclude("time.h"); @cDefine("_X_OPEN_SOURCE", "1"); @cInclude("wchar.h"); @cInclude("locale.h"); @@ -448,6 +449,43 @@ pub fn addnum(bg: Bg, v: u64) void { bg.fg(.default); } +// Print a file mode, takes 10 columns +pub fn addmode(mode: u32) void { + addch(switch (mode & std.os.S_IFMT) { + std.os.S_IFDIR => 'd', + std.os.S_IFREG => '-', + std.os.S_IFLNK => 'l', + std.os.S_IFIFO => 'p', + std.os.S_IFSOCK => 's', + std.os.S_IFCHR => 'c', + std.os.S_IFBLK => 'b', + else => '?' + }); + addch(if (mode & 0o400 > 0) 'r' else '-'); + addch(if (mode & 0o200 > 0) 'w' else '-'); + addch(if (mode & 0o4000 > 0) 's' else if (mode & 0o100 > 0) @as(u7, 'x') else '-'); + addch(if (mode & 0o040 > 0) 'r' else '-'); + addch(if (mode & 0o020 > 0) 'w' else '-'); + addch(if (mode & 0o2000 > 0) 's' else if (mode & 0o010 > 0) @as(u7, 'x') else '-'); + addch(if (mode & 0o004 > 0) 'r' else '-'); + addch(if (mode & 0o002 > 0) 'w' else '-'); + addch(if (mode & 0o1000 > 0) (if (std.os.S_ISDIR(mode)) @as(u7, 't') else 'T') else if (mode & 0o001 > 0) @as(u7, 'x') else '-'); +} + +// Print a timestamp, takes 25 columns +pub fn addts(bg: Bg, ts: u64) void { + const t = castClamp(c.time_t, ts); + 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) { + bg.fg(.num); + ui.addstr(buf[0..len:0]); + } else { + bg.fg(.default); + ui.addstr(" invalid mtime"); + } +} + pub fn hline(ch: c.chtype, len: u32) void { _ = c.hline(ch, @intCast(i32, len)); } |