Implement directory refresh

This complicated the scan code more than I had anticipated and has a
few inherent bugs with respect to calculating shared hardlink sizes.

Still, the merge approach avoids creating a full copy of the subtree, so
that's another memory usage related win compared to the C version.
On the other hand, it does leak memory if nodes can't be reused.

Not quite as well tested as I should have, so I'm sure there's bugs.
This commit is contained in:
Yorhel 2021-07-13 13:33:38 +02:00
parent ff3e3bccc6
commit 6c2ab5001c
7 changed files with 370 additions and 88 deletions

View file

@ -29,7 +29,6 @@ backported to the C version, depending on how viable a proper Zig release is.
Missing features:
- Help window
- Directory refresh
- File deletion
- Opening a shell
@ -43,6 +42,7 @@ Already implemented:
- Using separate structs for directory, file and hard link nodes, each storing
only the information necessary for that particular type of node.
- Using an arena allocator and getting rid of data alignment.
- Refreshing a directory no longer creates a full copy of the (sub)tree.
- Improved performance of hard link counting (fixing
[#121](https://code.blicky.net/yorhel/ncdu/issues/121)).
- Add support for separate counting hard links that are shared with other
@ -70,12 +70,14 @@ Aside from this implementation being unfinished:
the in-memory directory tree.
- Not nearly as well tested.
- Directories that could not be opened are displayed as files.
- The disk usage of directory entries themselves is not updated during refresh.
### Minor UI differences
Not sure if these count as improvements or regressions, so I'll just list these
separately:
- The browsing UI is not visible during refresh.
- 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

View file

@ -411,25 +411,25 @@ directory, as some inodes may still be accessible from hard links outside it.
=head1 BUGS
Directory hard links are not supported. They will not be detected as being hard
links, and will thus be scanned and counted multiple times.
Directory hard links and firmlinks (MacOS) are not supported. They will not be
detected as being hard links, and may thus be scanned and counted multiple
times.
Some minor glitches may appear when displaying filenames that contain multibyte
or multicolumn characters.
The unique and shared directory sizes are calculated based on the assumption
that the link count of hard links does not change during a filesystem scan or
in between refreshes. If it does, for example after deleting a hard link, then
these numbers will be very much incorrect and a full refresh by restarting ncdu
is needed to get correct numbers again.
All sizes are internally represented as a signed 64bit integer. If you have a
directory larger than 8 EiB minus one byte, ncdu will clip its size to 8 EiB
minus one byte. When deleting items in a directory with a clipped size, the
resulting sizes will be incorrect.
Item counts are stored in a signed 32-bit integer without overflow detection.
If you have a directory with more than 2 billion files, quite literally
anything can happen.
On macOS 10.15 and later, running ncdu on the root directory without
`--exclude-firmlinks` may cause directories to be scanned and counted multiple
times. Firmlink cycles are currently (1.15.1) not detected, so it may also
cause ncdu to get stuck in an infinite loop and eventually run out of memory.
minus one byte. When deleting or refreshing items in a directory with a clipped
size, the resulting sizes will be incorrect. Likewise, item counts are stored
in a 32-bit integer, so will be incorrect in the unlikely event that you happen
to have more than 4 billion items in a directory.
Please report any other bugs you may find at the bug tracker, which can be
found on the web site at https://dev.yorhel.nl/ncdu

View file

@ -1,6 +1,7 @@
const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
const scan = @import("scan.zig");
const ui = @import("ui.zig");
const c = @cImport(@cInclude("time.h"));
usingnamespace @import("util.zig");
@ -664,6 +665,14 @@ pub fn keyInput(ch: i32) void {
switch (ch) {
'q' => if (main.config.confirm_quit) { state = .quit; } else ui.quit(),
'i' => info.set(dir_items.items[cursor_idx], .info),
'r' => {
if (main.config.imported) {
// TODO: Display message
} else {
main.state = .refresh;
scan.setupRefresh(dir_parents.copy());
}
},
// Sort & filter settings
'n' => sortToggle(.name, .asc),

View file

@ -30,6 +30,8 @@ var allocator_state = std.mem.Allocator{
.resizeFn = wrapResize,
};
pub const allocator = &allocator_state;
//var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
//pub const allocator = &general_purpose_allocator.allocator;
pub const config = struct {
pub const SortCol = enum { name, blocks, size, items, mtime };
@ -65,7 +67,7 @@ pub const config = struct {
pub var confirm_quit: bool = false;
};
pub var state: enum { scan, browse } = .browse;
pub var state: enum { scan, browse, refresh } = .scan;
// 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.:
@ -257,7 +259,6 @@ pub fn main() void {
event_delay_timer = std.time.Timer.start() catch unreachable;
defer ui.deinit();
state = .scan;
var out_file = if (export_file) |f| (
if (std.mem.eql(u8, f, "-")) std.io.getStdOut()
@ -265,9 +266,11 @@ pub fn main() void {
catch |e| ui.die("Error opening export file: {s}.\n", .{ui.errorString(e)})
) else null;
if (import_file) |f| scan.importRoot(f, out_file)
else scan.scanRoot(scan_dir orelse ".", out_file)
catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
if (import_file) |f| {
scan.importRoot(f, out_file);
config.imported = true;
} else scan.scanRoot(scan_dir orelse ".", out_file)
catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
if (out_file != null) return;
config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
@ -275,7 +278,14 @@ pub fn main() void {
state = .browse;
browser.loadDir();
while (true) handleEvent(true, false);
while (true) {
if (state == .refresh) {
scan.scan();
state = .browse;
browser.loadDir();
} else
handleEvent(true, false);
}
}
var event_delay_timer: std.time.Timer = undefined;
@ -286,7 +296,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
if (block or force_draw or event_delay_timer.read() > config.update_delay) {
if (ui.inited) _ = ui.c.erase();
switch (state) {
.scan => scan.draw(),
.scan, .refresh => scan.draw(),
.browse => browser.draw(),
}
if (ui.inited) _ = ui.c.refresh();
@ -303,7 +313,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
if (ch == 0) return;
if (ch == -1) return handleEvent(firstblock, true);
switch (state) {
.scan => scan.keyInput(ch),
.scan, .refresh => scan.keyInput(ch),
.browse => browser.keyInput(ch),
}
firstblock = false;

View file

@ -13,6 +13,9 @@ var allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator);
pub const EType = packed enum(u2) { dir, link, file };
// Type for the Entry.blocks field. Smaller than a u64 to make room for flags.
pub const Blocks = u60;
// Memory layout:
// Dir + name (+ alignment + Ext)
// or: Link + name (+ alignment + Ext)
@ -31,7 +34,8 @@ pub const EType = packed enum(u2) { dir, link, file };
pub const Entry = packed struct {
etype: EType,
isext: bool,
blocks: u61, // 512-byte blocks
counted: bool, // Whether or not this entry's size has been counted in its parents
blocks: Blocks, // 512-byte blocks
size: u64,
next: ?*Entry,
@ -107,7 +111,10 @@ pub const Entry = packed struct {
}
}
fn addStats(self: *Entry, parents: *const Parents) void {
pub fn addStats(self: *Entry, parents: *const Parents) void {
if (self.counted) return;
self.counted = true;
const dev = parents.top().dev;
// Set if this is the first time we've found this hardlink in the bottom-most directory of the given dev.
// Means we should count it for other-dev parent dirs, too.
@ -154,6 +161,64 @@ pub const Entry = packed struct {
}
}
// Opposite of addStats(), but has some limitations:
// - shared_* parent sizes are not updated; there's just no way to
// correctly adjust these without a full rescan of the tree
// - If addStats() saturated adding sizes, then the sizes after delStats()
// will be incorrect.
// - mtime of parents is not adjusted (but that's a feature, possibly?)
//
// The first point can be relaxed so that a delStats() followed by
// addStats() with the same data will not result in broken shared_*
// numbers, but for now the easy (and more efficient) approach is to try
// and avoid using delStats() when not strictly necessary.
//
// This function assumes that, for directories, all sub-entries have
// already been un-counted.
pub fn delStats(self: *Entry, parents: *const Parents) void {
if (!self.counted) return;
self.counted = false;
const dev = parents.top().dev;
var del_hl = false;
var it = parents.iter();
while(it.next()) |p| {
var del_total = false;
p.items = saturateSub(p.items, 1);
if (self.etype == .link and dev != p.dev) {
del_total = del_hl;
} else if (self.link()) |l| {
const n = devices.HardlinkNode{ .ino = l.ino, .dir = p };
var dp = devices.list.items[dev].hardlinks.getEntry(n);
if (dp) |d| {
d.value_ptr.* -= 1;
del_total = d.value_ptr.* == 0;
del_hl = del_total;
if (del_total)
_ = devices.list.items[dev].hardlinks.remove(n);
}
} else
del_total = true;
if(del_total) {
p.entry.size = saturateSub(p.entry.size, self.size);
p.entry.blocks = saturateSub(p.entry.blocks, self.blocks);
}
}
}
pub fn delStatsRec(self: *Entry, parents: *Parents) void {
if (self.dir()) |d| {
parents.push(d);
var it = d.sub;
while (it) |e| : (it = e.next)
e.delStatsRec(parents);
parents.pop();
}
self.delStats(parents);
}
// Insert this entry into the tree at the given directory, updating parent sizes and item counts.
pub fn insert(self: *Entry, parents: *const Parents) void {
self.next = parents.top().sub;
@ -220,6 +285,14 @@ pub const File = packed struct {
_pad: u3,
name: u8,
pub fn resetFlags(f: *@This()) void {
f.err = false;
f.excluded = false;
f.other_fs = false;
f.kernfs = false;
f.notreg = false;
}
};
pub const Ext = packed struct {

View file

@ -9,7 +9,7 @@ const c_fnmatch = @cImport(@cInclude("fnmatch.h"));
// Concise stat struct for fields we're interested in, with the types used by the model.
const Stat = struct {
blocks: u61 = 0,
blocks: model.Blocks = 0,
size: u64 = 0,
dev: u64 = 0,
ino: u64 = 0,
@ -100,6 +100,155 @@ fn writeJsonString(wr: anytype, s: []const u8) !void {
try wr.writeByte('"');
}
// A ScanDir represents an in-memory directory listing (i.e. model.Dir) where
// entries read from disk can be merged into, without doing an O(1) lookup for
// each entry.
const ScanDir = struct {
// Lookup table for name -> *entry.
// null is never stored in the table, but instead used pass a name string
// as out-of-band argument for lookups.
entries: Map,
const Map = std.HashMap(?*model.Entry, void, HashContext, 80);
const HashContext = struct {
cmp: []const u8 = "",
pub fn hash(self: @This(), v: ?*model.Entry) u64 {
return std.hash.Wyhash.hash(0, if (v) |e| @as([]const u8, e.name()) else self.cmp);
}
pub fn eql(self: @This(), ap: ?*model.Entry, bp: ?*model.Entry) bool {
if (ap == bp) return true;
const a = if (ap) |e| @as([]const u8, e.name()) else self.cmp;
const b = if (bp) |e| @as([]const u8, e.name()) else self.cmp;
return std.mem.eql(u8, a, b);
}
};
const Self = @This();
fn init(parents: *const model.Parents) Self {
var self = Self{ .entries = Map.initContext(main.allocator, HashContext{}) };
var count: Map.Size = 0;
var it = parents.top().sub;
while (it) |e| : (it = e.next) count += 1;
self.entries.ensureCapacity(count) catch unreachable;
it = parents.top().sub;
while (it) |e| : (it = e.next)
self.entries.putAssumeCapacity(e, @as(void,undefined));
return self;
}
fn addSpecial(self: *Self, parents: *model.Parents, name: []const u8, t: Context.Special) void {
var e = blk: {
if (self.entries.getEntryAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name })) |entry| {
// XXX: If the type doesn't match, we could always do an
// in-place conversion to a File entry. That's more efficient,
// but also more code. I don't expect this to happen often.
var e = entry.key_ptr.*.?;
if (e.etype == .file) {
if (e.size > 0 or e.blocks > 0) {
e.delStats(parents);
e.size = 0;
e.blocks = 0;
e.addStats(parents);
}
e.file().?.resetFlags();
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
break :blk e;
} else e.delStatsRec(parents);
}
var e = model.Entry.create(.file, false, name);
e.next = parents.top().sub;
parents.top().sub = e;
e.addStats(parents);
break :blk e;
};
var f = e.file().?;
switch (t) {
.err => e.set_err(parents),
.other_fs => f.other_fs = true,
.kernfs => f.kernfs = true,
.excluded => f.excluded = true,
}
}
fn addStat(self: *Self, parents: *model.Parents, name: []const u8, stat: *Stat) *model.Entry {
const etype = if (stat.dir) model.EType.dir
else if (stat.hlinkc) model.EType.link
else model.EType.file;
var e = blk: {
if (self.entries.getEntryAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name })) |entry| {
// XXX: In-place conversion may also be possible here.
var e = entry.key_ptr.*.?;
// changes of dev/ino affect hard link counting in a way we can't simple merge.
const samedev = if (e.dir()) |d| d.dev == model.devices.getId(stat.dev) else true;
const sameino = if (e.link()) |l| l.ino == stat.ino else true;
if (e.etype == etype and samedev and sameino) {
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
break :blk e;
} else e.delStatsRec(parents);
}
var e = model.Entry.create(etype, main.config.extended, name);
e.next = parents.top().sub;
parents.top().sub = e;
break :blk e;
};
// Ignore the new size/blocks field for directories, as we don't know
// what the original values were without calling delStats() on the
// entire subtree, which, in turn, would break all shared hardlink
// sizes. The current approach may result in incorrect sizes after
// refresh, but I expect the difference to be fairly minor.
if (e.etype != .dir and (e.blocks != stat.blocks or e.size != stat.size)) {
e.delStats(parents);
e.blocks = stat.blocks;
e.size = stat.size;
}
if (e.dir()) |d| d.dev = model.devices.getId(stat.dev);
if (e.file()) |f| {
f.resetFlags();
f.notreg = !stat.dir and !stat.reg;
}
if (e.link()) |l| {
l.ino = stat.ino;
// BUG: shared sizes will be very incorrect if this is different
// from a previous scan. May want to warn the user about that.
l.nlink = stat.nlink;
}
if (e.ext()) |ext| {
if (ext.mtime > stat.ext.mtime)
stat.ext.mtime = ext.mtime;
ext.* = stat.ext;
}
// Assumption: l.link == 0 only happens on import, not refresh.
if (if (e.link()) |l| l.nlink == 0 else false)
model.link_count.add(parents.top().dev, e.link().?.ino)
else
e.addStats(parents);
return e;
}
fn final(self: *Self, parents: *model.Parents) void {
if (self.entries.count() == 0) // optimization for the common case
return;
var it = &parents.top().sub;
while (it.*) |e| {
if (self.entries.contains(e)) {
e.delStatsRec(parents);
it.* = e.next;
} else
it = &e.next;
}
}
fn deinit(self: *Self) void {
self.entries.deinit();
}
};
// Scan/import context. Entries are added in roughly the following way:
//
// ctx.pushPath(name)
@ -113,6 +262,7 @@ fn writeJsonString(wr: anytype, s: []const u8) !void {
const Context = struct {
// When scanning to RAM
parents: ?model.Parents = null,
parent_entries: std.ArrayList(ScanDir) = std.ArrayList(ScanDir).init(main.allocator),
// When scanning to a file
wr: ?*Writer = null,
@ -125,6 +275,7 @@ const Context = struct {
name: [:0]const u8 = undefined,
last_error: ?[:0]u8 = null,
fatal_error: ?anyerror = null,
stat: Stat = undefined,
@ -135,7 +286,7 @@ const Context = struct {
ui.die("Error writing to file: {s}.\n", .{ ui.errorString(e) });
}
fn initFile(out: std.fs.File) Self {
fn initFile(out: std.fs.File) *Self {
var buf = main.allocator.create(Writer) catch unreachable;
errdefer main.allocator.destroy(buf);
buf.* = std.io.bufferedWriter(out.writer());
@ -143,11 +294,17 @@ const Context = struct {
wr.writeAll("[1,2,{\"progname\":\"ncdu\",\"progver\":\"" ++ main.program_version ++ "\",\"timestamp\":") catch |e| writeErr(e);
wr.print("{d}", .{std.time.timestamp()}) catch |e| writeErr(e);
wr.writeByte('}') catch |e| writeErr(e);
return Self{ .wr = buf };
var self = main.allocator.create(Self) catch unreachable;
self.* = .{ .wr = buf };
return self;
}
fn initMem() Self {
return Self{ .parents = model.Parents{} };
// Ownership of p is passed to the object, it will be deallocated on deinit().
fn initMem(p: model.Parents) *Self {
var self = main.allocator.create(Self) catch unreachable;
self.* = .{ .parents = p };
return self;
}
fn final(self: *Self) void {
@ -171,11 +328,15 @@ const Context = struct {
}
fn popPath(self: *Self) void {
self.path.items.len = self.path_indices.items[self.path_indices.items.len-1];
self.path_indices.items.len -= 1;
self.path.items.len = self.path_indices.pop();
if (self.stat.dir) {
if (self.parents) |*p| if (!p.isRoot()) p.pop();
if (self.parents) |*p| {
var d = self.parent_entries.pop();
d.final(p);
d.deinit();
if (!p.isRoot()) p.pop();
}
if (self.wr) |w| w.writer().writeByte(']') catch |e| writeErr(e);
} else
self.stat.dir = true; // repeated popPath()s mean we're closing parent dirs.
@ -218,18 +379,9 @@ const Context = struct {
self.last_error = main.allocator.dupeZ(u8, self.path.items) catch unreachable;
}
if (self.parents) |*p| {
var e = model.Entry.create(.file, false, self.name);
e.insert(p);
var f = e.file().?;
switch (t) {
.err => e.set_err(p),
.other_fs => f.other_fs = true,
.kernfs => f.kernfs = true,
.excluded => f.excluded = true,
}
} else if (self.wr) |wr|
if (self.parents) |*p|
self.parent_entries.items[self.parent_entries.items.len-1].addSpecial(p, self.name, t)
else if (self.wr) |wr|
self.writeSpecial(wr.writer(), t) catch |e| writeErr(e);
self.items_seen += 1;
@ -254,25 +406,21 @@ const Context = struct {
// Insert current path as a counted file/dir/hardlink, with information from self.stat
fn addStat(self: *Self, dir_dev: u64) void {
if (self.parents) |*p| {
const etype = if (self.stat.dir) model.EType.dir
else if (self.stat.hlinkc) model.EType.link
else model.EType.file;
var e = model.Entry.create(etype, main.config.extended, self.name);
e.blocks = self.stat.blocks;
e.size = self.stat.size;
if (e.dir()) |d| d.dev = model.devices.getId(self.stat.dev);
if (e.file()) |f| f.notreg = !self.stat.dir and !self.stat.reg;
if (e.link()) |l| {
l.ino = self.stat.ino;
l.nlink = self.stat.nlink;
}
if (e.ext()) |ext| ext.* = self.stat.ext;
var e = if (self.items_seen == 0) blk: {
// Root entry
var e = model.Entry.create(.dir, main.config.extended, self.name);
e.blocks = self.stat.blocks;
e.size = self.stat.size;
if (e.ext()) |ext| ext.* = self.stat.ext;
model.root = e.dir().?;
model.root.dev = model.devices.getId(self.stat.dev);
break :blk e;
} else
self.parent_entries.items[self.parent_entries.items.len-1].addStat(p, self.name, &self.stat);
if (self.items_seen == 0)
model.root = e.dir().?
else {
e.insert(p);
if (e.dir()) |d| p.push(d); // Enter the directory
if (e.dir()) |d| { // Enter the directory
if (self.items_seen != 0) p.push(d);
self.parent_entries.append(ScanDir.init(p)) catch unreachable;
}
} else if (self.wr) |wr|
@ -287,11 +435,13 @@ const Context = struct {
if (self.wr) |p| main.allocator.destroy(p);
self.path.deinit();
self.path_indices.deinit();
self.parent_entries.deinit();
main.allocator.destroy(self);
}
};
// Context that is currently being used for scanning.
var active_context: ?*Context = null;
var active_context: *Context = undefined;
// Read and index entries of the given dir.
fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
@ -378,24 +528,44 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
}
pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
var ctx = if (out) |f| Context.initFile(f) else Context.initMem();
active_context = &ctx;
defer active_context = null;
defer ctx.deinit();
active_context = if (out) |f| Context.initFile(f) else Context.initMem(.{});
const full_path = std.fs.realpathAlloc(main.allocator, path) catch null;
defer if (full_path) |p| main.allocator.free(p);
ctx.pushPath(full_path orelse path);
active_context.pushPath(full_path orelse path);
ctx.stat = try Stat.read(std.fs.cwd(), ctx.pathZ(), true);
if (!ctx.stat.dir) return error.NotDir;
ctx.addStat(0);
active_context.stat = try Stat.read(std.fs.cwd(), active_context.pathZ(), true);
if (!active_context.stat.dir) return error.NotDir;
active_context.addStat(0);
scan();
}
var dir = try std.fs.cwd().openDirZ(ctx.pathZ(), .{ .access_sub_paths = true, .iterate = true });
pub fn setupRefresh(parents: model.Parents) void {
active_context = Context.initMem(parents);
var full_path = std.ArrayList(u8).init(main.allocator);
defer full_path.deinit();
parents.fmtPath(true, &full_path);
active_context.pushPath(full_path.items);
active_context.parent_entries.append(ScanDir.init(&parents)) catch unreachable;
active_context.stat.dir = true;
active_context.stat.dev = model.devices.getDev(parents.top().dev);
active_context.items_seen = 1; // The "root" item has already been added.
}
// To be called after setupRefresh() (or from scanRoot())
pub fn scan() void {
defer active_context.deinit();
var dir = std.fs.cwd().openDirZ(active_context.pathZ(), .{ .access_sub_paths = true, .iterate = true }) catch |e| {
active_context.last_error = main.allocator.dupeZ(u8, active_context.path.items) catch unreachable;
active_context.fatal_error = e;
while (main.state == .refresh or main.state == .scan)
main.handleEvent(true, true);
return;
};
defer dir.close();
scanDir(&ctx, dir, ctx.stat.dev);
ctx.popPath();
ctx.final();
scanDir(active_context, dir, active_context.stat.dev);
active_context.popPath();
active_context.final();
}
// Using a custom recursive descent JSON parser here. std.json is great, but
@ -409,7 +579,7 @@ pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
// worth factoring out the JSON parts into a separate abstraction for which
// tests can be written.
const Import = struct {
ctx: Context,
ctx: *Context,
rd: std.fs.File,
rdoff: usize = 0,
@ -611,7 +781,7 @@ const Import = struct {
},
'd' => {
if (eq(u8, key, "dsize")) {
self.ctx.stat.blocks = @intCast(u61, self.uint(u64)>>9);
self.ctx.stat.blocks = @intCast(model.Blocks, self.uint(u64)>>9);
return;
}
if (eq(u8, key, "dev")) {
@ -794,12 +964,8 @@ pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
catch |e| ui.die("Error reading file: {s}.\n", .{ui.errorString(e)});
defer fd.close();
var imp = Import{
.ctx = if (out) |f| Context.initFile(f) else Context.initMem(),
.rd = fd,
};
active_context = &imp.ctx;
defer active_context = null;
active_context = if (out) |f| Context.initFile(f) else Context.initMem(.{});
var imp = Import{ .ctx = active_context, .rd = fd };
defer imp.ctx.deinit();
imp.root();
imp.ctx.final();
@ -808,9 +974,26 @@ pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
var animation_pos: u32 = 0;
var need_confirm_quit = false;
fn drawError(err: anyerror) void {
const width = saturateSub(ui.cols, 5);
const box = ui.Box.create(7, width, "Scan error");
box.move(2, 2);
ui.addstr("Path: ");
ui.addstr(ui.shorten(ui.toUtf8(active_context.last_error.?), saturateSub(width, 10)));
box.move(3, 2);
ui.addstr("Error: ");
ui.addstr(ui.shorten(ui.errorString(err), saturateSub(width, 6)));
box.move(5, saturateSub(width, 27));
ui.addstr("Press any key to continue");
}
fn drawBox() void {
ui.init();
const ctx = active_context.?;
const ctx = active_context;
if (ctx.fatal_error) |err| return drawError(err);
const width = saturateSub(ui.cols, 5);
const box = ui.Box.create(10, width, "Scanning...");
box.move(2, 2);
@ -878,14 +1061,14 @@ pub fn draw() void {
.line => {
var buf: [256]u8 = undefined;
var line: []const u8 = undefined;
if (active_context.?.parents == null) {
if (active_context.parents == null) {
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8",
.{ ui.shorten(active_context.?.pathZ(), 63), active_context.?.items_seen }
.{ 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 }
.{ ui.shorten(active_context.pathZ(), 51), active_context.items_seen, r.num(), r.unit }
) catch return;
}
_ = std.io.getStdErr().write(line) catch {};
@ -895,6 +1078,11 @@ pub fn draw() void {
}
pub fn keyInput(ch: i32) void {
if (active_context.fatal_error != null) {
if (main.state == .scan) ui.quit()
else main.state = .browse;
return;
}
if (need_confirm_quit) {
switch (ch) {
'y', 'Y' => if (need_confirm_quit) ui.quit(),

View file

@ -50,7 +50,7 @@ pub fn oom() void {
// Lazy strerror() for Zig file I/O, not complete.
// (Would be nicer if Zig just exposed errno so I could call strerror() directly)
pub fn errorString(e: anyerror) []const u8 {
pub fn errorString(e: anyerror) [:0]const u8 {
return switch (e) {
error.DiskQuota => "Disk quota exceeded",
error.FileTooBig => "File too big",