mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-12 17:08:39 -09:00
Compare commits
19 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b3d0a670e | ||
|
|
f452244576 | ||
|
|
14bb8d0dd1 | ||
|
|
19cfdcf543 | ||
|
|
5129de737e | ||
|
|
68671a1af1 | ||
|
|
74c91768a0 | ||
|
|
ac4d689e22 | ||
|
|
66b875eb00 | ||
|
|
67f34090fb | ||
|
|
5b96a48f53 | ||
|
|
58e6458130 | ||
|
|
653c3bfe70 | ||
|
|
beac59fb12 | ||
|
|
d97a7f73dd | ||
|
|
35a9faadb2 | ||
|
|
e43d22ba3f | ||
|
|
f4e4694612 | ||
|
|
c9f3d39d3e |
17 changed files with 435 additions and 253 deletions
25
ChangeLog
25
ChangeLog
|
|
@ -1,6 +1,31 @@
|
|||
# SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
2.9.2 - 2025-10-24
|
||||
- Still requires Zig 0.14 or 0.15
|
||||
- Fix hang on loading config file when compiled with Zig 0.15.2
|
||||
|
||||
2.9.1 - 2025-08-21
|
||||
- Add support for building with Zig 0.15
|
||||
- Zig 0.14 is still supported
|
||||
|
||||
2.9 - 2025-08-16
|
||||
- Still requires Zig 0.14
|
||||
- Add --delete-command option to replace the built-in file deletion
|
||||
- Move term cursor to selected option in delete confirmation window
|
||||
- Support binary import on older Linux kernels lacking statx() (may break
|
||||
again in the future, Zig does not officially support such old kernels)
|
||||
|
||||
2.8.2 - 2025-05-01
|
||||
- Still requires Zig 0.14
|
||||
- Fix a build error on MacOS
|
||||
|
||||
2.8.1 - 2025-04-28
|
||||
- Still requires Zig 0.14
|
||||
- Fix integer overflow in binary export
|
||||
- Fix crash when `fstatat()` returns EINVAL
|
||||
- Minor build system improvements
|
||||
|
||||
2.8 - 2025-03-05
|
||||
- Now requires Zig 0.14
|
||||
- Add support for @-prefixed lines to ignore errors in config file
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ C version (1.x).
|
|||
|
||||
## Requirements
|
||||
|
||||
- Zig 0.14
|
||||
- Zig 0.14 or 0.15
|
||||
- Some sort of POSIX-like OS
|
||||
- ncurses
|
||||
- libzstd
|
||||
|
|
|
|||
20
build.zig
20
build.zig
|
|
@ -7,21 +7,24 @@ pub fn build(b: *std.Build) void {
|
|||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const pie = b.option(bool, "pie", "Build with PIE support (by default false)") orelse false;
|
||||
const pie = b.option(bool, "pie", "Build with PIE support (by default: target-dependant)");
|
||||
const strip = b.option(bool, "strip", "Strip debugging info (by default false)") orelse false;
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "ncdu",
|
||||
const main_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.strip = strip,
|
||||
.link_libc = true,
|
||||
});
|
||||
main_mod.linkSystemLibrary("ncursesw", .{});
|
||||
main_mod.linkSystemLibrary("zstd", .{});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "ncdu",
|
||||
.root_module = main_mod,
|
||||
});
|
||||
exe.pie = pie;
|
||||
exe.root_module.linkSystemLibrary("ncursesw", .{});
|
||||
exe.root_module.linkSystemLibrary("libzstd", .{});
|
||||
// https://github.com/ziglang/zig/blob/faccd79ca5debbe22fe168193b8de54393257604/build.zig#L745-L748
|
||||
if (target.result.os.tag.isDarwin()) {
|
||||
// useful for package maintainers
|
||||
|
|
@ -39,14 +42,9 @@ pub fn build(b: *std.Build) void {
|
|||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
const unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.root_module = main_mod,
|
||||
});
|
||||
unit_tests.pie = pie;
|
||||
unit_tests.root_module.linkSystemLibrary("ncursesw", .{});
|
||||
unit_tests.root_module.linkSystemLibrary("libzstd", .{});
|
||||
|
||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||
|
||||
|
|
|
|||
28
ncdu.1
28
ncdu.1
|
|
@ -1,6 +1,6 @@
|
|||
.\" SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.Dd March 5, 2025
|
||||
.Dd August 16, 2025
|
||||
.Dt NCDU 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
.Op Fl \-group\-directories\-first , \-no\-group\-directories\-first
|
||||
.Op Fl \-confirm\-quit , \-no\-confirm\-quit
|
||||
.Op Fl \-confirm\-delete , \-no\-confirm\-delete
|
||||
.Op Fl \-delete\-command Ar command
|
||||
.Op Fl \-color Ar off | dark | dark-bg
|
||||
.Op Ar path
|
||||
.Nm
|
||||
|
|
@ -359,6 +360,31 @@ Can be helpful when you accidentally press 'q' during or after a very long scan.
|
|||
Require a confirmation before deleting a file or directory.
|
||||
Enabled by default, but can be disabled if you're absolutely sure you won't
|
||||
accidentally press 'd'.
|
||||
.It Fl \-delete\-command Ar command
|
||||
When set to a non-empty string, replace the built-in file deletion feature with
|
||||
a custom shell command.
|
||||
.Pp
|
||||
The absolute path of the item to be deleted is appended to the given command
|
||||
and the result is evaluated in a shell.
|
||||
The command is run from the same directory that ncdu itself was started in.
|
||||
The
|
||||
.Ev NCDU_DELETE_PATH
|
||||
environment variable is set to the absolute path of the item to be deleted and
|
||||
.Ev NCDU_LEVEL
|
||||
is set in the same fashion as when spawning a shell from within ncdu.
|
||||
.Pp
|
||||
After command completion, the in-memory view of the selected item is refreshed
|
||||
and directory sizes are adjusted as necessary.
|
||||
This is not a full refresh of the complete directory tree, so if the item has
|
||||
been renamed or moved to another directory, it's new location is not
|
||||
automatically picked up.
|
||||
.Pp
|
||||
For example, to use
|
||||
.Xr rm 1
|
||||
interactive mode to prompt before each deletion:
|
||||
.Dl ncdu --no-confirm-delete --delete-command \[aq]rm -ri --\[aq]
|
||||
Or to move files to trash:
|
||||
.Dl ncdu --delete-command \[aq]gio trash --\[aq]
|
||||
.It Fl \-color Ar off | dark | dark-bg
|
||||
Set the color scheme.
|
||||
The following schemes are recognized:
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@ const c = @import("c.zig").c;
|
|||
|
||||
pub const global = struct {
|
||||
var fd: std.fs.File = undefined;
|
||||
var index = std.ArrayList(u8).init(main.allocator);
|
||||
var index: std.ArrayListUnmanaged(u8) = .empty;
|
||||
var file_off: u64 = 0;
|
||||
var lock: std.Thread.Mutex = .{};
|
||||
var root_itemref: u64 = 0;
|
||||
};
|
||||
|
||||
const BLOCK_SIZE: usize = 64*1024;
|
||||
|
||||
pub const SIGNATURE = "\xbfncduEX1";
|
||||
|
||||
pub const ItemKey = enum(u5) {
|
||||
|
|
@ -81,10 +79,15 @@ fn blockSize(num: u32) usize {
|
|||
else 2048<<10; // 32768
|
||||
}
|
||||
|
||||
// Upper bound on the return value of blockSize()
|
||||
// (config.export_block_size may be larger than the sizes listed above, let's
|
||||
// stick with the maximum block size supported by the file format to be safe)
|
||||
const MAX_BLOCK_SIZE: usize = 1<<28;
|
||||
|
||||
|
||||
pub const Thread = struct {
|
||||
buf: []u8 = undefined,
|
||||
off: usize = std.math.maxInt(usize) - (1<<10), // large number to trigger a flush() for the first write
|
||||
off: usize = MAX_BLOCK_SIZE, // pretend we have a full block to trigger a flush() for the first write
|
||||
block_num: u32 = std.math.maxInt(u32),
|
||||
itemref: u64 = 0, // ref of item currently being written
|
||||
|
||||
|
|
@ -102,11 +105,11 @@ pub const Thread = struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn createBlock(t: *Thread) std.ArrayList(u8) {
|
||||
var out = std.ArrayList(u8).init(main.allocator);
|
||||
fn createBlock(t: *Thread) std.ArrayListUnmanaged(u8) {
|
||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
||||
if (t.block_num == std.math.maxInt(u32) or t.off == 0) return out;
|
||||
|
||||
out.ensureTotalCapacityPrecise(12 + @as(usize, @intCast(c.ZSTD_COMPRESSBOUND(@as(c_int, @intCast(t.off)))))) catch unreachable;
|
||||
out.ensureTotalCapacityPrecise(main.allocator, 12 + @as(usize, @intCast(c.ZSTD_COMPRESSBOUND(@as(c_int, @intCast(t.off)))))) catch unreachable;
|
||||
out.items.len = out.capacity;
|
||||
const bodylen = compressZstd(t.buf[0..t.off], out.items[8..]);
|
||||
out.items.len = 12 + bodylen;
|
||||
|
|
@ -119,12 +122,12 @@ pub const Thread = struct {
|
|||
|
||||
fn flush(t: *Thread, expected_len: usize) void {
|
||||
@branchHint(.unlikely);
|
||||
const block = createBlock(t);
|
||||
defer block.deinit();
|
||||
var block = createBlock(t);
|
||||
defer block.deinit(main.allocator);
|
||||
|
||||
global.lock.lock();
|
||||
defer global.lock.unlock();
|
||||
// This can only really happen when the root path exceeds BLOCK_SIZE,
|
||||
// This can only really happen when the root path exceeds our block size,
|
||||
// in which case we would probably have error'ed out earlier anyway.
|
||||
if (expected_len > t.buf.len) ui.die("Error writing data: path too long.\n", .{});
|
||||
|
||||
|
|
@ -138,7 +141,7 @@ pub const Thread = struct {
|
|||
|
||||
t.off = 0;
|
||||
t.block_num = @intCast((global.index.items.len - 4) / 8);
|
||||
global.index.appendSlice(&[1]u8{0}**8) catch unreachable;
|
||||
global.index.appendSlice(main.allocator, &[1]u8{0}**8) catch unreachable;
|
||||
if (global.index.items.len + 12 >= (1<<28)) ui.die("Too many data blocks, please report a bug.\n", .{});
|
||||
|
||||
const newsize = blockSize(t.block_num);
|
||||
|
|
@ -430,7 +433,7 @@ pub const Dir = struct {
|
|||
|
||||
pub fn createRoot(stat: *const sink.Stat, threads: []sink.Thread) Dir {
|
||||
for (threads) |*t| {
|
||||
t.sink.bin.buf = main.allocator.alloc(u8, BLOCK_SIZE) catch unreachable;
|
||||
t.sink.bin.buf = main.allocator.alloc(u8, blockSize(0)) catch unreachable;
|
||||
}
|
||||
|
||||
return .{ .stat = stat.* };
|
||||
|
|
@ -444,12 +447,12 @@ pub fn done(threads: []sink.Thread) void {
|
|||
|
||||
while (std.mem.endsWith(u8, global.index.items, &[1]u8{0}**8))
|
||||
global.index.shrinkRetainingCapacity(global.index.items.len - 8);
|
||||
global.index.appendSlice(&bigu64(global.root_itemref)) catch unreachable;
|
||||
global.index.appendSlice(&blockHeader(1, @intCast(global.index.items.len + 4))) catch unreachable;
|
||||
global.index.appendSlice(main.allocator, &bigu64(global.root_itemref)) catch unreachable;
|
||||
global.index.appendSlice(main.allocator, &blockHeader(1, @intCast(global.index.items.len + 4))) catch unreachable;
|
||||
global.index.items[0..4].* = blockHeader(1, @intCast(global.index.items.len));
|
||||
global.fd.writeAll(global.index.items) catch |e|
|
||||
ui.die("Error writing to file: {s}.\n", .{ ui.errorString(e) });
|
||||
global.index.clearAndFree();
|
||||
global.index.clearAndFree(main.allocator);
|
||||
|
||||
global.fd.close();
|
||||
}
|
||||
|
|
@ -461,5 +464,5 @@ pub fn setupOutput(fd: std.fs.File) void {
|
|||
global.file_off = 8;
|
||||
|
||||
// Placeholder for the index block header.
|
||||
global.index.appendSlice("aaaa") catch unreachable;
|
||||
global.index.appendSlice(main.allocator, "aaaa") catch unreachable;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -504,7 +504,9 @@ pub fn import() void {
|
|||
pub fn open(fd: std.fs.File) !void {
|
||||
global.fd = fd;
|
||||
|
||||
const size = try fd.getEndPos();
|
||||
// Do not use fd.getEndPos() because that requires newer kernels supporting statx() #261.
|
||||
try fd.seekFromEnd(0);
|
||||
const size = try fd.getPos();
|
||||
if (size < 16) return error.EndOfStream;
|
||||
|
||||
// Read index block
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ const util = @import("util.zig");
|
|||
// Currently opened directory.
|
||||
pub var dir_parent: *model.Dir = undefined;
|
||||
pub var dir_path: [:0]u8 = undefined;
|
||||
var dir_parents = std.ArrayList(model.Ref).init(main.allocator);
|
||||
var dir_parents: std.ArrayListUnmanaged(model.Ref) = .empty;
|
||||
var dir_alloc = std.heap.ArenaAllocator.init(main.allocator);
|
||||
|
||||
// Used to keep track of which dir is which ref, so we can enter it.
|
||||
// Only used for binreader browsing.
|
||||
var dir_refs = std.ArrayList(struct { ptr: *model.Dir, ref: u64 }).init(main.allocator);
|
||||
var dir_refs: std.ArrayListUnmanaged(struct { ptr: *model.Dir, ref: u64 }) = .empty;
|
||||
|
||||
// Sorted list of all items in the currently opened directory.
|
||||
// (first item may be null to indicate the "parent directory" item)
|
||||
var dir_items = std.ArrayList(?*model.Entry).init(main.allocator);
|
||||
var dir_items: std.ArrayListUnmanaged(?*model.Entry) = .empty;
|
||||
|
||||
var dir_max_blocks: u64 = 0;
|
||||
var dir_max_size: u64 = 0;
|
||||
|
|
@ -146,7 +146,7 @@ pub fn loadDir(next_sel: u64) void {
|
|||
dir_has_shared = false;
|
||||
|
||||
if (dir_parents.items.len > 1)
|
||||
dir_items.append(null) catch unreachable;
|
||||
dir_items.append(main.allocator, null) catch unreachable;
|
||||
var ref = dir_parent.sub;
|
||||
while (!ref.isNull()) {
|
||||
const e =
|
||||
|
|
@ -164,10 +164,10 @@ pub fn loadDir(next_sel: u64) void {
|
|||
break :blk !excl and name[0] != '.' and name[name.len-1] != '~';
|
||||
};
|
||||
if (shown) {
|
||||
dir_items.append(e) catch unreachable;
|
||||
dir_items.append(main.allocator, e) catch unreachable;
|
||||
if (e.dir()) |d| {
|
||||
if (d.shared_blocks > 0 or d.shared_size > 0) dir_has_shared = true;
|
||||
if (main.config.binreader) dir_refs.append(.{ .ptr = d, .ref = ref.ref }) catch unreachable;
|
||||
if (main.config.binreader) dir_refs.append(main.allocator, .{ .ptr = d, .ref = ref.ref }) catch unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,10 +185,10 @@ pub fn initRoot() void {
|
|||
if (main.config.binreader) {
|
||||
const ref = bin_reader.getRoot();
|
||||
dir_parent = bin_reader.get(ref, main.allocator).dir() orelse ui.die("Invalid import\n", .{});
|
||||
dir_parents.append(.{ .ref = ref }) catch unreachable;
|
||||
dir_parents.append(main.allocator, .{ .ref = ref }) catch unreachable;
|
||||
} else {
|
||||
dir_parent = model.root;
|
||||
dir_parents.append(.{ .ptr = &dir_parent.entry }) catch unreachable;
|
||||
dir_parents.append(main.allocator, .{ .ptr = &dir_parent.entry }) catch unreachable;
|
||||
}
|
||||
dir_path = main.allocator.dupeZ(u8, dir_parent.entry.name()) catch unreachable;
|
||||
loadDir(0);
|
||||
|
|
@ -202,10 +202,10 @@ fn enterSub(e: *model.Dir) void {
|
|||
};
|
||||
dir_parent.entry.destroy(main.allocator);
|
||||
dir_parent = bin_reader.get(ref, main.allocator).dir() orelse unreachable;
|
||||
dir_parents.append(.{ .ref = ref }) catch unreachable;
|
||||
dir_parents.append(main.allocator, .{ .ref = ref }) catch unreachable;
|
||||
} else {
|
||||
dir_parent = e;
|
||||
dir_parents.append(.{ .ptr = &e.entry }) catch unreachable;
|
||||
dir_parents.append(main.allocator, .{ .ptr = &e.entry }) catch unreachable;
|
||||
}
|
||||
|
||||
const newpath = std.fs.path.joinZ(main.allocator, &[_][]const u8{ dir_path, e.entry.name() }) catch unreachable;
|
||||
|
|
@ -430,7 +430,7 @@ const info = struct {
|
|||
|
||||
var tab: Tab = .info;
|
||||
var entry: ?*model.Entry = null;
|
||||
var links: ?std.ArrayList(*model.Link) = null;
|
||||
var links: ?std.ArrayListUnmanaged(*model.Link) = null;
|
||||
var links_top: usize = 0;
|
||||
var links_idx: usize = 0;
|
||||
|
||||
|
|
@ -445,7 +445,7 @@ const info = struct {
|
|||
// Set the displayed entry to the currently selected item and open the tab.
|
||||
fn set(e: ?*model.Entry, t: Tab) void {
|
||||
if (e != entry) {
|
||||
if (links) |*l| l.deinit();
|
||||
if (links) |*l| l.deinit(main.allocator);
|
||||
links = null;
|
||||
links_top = 0;
|
||||
links_idx = 0;
|
||||
|
|
@ -458,10 +458,10 @@ const info = struct {
|
|||
state = .info;
|
||||
tab = t;
|
||||
if (tab == .links and links == null and !main.config.binreader) {
|
||||
var list = std.ArrayList(*model.Link).init(main.allocator);
|
||||
var list: std.ArrayListUnmanaged(*model.Link) = .empty;
|
||||
var l = e.?.link().?;
|
||||
while (true) {
|
||||
list.append(l) catch unreachable;
|
||||
list.append(main.allocator, l) catch unreachable;
|
||||
l = l.next;
|
||||
if (&l.entry == e)
|
||||
break;
|
||||
|
|
|
|||
131
src/delete.zig
131
src/delete.zig
|
|
@ -6,6 +6,9 @@ const main = @import("main.zig");
|
|||
const model = @import("model.zig");
|
||||
const ui = @import("ui.zig");
|
||||
const browser = @import("browser.zig");
|
||||
const scan = @import("scan.zig");
|
||||
const sink = @import("sink.zig");
|
||||
const mem_sink = @import("mem_sink.zig");
|
||||
const util = @import("util.zig");
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
|
|
@ -68,6 +71,57 @@ fn deleteItem(dir: std.fs.Dir, path: [:0]const u8, ptr: *align(1) ?*model.Entry)
|
|||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the item has been deleted successfully.
|
||||
fn deleteCmd(path: [:0]const u8, ptr: *align(1) ?*model.Entry) bool {
|
||||
{
|
||||
var env = std.process.getEnvMap(main.allocator) catch unreachable;
|
||||
defer env.deinit();
|
||||
env.put("NCDU_DELETE_PATH", path) catch unreachable;
|
||||
|
||||
// Since we're passing the path as an environment variable and go through
|
||||
// the shell anyway, we can refer to the variable and avoid error-prone
|
||||
// shell escaping.
|
||||
const cmd = std.fmt.allocPrint(main.allocator, "{s} \"$NCDU_DELETE_PATH\"", .{main.config.delete_command}) catch unreachable;
|
||||
defer main.allocator.free(cmd);
|
||||
ui.runCmd(&.{"/bin/sh", "-c", cmd}, null, &env, true);
|
||||
}
|
||||
|
||||
const stat = scan.statAt(std.fs.cwd(), path, false, null) catch {
|
||||
// Stat failed. Would be nice to display an error if it's not
|
||||
// 'FileNotFound', but w/e, let's just assume the item has been
|
||||
// deleted as expected.
|
||||
ptr.*.?.zeroStats(parent);
|
||||
ptr.* = ptr.*.?.next.ptr;
|
||||
return true;
|
||||
};
|
||||
|
||||
// If either old or new entry is not a dir, remove & re-add entry in the in-memory tree.
|
||||
if (ptr.*.?.pack.etype != .dir or stat.etype != .dir) {
|
||||
ptr.*.?.zeroStats(parent);
|
||||
const e = model.Entry.create(main.allocator, stat.etype, main.config.extended and !stat.ext.isEmpty(), ptr.*.?.name());
|
||||
e.next.ptr = ptr.*.?.next.ptr;
|
||||
mem_sink.statToEntry(&stat, e, parent);
|
||||
ptr.* = e;
|
||||
|
||||
var it : ?*model.Dir = parent;
|
||||
while (it) |p| : (it = p.parent) {
|
||||
if (stat.etype != .link) {
|
||||
p.entry.pack.blocks +|= e.pack.blocks;
|
||||
p.entry.size +|= e.size;
|
||||
}
|
||||
p.items +|= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If new entry is a dir, recursively scan.
|
||||
if (ptr.*.?.dir()) |d| {
|
||||
main.state = .refresh;
|
||||
sink.global.sink = .mem;
|
||||
mem_sink.global.root = d;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns the item that should be selected in the browser.
|
||||
pub fn delete() ?*model.Entry {
|
||||
while (main.state == .delete and state == .confirm)
|
||||
|
|
@ -82,30 +136,46 @@ pub fn delete() ?*model.Entry {
|
|||
if (it.* == entry)
|
||||
break;
|
||||
|
||||
var path = std.ArrayList(u8).init(main.allocator);
|
||||
defer path.deinit();
|
||||
parent.fmtPath(true, &path);
|
||||
var path: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer path.deinit(main.allocator);
|
||||
parent.fmtPath(main.allocator, true, &path);
|
||||
if (path.items.len == 0 or path.items[path.items.len-1] != '/')
|
||||
path.append('/') catch unreachable;
|
||||
path.appendSlice(entry.name()) catch unreachable;
|
||||
path.append(main.allocator, '/') catch unreachable;
|
||||
path.appendSlice(main.allocator, entry.name()) catch unreachable;
|
||||
|
||||
_ = deleteItem(std.fs.cwd(), util.arrayListBufZ(&path), it);
|
||||
model.inodes.addAllStats();
|
||||
return if (it.* == e) e else next_sel;
|
||||
if (main.config.delete_command.len == 0) {
|
||||
_ = deleteItem(std.fs.cwd(), util.arrayListBufZ(&path, main.allocator), it);
|
||||
model.inodes.addAllStats();
|
||||
return if (it.* == e) e else next_sel;
|
||||
} else {
|
||||
const isdel = deleteCmd(util.arrayListBufZ(&path, main.allocator), it);
|
||||
model.inodes.addAllStats();
|
||||
return if (isdel) next_sel else it.*;
|
||||
}
|
||||
}
|
||||
|
||||
fn drawConfirm() void {
|
||||
browser.draw();
|
||||
const box = ui.Box.create(6, 60, "Confirm delete");
|
||||
box.move(1, 2);
|
||||
ui.addstr("Are you sure you want to delete \"");
|
||||
ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 21));
|
||||
ui.addch('"');
|
||||
if (entry.pack.etype != .dir)
|
||||
ui.addch('?')
|
||||
else {
|
||||
box.move(2, 18);
|
||||
ui.addstr("and all of its contents?");
|
||||
if (main.config.delete_command.len == 0) {
|
||||
ui.addstr("Are you sure you want to delete \"");
|
||||
ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 21));
|
||||
ui.addch('"');
|
||||
if (entry.pack.etype != .dir)
|
||||
ui.addch('?')
|
||||
else {
|
||||
box.move(2, 18);
|
||||
ui.addstr("and all of its contents?");
|
||||
}
|
||||
} else {
|
||||
ui.addstr("Are you sure you want to run \"");
|
||||
ui.addstr(ui.shorten(ui.toUtf8(main.config.delete_command), 25));
|
||||
ui.addch('"');
|
||||
box.move(2, 4);
|
||||
ui.addstr("on \"");
|
||||
ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 50));
|
||||
ui.addch('"');
|
||||
}
|
||||
|
||||
box.move(4, 15);
|
||||
|
|
@ -119,20 +189,25 @@ fn drawConfirm() void {
|
|||
box.move(4, 31);
|
||||
ui.style(if (confirm == .ignore) .sel else .default);
|
||||
ui.addstr("don't ask me again");
|
||||
box.move(4, switch (confirm) {
|
||||
.yes => 15,
|
||||
.no => 25,
|
||||
.ignore => 31
|
||||
});
|
||||
}
|
||||
|
||||
fn drawProgress() void {
|
||||
var path = std.ArrayList(u8).init(main.allocator);
|
||||
defer path.deinit();
|
||||
parent.fmtPath(false, &path);
|
||||
path.append('/') catch unreachable;
|
||||
path.appendSlice(entry.name()) catch unreachable;
|
||||
var path: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer path.deinit(main.allocator);
|
||||
parent.fmtPath(main.allocator, false, &path);
|
||||
path.append(main.allocator, '/') catch unreachable;
|
||||
path.appendSlice(main.allocator, entry.name()) catch unreachable;
|
||||
|
||||
// TODO: Item counts and progress bar would be nice.
|
||||
|
||||
const box = ui.Box.create(6, 60, "Deleting...");
|
||||
box.move(2, 2);
|
||||
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path)), 56));
|
||||
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path, main.allocator)), 56));
|
||||
box.move(4, 41);
|
||||
ui.addstr("Press ");
|
||||
ui.style(.key);
|
||||
|
|
@ -142,16 +217,16 @@ fn drawProgress() void {
|
|||
}
|
||||
|
||||
fn drawErr() void {
|
||||
var path = std.ArrayList(u8).init(main.allocator);
|
||||
defer path.deinit();
|
||||
parent.fmtPath(false, &path);
|
||||
path.append('/') catch unreachable;
|
||||
path.appendSlice(entry.name()) catch unreachable;
|
||||
var path: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer path.deinit(main.allocator);
|
||||
parent.fmtPath(main.allocator, false, &path);
|
||||
path.append(main.allocator, '/') catch unreachable;
|
||||
path.appendSlice(main.allocator, entry.name()) catch unreachable;
|
||||
|
||||
const box = ui.Box.create(6, 60, "Error");
|
||||
box.move(1, 2);
|
||||
ui.addstr("Error deleting ");
|
||||
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path)), 41));
|
||||
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path, main.allocator)), 41));
|
||||
box.move(2, 4);
|
||||
ui.addstr(ui.errorString(error_code));
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ test "parse" {
|
|||
fn PatternList(comptime withsub: bool) type {
|
||||
return struct {
|
||||
literals: std.HashMapUnmanaged(*const Pattern, Val, Ctx, 80) = .{},
|
||||
wild: std.ArrayListUnmanaged(*const Pattern) = .{},
|
||||
wild: std.ArrayListUnmanaged(*const Pattern) = .empty,
|
||||
|
||||
// Not a fan of the map-of-arrays approach in the 'withsub' case, it
|
||||
// has a lot of extra allocations. Linking the Patterns together in a
|
||||
|
|
|
|||
118
src/main.zig
118
src/main.zig
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pub const program_version = "2.8";
|
||||
pub const program_version = "2.9.2";
|
||||
|
||||
const std = @import("std");
|
||||
const model = @import("model.zig");
|
||||
|
|
@ -80,7 +80,6 @@ pub const config = struct {
|
|||
pub var follow_symlinks: bool = false;
|
||||
pub var exclude_caches: bool = false;
|
||||
pub var exclude_kernfs: bool = false;
|
||||
pub var exclude_patterns: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(allocator);
|
||||
pub var threads: usize = 1;
|
||||
pub var complevel: u8 = 4;
|
||||
pub var compress: bool = false;
|
||||
|
|
@ -114,10 +113,14 @@ pub const config = struct {
|
|||
pub var confirm_quit: bool = false;
|
||||
pub var confirm_delete: bool = true;
|
||||
pub var ignore_delete_errors: bool = false;
|
||||
pub var delete_command: [:0]const u8 = "";
|
||||
};
|
||||
|
||||
pub var state: enum { scan, browse, refresh, shell, delete } = .scan;
|
||||
|
||||
const stdin = if (@hasDecl(std.io, "getStdIn")) std.io.getStdIn() else std.fs.File.stdin();
|
||||
const stdout = if (@hasDecl(std.io, "getStdOut")) std.io.getStdOut() else std.fs.File.stdout();
|
||||
|
||||
// Simple generic argument parser, supports getopt_long() style arguments.
|
||||
const Args = struct {
|
||||
lst: []const [:0]const u8,
|
||||
|
|
@ -306,6 +309,7 @@ fn argConfig(args: *Args, opt: Args.Option, infile: bool) !void {
|
|||
else if (opt.is("--no-confirm-quit")) config.confirm_quit = false
|
||||
else if (opt.is("--confirm-delete")) config.confirm_delete = true
|
||||
else if (opt.is("--no-confirm-delete")) config.confirm_delete = false
|
||||
else if (opt.is("--delete-command")) config.delete_command = allocator.dupeZ(u8, try args.arg()) catch unreachable
|
||||
else if (opt.is("--color")) {
|
||||
const val = try args.arg();
|
||||
if (std.mem.eql(u8, val, "off")) config.ui_color = .off
|
||||
|
|
@ -326,19 +330,13 @@ fn tryReadArgsFile(path: [:0]const u8) void {
|
|||
};
|
||||
defer f.close();
|
||||
|
||||
var rd_ = std.io.bufferedReader(f.reader());
|
||||
const rd = rd_.reader();
|
||||
|
||||
var line_buf: [4096]u8 = undefined;
|
||||
var line_fbs = std.io.fixedBufferStream(&line_buf);
|
||||
const line_writer = line_fbs.writer();
|
||||
var line_rd = util.LineReader.init(f, &line_buf);
|
||||
|
||||
while (true) : (line_fbs.reset()) {
|
||||
rd.streamUntilDelimiter(line_writer, '\n', line_buf.len) catch |err| switch (err) {
|
||||
error.EndOfStream => if (line_fbs.getPos() catch unreachable == 0) break,
|
||||
else => |e| ui.die("Error reading from {s}: {s}\nRun with --ignore-config to skip reading config files.\n", .{ path, ui.errorString(e) }),
|
||||
};
|
||||
const line_ = line_fbs.getWritten();
|
||||
while (true) {
|
||||
const line_ = (line_rd.read() catch |e|
|
||||
ui.die("Error reading from {s}: {s}\nRun with --ignore-config to skip reading config files.\n", .{ path, ui.errorString(e) })
|
||||
) orelse break;
|
||||
|
||||
var argc: usize = 0;
|
||||
var ignerror = false;
|
||||
|
|
@ -373,13 +371,11 @@ fn tryReadArgsFile(path: [:0]const u8) void {
|
|||
}
|
||||
|
||||
fn version() noreturn {
|
||||
const stdout = std.io.getStdOut();
|
||||
stdout.writeAll("ncdu " ++ program_version ++ "\n") catch {};
|
||||
std.process.exit(0);
|
||||
}
|
||||
|
||||
fn help() noreturn {
|
||||
const stdout = std.io.getStdOut();
|
||||
stdout.writeAll(
|
||||
\\ncdu <options> <directory>
|
||||
\\
|
||||
|
|
@ -428,6 +424,7 @@ fn help() noreturn {
|
|||
\\ --group-directories-first Sort directories before files
|
||||
\\ --confirm-quit Ask confirmation before quitting ncdu
|
||||
\\ --no-confirm-delete Don't ask confirmation before deletion
|
||||
\\ --delete-command CMD Command to run for file deletion
|
||||
\\ --color SCHEME off / dark / dark-bg
|
||||
\\
|
||||
\\Refer to `man ncdu` for more information.
|
||||
|
|
@ -437,75 +434,13 @@ fn help() noreturn {
|
|||
}
|
||||
|
||||
|
||||
fn spawnShell() void {
|
||||
ui.deinit();
|
||||
defer ui.init();
|
||||
|
||||
var env = std.process.getEnvMap(allocator) catch unreachable;
|
||||
defer env.deinit();
|
||||
// NCDU_LEVEL can only count to 9, keeps the implementation simple.
|
||||
if (env.get("NCDU_LEVEL")) |l|
|
||||
env.put("NCDU_LEVEL", if (l.len == 0) "1" else switch (l[0]) {
|
||||
'0'...'8' => |d| &[1] u8{d+1},
|
||||
'9' => "9",
|
||||
else => "1"
|
||||
}) catch unreachable
|
||||
else
|
||||
env.put("NCDU_LEVEL", "1") catch unreachable;
|
||||
|
||||
const shell = std.posix.getenvZ("NCDU_SHELL") orelse std.posix.getenvZ("SHELL") orelse "/bin/sh";
|
||||
var child = std.process.Child.init(&.{shell}, allocator);
|
||||
child.cwd = browser.dir_path;
|
||||
child.env_map = &env;
|
||||
|
||||
const stdin = std.io.getStdIn();
|
||||
const stderr = std.io.getStdErr();
|
||||
const term = child.spawnAndWait() catch |e| blk: {
|
||||
stderr.writer().print(
|
||||
"Error spawning shell: {s}\n\nPress enter to continue.\n",
|
||||
.{ ui.errorString(e) }
|
||||
) catch {};
|
||||
stdin.reader().skipUntilDelimiterOrEof('\n') catch unreachable;
|
||||
break :blk std.process.Child.Term{ .Exited = 0 };
|
||||
};
|
||||
if (term != .Exited) {
|
||||
const n = switch (term) {
|
||||
.Exited => "status",
|
||||
.Signal => "signal",
|
||||
.Stopped => "stopped",
|
||||
.Unknown => "unknown",
|
||||
};
|
||||
const v = switch (term) {
|
||||
.Exited => |v| v,
|
||||
.Signal => |v| v,
|
||||
.Stopped => |v| v,
|
||||
.Unknown => |v| v,
|
||||
};
|
||||
stderr.writer().print(
|
||||
"Shell returned with {s} code {}.\n\nPress enter to continue.\n", .{ n, v }
|
||||
) catch {};
|
||||
stdin.reader().skipUntilDelimiterOrEof('\n') catch unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn readExcludeFile(path: [:0]const u8) !void {
|
||||
const f = try std.fs.cwd().openFileZ(path, .{});
|
||||
defer f.close();
|
||||
|
||||
var rd_ = std.io.bufferedReader(f.reader());
|
||||
const rd = rd_.reader();
|
||||
|
||||
var line_buf: [4096]u8 = undefined;
|
||||
var line_fbs = std.io.fixedBufferStream(&line_buf);
|
||||
const line_writer = line_fbs.writer();
|
||||
|
||||
while (true) : (line_fbs.reset()) {
|
||||
rd.streamUntilDelimiter(line_writer, '\n', line_buf.len) catch |err| switch (err) {
|
||||
error.EndOfStream => if (line_fbs.getPos() catch unreachable == 0) break,
|
||||
else => |e| return e,
|
||||
};
|
||||
const line = line_fbs.getWritten();
|
||||
var line_rd = util.LineReader.init(f, &line_buf);
|
||||
while (try line_rd.read()) |line| {
|
||||
if (line.len > 0)
|
||||
exclude.addPattern(line);
|
||||
}
|
||||
|
|
@ -513,12 +448,12 @@ fn readExcludeFile(path: [:0]const u8) !void {
|
|||
|
||||
fn readImport(path: [:0]const u8) !void {
|
||||
const fd =
|
||||
if (std.mem.eql(u8, "-", path)) std.io.getStdIn()
|
||||
if (std.mem.eql(u8, "-", path)) stdin
|
||||
else try std.fs.cwd().openFileZ(path, .{});
|
||||
errdefer fd.close();
|
||||
|
||||
var buf: [8]u8 = undefined;
|
||||
try fd.reader().readNoEof(&buf);
|
||||
if (8 != try fd.readAll(&buf)) return error.EndOfStream;
|
||||
if (std.mem.eql(u8, &buf, bin_export.SIGNATURE)) {
|
||||
try bin_reader.open(fd);
|
||||
config.binreader = true;
|
||||
|
|
@ -600,8 +535,6 @@ pub fn main() void {
|
|||
if (@import("builtin").os.tag != .linux and config.exclude_kernfs)
|
||||
ui.die("The --exclude-kernfs flag is currently only supported on Linux.\n", .{});
|
||||
|
||||
const stdin = std.io.getStdIn();
|
||||
const stdout = std.io.getStdOut();
|
||||
const out_tty = stdout.isTty();
|
||||
const in_tty = stdin.isTty();
|
||||
if (config.scan_ui == null) {
|
||||
|
|
@ -659,10 +592,10 @@ pub fn main() void {
|
|||
while (true) {
|
||||
switch (state) {
|
||||
.refresh => {
|
||||
var full_path = std.ArrayList(u8).init(allocator);
|
||||
defer full_path.deinit();
|
||||
mem_sink.global.root.?.fmtPath(true, &full_path);
|
||||
scan.scan(util.arrayListBufZ(&full_path)) catch {
|
||||
var full_path: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer full_path.deinit(allocator);
|
||||
mem_sink.global.root.?.fmtPath(allocator, true, &full_path);
|
||||
scan.scan(util.arrayListBufZ(&full_path, allocator)) catch {
|
||||
sink.global.last_error = allocator.dupeZ(u8, full_path.items) catch unreachable;
|
||||
sink.global.state = .err;
|
||||
while (state == .refresh) handleEvent(true, true);
|
||||
|
|
@ -671,13 +604,18 @@ pub fn main() void {
|
|||
browser.loadDir(0);
|
||||
},
|
||||
.shell => {
|
||||
spawnShell();
|
||||
const shell = std.posix.getenvZ("NCDU_SHELL") orelse std.posix.getenvZ("SHELL") orelse "/bin/sh";
|
||||
var env = std.process.getEnvMap(allocator) catch unreachable;
|
||||
defer env.deinit();
|
||||
ui.runCmd(&.{shell}, browser.dir_path, &env, false);
|
||||
state = .browse;
|
||||
},
|
||||
.delete => {
|
||||
const next = delete.delete();
|
||||
state = .browse;
|
||||
browser.loadDir(if (next) |n| n.nameHash() else 0);
|
||||
if (state != .refresh) {
|
||||
state = .browse;
|
||||
browser.loadDir(if (next) |n| n.nameHash() else 0);
|
||||
}
|
||||
},
|
||||
else => handleEvent(true, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,24 @@ pub const Thread = struct {
|
|||
arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator),
|
||||
};
|
||||
|
||||
pub fn statToEntry(stat: *const sink.Stat, e: *model.Entry, parent: *model.Dir) void {
|
||||
e.pack.blocks = stat.blocks;
|
||||
e.size = stat.size;
|
||||
if (e.dir()) |d| {
|
||||
d.parent = parent;
|
||||
d.pack.dev = model.devices.getId(stat.dev);
|
||||
}
|
||||
if (e.link()) |l| {
|
||||
l.parent = parent;
|
||||
l.ino = stat.ino;
|
||||
l.pack.nlink = stat.nlink;
|
||||
model.inodes.lock.lock();
|
||||
defer model.inodes.lock.unlock();
|
||||
l.addLink();
|
||||
}
|
||||
if (e.ext()) |ext| ext.* = stat.ext;
|
||||
}
|
||||
|
||||
pub const Dir = struct {
|
||||
dir: *model.Dir,
|
||||
entries: Map,
|
||||
|
|
@ -107,21 +125,7 @@ pub const Dir = struct {
|
|||
}
|
||||
|
||||
const e = self.getEntry(t, stat.etype, main.config.extended and !stat.ext.isEmpty(), name);
|
||||
e.pack.blocks = stat.blocks;
|
||||
e.size = stat.size;
|
||||
if (e.dir()) |d| {
|
||||
d.parent = self.dir;
|
||||
d.pack.dev = model.devices.getId(stat.dev);
|
||||
}
|
||||
if (e.link()) |l| {
|
||||
l.parent = self.dir;
|
||||
l.ino = stat.ino;
|
||||
l.pack.nlink = stat.nlink;
|
||||
model.inodes.lock.lock();
|
||||
defer model.inodes.lock.unlock();
|
||||
l.addLink();
|
||||
}
|
||||
if (e.ext()) |ext| ext.* = stat.ext;
|
||||
statToEntry(stat, e, self.dir);
|
||||
return e;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,10 +60,10 @@ pub fn run(d: *model.Dir) void {
|
|||
.sink = &sink_threads[0],
|
||||
.stat = toStat(&d.entry),
|
||||
};
|
||||
var buf = std.ArrayList(u8).init(main.allocator);
|
||||
d.fmtPath(true, &buf);
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
d.fmtPath(main.allocator, true, &buf);
|
||||
const root = sink.createRoot(buf.items, &ctx.stat);
|
||||
buf.deinit();
|
||||
buf.deinit(main.allocator);
|
||||
|
||||
var it = d.sub.ptr;
|
||||
while (it) |e| : (it = e.next.ptr) rec(&ctx, root, e);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ pub const Entry = extern struct {
|
|||
fn alloc(comptime T: type, allocator: std.mem.Allocator, etype: EType, isext: bool, ename: []const u8) *Entry {
|
||||
const size = (if (isext) @as(usize, @sizeOf(Ext)) else 0) + @sizeOf(T) + ename.len + 1;
|
||||
var ptr = blk: while (true) {
|
||||
if (allocator.allocWithOptions(u8, size, 1, null)) |p| break :blk p
|
||||
const alignment = if (@typeInfo(@TypeOf(std.mem.Allocator.allocWithOptions)).@"fn".params[3].type == ?u29) 1 else std.mem.Alignment.@"1";
|
||||
if (allocator.allocWithOptions(u8, size, alignment, null)) |p| break :blk p
|
||||
else |_| {}
|
||||
ui.oom();
|
||||
};
|
||||
|
|
@ -217,19 +218,20 @@ pub const Dir = extern struct {
|
|||
suberr: bool = false,
|
||||
};
|
||||
|
||||
pub fn fmtPath(self: *const @This(), withRoot: bool, out: *std.ArrayList(u8)) void {
|
||||
pub fn fmtPath(self: *const @This(), alloc: std.mem.Allocator, withRoot: bool, out: *std.ArrayListUnmanaged(u8)) void {
|
||||
if (!withRoot and self.parent == null) return;
|
||||
var components = std.ArrayList([:0]const u8).init(main.allocator);
|
||||
defer components.deinit();
|
||||
var components: std.ArrayListUnmanaged([:0]const u8) = .empty;
|
||||
defer components.deinit(main.allocator);
|
||||
var it: ?*const @This() = self;
|
||||
while (it) |e| : (it = e.parent)
|
||||
if (withRoot or e.parent != null)
|
||||
components.append(e.entry.name()) catch unreachable;
|
||||
components.append(main.allocator, e.entry.name()) catch unreachable;
|
||||
|
||||
var i: usize = components.items.len-1;
|
||||
while (true) {
|
||||
if (i != components.items.len-1 and !(out.items.len != 0 and out.items[out.items.len-1] == '/')) out.append('/') catch unreachable;
|
||||
out.appendSlice(components.items[i]) catch unreachable;
|
||||
if (i != components.items.len-1 and !(out.items.len != 0 and out.items[out.items.len-1] == '/'))
|
||||
out.append(main.allocator, '/') catch unreachable;
|
||||
out.appendSlice(alloc, components.items[i]) catch unreachable;
|
||||
if (i == 0) break;
|
||||
i -= 1;
|
||||
}
|
||||
|
|
@ -271,11 +273,11 @@ pub const Link = extern struct {
|
|||
|
||||
// Return value should be freed with main.allocator.
|
||||
pub fn path(self: *const @This(), withRoot: bool) [:0]const u8 {
|
||||
var out = std.ArrayList(u8).init(main.allocator);
|
||||
self.parent.fmtPath(withRoot, &out);
|
||||
out.append('/') catch unreachable;
|
||||
out.appendSlice(self.entry.name()) catch unreachable;
|
||||
return out.toOwnedSliceSentinel(0) catch unreachable;
|
||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
||||
self.parent.fmtPath(main.allocator, withRoot, &out);
|
||||
out.append(main.allocator, '/') catch unreachable;
|
||||
out.appendSlice(main.allocator, self.entry.name()) catch unreachable;
|
||||
return out.toOwnedSliceSentinel(main.allocator, 0) catch unreachable;
|
||||
}
|
||||
|
||||
// Add this link to the inodes map and mark it as 'uncounted'.
|
||||
|
|
@ -350,7 +352,7 @@ pub const Ext = extern struct {
|
|||
pub const devices = struct {
|
||||
var lock = std.Thread.Mutex{};
|
||||
// id -> dev
|
||||
pub var list = std.ArrayList(u64).init(main.allocator);
|
||||
pub var list: std.ArrayListUnmanaged(u64) = .empty;
|
||||
// dev -> id
|
||||
var lookup = std.AutoHashMap(u64, DevId).init(main.allocator);
|
||||
|
||||
|
|
@ -361,7 +363,7 @@ pub const devices = struct {
|
|||
if (!d.found_existing) {
|
||||
if (list.items.len >= std.math.maxInt(DevId)) ui.die("Maximum number of device identifiers exceeded.\n", .{});
|
||||
d.value_ptr.* = @as(DevId, @intCast(list.items.len));
|
||||
list.append(dev) catch unreachable;
|
||||
list.append(main.allocator, dev) catch unreachable;
|
||||
}
|
||||
return d.value_ptr.*;
|
||||
}
|
||||
|
|
|
|||
30
src/scan.zig
30
src/scan.zig
|
|
@ -46,14 +46,24 @@ fn truncate(comptime T: type, comptime field: anytype, x: anytype) std.meta.fiel
|
|||
}
|
||||
|
||||
|
||||
fn statAt(parent: std.fs.Dir, name: [:0]const u8, follow: bool, symlink: *bool) !sink.Stat {
|
||||
const stat = try std.posix.fstatatZ(parent.fd, name, if (follow) 0 else std.posix.AT.SYMLINK_NOFOLLOW);
|
||||
symlink.* = std.posix.S.ISLNK(stat.mode);
|
||||
pub fn statAt(parent: std.fs.Dir, name: [:0]const u8, follow: bool, symlink: ?*bool) !sink.Stat {
|
||||
// std.posix.fstatatZ() in Zig 0.14 is not suitable due to https://github.com/ziglang/zig/issues/23463
|
||||
var stat: std.c.Stat = undefined;
|
||||
if (std.c.fstatat(parent.fd, name, &stat, if (follow) 0 else std.c.AT.SYMLINK_NOFOLLOW) != 0) {
|
||||
return switch (std.c._errno().*) {
|
||||
@intFromEnum(std.c.E.NOENT) => error.FileNotFound,
|
||||
@intFromEnum(std.c.E.NAMETOOLONG) => error.NameTooLong,
|
||||
@intFromEnum(std.c.E.NOMEM) => error.OutOfMemory,
|
||||
@intFromEnum(std.c.E.ACCES) => error.AccessDenied,
|
||||
else => error.Unexpected,
|
||||
};
|
||||
}
|
||||
if (symlink) |s| s.* = std.c.S.ISLNK(stat.mode);
|
||||
return sink.Stat{
|
||||
.etype =
|
||||
if (std.posix.S.ISDIR(stat.mode)) .dir
|
||||
if (std.c.S.ISDIR(stat.mode)) .dir
|
||||
else if (stat.nlink > 1) .link
|
||||
else if (!std.posix.S.ISREG(stat.mode)) .nonreg
|
||||
else if (!std.c.S.ISREG(stat.mode)) .nonreg
|
||||
else .reg,
|
||||
.blocks = clamp(sink.Stat, .blocks, stat.blocks),
|
||||
.size = clamp(sink.Stat, .size, stat.size),
|
||||
|
|
@ -81,7 +91,7 @@ fn isCacheDir(dir: std.fs.Dir) bool {
|
|||
const f = dir.openFileZ("CACHEDIR.TAG", .{}) catch return false;
|
||||
defer f.close();
|
||||
var buf: [sig.len]u8 = undefined;
|
||||
const len = f.reader().readAll(&buf) catch return false;
|
||||
const len = f.readAll(&buf) catch return false;
|
||||
return len == sig.len and std.mem.eql(u8, &buf, sig);
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +184,7 @@ const Thread = struct {
|
|||
thread_num: usize,
|
||||
sink: *sink.Thread,
|
||||
state: *State,
|
||||
stack: std.ArrayList(*Dir) = std.ArrayList(*Dir).init(main.allocator),
|
||||
stack: std.ArrayListUnmanaged(*Dir) = .empty,
|
||||
thread: std.Thread = undefined,
|
||||
namebuf: [4096]u8 = undefined,
|
||||
|
||||
|
|
@ -255,13 +265,13 @@ const Thread = struct {
|
|||
const s = dir.sink.addDir(t.sink, name, &stat);
|
||||
const ndir = Dir.create(edir, stat.dev, dir.pat.enter(name), s);
|
||||
if (main.config.threads == 1 or !t.state.tryPush(ndir))
|
||||
t.stack.append(ndir) catch unreachable;
|
||||
t.stack.append(main.allocator, ndir) catch unreachable;
|
||||
}
|
||||
|
||||
fn run(t: *Thread) void {
|
||||
defer t.stack.deinit();
|
||||
defer t.stack.deinit(main.allocator);
|
||||
while (t.state.waitPop()) |dir| {
|
||||
t.stack.append(dir) catch unreachable;
|
||||
t.stack.append(main.allocator, dir) catch unreachable;
|
||||
|
||||
while (t.stack.items.len > 0) {
|
||||
const d = t.stack.items[t.stack.items.len - 1];
|
||||
|
|
|
|||
58
src/sink.zig
58
src/sink.zig
|
|
@ -140,20 +140,21 @@ pub const Dir = struct {
|
|||
}
|
||||
|
||||
fn path(d: *Dir) [:0]u8 {
|
||||
var components = std.ArrayList([]const u8).init(main.allocator);
|
||||
defer components.deinit();
|
||||
var components: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
defer components.deinit(main.allocator);
|
||||
var it: ?*Dir = d;
|
||||
while (it) |e| : (it = e.parent) components.append(e.name) catch unreachable;
|
||||
while (it) |e| : (it = e.parent) components.append(main.allocator, e.name) catch unreachable;
|
||||
|
||||
var out = std.ArrayList(u8).init(main.allocator);
|
||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
||||
var i: usize = components.items.len-1;
|
||||
while (true) {
|
||||
if (i != components.items.len-1 and !(out.items.len != 0 and out.items[out.items.len-1] == '/')) out.append('/') catch unreachable;
|
||||
out.appendSlice(components.items[i]) catch unreachable;
|
||||
if (i != components.items.len-1 and !(out.items.len != 0 and out.items[out.items.len-1] == '/'))
|
||||
out.append(main.allocator, '/') catch unreachable;
|
||||
out.appendSlice(main.allocator, components.items[i]) catch unreachable;
|
||||
if (i == 0) break;
|
||||
i -= 1;
|
||||
}
|
||||
return out.toOwnedSliceSentinel(0) catch unreachable;
|
||||
return out.toOwnedSliceSentinel(main.allocator, 0) catch unreachable;
|
||||
}
|
||||
|
||||
fn ref(d: *Dir) void {
|
||||
|
|
@ -291,7 +292,7 @@ fn drawConsole() void {
|
|||
var ansi: ?bool = null;
|
||||
var lines_written: usize = 0;
|
||||
};
|
||||
const stderr = std.io.getStdErr();
|
||||
const stderr = if (@hasDecl(std.io, "getStdErr")) std.io.getStdErr() else std.fs.File.stderr();
|
||||
const ansi = st.ansi orelse blk: {
|
||||
const t = stderr.supportsAnsiEscapeCodes();
|
||||
st.ansi = t;
|
||||
|
|
@ -450,25 +451,28 @@ pub fn draw() void {
|
|||
switch (main.config.scan_ui.?) {
|
||||
.none => {},
|
||||
.line => drawConsole(),
|
||||
.full => switch (global.state) {
|
||||
.done => {},
|
||||
.err => drawError(),
|
||||
.zeroing => {
|
||||
const box = ui.Box.create(4, ui.cols -| 5, "Initializing");
|
||||
box.move(2, 2);
|
||||
ui.addstr("Clearing directory counts...");
|
||||
},
|
||||
.hlcnt => {
|
||||
const box = ui.Box.create(4, ui.cols -| 5, "Finalizing");
|
||||
box.move(2, 2);
|
||||
ui.addstr("Counting hardlinks... ");
|
||||
if (model.inodes.add_total > 0) {
|
||||
ui.addnum(.default, model.inodes.add_done);
|
||||
ui.addstr(" / ");
|
||||
ui.addnum(.default, model.inodes.add_total);
|
||||
}
|
||||
},
|
||||
.running => drawProgress(),
|
||||
.full => {
|
||||
ui.init();
|
||||
switch (global.state) {
|
||||
.done => {},
|
||||
.err => drawError(),
|
||||
.zeroing => {
|
||||
const box = ui.Box.create(4, ui.cols -| 5, "Initializing");
|
||||
box.move(2, 2);
|
||||
ui.addstr("Clearing directory counts...");
|
||||
},
|
||||
.hlcnt => {
|
||||
const box = ui.Box.create(4, ui.cols -| 5, "Finalizing");
|
||||
box.move(2, 2);
|
||||
ui.addstr("Counting hardlinks... ");
|
||||
if (model.inodes.add_total > 0) {
|
||||
ui.addnum(.default, model.inodes.add_done);
|
||||
ui.addstr(" / ");
|
||||
ui.addnum(.default, model.inodes.add_total);
|
||||
}
|
||||
},
|
||||
.running => drawProgress(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
src/ui.zig
82
src/ui.zig
|
|
@ -17,8 +17,7 @@ pub var cols: u32 = undefined;
|
|||
|
||||
pub fn die(comptime fmt: []const u8, args: anytype) noreturn {
|
||||
deinit();
|
||||
const stderr = std.io.getStdErr();
|
||||
stderr.writer().print(fmt, args) catch {};
|
||||
std.debug.print(fmt, args);
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +26,8 @@ pub fn quit() noreturn {
|
|||
std.process.exit(0);
|
||||
}
|
||||
|
||||
const sleep = if (@hasDecl(std.time, "sleep")) std.time.sleep else std.Thread.sleep;
|
||||
|
||||
// Should be called when malloc fails. Will show a message to the user, wait
|
||||
// for a second and return to give it another try.
|
||||
// Glitch: this function may be called while we're in the process of drawing
|
||||
|
|
@ -41,14 +42,13 @@ pub fn oom() void {
|
|||
if (main_thread == std.Thread.getCurrentId()) {
|
||||
const haveui = inited;
|
||||
deinit();
|
||||
const stderr = std.io.getStdErr();
|
||||
stderr.writeAll("\x1b7\x1b[JOut of memory, trying again in 1 second. Hit Ctrl-C to abort.\x1b8") catch {};
|
||||
std.time.sleep(std.time.ns_per_s);
|
||||
std.debug.print("\x1b7\x1b[JOut of memory, trying again in 1 second. Hit Ctrl-C to abort.\x1b8", .{});
|
||||
sleep(std.time.ns_per_s);
|
||||
if (haveui)
|
||||
init();
|
||||
} else {
|
||||
_ = oom_threads.fetchAdd(1, .monotonic);
|
||||
std.time.sleep(std.time.ns_per_s);
|
||||
sleep(std.time.ns_per_s);
|
||||
_ = oom_threads.fetchSub(1, .monotonic);
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ pub fn errorString(e: anyerror) [:0]const u8 {
|
|||
};
|
||||
}
|
||||
|
||||
var to_utf8_buf = std.ArrayList(u8).init(main.allocator);
|
||||
var to_utf8_buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
|
||||
fn toUtf8BadChar(ch: u8) bool {
|
||||
return switch (ch) {
|
||||
|
|
@ -107,19 +107,19 @@ pub fn toUtf8(in: [:0]const u8) [:0]const u8 {
|
|||
if (std.unicode.utf8ByteSequenceLength(in[i])) |cp_len| {
|
||||
if (!toUtf8BadChar(in[i]) and i + cp_len <= in.len) {
|
||||
if (std.unicode.utf8Decode(in[i .. i + cp_len])) |_| {
|
||||
to_utf8_buf.appendSlice(in[i .. i + cp_len]) catch unreachable;
|
||||
to_utf8_buf.appendSlice(main.allocator, in[i .. i + cp_len]) catch unreachable;
|
||||
i += cp_len;
|
||||
continue;
|
||||
} else |_| {}
|
||||
}
|
||||
} else |_| {}
|
||||
to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]}) catch unreachable;
|
||||
to_utf8_buf.writer(main.allocator).print("\\x{X:0>2}", .{in[i]}) catch unreachable;
|
||||
i += 1;
|
||||
}
|
||||
return util.arrayListBufZ(&to_utf8_buf);
|
||||
return util.arrayListBufZ(&to_utf8_buf, main.allocator);
|
||||
}
|
||||
|
||||
var shorten_buf = std.ArrayList(u8).init(main.allocator);
|
||||
var shorten_buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
|
||||
// Shorten the given string to fit in the given number of columns.
|
||||
// If the string is too long, only the prefix and suffix will be printed, with '...' in between.
|
||||
|
|
@ -150,8 +150,8 @@ pub fn shorten(in: [:0]const u8, max_width: u32) [:0] const u8 {
|
|||
if (total_width <= max_width) return in;
|
||||
|
||||
shorten_buf.shrinkRetainingCapacity(0);
|
||||
shorten_buf.appendSlice(in[0..prefix_end]) catch unreachable;
|
||||
shorten_buf.appendSlice("...") catch unreachable;
|
||||
shorten_buf.appendSlice(main.allocator, in[0..prefix_end]) catch unreachable;
|
||||
shorten_buf.appendSlice(main.allocator, "...") catch unreachable;
|
||||
|
||||
var start_width: u32 = prefix_width;
|
||||
var start_len: u32 = prefix_end;
|
||||
|
|
@ -163,11 +163,11 @@ pub fn shorten(in: [:0]const u8, max_width: u32) [:0] const u8 {
|
|||
start_width += cp_width;
|
||||
start_len += cp_len;
|
||||
if (total_width - start_width <= max_width - prefix_width - 3) {
|
||||
shorten_buf.appendSlice(in[start_len..]) catch unreachable;
|
||||
shorten_buf.appendSlice(main.allocator, in[start_len..]) catch unreachable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return util.arrayListBufZ(&shorten_buf);
|
||||
return util.arrayListBufZ(&shorten_buf, main.allocator);
|
||||
}
|
||||
|
||||
fn shortenTest(in: [:0]const u8, max_width: u32, out: [:0]const u8) !void {
|
||||
|
|
@ -335,8 +335,7 @@ fn updateSize() void {
|
|||
fn clearScr() void {
|
||||
// Send a "clear from cursor to end of screen" instruction, to clear a
|
||||
// potential line left behind from scanning in -1 mode.
|
||||
const stderr = std.io.getStdErr();
|
||||
stderr.writeAll("\x1b[J") catch {};
|
||||
std.debug.print("\x1b[J", .{});
|
||||
}
|
||||
|
||||
pub fn init() void {
|
||||
|
|
@ -634,7 +633,7 @@ pub fn getch(block: bool) i32 {
|
|||
}
|
||||
if (ch == c.ERR) {
|
||||
if (!block) return 0;
|
||||
std.time.sleep(10*std.time.ns_per_ms);
|
||||
sleep(10*std.time.ns_per_ms);
|
||||
continue;
|
||||
}
|
||||
return ch;
|
||||
|
|
@ -642,3 +641,50 @@ pub fn getch(block: bool) i32 {
|
|||
die("Error reading keyboard input, assuming TTY has been lost.\n(Potentially nonsensical error message: {s})\n",
|
||||
.{ c.strerror(@intFromEnum(std.posix.errno(-1))) });
|
||||
}
|
||||
|
||||
fn waitInput() void {
|
||||
if (@hasDecl(std.io, "getStdIn")) {
|
||||
std.io.getStdIn().reader().skipUntilDelimiterOrEof('\n') catch unreachable;
|
||||
} else {
|
||||
var buf: [512]u8 = undefined;
|
||||
var rd = std.fs.File.stdin().reader(&buf);
|
||||
_ = rd.interface.discardDelimiterExclusive('\n') catch unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runCmd(cmd: []const []const u8, cwd: ?[]const u8, env: *std.process.EnvMap, reporterr: bool) void {
|
||||
deinit();
|
||||
defer init();
|
||||
|
||||
// NCDU_LEVEL can only count to 9, keeps the implementation simple.
|
||||
if (env.get("NCDU_LEVEL")) |l|
|
||||
env.put("NCDU_LEVEL", if (l.len == 0) "1" else switch (l[0]) {
|
||||
'0'...'8' => |d| &[1] u8{d+1},
|
||||
'9' => "9",
|
||||
else => "1"
|
||||
}) catch unreachable
|
||||
else
|
||||
env.put("NCDU_LEVEL", "1") catch unreachable;
|
||||
|
||||
var child = std.process.Child.init(cmd, main.allocator);
|
||||
child.cwd = cwd;
|
||||
child.env_map = env;
|
||||
|
||||
const term = child.spawnAndWait() catch |e| blk: {
|
||||
std.debug.print("Error running command: {s}\n\nPress enter to continue.\n", .{ ui.errorString(e) });
|
||||
waitInput();
|
||||
break :blk std.process.Child.Term{ .Exited = 0 };
|
||||
};
|
||||
|
||||
const n = switch (term) {
|
||||
.Exited => "error",
|
||||
.Signal => "signal",
|
||||
.Stopped => "stopped",
|
||||
.Unknown => "unknown",
|
||||
};
|
||||
const v = switch (term) { inline else => |v| v };
|
||||
if (term != .Exited or (reporterr and v != 0)) {
|
||||
std.debug.print("\nCommand returned with {s} code {}.\nPress enter to continue.\n", .{ n, v });
|
||||
waitInput();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
src/util.zig
55
src/util.zig
|
|
@ -32,8 +32,8 @@ pub fn blocksToSize(b: u64) u64 {
|
|||
// Ensure the given arraylist buffer gets zero-terminated and returns a slice
|
||||
// into the buffer. The returned buffer is invalidated whenever the arraylist
|
||||
// is freed or written to.
|
||||
pub fn arrayListBufZ(buf: *std.ArrayList(u8)) [:0]const u8 {
|
||||
buf.append(0) catch unreachable;
|
||||
pub fn arrayListBufZ(buf: *std.ArrayListUnmanaged(u8), alloc: std.mem.Allocator) [:0]const u8 {
|
||||
buf.append(alloc, 0) catch unreachable;
|
||||
defer buf.items.len -= 1;
|
||||
return buf.items[0..buf.items.len-1:0];
|
||||
}
|
||||
|
|
@ -196,5 +196,54 @@ pub fn expanduser(path: []const u8, alloc: std.mem.Allocator) ![:0]u8 {
|
|||
const home = std.mem.trimRight(u8, home_raw, "/");
|
||||
|
||||
if (home.len == 0 and path.len == len) return alloc.dupeZ(u8, "/");
|
||||
return try std.fmt.allocPrintZ(alloc, "{s}{s}", .{ home, path[len..] });
|
||||
return try std.mem.concatWithSentinel(alloc, u8, &.{ home, path[len..] }, 0);
|
||||
}
|
||||
|
||||
|
||||
// Silly abstraction to read a file one line at a time. Only exists to help
|
||||
// with supporting both Zig 0.14 and 0.15, can be removed once 0.14 support is
|
||||
// dropped.
|
||||
pub const LineReader = if (@hasDecl(std.io, "bufferedReader")) struct {
|
||||
rd: std.io.BufferedReader(4096, std.fs.File.Reader),
|
||||
fbs: std.io.FixedBufferStream([]u8),
|
||||
|
||||
pub fn init(f: std.fs.File, buf: []u8) @This() {
|
||||
return .{
|
||||
.rd = std.io.bufferedReader(f.reader()),
|
||||
.fbs = std.io.fixedBufferStream(buf),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn read(s: *@This()) !?[]u8 {
|
||||
s.fbs.reset();
|
||||
s.rd.reader().streamUntilDelimiter(s.fbs.writer(), '\n', s.fbs.buffer.len) catch |err| switch (err) {
|
||||
error.EndOfStream => if (s.fbs.getPos() catch unreachable == 0) return null,
|
||||
else => |e| return e,
|
||||
};
|
||||
return s.fbs.getWritten();
|
||||
}
|
||||
|
||||
} else struct {
|
||||
rd: std.fs.File.Reader,
|
||||
|
||||
pub fn init(f: std.fs.File, buf: []u8) @This() {
|
||||
return .{ .rd = f.readerStreaming(buf) };
|
||||
}
|
||||
|
||||
pub fn read(s: *@This()) !?[]u8 {
|
||||
// Can't use takeDelimiter() because that's not available in 0.15.1,
|
||||
// Can't use takeDelimiterExclusive() because that changed behavior in 0.15.2.
|
||||
const r = &s.rd.interface;
|
||||
const result = r.peekDelimiterInclusive('\n') catch |err| switch (err) {
|
||||
error.EndOfStream => {
|
||||
const remaining = r.buffer[r.seek..r.end];
|
||||
if (remaining.len == 0) return null;
|
||||
r.toss(remaining.len);
|
||||
return remaining;
|
||||
},
|
||||
else => |e| return e,
|
||||
};
|
||||
r.toss(result.len);
|
||||
return result[0 .. result.len - 1];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue