summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2021-06-11 13:05:39 +0200
committerYorhel <git@yorhel.nl>2021-06-11 13:12:00 +0200
commit618972b82bfb08c30b21366491494b79f1e2ad37 (patch)
treed9e0a2a7264b2de024e11b9b1dcf011c39584976
parentd910ed8b9f2442db3453b59b490c7c528483b21a (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.md15
-rw-r--r--src/browser.zig226
-rw-r--r--src/model.zig6
-rw-r--r--src/ui.zig38
4 files changed, 240 insertions, 45 deletions
diff --git a/README.md b/README.md
index 93de39f..668c978 100644
--- a/README.md
+++ b/README.md
@@ -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"),
diff --git a/src/ui.zig b/src/ui.zig
index 3c7e0ab..57a09ab 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -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));
}