summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2021-05-09 20:58:17 +0200
committerYorhel <git@yorhel.nl>2021-05-09 20:59:09 +0200
commitb0e81ea4e9cb79c86d87c157deae3f6457acc349 (patch)
treebfab6a080d2c60da239e403e889e9e1bb92557dd /src
parent9b59d3dac4c9aa6e11349b7d09e148af98275a5e (diff)
Implement scanning UI (-0,-1,-2)
Diffstat (limited to 'src')
-rw-r--r--src/browser.zig2
-rw-r--r--src/main.zig50
-rw-r--r--src/scan.zig131
-rw-r--r--src/ui.zig135
4 files changed, 260 insertions, 58 deletions
diff --git a/src/browser.zig b/src/browser.zig
index 571018d..697187f 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -257,7 +257,7 @@ pub fn key(ch: i32) !void {
defer current_view.save();
switch (ch) {
- 'q' => main.state = .quit,
+ 'q' => ui.quit(), // TODO: Confirm quit
// Selection
'j', ui.c.KEY_DOWN => {
diff --git a/src/main.zig b/src/main.zig
index f409fc5..44f06f4 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -21,6 +21,7 @@ pub const Config = struct {
exclude_patterns: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(allocator),
update_delay: u64 = 100*std.time.ns_per_ms,
+ scan_ui: enum { none, line, full } = .full,
si: bool = false,
nc_tty: bool = false,
ui_color: enum { off, dark } = .off,
@@ -39,7 +40,7 @@ pub const Config = struct {
pub var config = Config{};
-pub var state: enum { browse, quit } = .browse;
+pub var state: enum { scan, browse } = .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.:
@@ -173,6 +174,9 @@ pub fn main() anyerror!void {
var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init());
var scan_dir: ?[]const u8 = null;
+ var import_file: ?[]const u8 = null;
+ var export_file: ?[]const u8 = null;
+ var has_scan_ui = false;
_ = args.next(); // program name
while (args.next()) |opt| {
if (!opt.opt) {
@@ -188,6 +192,11 @@ pub fn main() anyerror!void {
else if(opt.is("-e")) config.extended = true
else if(opt.is("-r") and config.read_only) config.can_shell = false
else if(opt.is("-r")) config.read_only = true
+ else if(opt.is("-0")) { has_scan_ui = true; config.scan_ui = .none; }
+ else if(opt.is("-1")) { has_scan_ui = true; config.scan_ui = .line; }
+ else if(opt.is("-2")) { has_scan_ui = true; config.scan_ui = .full; }
+ else if(opt.is("-o")) export_file = args.arg()
+ else if(opt.is("-f")) import_file = args.arg()
else if(opt.is("--si")) config.si = true
else if(opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true
else if(opt.is("--exclude")) try config.exclude_patterns.append(args.arg())
@@ -203,23 +212,34 @@ pub fn main() anyerror!void {
else if (std.mem.eql(u8, val, "dark")) config.ui_color = .dark
else ui.die("Unknown --color option: {s}.\n", .{val});
} else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
- // TODO: -o, -f, -0, -1, -2
}
if (std.builtin.os.tag != .linux and config.exclude_kernfs)
ui.die("The --exclude-kernfs tag is currently only supported on Linux.\n", .{});
+ const is_out_tty = std.io.getStdOut().isTty();
+ if (!has_scan_ui) {
+ if (export_file) |f| {
+ if (!is_out_tty or std.mem.eql(u8, f, "-")) config.scan_ui = .none
+ else config.scan_ui = .line;
+ }
+ }
+ if (!is_out_tty and (export_file == null or config.scan_ui != .none))
+ ui.die("Standard output is not a TTY, can't initialize ncurses UI.\n", .{});
+
event_delay_timer = try std.time.Timer.start();
+ defer ui.deinit();
+ state = .scan;
try scan.scanRoot(scan_dir orelse ".");
- try browser.loadDir();
+ config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
ui.init();
- defer ui.deinit();
+ state = .browse;
+ try browser.loadDir();
// TODO: Handle OOM errors
- // TODO: Confirm quit
- while (state != .quit) try handleEvent(true, false);
+ while (true) try handleEvent(true, false);
}
var event_delay_timer: std.time.Timer = undefined;
@@ -228,16 +248,26 @@ var event_delay_timer: std.time.Timer = undefined;
// 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();
+ if (ui.inited) _ = ui.c.erase();
+ switch (state) {
+ .scan => try scan.draw(),
+ .browse => try browser.draw(),
+ }
+ if (ui.inited) _ = ui.c.refresh();
event_delay_timer.reset();
}
+ if (!ui.inited) {
+ std.debug.assert(!block);
+ return;
+ }
var ch = ui.getch(block);
if (ch == 0) return;
if (ch == -1) return handleEvent(block, true);
- try browser.key(ch);
+ switch (state) {
+ .scan => try scan.key(ch),
+ .browse => try browser.key(ch),
+ }
}
diff --git a/src/scan.zig b/src/scan.zig
index d640150..04af037 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -1,6 +1,8 @@
const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
+const ui = @import("ui.zig");
+usingnamespace @import("util.zig");
const c_statfs = @cImport(@cInclude("sys/vfs.h"));
const c_fnmatch = @cImport(@cInclude("fnmatch.h"));
@@ -98,11 +100,14 @@ const Context = struct {
parents: model.Parents = .{},
path: std.ArrayList(u8) = std.ArrayList(u8).init(main.allocator),
path_indices: std.ArrayList(usize) = std.ArrayList(usize).init(main.allocator),
+ items_seen: u32 = 1,
// 0-terminated name of the top entry, points into 'path', invalid after popPath().
// This is a workaround to Zig's directory iterator not returning a [:0]const u8.
name: [:0]const u8 = undefined,
+ last_error: ?[:0]u8 = null,
+
const Self = @This();
fn pushPath(self: *Self, name: []const u8) !void {
@@ -120,8 +125,27 @@ const Context = struct {
self.path.items.len = self.path_indices.items[self.path_indices.items.len-1];
self.path_indices.items.len -= 1;
}
+
+ 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];
+ }
+
+ // Insert the current path as an error entry
+ fn setError(self: *Self) !void {
+ var e = try model.Entry.create(.file, false, self.name);
+ e.insert(&self.parents) catch unreachable;
+ e.set_err(&self.parents);
+
+ if (self.last_error) |p| main.allocator.free(p);
+ self.last_error = try main.allocator.dupeZ(u8, self.path.items);
+ }
};
+// Context that is currently being used for scanning.
+var active_context: ?*Context = null;
+
// Read and index entries of the given dir. The entry for the directory is already assumed to be in 'ctx.parents'.
// (TODO: shouldn't error on OOM but instead call a function that waits or something)
fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
@@ -131,16 +155,16 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
ctx.parents.top().entry.set_err(&ctx.parents);
return;
} orelse break;
+ ctx.items_seen += 1;
try ctx.pushPath(entry.name);
+ try main.handleEvent(false, false);
defer ctx.popPath();
// XXX: This algorithm is extremely slow, can be optimized with some clever pattern parsing.
const excluded = blk: {
for (main.config.exclude_patterns.items) |pat| {
- ctx.path.append(0) catch unreachable;
- var path = ctx.path.items[0..ctx.path.items.len-1:0];
- ctx.path.items.len -= 1;
+ var path = ctx.pathZ();
while (path.len > 0) {
if (c_fnmatch.fnmatch(pat, path, 0) == 0) break :blk true;
if (std.mem.indexOfScalar(u8, path, '/')) |idx| path = path[idx+1..:0]
@@ -157,16 +181,12 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
}
var stat = Stat.read(dir, ctx.name, false) catch {
- var e = try model.Entry.create(.file, false, entry.name);
- e.insert(&ctx.parents) catch unreachable;
- e.set_err(&ctx.parents);
+ try ctx.setError();
continue;
};
if (main.config.same_fs and stat.dev != model.getDev(ctx.parents.top().dev)) {
- var e = try model.Entry.create(.file, false, entry.name);
- e.file().?.other_fs = true;
- e.insert(&ctx.parents) catch unreachable;
+ try ctx.setError();
continue;
}
@@ -184,9 +204,7 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
var edir =
if (stat.dir) dir.openDirZ(ctx.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch {
- var e = try model.Entry.create(.file, false, entry.name);
- e.insert(&ctx.parents) catch unreachable;
- e.set_err(&ctx.parents);
+ try ctx.setError();
continue;
} else null;
defer if (edir != null) edir.?.close();
@@ -248,5 +266,94 @@ pub fn scanRoot(path: []const u8) !void {
var ctx = Context{};
try ctx.pushPath(full_path);
const dir = try std.fs.cwd().openDirZ(model.root.entry.name(), .{ .access_sub_paths = true, .iterate = true });
+
+ active_context = &ctx;
+ defer active_context = null;
try scanDir(&ctx, dir);
}
+
+var animation_pos: u32 = 0;
+
+fn drawBox() !void {
+ ui.init();
+ const ctx = active_context.?;
+ const width = saturateSub(ui.cols, 5);
+ const box = ui.Box.create(10, width, "Scanning...");
+ box.move(2, 2);
+ ui.addstr("Total items: ");
+ ui.addnum(.default, ctx.items_seen);
+
+ if (width > 48 and true) { // TODO: When not exporting to file
+ box.move(2, 30);
+ ui.addstr("size: ");
+ ui.addsize(.default, blocksToSize(model.root.entry.blocks));
+ }
+
+ box.move(3, 2);
+ ui.addstr("Current item: ");
+ ui.addstr(try ui.shorten(try ui.toUtf8(ctx.pathZ()), saturateSub(width, 18)));
+
+ if (ctx.last_error) |path| {
+ box.move(5, 2);
+ ui.style(.bold);
+ ui.addstr("Warning: ");
+ ui.style(.default);
+ ui.addstr("error scanning ");
+ ui.addstr(try ui.shorten(try ui.toUtf8(path), saturateSub(width, 28)));
+ box.move(6, 3);
+ ui.addstr("some directory sizes may not be correct.");
+ }
+
+ box.move(8, saturateSub(width, 18));
+ ui.addstr("Press ");
+ ui.style(.key);
+ ui.addch('q');
+ ui.style(.default);
+ ui.addstr(" to abort");
+
+ if (main.config.update_delay < std.time.ns_per_s and width > 40) {
+ const txt = "Scanning...";
+ animation_pos += 1;
+ if (animation_pos >= txt.len*2) animation_pos = 0;
+ if (animation_pos < txt.len) {
+ var i: u32 = 0;
+ box.move(8, 2);
+ while (i <= animation_pos) : (i += 1) ui.addch(txt[i]);
+ } else {
+ var i: u32 = txt.len-1;
+ while (i > animation_pos-txt.len) : (i -= 1) {
+ box.move(8, 2+i);
+ ui.addch(txt[i]);
+ }
+ }
+ }
+}
+
+pub fn draw() !void {
+ switch (main.config.scan_ui) {
+ .none => {},
+ .line => {
+ var buf: [256]u8 = undefined;
+ var line: []const u8 = undefined;
+ if (false) { // TODO: When exporting to file; no total size known
+ line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8",
+ .{ ui.shorten(active_context.?.pathZ(), 63), active_context.?.items_seen }
+ ) catch return;
+ } else {
+ const r = ui.FmtSize.fmt(blocksToSize(model.root.entry.blocks));
+ line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <51} {d:>9} files / {s}{s}\x1b8",
+ .{ ui.shorten(active_context.?.pathZ(), 51), active_context.?.items_seen, r.num(), r.unit }
+ ) catch return;
+ }
+ _ = std.io.getStdErr().write(line) catch {};
+ },
+ .full => try drawBox(),
+ }
+}
+
+pub fn key(ch: i32) !void {
+ switch (ch) {
+ 'q' => ui.quit(), // TODO: Confirm quit
+ else => {},
+ }
+}
diff --git a/src/ui.zig b/src/ui.zig
index 02f7e6a..30b4657 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -2,6 +2,7 @@
const std = @import("std");
const main = @import("main.zig");
+usingnamespace @import("util.zig");
pub const c = @cImport({
@cInclude("stdio.h");
@@ -12,7 +13,7 @@ pub const c = @cImport({
@cInclude("locale.h");
});
-var inited: bool = false;
+pub var inited: bool = false;
pub var rows: u32 = undefined;
pub var cols: u32 = undefined;
@@ -23,6 +24,11 @@ pub fn die(comptime fmt: []const u8, args: anytype) noreturn {
std.process.exit(1);
}
+pub fn quit() noreturn {
+ deinit();
+ std.process.exit(0);
+}
+
var to_utf8_buf = std.ArrayList(u8).init(main.allocator);
fn toUtf8BadChar(ch: u8) bool {
@@ -141,13 +147,6 @@ extern fn ncdu_acs_lrcorner() c.chtype;
extern fn ncdu_acs_hline() c.chtype;
extern fn ncdu_acs_vline() c.chtype;
-pub fn acs_ulcorner() c.chtype { return ncdu_acs_ulcorner(); }
-pub fn acs_llcorner() c.chtype { return ncdu_acs_llcorner(); }
-pub fn acs_urcorner() c.chtype { return ncdu_acs_urcorner(); }
-pub fn acs_lrcorner() c.chtype { return ncdu_acs_lrcorner(); }
-pub fn acs_hline() c.chtype { return ncdu_acs_hline() ; }
-pub fn acs_vline() c.chtype { return ncdu_acs_vline() ; }
-
const StyleAttr = struct { fg: i16, bg: i16, attr: u32 };
const StyleDef = struct {
name: []const u8,
@@ -165,6 +164,9 @@ const styles = [_]StyleDef{
.{ .name = "default",
.off = .{ .fg = -1, .bg = -1, .attr = 0 },
.dark = .{ .fg = -1, .bg = -1, .attr = 0 } },
+ .{ .name = "bold",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
+ .dark = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD } },
.{ .name = "box_title",
.off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
.dark = .{ .fg = c.COLOR_BLUE, .bg = -1, .attr = c.A_BOLD } },
@@ -266,6 +268,9 @@ fn updateSize() void {
pub fn init() void {
if (inited) return;
+ // Send a "clear from cursor to end of screen" instruction, to clear a
+ // potential line left behind from scanning in -1 mode.
+ _ = std.io.getStdErr().write("\x1b[J") catch {};
if (main.config.nc_tty) {
var tty = c.fopen("/dev/tty", "r+");
if (tty == null) die("Error opening /dev/tty: {s}.\n", .{ c.strerror(std.c.getErrno(-1)) });
@@ -317,37 +322,51 @@ pub fn addch(ch: c.chtype) void {
_ = c.addch(ch);
}
-// Print a human-readable size string, formatted into the given bavkground.
-// Takes 8 columns in SI mode, 9 otherwise.
-// "###.# XB"
-// "###.# XiB"
-pub fn addsize(bg: Bg, v: u64) void {
- var f = @intToFloat(f32, v);
- var unit: [:0]const u8 = undefined;
- if (main.config.si) {
- if(f < 1000.0) { unit = " B"; }
- else if(f < 1e6) { unit = " KB"; f /= 1e3; }
- else if(f < 1e9) { unit = " MB"; f /= 1e6; }
- else if(f < 1e12) { unit = " GB"; f /= 1e9; }
- else if(f < 1e15) { unit = " TB"; f /= 1e12; }
- else if(f < 1e18) { unit = " PB"; f /= 1e15; }
- else { unit = " EB"; f /= 1e18; }
+// Format an integer to a human-readable size string.
+// num() = "###.#"
+// unit = " XB" or " XiB"
+// Concatenated, these take 8 columns in SI mode or 9 otherwise.
+pub const FmtSize = struct {
+ buf: [8:0]u8,
+ unit: [:0]const u8,
+
+ pub fn fmt(v: u64) @This() {
+ var r: @This() = undefined;
+ var f = @intToFloat(f32, v);
+ if (main.config.si) {
+ if(f < 1000.0) { r.unit = " B"; }
+ else if(f < 1e6) { r.unit = " KB"; f /= 1e3; }
+ else if(f < 1e9) { r.unit = " MB"; f /= 1e6; }
+ else if(f < 1e12) { r.unit = " GB"; f /= 1e9; }
+ else if(f < 1e15) { r.unit = " TB"; f /= 1e12; }
+ else if(f < 1e18) { r.unit = " PB"; f /= 1e15; }
+ else { r.unit = " EB"; f /= 1e18; }
+ }
+ else {
+ if(f < 1000.0) { r.unit = " B"; }
+ else if(f < 1023e3) { r.unit = " KiB"; f /= 1024.0; }
+ else if(f < 1023e6) { r.unit = " MiB"; f /= 1048576.0; }
+ else if(f < 1023e9) { r.unit = " GiB"; f /= 1073741824.0; }
+ else if(f < 1023e12) { r.unit = " TiB"; f /= 1099511627776.0; }
+ else if(f < 1023e15) { r.unit = " PiB"; f /= 1125899906842624.0; }
+ else { r.unit = " EiB"; f /= 1152921504606846976.0; }
+ }
+ _ = std.fmt.bufPrintZ(&r.buf, "{d:>5.1}", .{f}) catch unreachable;
+ return r;
}
- else {
- if(f < 1000.0) { unit = " B"; }
- else if(f < 1023e3) { unit = " KiB"; f /= 1024.0; }
- else if(f < 1023e6) { unit = " MiB"; f /= 1048576.0; }
- else if(f < 1023e9) { unit = " GiB"; f /= 1073741824.0; }
- else if(f < 1023e12) { unit = " TiB"; f /= 1099511627776.0; }
- else if(f < 1023e15) { unit = " PiB"; f /= 1125899906842624.0; }
- else { unit = " EiB"; f /= 1152921504606846976.0; }
+
+ pub fn num(self: *const @This()) [:0]const u8 {
+ return std.mem.spanZ(&self.buf);
}
- var buf: [8:0]u8 = undefined;
- _ = std.fmt.bufPrintZ(&buf, "{d:>5.1}", .{f}) catch unreachable;
+};
+
+// Print a formatted human-readable size string onto the given background.
+pub fn addsize(bg: Bg, v: u64) void {
+ const r = FmtSize.fmt(v);
bg.fg(.num);
- addstr(&buf);
+ addstr(r.num());
bg.fg(.default);
- addstr(unit);
+ addstr(r.unit);
}
// Print a full decimal number with thousand separators.
@@ -378,6 +397,52 @@ pub fn hline(ch: c.chtype, len: u32) void {
_ = c.hline(ch, @intCast(i32, len));
}
+// Draws a bordered box in the center of the screen.
+pub const Box = struct {
+ start_row: u32,
+ start_col: u32,
+
+ const Self = @This();
+
+ pub fn create(height: u32, width: u32, title: [:0]const u8) Self {
+ const s = Self{
+ .start_row = saturateSub(rows>>1, height>>1),
+ .start_col = saturateSub(cols>>1, width>>1),
+ };
+ style(.default);
+ if (width < 6 or height < 3) return s;
+
+ const ulcorner = ncdu_acs_ulcorner();
+ const llcorner = ncdu_acs_llcorner();
+ const urcorner = ncdu_acs_urcorner();
+ const lrcorner = ncdu_acs_lrcorner();
+ const acs_hline = ncdu_acs_hline();
+ const acs_vline = ncdu_acs_vline();
+
+ var i: u32 = 0;
+ while (i < height) : (i += 1) {
+ s.move(i, 0);
+ addch(if (i == 0) ulcorner else if (i == height-1) llcorner else acs_hline);
+ hline(if (i == 0 or i == height-1) acs_vline else ' ', width-2);
+ s.move(i, width-1);
+ addch(if (i == 0) urcorner else if (i == height-1) lrcorner else acs_hline);
+ }
+
+ s.move(0, 3);
+ style(.box_title);
+ addch(' ');
+ addstr(title);
+ addch(' ');
+ style(.default);
+ return s;
+ }
+
+ // Move the global cursor to the given coordinates inside the box.
+ pub fn move(s: Self, row: u32, col: u32) void {
+ ui.move(s.start_row + row, s.start_col + col);
+ }
+};
+
// 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 {