Skip to content
Snippets Groups Projects
Commit b0e81ea4 authored by Yorhel's avatar Yorhel
Browse files

Implement scanning UI (-0,-1,-2)

parent 9b59d3da
No related branches found
No related tags found
No related merge requests found
......@@ -30,11 +30,11 @@ Missing features:
- Export/import
- Most directory listing settings
- Scaning UI
- Lots of informational UI windows
- Directory refresh
- File deletion
- Opening a shell
- OOM handling
### Improvements compared to the C version
......@@ -54,6 +54,7 @@ Already implemented:
(Implemented in the data model, but not displayed in the UI yet)
- Faster --exclude-kernfs thanks to `statfs()` caching.
- Improved handling of Unicode and special characters.
- Remembers item position when switching directories.
Potentially to be implemented:
......
......@@ -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 => {
......
......@@ -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),
}
}
......
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 => {},
}
}
......@@ -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 {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment