diff --git a/README.md b/README.md index 5401904f0be9c7457fb6d6317d4471b643f8a972..678f195350667a479757bdb95a62c45cc47aa614 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 7fc04f5d487b91d1716ab420bef1abd6ea0111f2..c61dcdcdfa416d411e32564657fc542da4ddb0cd 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 2cbcafb1b4367503b4f24425cd818899d01c820a..91c4cc63fad15dd8bfb1f906f271f48a8328f16e 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 1063405aef98faae00a7c15b92b63da708c59320..71afcd1c5c227a37d6b22209ecbc30ca2547673d 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 47c2636208499073a20ec97c0655535bd0ae2559..ccc7657d36c1d7f138dd4b926072466c3f7f3681 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 f653737a0ed6930bcc513f2beb01ee1f75e2d987..4ac7c5f2cdf90fe885c0964e618d577c36babc88 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 66cad66fd854e5c63e0a6454ef029b95d5b2cf8a..628cefe4310453e70872ba3e7b4789c415138d15 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",