summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2021-07-13 13:33:38 +0200
committerYorhel <git@yorhel.nl>2021-07-13 13:45:08 +0200
commit6c2ab5001c24150b74bf1acffbc571abc8817232 (patch)
tree8f60f5cecaa42df4dd576dce9bf472593e929251
parentff3e3bccc62c9fd14642ae99787c53b80ad25fd0 (diff)
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.
-rw-r--r--README.md4
-rw-r--r--doc/ncdu.pod26
-rw-r--r--src/browser.zig9
-rw-r--r--src/main.zig26
-rw-r--r--src/model.zig77
-rw-r--r--src/scan.zig316
-rw-r--r--src/ui.zig2
7 files changed, 371 insertions, 89 deletions
diff --git a/README.md b/README.md
index 5401904..678f195 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/doc/ncdu.pod b/doc/ncdu.pod
index 7fc04f5..c61dcdc 100644
--- a/doc/ncdu.pod
+++ b/doc/ncdu.pod
@@ -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
diff --git a/src/browser.zig b/src/browser.zig
index 2cbcafb..91c4cc6 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -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),
diff --git a/src/main.zig b/src/main.zig
index 1063405..71afcd1 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -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;
diff --git a/src/model.zig b/src/model.zig
index 47c2636..ccc7657 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -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 {
diff --git a/src/scan.zig b/src/scan.zig
index f653737..4ac7c5f 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -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;
-
- if (self.items_seen == 0)
- model.root = e.dir().?
- else {
- e.insert(p);
- if (e.dir()) |d| p.push(d); // Enter the directory
+ 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 (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(),
diff --git a/src/ui.zig b/src/ui.zig
index 66cad66..628cefe 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -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",