mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-12 17:08:39 -09:00
Add item info window
Doesn't display the item's path anymore (seems rather redundant) but adds a few more other fields.
This commit is contained in:
parent
d910ed8b9f
commit
618972b82b
4 changed files with 240 additions and 45 deletions
15
README.md
15
README.md
|
|
@ -28,7 +28,8 @@ backported to the C version, depending on how viable a proper Zig release is.
|
||||||
|
|
||||||
Missing features:
|
Missing features:
|
||||||
|
|
||||||
- Lots of informational UI windows
|
- Listing paths for the same hard link
|
||||||
|
- Help window
|
||||||
- Directory refresh
|
- Directory refresh
|
||||||
- File deletion
|
- File deletion
|
||||||
- Opening a shell
|
- Opening a shell
|
||||||
|
|
@ -70,6 +71,18 @@ Aside from this implementation being unfinished:
|
||||||
- Not nearly as well tested.
|
- Not nearly as well tested.
|
||||||
- Directories that could not be opened are displayed as files.
|
- 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
|
## Requirements
|
||||||
|
|
||||||
- Latest Zig compiler
|
- Latest Zig compiler
|
||||||
|
|
|
||||||
226
src/browser.zig
226
src/browser.zig
|
|
@ -72,8 +72,8 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
|
||||||
const a = ap.?;
|
const a = ap.?;
|
||||||
const b = bp.?;
|
const b = bp.?;
|
||||||
|
|
||||||
if (main.config.sort_dirsfirst and (a.etype == .dir) != (b.etype == .dir))
|
if (main.config.sort_dirsfirst and a.isDirectory() != b.isDirectory())
|
||||||
return a.etype == .dir;
|
return a.isDirectory();
|
||||||
|
|
||||||
switch (main.config.sort_col) {
|
switch (main.config.sort_col) {
|
||||||
.name => {}, // name sorting is the fallback
|
.name => {}, // name sorting is the fallback
|
||||||
|
|
@ -272,24 +272,15 @@ const Row = struct {
|
||||||
defer self.col += 27;
|
defer self.col += 27;
|
||||||
ui.move(self.row, self.col+1);
|
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();
|
const ext = (if (self.item) |e| e.ext() else @as(?*model.Ext, null)) orelse dir_parents.top().entry.ext();
|
||||||
if (ext) |e| {
|
if (ext) |e| ui.addts(self.bg, e.mtime)
|
||||||
const t = castClamp(c.time_t, e.mtime);
|
else ui.addstr(" no 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 {
|
fn name(self: *Self) void {
|
||||||
ui.move(self.row, self.col);
|
ui.move(self.row, self.col);
|
||||||
if (self.item) |i| {
|
if (self.item) |i| {
|
||||||
self.bg.fg(if (i.etype == .dir) .dir else .default);
|
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)));
|
ui.addstr(ui.shorten(ui.toUtf8(i.name()), saturateSub(ui.cols, self.col + 1)));
|
||||||
} else {
|
} else {
|
||||||
self.bg.fg(.dir);
|
self.bg.fg(.dir);
|
||||||
|
|
@ -312,21 +303,160 @@ const Row = struct {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var need_confirm_quit = false;
|
var state: enum { main, quit, info } = .main;
|
||||||
|
|
||||||
fn drawQuit() void {
|
const quit = struct {
|
||||||
const box = ui.Box.create(4, 22, "Confirm quit");
|
fn draw() void {
|
||||||
box.move(2, 2);
|
const box = ui.Box.create(4, 22, "Confirm quit");
|
||||||
ui.addstr("Really quit? (");
|
box.move(2, 2);
|
||||||
ui.style(.key);
|
ui.addstr("Really quit? (");
|
||||||
ui.addch('y');
|
ui.style(.key);
|
||||||
ui.style(.default);
|
ui.addch('y');
|
||||||
ui.addch('/');
|
ui.style(.default);
|
||||||
ui.style(.key);
|
ui.addch('/');
|
||||||
ui.addch('N');
|
ui.style(.key);
|
||||||
ui.style(.default);
|
ui.addch('N');
|
||||||
ui.addch(')');
|
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 {
|
pub fn draw() void {
|
||||||
ui.style(.hd);
|
ui.style(.hd);
|
||||||
|
|
@ -391,7 +521,11 @@ pub fn draw() void {
|
||||||
ui.addstr(" Items: ");
|
ui.addstr(" Items: ");
|
||||||
ui.addnum(.hd, dir_parents.top().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);
|
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();
|
sortDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keyInput(ch: i32) void {
|
fn keyInputSelection(ch: i32) bool {
|
||||||
if (need_confirm_quit) {
|
|
||||||
switch (ch) {
|
|
||||||
'y', 'Y' => if (need_confirm_quit) ui.quit(),
|
|
||||||
else => need_confirm_quit = false,
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
defer current_view.save();
|
|
||||||
|
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
'q' => if (main.config.confirm_quit) { need_confirm_quit = true; } else ui.quit(),
|
|
||||||
|
|
||||||
// Selection
|
|
||||||
'j', ui.c.KEY_DOWN => {
|
'j', ui.c.KEY_DOWN => {
|
||||||
if (cursor_idx+1 < dir_items.items.len) cursor_idx += 1;
|
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_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_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)),
|
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
|
// Sort & filter settings
|
||||||
'n' => sortToggle(.name, .asc),
|
'n' => sortToggle(.name, .asc),
|
||||||
|
|
@ -490,6 +628,6 @@ pub fn keyInput(ch: i32) void {
|
||||||
.unique => .off,
|
.unique => .off,
|
||||||
},
|
},
|
||||||
|
|
||||||
else => {}
|
else => _ = keyInputSelection(ch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ pub const Entry = packed struct {
|
||||||
return if (self.etype == .file) @ptrCast(*File, self) else null;
|
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 {
|
fn nameOffset(etype: EType) usize {
|
||||||
return switch (etype) {
|
return switch (etype) {
|
||||||
.dir => @byteOffsetOf(Dir, "name"),
|
.dir => @byteOffsetOf(Dir, "name"),
|
||||||
|
|
|
||||||
38
src/ui.zig
38
src/ui.zig
|
|
@ -8,6 +8,7 @@ pub const c = @cImport({
|
||||||
@cInclude("stdio.h");
|
@cInclude("stdio.h");
|
||||||
@cInclude("string.h");
|
@cInclude("string.h");
|
||||||
@cInclude("curses.h");
|
@cInclude("curses.h");
|
||||||
|
@cInclude("time.h");
|
||||||
@cDefine("_X_OPEN_SOURCE", "1");
|
@cDefine("_X_OPEN_SOURCE", "1");
|
||||||
@cInclude("wchar.h");
|
@cInclude("wchar.h");
|
||||||
@cInclude("locale.h");
|
@cInclude("locale.h");
|
||||||
|
|
@ -448,6 +449,43 @@ pub fn addnum(bg: Bg, v: u64) void {
|
||||||
bg.fg(.default);
|
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 {
|
pub fn hline(ch: c.chtype, len: u32) void {
|
||||||
_ = c.hline(ch, @intCast(i32, len));
|
_ = c.hline(ch, @intCast(i32, len));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue