diff options
-rw-r--r-- | src/browser.zig | 4 | ||||
-rw-r--r-- | src/main.zig | 4 | ||||
-rw-r--r-- | src/scan.zig | 928 |
3 files changed, 486 insertions, 450 deletions
diff --git a/src/browser.zig b/src/browser.zig index 7078569..58538fd 100644 --- a/src/browser.zig +++ b/src/browser.zig @@ -848,10 +848,8 @@ pub fn keyInput(ch: i32) void { 'r' => { if (!main.config.can_refresh.?) message = "Directory refresh feature disabled." - else { + else main.state = .refresh; - scan.setupRefresh(dir_parent); - } }, 'b' => { if (!main.config.can_shell.?) diff --git a/src/main.zig b/src/main.zig index bffe98e..55e7501 100644 --- a/src/main.zig +++ b/src/main.zig @@ -428,7 +428,7 @@ pub fn main() void { } } - var scan_dir: ?[]const u8 = null; + var scan_dir: ?[:0]const u8 = null; var import_file: ?[:0]const u8 = null; var export_file: ?[:0]const u8 = null; { @@ -499,7 +499,7 @@ pub fn main() void { while (true) { switch (state) { .refresh => { - scan.scan(); + scan.refresh(browser.dir_parent); state = .browse; browser.loadDir(null); }, diff --git a/src/scan.zig b/src/scan.zig index baf1281..a8c0977 100644 --- a/src/scan.zig +++ b/src/scan.zig @@ -9,6 +9,15 @@ const util = @import("util.zig"); const c_statfs = @cImport(@cInclude("sys/vfs.h")); const c_fnmatch = @cImport(@cInclude("fnmatch.h")); +var file_writer: ?*FileWriter = null; +var items_seen: u32 = 0; +var last_level: ?*Level = null; +var last_error = std.ArrayList(u8).init(main.allocator); +var fatal_error: ?anyerror = null; + +const FileWriter = std.io.BufferedWriter(4096, std.fs.File.Writer); +const Special = enum { err, other_fs, kernfs, excluded }; + // Concise stat struct for fields we're interested in, with the types used by the model. const Stat = struct { @@ -53,36 +62,9 @@ const Stat = struct { } }; -// This function only works on Linux -fn isKernfs(dir: std.fs.Dir, dev: u64) bool { - const state = struct { - var cache = std.AutoHashMap(u64,bool).init(main.allocator); - var lock = std.Thread.Mutex{}; - }; - state.lock.lock(); - defer state.lock.unlock(); - if (state.cache.get(dev)) |e| return e; - var buf: c_statfs.struct_statfs = undefined; - if (c_statfs.fstatfs(dir.fd, &buf) != 0) return false; // silently ignoring errors isn't too nice. - const iskern = switch (buf.f_type) { - // These numbers are documented in the Linux 'statfs(2)' man page, so I assume they're stable. - 0x42494e4d, // BINFMTFS_MAGIC - 0xcafe4a11, // BPF_FS_MAGIC - 0x27e0eb, // CGROUP_SUPER_MAGIC - 0x63677270, // CGROUP2_SUPER_MAGIC - 0x64626720, // DEBUGFS_MAGIC - 0x1cd1, // DEVPTS_SUPER_MAGIC - 0x9fa0, // PROC_SUPER_MAGIC - 0x6165676c, // PSTOREFS_MAGIC - 0x73636673, // SECURITYFS_MAGIC - 0xf97cff8c, // SELINUX_MAGIC - 0x62656572, // SYSFS_MAGIC - 0x74726163 // TRACEFS_MAGIC - => true, - else => false, - }; - state.cache.put(dev, iskern) catch {}; - return iskern; + +fn writeErr(e: anyerror) noreturn { + ui.die("Error writing to file: {s}.\n", .{ ui.errorString(e) }); } // Output a JSON string. @@ -107,11 +89,44 @@ fn writeJsonString(wr: anytype, s: []const u8) !void { try wr.writeByte('"'); } +fn writeSpecial(w: anytype, name: []const u8, t: Special, isdir: bool) !void { + try w.writeAll(",\n"); + if (isdir) try w.writeByte('['); + try w.writeAll("{\"name\":"); + try writeJsonString(w, name); + switch (t) { + .err => try w.writeAll(",\"read_error\":true"), + .other_fs => try w.writeAll(",\"excluded\":\"othfs\""), + .kernfs => try w.writeAll(",\"excluded\":\"kernfs\""), + .excluded => try w.writeAll(",\"excluded\":\"pattern\""), + } + try w.writeByte('}'); + if (isdir) try w.writeByte(']'); +} + +fn writeStat(w: anytype, name: []const u8, stat: *const Stat, read_error: bool, dir_dev: u64) !void { + try w.writeAll(",\n"); + if (stat.dir) try w.writeByte('['); + try w.writeAll("{\"name\":"); + try writeJsonString(w, name); + if (stat.size > 0) try w.print(",\"asize\":{d}", .{ stat.size }); + if (stat.blocks > 0) try w.print(",\"dsize\":{d}", .{ util.blocksToSize(stat.blocks) }); + if (stat.dir and stat.dev != 0 and stat.dev != dir_dev) try w.print(",\"dev\":{d}", .{ stat.dev }); + if (stat.hlinkc) try w.print(",\"ino\":{d},\"hlnkc\":true,\"nlink\":{d}", .{ stat.ino, stat.nlink }); + if (!stat.dir and !stat.reg) try w.writeAll(",\"notreg\":true"); + if (read_error) try w.writeAll(",\"read_error\":true"); + if (main.config.extended) + try w.print(",\"uid\":{d},\"gid\":{d},\"mode\":{d},\"mtime\":{d}", + .{ stat.ext.uid, stat.ext.gid, stat.ext.mode, stat.ext.mtime }); + try w.writeByte('}'); +} + + // A MemDir 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 MemDir = struct { - dir: *model.Dir, + dir: ?*model.Dir, // Lookup table for name -> *entry. // null is never stored in the table, but instead used pass a name string @@ -136,24 +151,25 @@ const MemDir = struct { const Self = @This(); - fn init(dir: *model.Dir) Self { + fn init(dir: ?*model.Dir) Self { var self = Self{ .dir = dir, .entries = Map.initContext(main.allocator, HashContext{}), }; var count: Map.Size = 0; - var it = dir.sub; + var it = if (dir) |d| d.sub else null; while (it) |e| : (it = e.next) count += 1; self.entries.ensureUnusedCapacity(count) catch unreachable; - it = dir.sub; + it = if (dir) |d| d.sub else null; while (it) |e| : (it = e.next) self.entries.putAssumeCapacity(e, @as(void,undefined)); return self; } - fn addSpecial(self: *Self, name: []const u8, t: Context.Special) void { + fn addSpecial(self: *Self, name: []const u8, t: Special) void { + var dir = self.dir orelse unreachable; // root can't be a Special 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 @@ -162,32 +178,32 @@ const MemDir = struct { var e = entry.key_ptr.*.?; if (e.etype == .file) { if (e.size > 0 or e.blocks > 0) { - e.delStats(self.dir); + e.delStats(dir); e.size = 0; e.blocks = 0; - e.addStats(self.dir, 0); + e.addStats(dir, 0); } e.file().?.resetFlags(); _ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name }); break :blk e; - } else e.delStatsRec(self.dir); + } else e.delStatsRec(dir); } var e = model.Entry.create(.file, false, name); - e.next = self.dir.sub; - self.dir.sub = e; - e.addStats(self.dir, 0); + e.next = dir.sub; + dir.sub = e; + e.addStats(dir, 0); break :blk e; }; var f = e.file().?; switch (t) { - .err => e.setErr(self.dir), + .err => e.setErr(dir), .other_fs => f.other_fs = true, .kernfs => f.kernfs = true, .excluded => f.excluded = true, } } - fn addStat(self: *Self, name: []const u8, stat: *Stat) *model.Entry { + fn addStat(self: *Self, name: []const u8, stat: *const Stat) *model.Entry { const etype = if (stat.dir) model.EType.dir else if (stat.hlinkc) model.EType.link else model.EType.file; @@ -201,11 +217,14 @@ const MemDir = struct { if (e.etype == etype and samedev and sameino) { _ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name }); break :blk e; - } else e.delStatsRec(self.dir); + } else e.delStatsRec(self.dir.?); } var e = model.Entry.create(etype, main.config.extended, name); - e.next = self.dir.sub; - self.dir.sub = e; + if (self.dir) |d| { + e.next = d.sub; + d.sub = e; + } else + model.root = e.dir() orelse unreachable; break :blk e; }; // Ignore the new size/blocks field for directories, as we don't know @@ -214,7 +233,7 @@ const MemDir = struct { // 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.counted) and (e.blocks != stat.blocks or e.size != stat.size)) { - e.delStats(self.dir); + if (self.dir) |d| e.delStats(d); e.blocks = stat.blocks; e.size = stat.size; } @@ -228,22 +247,23 @@ const MemDir = struct { } if (e.link()) |l| l.ino = stat.ino; if (e.ext()) |ext| { - if (ext.mtime > stat.ext.mtime) - stat.ext.mtime = ext.mtime; + const mtime = ext.mtime; ext.* = stat.ext; + if (mtime > ext.mtime) ext.mtime = mtime; } - e.addStats(self.dir, stat.nlink); + if (self.dir) |d| e.addStats(d, stat.nlink); return e; } fn final(self: *Self) void { if (self.entries.count() == 0) // optimization for the common case return; - var it = &self.dir.sub; + var dir = self.dir orelse return; + var it = &dir.sub; while (it.*) |e| { if (self.entries.contains(e)) { - e.delStatsRec(self.dir); + e.delStatsRec(dir); it.* = e.next; } else it = &e.next; @@ -255,206 +275,214 @@ const MemDir = struct { } }; -// Scan/import context. Entries are added in roughly the following way: -// -// ctx.pushPath(name) -// ctx.stat = ..; -// ctx.addSpecial() or ctx.addStat() -// if (ctx.stat.dir) { -// // repeat top-level steps for files in dir, recursively. -// } -// ctx.popPath(); -// -const Context = struct { - // When scanning to RAM - parents: ?std.ArrayList(MemDir) = null, - // When scanning to a file - wr: ?*Writer = null, - path: std.ArrayList(u8) = std.ArrayList(u8).init(main.allocator), - path_indices: std.ArrayList(usize) = std.ArrayList(usize).init(main.allocator), - items_seen: u32 = 0, +// Abstract "directory level" API for processing scan/import results and +// assembling those into an in-memory representation for browsing or to a JSON +// format for exporting. +const Level = struct { + sub: ?*Level = null, + parent: ?*Level = null, + ctx: Ctx, - // 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, - fatal_error: ?anyerror = null, - - stat: Stat = undefined, - - const Writer = std.io.BufferedWriter(4096, std.fs.File.Writer); - const Self = @This(); + const Ctx = union(enum) { + mem: MemDir, + file: File, + }; - fn writeErr(e: anyerror) noreturn { - ui.die("Error writing to file: {s}.\n", .{ ui.errorString(e) }); - } + const File = struct { + // buffer for entries we can output once the sub-levels are finished. + buf: std.ArrayList(u8) = std.ArrayList(u8).init(main.allocator), + dir_dev: u64, + name: []u8, // Separate allocation, only used for reporting + }; - 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()); - var wr = buf.writer(); - 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); - - var self = main.allocator.create(Self) catch unreachable; - self.* = .{ .wr = buf }; - return self; - } + const LevelWriter = std.io.Writer(*Level, FileWriter.Error || error{OutOfMemory}, Level.write); - fn initMem(dir: ?*model.Dir) *Self { - var self = main.allocator.create(Self) catch unreachable; - self.* = .{ .parents = std.ArrayList(MemDir).init(main.allocator) }; - if (dir) |d| self.parents.?.append(MemDir.init(d)) catch unreachable; - return self; - } - - fn final(self: *Self) void { - if (self.parents) |_| { - counting_hardlinks = true; - defer counting_hardlinks = false; - main.handleEvent(false, true); - model.inodes.addAllStats(); - } - if (self.wr) |wr| { - wr.writer().writeByte(']') catch |e| writeErr(e); - wr.flush() catch |e| writeErr(e); + fn write(self: *Level, bytes: []const u8) !usize { + if (self.sub == null) return try file_writer.?.write(bytes); + switch (self.ctx) { + Ctx.mem => unreachable, + Ctx.file => |*f| { + f.buf.appendSlice(bytes) catch unreachable; + return bytes.len; + } } } - // Add the name of the file/dir entry we're currently inspecting - fn pushPath(self: *Self, name: []const u8) void { - self.path_indices.append(self.path.items.len) catch unreachable; - if (self.path.items.len > 1) self.path.append('/') catch unreachable; - const start = self.path.items.len; - self.path.appendSlice(name) catch unreachable; - - self.path.append(0) catch unreachable; - self.name = self.path.items[start..self.path.items.len-1:0]; - self.path.items.len -= 1; + fn writer(self: *Level) LevelWriter { + return .{ .context = self }; } - fn popPath(self: *Self) void { - self.path.items.len = self.path_indices.pop(); - - if (self.stat.dir) { - if (self.parents) |*p| { - if (p.items.len > 0) { - var d = p.pop(); - d.final(); - d.deinit(); + fn fmtPath(self: *Level, out: *std.ArrayList(u8)) void { + switch (self.ctx) { + Ctx.mem => |m| { + if (m.dir) |d| d.fmtPath(true, out) + else out.append('/') catch unreachable; + }, + Ctx.file => |f| { + if (self.parent) |p| { + p.fmtPath(out); + out.append('/') catch unreachable; } - } - 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. + out.appendSlice(f.name) catch unreachable; + }, + } } - fn pathZ(self: *Self) [:0]const u8 { - return util.arrayListBufZ(&self.path); - } + fn addSpecial(self: *Level, name: []const u8, t: Special, isdir: bool) void { + if (t == .err and main.config.scan_ui.? != .none) { + last_error.clearRetainingCapacity(); + self.fmtPath(&last_error); + last_error.append('/') catch unreachable; + last_error.appendSlice(name) catch unreachable; + } - // Set a flag to indicate that there was an error listing file entries in the current directory. - // (Such errors are silently ignored when exporting to a file, as the directory metadata has already been written) - fn setDirlistError(self: *Self) void { - if (self.parents) |*p| p.items[p.items.len-1].dir.entry.setErr(p.items[p.items.len-1].dir); + switch (self.ctx) { + Ctx.mem => |*m| m.addSpecial(name, t), + Ctx.file => writeSpecial(self.writer(), name, t, isdir) catch |e| writeErr(e), + } + items_seen += 1; } - const Special = enum { err, other_fs, kernfs, excluded }; - - fn writeSpecial(self: *Self, w: anytype, t: Special) !void { - try w.writeAll(",\n"); - if (self.stat.dir) try w.writeByte('['); - try w.writeAll("{\"name\":"); - try writeJsonString(w, self.name); - switch (t) { - .err => try w.writeAll(",\"read_error\":true"), - .other_fs => try w.writeAll(",\"excluded\":\"othfs\""), - .kernfs => try w.writeAll(",\"excluded\":\"kernfs\""), - .excluded => try w.writeAll(",\"excluded\":\"pattern\""), + // (can also be used for empty dirs) + fn addFile(self: *Level, name: []const u8, stat: *const Stat, read_error: bool) void { + switch (self.ctx) { + Ctx.mem => |*m| _ = m.addStat(name, stat), + Ctx.file => { + writeStat(self.writer(), name, stat, read_error, 0) catch |e| writeErr(e); + if (stat.dir) self.writer().writeByte(']') catch |e| writeErr(e); + }, } - try w.writeByte('}'); - if (self.stat.dir) try w.writeByte(']'); + items_seen += 1; } - // Insert the current path as a special entry (i.e. a file/dir that is not counted) - // Ignores self.stat except for the 'dir' option. - fn addSpecial(self: *Self, t: Special) void { - std.debug.assert(self.items_seen > 0); // root item can't be a special - - if (t == .err) { - if (self.last_error) |p| main.allocator.free(p); - self.last_error = main.allocator.dupeZ(u8, self.path.items) catch unreachable; + fn addDir(self: *Level, name: []const u8, stat: *const Stat, list_error: bool, sub_lvl: *Level) void { + std.debug.assert(stat.dir); + std.debug.assert(self.sub == null); // We don't support disjoint trees, that would require extra buffering. + switch (self.ctx) { + Ctx.mem => |*m| { + const dir = m.addStat(name, stat).dir() orelse unreachable; + if (list_error) dir.entry.setErr(dir); + sub_lvl.* = .{ .parent = self, .ctx = .{ .mem = MemDir.init(dir) } }; + }, + Ctx.file => |f| { + writeStat(self.writer(), name, stat, list_error, f.dir_dev) catch |e| writeErr(e); + sub_lvl.* = .{ .parent = self, .ctx = .{ .file = .{ + .dir_dev = stat.dev, + .name = main.allocator.dupe(u8, name) catch unreachable, + } } }; + }, } - - if (self.parents) |*p| - p.items[p.items.len-1].addSpecial(self.name, t) - else if (self.wr) |wr| - self.writeSpecial(wr.writer(), t) catch |e| writeErr(e); - - self.stat.dir = false; // So that popPath() doesn't consider this as leaving a dir. - self.items_seen += 1; + self.sub = sub_lvl; + last_level = sub_lvl; + items_seen += 1; } - fn writeStat(self: *Self, w: anytype, dir_dev: u64) !void { - try w.writeAll(",\n"); - if (self.stat.dir) try w.writeByte('['); - try w.writeAll("{\"name\":"); - try writeJsonString(w, self.name); - if (self.stat.size > 0) try w.print(",\"asize\":{d}", .{ self.stat.size }); - if (self.stat.blocks > 0) try w.print(",\"dsize\":{d}", .{ util.blocksToSize(self.stat.blocks) }); - if (self.stat.dir and self.stat.dev != dir_dev) try w.print(",\"dev\":{d}", .{ self.stat.dev }); - if (self.stat.hlinkc) try w.print(",\"ino\":{d},\"hlnkc\":true,\"nlink\":{d}", .{ self.stat.ino, self.stat.nlink }); - if (!self.stat.dir and !self.stat.reg) try w.writeAll(",\"notreg\":true"); - if (main.config.extended) - try w.print(",\"uid\":{d},\"gid\":{d},\"mode\":{d},\"mtime\":{d}", - .{ self.stat.ext.uid, self.stat.ext.gid, self.stat.ext.mode, self.stat.ext.mtime }); - try w.writeByte('}'); + fn close(self: *Level) void { + std.debug.assert(self.sub == null); + switch (self.ctx) { + Ctx.mem => |*m| { + m.final(); + m.deinit(); + }, + Ctx.file => |*f| { + file_writer.?.writer().writeAll(f.buf.items) catch |e| writeErr(e); + file_writer.?.writer().writeByte(']') catch |e| writeErr(e); + f.buf.deinit(); + main.allocator.free(f.name); + }, + } + if (self.parent) |p| { + p.sub = null; + last_level = p; + switch (p.ctx) { + Ctx.file => |*f| { + file_writer.?.writer().writeAll(f.buf.items) catch |e| writeErr(e); + f.buf.clearRetainingCapacity(); + }, + else => {}, + } + } else { + switch (self.ctx) { + Ctx.mem => { + counting_hardlinks = true; + defer counting_hardlinks = false; + main.handleEvent(false, true); + model.inodes.addAllStats(); + }, + Ctx.file => { + var w = file_writer.?; + w.flush() catch |e| writeErr(e); + main.allocator.destroy(w); + }, + } + } + self.* = undefined; } +}; - // 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| { - var e = if (p.items.len == 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 - p.items[p.items.len-1].addStat(self.name, &self.stat); - - if (e.dir()) |d| // Enter the directory - p.append(MemDir.init(d)) catch unreachable; - - } else if (self.wr) |wr| - self.writeStat(wr.writer(), dir_dev) catch |e| writeErr(e); +fn initFile(out: std.fs.File, lvl: *Level) void { + var buf = main.allocator.create(FileWriter) catch unreachable; + errdefer main.allocator.destroy(buf); + buf.* = std.io.bufferedWriter(out.writer()); + var wr = buf.writer(); + 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); + + file_writer = buf; + lvl.* = .{ .ctx = Level.Ctx{ .file = .{ + .dir_dev = 0, + .name = main.allocator.dupe(u8, "") catch unreachable, + } } }; + + last_error.clearRetainingCapacity(); + last_level = lvl; + fatal_error = null; + items_seen = 0; +} - self.items_seen += 1; - } +fn initMem(dir: ?*model.Dir, lvl: *Level) void { + lvl.* = .{ .ctx = Level.Ctx{ .mem = MemDir.init(dir) } }; - fn deinit(self: *Self) void { - if (self.last_error) |p| main.allocator.free(p); - if (self.parents) |*p| { - for (p.items) |*i| i.deinit(); - p.deinit(); - } - if (self.wr) |p| main.allocator.destroy(p); - self.path.deinit(); - self.path_indices.deinit(); - main.allocator.destroy(self); - } -}; + last_error.clearRetainingCapacity(); + last_level = lvl; + fatal_error = null; + items_seen = 0; +} -// Context that is currently being used for scanning. -var active_context: *Context = undefined; +// This function only works on Linux +fn isKernfs(dir: std.fs.Dir, dev: u64) bool { + const state = struct { + var cache = std.AutoHashMap(u64,bool).init(main.allocator); + var lock = std.Thread.Mutex{}; + }; + state.lock.lock(); + defer state.lock.unlock(); + if (state.cache.get(dev)) |e| return e; + var buf: c_statfs.struct_statfs = undefined; + if (c_statfs.fstatfs(dir.fd, &buf) != 0) return false; // silently ignoring errors isn't too nice. + const iskern = switch (buf.f_type) { + // These numbers are documented in the Linux 'statfs(2)' man page, so I assume they're stable. + 0x42494e4d, // BINFMTFS_MAGIC + 0xcafe4a11, // BPF_FS_MAGIC + 0x27e0eb, // CGROUP_SUPER_MAGIC + 0x63677270, // CGROUP2_SUPER_MAGIC + 0x64626720, // DEBUGFS_MAGIC + 0x1cd1, // DEVPTS_SUPER_MAGIC + 0x9fa0, // PROC_SUPER_MAGIC + 0x6165676c, // PSTOREFS_MAGIC + 0x73636673, // SECURITYFS_MAGIC + 0xf97cff8c, // SELINUX_MAGIC + 0x62656572, // SYSFS_MAGIC + 0x74726163 // TRACEFS_MAGIC + => true, + else => false, + }; + state.cache.put(dev, iskern) catch {}; + return iskern; +} // The following filesystem scanning implementation is designed to support // some degree of parallelism while generating a serialized tree without @@ -468,9 +496,10 @@ var active_context: *Context = undefined; // filesystem in the required order and lose some opportunities for // parallelism. This is an attempt at doing the latter. const scanner = struct { - var tail: *Level = undefined; + var tail: *Dir = undefined; + var head: *Dir = undefined; // Currently used to protect both the scan stack state and the output - // Context, may be worth trying to split in two. + // context, may be worth trying to split in two. var lock = std.Thread.Mutex{}; var cond = std.Thread.Condition{}; @@ -492,100 +521,66 @@ const scanner = struct { const SpecialEntry = struct { name: [:0]u8, - t: Context.Special, + t: Special, }; - const NextLevel = struct { + const NextDir = struct { name: [:0]u8, stat: Stat, - level: *Level, // XXX: Only 'dir', 'names', and 'specials' are really relevant here + fd: std.fs.Dir, + names: std.ArrayListUnmanaged([:0]u8) = .{}, + specials: std.ArrayListUnmanaged(SpecialEntry) = .{}, + list_error: bool = false, }; // Represents a directory that is being scanned. - const Level = struct { - dir: std.fs.Dir, + const Dir = struct { + lvl: Level = undefined, + fd: std.fs.Dir, dir_dev: u64, - dirListError: bool = false, names: std.ArrayListUnmanaged([:0]u8) = .{}, // Queue of names to stat() names_busy: u8 = 0, // Number of threads running stat() - files: std.ArrayListUnmanaged(StatEntry) = .{}, // Queue of files we can output - specials: std.ArrayListUnmanaged(SpecialEntry) = .{}, // Queue of "special" items we can output dirs: std.ArrayListUnmanaged(StatEntry) = .{}, // Queue of dirs we can read dirs_busy: u8 = 0, // Number of 'dirs' being processed at the moment - next: std.ArrayListUnmanaged(NextLevel) = .{}, // Queue of subdirs to scan next - sub: ?*Level = null, // Subdirectory currently being scanned - parent: ?*Level, + next: std.ArrayListUnmanaged(NextDir) = .{}, // Queue of subdirs to scan next // Assumption: all queues are empty - fn destroy(lvl: *Level) void { - lvl.dir.close(); - lvl.names.deinit(main.allocator); - lvl.files.deinit(main.allocator); - lvl.specials.deinit(main.allocator); - lvl.dirs.deinit(main.allocator); - lvl.next.deinit(main.allocator); - main.allocator.destroy(lvl); + fn destroy(dir: *Dir) void { + dir.fd.close(); + dir.names.deinit(main.allocator); + dir.dirs.deinit(main.allocator); + dir.next.deinit(main.allocator); + main.allocator.destroy(dir); } }; - // Drain the output queue ('files', 'specials') if we can. - // Assumes we hold the lock. - fn outputQueue(lvl: *Level) void { - if (lvl.sub != null) return; - - if (lvl.dirListError) { - active_context.setDirlistError(); - lvl.dirListError = false; - } - - for (lvl.specials.items) |e| { - active_context.stat.dir = false; - active_context.pushPath(e.name); - active_context.addSpecial(e.t); - active_context.popPath(); - main.allocator.free(e.name); - } - for (lvl.files.items) |e| { - // TODO: ctx API is inefficient here, no need to copy that Stat - active_context.stat.dir = false; - active_context.pushPath(e.name); - active_context.stat = e.stat; - active_context.addStat(lvl.dir_dev); - active_context.popPath(); - main.allocator.free(e.name); - } - lvl.specials.clearRetainingCapacity(); - lvl.files.clearRetainingCapacity(); - } - // Leave the current dir if we're done with it and find a new dir to enter. fn navigate() void { //std.debug.print("ctx={s}, names={} dirs={} next={}\n", .{ active_context.path.items, tail.names.items.len, tail.dirs.items.len, tail.next.items.len }); - // Assumption: outputQueue() has been called on the tail, so - // 'files' and 'specials' are always empty. - while (tail.parent != null and tail.sub == null + while (tail != head and tail.names.items.len == 0 and tail.names_busy == 0 and tail.dirs.items.len == 0 and tail.dirs_busy == 0 and tail.next.items.len == 0 ) { //std.debug.print("Pop\n", .{}); - active_context.popPath(); - const lvl = tail; - lvl.parent.?.sub = null; - tail = lvl.parent.?; - lvl.destroy(); - outputQueue(tail); + const dir = tail; + tail = @fieldParentPtr(Dir, "lvl", dir.lvl.parent.?); + dir.lvl.close(); + dir.destroy(); } - if (tail.sub == null and tail.next.items.len > 0) { - const sub = tail.next.pop(); - //std.debug.print("Push {s}\n", .{sub.name}); - active_context.pushPath(sub.name); - active_context.stat = sub.stat; - active_context.addStat(tail.dir_dev); - main.allocator.free(sub.name); - tail.sub = sub.level; - tail = sub.level; + if (tail.next.items.len > 0) { + var next_sub = tail.next.pop(); + var sub = main.allocator.create(Dir) catch unreachable; + sub.* = .{ + .fd = next_sub.fd, + .dir_dev = next_sub.stat.dev, + .names = next_sub.names, + }; + tail.lvl.addDir(next_sub.name, &next_sub.stat, next_sub.list_error, &sub.lvl); + outputNextDirSpecials(&next_sub, &sub.lvl); + main.allocator.free(next_sub.name); + tail = sub; } // TODO: Only wake up threads when there's enough new work queued, all @@ -593,88 +588,94 @@ const scanner = struct { cond.broadcast(); } - fn readNamesDir(lvl: *Level) void { - var it = lvl.dir.iterate(); + fn readNamesDir(dir: *NextDir) void { + var it = dir.fd.iterate(); while (true) { const entry = it.next() catch { - lvl.dirListError = true; + dir.list_error = true; break; } orelse break; // TODO: Check for exclude patterns - lvl.names.append(main.allocator, main.allocator.dupeZ(u8, entry.name) catch unreachable) catch unreachable; + dir.names.append(main.allocator, main.allocator.dupeZ(u8, entry.name) catch unreachable) catch unreachable; + } + } + + fn outputNextDirSpecials(dir: *NextDir, lvl: *Level) void { + for (dir.specials.items) |e| { + lvl.addSpecial(e.name, e.t, false); + main.allocator.free(e.name); } + dir.specials.deinit(main.allocator); } - fn readNames(parent: *Level) void { + fn readNames(parent: *Dir) void { const stat = parent.dirs.pop(); lock.unlock(); - var dir = parent.dir.openDirZ(stat.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch { + var fd = parent.fd.openDirZ(stat.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch { lock.lock(); - parent.specials.append(main.allocator, .{ .name = stat.name, .t = .err }) catch unreachable; + parent.lvl.addSpecial(stat.name, .err, true); + main.allocator.free(stat.name); return; }; - if (@import("builtin").os.tag == .linux and main.config.exclude_kernfs and isKernfs(dir, stat.stat.dev)) { + if (@import("builtin").os.tag == .linux and main.config.exclude_kernfs and isKernfs(fd, stat.stat.dev)) { lock.lock(); - parent.specials.append(main.allocator, .{ .name = stat.name, .t = .kernfs }) catch unreachable; + parent.lvl.addSpecial(stat.name, .kernfs, true); + main.allocator.free(stat.name); return; } if (main.config.exclude_caches) { - if (dir.openFileZ("CACHEDIR.TAG", .{})) |f| { + if (fd.openFileZ("CACHEDIR.TAG", .{})) |f| { const sig = "Signature: 8a477f597d28d172789f06886806bc55"; var buf: [sig.len]u8 = undefined; if (f.reader().readAll(&buf)) |len| { if (len == sig.len and std.mem.eql(u8, &buf, sig)) { lock.lock(); - parent.specials.append(main.allocator, .{ .name = stat.name, .t = .excluded }) catch unreachable; + parent.lvl.addSpecial(stat.name, .excluded, true); + main.allocator.free(stat.name); return; } } else |_| {} } else |_| {} } - var lvl = main.allocator.create(Level) catch unreachable; - lvl.* = .{ .dir = dir, .dir_dev = stat.stat.dev, .parent = parent }; - readNamesDir(lvl); + var dir = NextDir{ .name = stat.name, .fd = fd, .stat = stat.stat }; + readNamesDir(&dir); lock.lock(); - // Treat empty directories as files - if (lvl.names.items.len == 0 and lvl.specials.items.len == 0) { - if (lvl.dirListError) { // XXX: this loses information about this entry being a directory :( - parent.specials.append(main.allocator, .{ .name = stat.name, .t = .err }) catch unreachable; - } else - parent.files.append(main.allocator, stat) catch unreachable; - dir.close(); - main.allocator.destroy(lvl); + if (dir.names.items.len == 0 and dir.specials.items.len == 0) { + parent.lvl.addFile(stat.name, &stat.stat, dir.list_error); + main.allocator.free(stat.name); + fd.close(); } else { - parent.next.append(main.allocator, .{ .name = stat.name, .stat = stat.stat, .level = lvl }) catch unreachable; + parent.next.append(main.allocator, dir) catch unreachable; } } - fn statNames(lvl: *Level) void { + fn statNames(dir: *Dir) void { var names: [BATCH][:0]u8 = undefined; var stats: [BATCH]Stat = undefined; var errs: [BATCH]bool = undefined; - const len = std.math.min(names.len, lvl.names.items.len); - std.mem.copy([]u8, &names, lvl.names.items[lvl.names.items.len-len..]); - lvl.names.items.len -= len; + const len = std.math.min(names.len, dir.names.items.len); + std.mem.copy([]u8, &names, dir.names.items[dir.names.items.len-len..]); + dir.names.items.len -= len; lock.unlock(); var i: usize = 0; while (i < len) : (i += 1) { - if (Stat.read(lvl.dir, names[i], false)) |s| { + if (Stat.read(dir.fd, names[i], false)) |s| { errs[i] = false; if (main.config.follow_symlinks and s.symlink) { - if (Stat.read(lvl.dir, names[i], true)) |nstat| { + if (Stat.read(dir.fd, names[i], true)) |nstat| { if (!nstat.dir) { stats[i] = nstat; // Symlink targets may reside on different filesystems, // this will break hardlink detection and counting so let's disable it. - if (nstat.hlinkc and nstat.dev != lvl.dir_dev) + if (nstat.hlinkc and nstat.dev != dir.dir_dev) stats[i].hlinkc = false; } } else |_| stats[i] = s; @@ -687,46 +688,57 @@ const scanner = struct { lock.lock(); i = 0; while (i < len) : (i += 1) { - if (errs[i]) - lvl.specials.append(main.allocator, .{ .name = names[i], .t = .err }) catch unreachable - else if (main.config.same_fs and stats[i].dev != lvl.dir_dev) - lvl.specials.append(main.allocator, .{ .name = names[i], .t = .other_fs }) catch unreachable - else if (stats[i].dir) - lvl.dirs.append(main.allocator, .{ .name = names[i], .stat = stats[i] }) catch unreachable - else - lvl.files.append(main.allocator, .{ .name = names[i], .stat = stats[i] }) catch unreachable; + if (errs[i]) { + dir.lvl.addSpecial(names[i], .err, false); + main.allocator.free(names[i]); + } else if (main.config.same_fs and stats[i].dev != dir.dir_dev) { + dir.lvl.addSpecial(names[i], .other_fs, stats[i].dir); + main.allocator.free(names[i]); + } else if (stats[i].dir) { + dir.dirs.append(main.allocator, .{ .name = names[i], .stat = stats[i] }) catch unreachable; + } else { + dir.lvl.addFile(names[i], &stats[i], false); + main.allocator.free(names[i]); + } } } - fn runThread() void { + fn runThread(main_thread: bool) void { lock.lock(); outer: while (true) { - var curlvl: ?*Level = tail; - while (curlvl) |lvl| : (curlvl = lvl.parent) { + if (main_thread and (items_seen & 128) == 0) { + lock.unlock(); + main.handleEvent(false, false); + lock.lock(); + } + + var dir = tail; + while (true) { // If we have subdirectories to read, do that first to keep the 'names' queues filled up. - if (lvl.dirs.items.len > 0 and lvl.dirs_busy + lvl.next.items.len < SUBDIRS_PER_LEVEL) { - lvl.dirs_busy += 1; - readNames(lvl); - lvl.dirs_busy -= 1; - outputQueue(lvl); + if (dir.dirs.items.len > 0 and dir.dirs_busy + dir.next.items.len < SUBDIRS_PER_LEVEL) { + dir.dirs_busy += 1; + readNames(dir); + dir.dirs_busy -= 1; navigate(); continue :outer; } // Then look for names to stat - if (lvl.names.items.len > 0) { - lvl.names_busy += 1; - statNames(lvl); - lvl.names_busy -= 1; - outputQueue(lvl); + if (dir.names.items.len > 0) { + dir.names_busy += 1; + statNames(dir); + dir.names_busy -= 1; navigate(); continue :outer; } + + if (dir == head) break + else dir = @fieldParentPtr(Dir, "lvl", dir.lvl.parent.?); } // If we're here, then we found no work to do. - if (tail.parent == null and tail.dirs_busy == 0 and tail.names_busy == 0) { + if (tail == head and tail.dirs_busy == 0 and tail.names_busy == 0) { cond.broadcast(); // only necessary if we don't always wake up threads when there's work to do. break; } @@ -734,64 +746,71 @@ const scanner = struct { } lock.unlock(); } - // TODO: progress UI - - // Scan the given dir. The active_context is assumed to have been - // initialized already and the entry for the given *dir has already been - // output. - // The given dir is closed when done. - fn scan(dir: std.fs.Dir, dir_dev: u64) void { - tail = main.allocator.create(Level) catch unreachable; - tail.* = .{ .dir = dir, .dir_dev = dir_dev, .parent = null }; - readNamesDir(tail); + + // Open the given path and scan it into *Dir. + fn scan(dir: *Dir, path: [:0]const u8) void { + tail = dir; + head = dir; + dir.fd = std.fs.cwd().openDirZ(path, .{ .access_sub_paths = true, .iterate = true }) catch |e| { + last_error.appendSlice(path) catch unreachable; + fatal_error = e; + while (main.state == .refresh or main.state == .scan) + main.handleEvent(true, true); + return; + }; + + var next_dir = NextDir{ .name = undefined, .stat = undefined, .fd = dir.fd }; + readNamesDir(&next_dir); + outputNextDirSpecials(&next_dir, &dir.lvl); + dir.names = next_dir.names; + var threads = main.allocator.alloc(std.Thread, main.config.parallel-1) catch unreachable; - for (threads) |*t| t.* = std.Thread.spawn(.{ .stack_size = 128*1024 }, runThread, .{}) catch unreachable; - runThread(); + for (threads) |*t| t.* = std.Thread.spawn(.{ .stack_size = 128*1024 }, runThread, .{false}) catch unreachable; + runThread(true); for (threads) |*t| t.join(); - tail.destroy(); + main.allocator.free(threads); + head.lvl.close(); + head.destroy(); + head = undefined; tail = undefined; } }; -pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void { - active_context = if (out) |f| Context.initFile(f) else Context.initMem(null); +pub fn scanRoot(orig_path: [:0]const u8, out: ?std.fs.File) !void { + var lvl: Level = undefined; + if (out) |f| initFile(f, &lvl) else initMem(null, &lvl); - const full_path = std.fs.realpathAlloc(main.allocator, path) catch null; + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const full_path = + if (std.os.realpathZ(orig_path, &buf)) |p| main.allocator.dupeZ(u8, p) catch unreachable + else |_| null; defer if (full_path) |p| main.allocator.free(p); - active_context.pushPath(full_path orelse path); + const path = full_path orelse orig_path; + + const stat = try Stat.read(std.fs.cwd(), path, true); + if (!stat.dir) return error.NotDir; - 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 sub = main.allocator.create(scanner.Dir) catch unreachable; + sub.* = .{ .fd = undefined, .dir_dev = undefined }; + lvl.addDir(path, &stat, false, &sub.lvl); + sub.dir_dev = stat.dev; + scanner.scan(sub, path); + lvl.close(); } -pub fn setupRefresh(parent: *model.Dir) void { - active_context = Context.initMem(parent); +pub fn refresh(parent: *model.Dir) void { var full_path = std.ArrayList(u8).init(main.allocator); defer full_path.deinit(); parent.fmtPath(true, &full_path); - active_context.pushPath(full_path.items); - active_context.stat.dir = true; - active_context.stat.dev = model.devices.list.items[parent.dev]; -} -// 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; - }; - scanner.scan(dir, active_context.stat.dev); - active_context.popPath(); - active_context.final(); + var sub = main.allocator.create(scanner.Dir) catch unreachable; + sub.* = .{ .fd = undefined, .dir_dev = model.devices.list.items[parent.dev] }; + initMem(parent, &sub.lvl); + scanner.scan(sub, util.arrayListBufZ(&full_path)); } + // Using a custom recursive descent JSON parser here. std.json is great, but // has two major downsides: // - It does strict UTF-8 validation. Which is great in general, but not so @@ -803,8 +822,6 @@ pub fn scan() void { // worth factoring out the JSON parts into a separate abstraction for which // tests can be written. const Import = struct { - ctx: *Context, - rd: std.fs.File, rdoff: usize = 0, rdsize: usize = 0, @@ -814,6 +831,8 @@ const Import = struct { byte: u64 = 1, line: u64 = 1, namebuf: [32*1024]u8 = undefined, + statbuf: Stat = undefined, + root_level: Level = undefined, const Self = @This(); @@ -994,22 +1013,22 @@ const Import = struct { } } - fn itemkey(self: *Self, key: []const u8, name: *?[]u8, special: *?Context.Special) void { + fn itemkey(self: *Self, key: []const u8, name: *?[]u8, special: *?Special) void { const eq = std.mem.eql; switch (if (key.len > 0) key[0] else @as(u8,0)) { 'a' => { if (eq(u8, key, "asize")) { - self.ctx.stat.size = self.uint(u64); + self.statbuf.size = self.uint(u64); return; } }, 'd' => { if (eq(u8, key, "dsize")) { - self.ctx.stat.blocks = @intCast(model.Blocks, self.uint(u64)>>9); + self.statbuf.blocks = @intCast(model.Blocks, self.uint(u64)>>9); return; } if (eq(u8, key, "dev")) { - self.ctx.stat.dev = self.uint(u64); + self.statbuf.dev = self.uint(u64); return; } }, @@ -1026,29 +1045,29 @@ const Import = struct { }, 'g' => { if (eq(u8, key, "gid")) { - self.ctx.stat.ext.gid = self.uint(u32); + self.statbuf.ext.gid = self.uint(u32); return; } }, 'h' => { if (eq(u8, key, "hlnkc")) { - self.ctx.stat.hlinkc = self.boolean(); + self.statbuf.hlinkc = self.boolean(); return; } }, 'i' => { if (eq(u8, key, "ino")) { - self.ctx.stat.ino = self.uint(u64); + self.statbuf.ino = self.uint(u64); return; } }, 'm' => { if (eq(u8, key, "mode")) { - self.ctx.stat.ext.mode = self.uint(u16); + self.statbuf.ext.mode = self.uint(u16); return; } if (eq(u8, key, "mtime")) { - self.ctx.stat.ext.mtime = self.uint(u64); + self.statbuf.ext.mtime = self.uint(u64); // Accept decimal numbers, but discard the fractional part because our data model doesn't support it. if (self.ch == '.') { self.con(); @@ -1066,13 +1085,13 @@ const Import = struct { return; } if (eq(u8, key, "nlink")) { - self.ctx.stat.nlink = self.uint(u31); - if (!self.ctx.stat.dir and self.ctx.stat.nlink > 1) - self.ctx.stat.hlinkc = true; + self.statbuf.nlink = self.uint(u31); + if (!self.statbuf.dir and self.statbuf.nlink > 1) + self.statbuf.hlinkc = true; return; } if (eq(u8, key, "notreg")) { - self.ctx.stat.reg = !self.boolean(); + self.statbuf.reg = !self.boolean(); return; } }, @@ -1085,7 +1104,7 @@ const Import = struct { }, 'u' => { if (eq(u8, key, "uid")) { - self.ctx.stat.ext.uid = self.uint(u32); + self.statbuf.ext.uid = self.uint(u32); return; } }, @@ -1094,11 +1113,11 @@ const Import = struct { self.conval(); } - fn iteminfo(self: *Self, dir_dev: u64) void { + fn iteminfo(self: *Self, dir_dev: u64, lvl: *Level, sub: *Level) void { if (self.next() != '{') self.die("expected '{'"); - self.ctx.stat.dev = dir_dev; + self.statbuf.dev = dir_dev; var name: ?[]u8 = null; - var special: ?Context.Special = null; + var special: ?Special = null; while (true) { self.conws(); var keybuf: [32]u8 = undefined; @@ -1114,38 +1133,38 @@ const Import = struct { else => self.die("expected ',' or '}'"), } } - if (name) |n| self.ctx.pushPath(n) - else self.die("missing \"name\" field"); - if (special) |s| self.ctx.addSpecial(s) - else self.ctx.addStat(dir_dev); + const nname = name orelse self.die("missing \"name\" field"); + if (self.statbuf.dir) lvl.addDir(nname, &self.statbuf, if (special) |s| s == .err else false, sub) + else if (special) |s| lvl.addSpecial(nname, s, self.statbuf.dir) + else lvl.addFile(nname, &self.statbuf, false); } - fn item(self: *Self, dev: u64) void { - self.ctx.stat = .{}; + fn item(self: *Self, lvl: *Level, dev: u64) void { + self.statbuf = .{}; var isdir = false; if (self.ch == '[') { isdir = true; - self.ctx.stat.dir = true; + self.statbuf.dir = true; self.con(); self.conws(); } - self.iteminfo(dev); + var sub: Level = undefined; + self.iteminfo(dev, lvl, &sub); self.conws(); if (isdir) { - const ndev = self.ctx.stat.dev; while (self.ch == ',') { self.con(); self.conws(); - self.item(ndev); + self.item(&sub, self.statbuf.dev); self.conws(); } if (self.next() != ']') self.die("expected ',' or ']'"); + sub.close(); } - self.ctx.popPath(); - if ((self.ctx.items_seen & 1023) == 0) + if ((items_seen & 1023) == 0) main.handleEvent(false, false); } @@ -1170,7 +1189,7 @@ const Import = struct { self.conws(); // root element if (self.ch != '[') self.die("expected '['"); // top-level entry must be a dir - self.item(0); + self.item(&self.root_level, 0); self.conws(); // any trailing elements while (self.ch == ',') { @@ -1191,13 +1210,15 @@ 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(); - active_context = if (out) |f| Context.initFile(f) else Context.initMem(null); - var imp = Import{ .ctx = active_context, .rd = fd }; - defer imp.ctx.deinit(); + var imp = Import{ .rd = fd }; + if (out) |f| initFile(f, &imp.root_level) + else initMem(null, &imp.root_level); imp.root(); - imp.ctx.final(); + imp.root_level.close(); } + + var animation_pos: u32 = 0; var counting_hardlinks: bool = false; var need_confirm_quit = false; @@ -1208,7 +1229,7 @@ fn drawError(err: anyerror) void { box.move(2, 2); ui.addstr("Path: "); - ui.addstr(ui.shorten(ui.toUtf8(active_context.last_error.?), width -| 10)); + ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&last_error)), width -| 10)); box.move(3, 2); ui.addstr("Error: "); @@ -1226,33 +1247,41 @@ fn drawCounting() void { fn drawBox() void { ui.init(); - const ctx = active_context; - if (ctx.fatal_error) |err| return drawError(err); + if (fatal_error) |err| return drawError(err); if (counting_hardlinks) return drawCounting(); + + scanner.lock.lock(); + defer scanner.lock.unlock(); + const width = 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); + ui.addnum(.default, items_seen); - if (width > 48 and ctx.parents != null) { + if (width > 48 and items_seen > 0) { box.move(2, 30); ui.addstr("size: "); // TODO: Should display the size of the dir-to-be-refreshed on refreshing, not the root. ui.addsize(.default, util.blocksToSize(model.root.entry.blocks +| model.inodes.total_blocks)); } - box.move(3, 2); - ui.addstr("Current item: "); - ui.addstr(ui.shorten(ui.toUtf8(ctx.pathZ()), width -| 18)); + if (last_level) |l| { + box.move(3, 2); + ui.addstr("Current dir: "); + var path = std.ArrayList(u8).init(main.allocator); + defer path.deinit(); + l.fmtPath(&path); + ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path)), width -| 18)); + } - if (ctx.last_error) |path| { + if (last_error.items.len > 0) { box.move(5, 2); ui.style(.bold); ui.addstr("Warning: "); ui.style(.default); ui.addstr("error scanning "); - ui.addstr(ui.shorten(ui.toUtf8(path), width -| 28)); + ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&last_error)), width -| 28)); box.move(6, 3); ui.addstr("some directory sizes may not be correct."); } @@ -1292,24 +1321,33 @@ fn drawBox() void { } pub fn draw() void { - if (active_context.fatal_error != null and main.config.scan_ui.? != .full) - ui.die("Error reading {s}: {s}\n", .{ active_context.last_error.?, ui.errorString(active_context.fatal_error.?) }); + if (fatal_error != null and main.config.scan_ui.? != .full) + ui.die("Error reading {s}: {s}\n", .{ last_error.items, ui.errorString(fatal_error.?) }); switch (main.config.scan_ui.?) { .none => {}, .line => { var buf: [256]u8 = undefined; var line: []const u8 = undefined; - if (counting_hardlinks) { - line = "\x1b7\x1b[JCounting hardlinks...\x1b8"; - } else 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 } - ) catch return; - } else { - const r = ui.FmtSize.fmt(util.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; + { + scanner.lock.lock(); + defer scanner.lock.unlock(); + var path = std.ArrayList(u8).init(main.allocator); + defer path.deinit(); + if (last_level) |l| l.fmtPath(&path); + const pathZ = util.arrayListBufZ(&path); + + if (counting_hardlinks) { + line = "\x1b7\x1b[JCounting hardlinks...\x1b8"; + } else if (file_writer != null) { + line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8", + .{ ui.shorten(pathZ, 63), items_seen } + ) catch return; + } else { + const r = ui.FmtSize.fmt(util.blocksToSize(model.root.entry.blocks)); + line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <51} {d:>9} files / {s}{s}\x1b8", + .{ ui.shorten(pathZ, 51), items_seen, r.num(), r.unit } + ) catch return; + } } _ = std.io.getStdErr().write(line) catch {}; }, @@ -1318,7 +1356,7 @@ pub fn draw() void { } pub fn keyInput(ch: i32) void { - if (active_context.fatal_error != null) { + if (fatal_error != null) { if (main.state == .scan) ui.quit() else main.state = .browse; return; |