Compare commits

..

No commits in common. "zig" and "v2.7" have entirely different histories.
zig ... v2.7

19 changed files with 352 additions and 590 deletions

View file

@ -1,37 +1,6 @@
# 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
- List all supported options in `--help`
- Use `kB` instead of `KB` in `--si` mode
2.7 - 2024-11-19
- Still requires Zig 0.12 or 0.13
- Support transparent reading/writing of zstandard-compressed JSON

View file

@ -19,7 +19,7 @@ C version (1.x).
## Requirements
- Zig 0.14 or 0.15
- Zig 0.12 or 0.13.
- Some sort of POSIX-like OS
- ncurses
- libzstd

View file

@ -7,26 +7,23 @@ 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: target-dependant)");
const pie = b.option(bool, "pie", "Build with PIE support (by default false)") orelse false;
const strip = b.option(bool, "strip", "Strip debugging info (by default false)") orelse false;
const main_mod = b.createModule(.{
const exe = b.addExecutable(.{
.name = "ncdu",
.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;
// https://github.com/ziglang/zig/blob/faccd79ca5debbe22fe168193b8de54393257604/build.zig#L745-L748
if (target.result.os.tag.isDarwin()) {
exe.root_module.linkSystemLibrary("ncursesw", .{});
exe.root_module.linkSystemLibrary("libzstd", .{});
// https://github.com/ziglang/zig/blob/b52be973dfb7d1408218b8e75800a2da3dc69108/build.zig#L551-L554
if (target.result.isDarwin()) {
// useful for package maintainers
exe.headerpad_max_install_names = true;
}
@ -42,9 +39,14 @@ pub fn build(b: *std.Build) void {
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_module = main_mod,
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
unit_tests.pie = pie;
unit_tests.root_module.linkSystemLibrary("ncursesw", .{});
unit_tests.root_module.linkSystemLibrary("libzstd", .{});
const run_unit_tests = b.addRunArtifact(unit_tests);

34
ncdu.1
View file

@ -1,6 +1,6 @@
.\" SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
.\" SPDX-License-Identifier: MIT
.Dd August 16, 2025
.Dd November 19, 2024
.Dt NCDU 1
.Os
.Sh NAME
@ -44,7 +44,6 @@
.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
@ -287,7 +286,7 @@ when given twice it will also add
thus ensuring that there is no way to modify the file system from within
.Nm .
.It Fl \-si , \-no\-si
List sizes using base 10 prefixes, that is, powers of 1000 (kB, MB, etc), as
List sizes using base 10 prefixes, that is, powers of 1000 (KB, MB, etc), as
defined in the International System of Units (SI), instead of the usual base 2
prefixes (KiB, MiB, etc).
.It Fl \-disk\-usage , \-apparent\-size
@ -360,31 +359,6 @@ 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:
@ -418,7 +392,6 @@ is given on the command line.
.Pp
The configuration file format is simply one command line option per line.
Lines starting with '#' are ignored.
A line can be prefixed with '@' to suppress errors while parsing the option.
Example configuration file:
.Bd -literal -offset indent
# Always enable extended mode
@ -429,9 +402,6 @@ Example configuration file:
# Exclude .git directories
\-\-exclude .git
# Read excludes from ~/.ncduexcludes, ignore error if the file does not exist
@--exclude-from ~/.ncduexcludes
.Ed
.
.Sh KEYS

View file

@ -11,12 +11,14 @@ const c = @import("c.zig").c;
pub const global = struct {
var fd: std.fs.File = undefined;
var index: std.ArrayListUnmanaged(u8) = .empty;
var index = std.ArrayList(u8).init(main.allocator);
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) {
@ -49,7 +51,7 @@ pub const ItemKey = enum(u5) {
// Pessimistic upper bound on the encoded size of an item, excluding the name field.
// 2 bytes for map start/end, 11 per field (2 for the key, 9 for a full u64).
const MAX_ITEM_LEN = 2 + 11 * @typeInfo(ItemKey).@"enum".fields.len;
const MAX_ITEM_LEN = 2 + 11 * @typeInfo(ItemKey).Enum.fields.len;
pub const CborMajor = enum(u3) { pos, neg, bytes, text, array, map, tag, simple };
@ -79,15 +81,10 @@ 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 = MAX_BLOCK_SIZE, // pretend we have a full block to trigger a flush() for the first write
off: usize = std.math.maxInt(usize) - (1<<10), // large number to trigger a flush() for the first write
block_num: u32 = std.math.maxInt(u32),
itemref: u64 = 0, // ref of item currently being written
@ -105,11 +102,11 @@ pub const Thread = struct {
}
}
fn createBlock(t: *Thread) std.ArrayListUnmanaged(u8) {
var out: std.ArrayListUnmanaged(u8) = .empty;
fn createBlock(t: *Thread) std.ArrayList(u8) {
var out = std.ArrayList(u8).init(main.allocator);
if (t.block_num == std.math.maxInt(u32) or t.off == 0) return out;
out.ensureTotalCapacityPrecise(main.allocator, 12 + @as(usize, @intCast(c.ZSTD_COMPRESSBOUND(@as(c_int, @intCast(t.off)))))) catch unreachable;
out.ensureTotalCapacityPrecise(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;
@ -121,13 +118,13 @@ pub const Thread = struct {
}
fn flush(t: *Thread, expected_len: usize) void {
@branchHint(.unlikely);
var block = createBlock(t);
defer block.deinit(main.allocator);
@setCold(true);
const block = createBlock(t);
defer block.deinit();
global.lock.lock();
defer global.lock.unlock();
// This can only really happen when the root path exceeds our block size,
// This can only really happen when the root path exceeds 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", .{});
@ -141,7 +138,7 @@ pub const Thread = struct {
t.off = 0;
t.block_num = @intCast((global.index.items.len - 4) / 8);
global.index.appendSlice(main.allocator, &[1]u8{0}**8) catch unreachable;
global.index.appendSlice(&[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);
@ -433,7 +430,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, blockSize(0)) catch unreachable;
t.sink.bin.buf = main.allocator.alloc(u8, BLOCK_SIZE) catch unreachable;
}
return .{ .stat = stat.* };
@ -447,12 +444,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(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.appendSlice(&bigu64(global.root_itemref)) catch unreachable;
global.index.appendSlice(&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(main.allocator);
global.index.clearAndFree();
global.fd.close();
}
@ -464,5 +461,5 @@ pub fn setupOutput(fd: std.fs.File) void {
global.file_off = 8;
// Placeholder for the index block header.
global.index.appendSlice(main.allocator, "aaaa") catch unreachable;
global.index.appendSlice("aaaa") catch unreachable;
}

View file

@ -63,7 +63,7 @@ inline fn bigu32(v: [4]u8) u32 { return std.mem.bigToNative(u32, @bitCast(v)); }
inline fn bigu64(v: [8]u8) u64 { return std.mem.bigToNative(u64, @bitCast(v)); }
fn die() noreturn {
@branchHint(.cold);
@setCold(true);
if (global.lastitem) |e| ui.die("Error reading item {x} from file\n", .{e})
else ui.die("Error reading from file\n", .{});
}
@ -338,7 +338,7 @@ const ItemParser = struct {
// Skips over any fields that don't fit into an ItemKey.
fn next(r: *ItemParser) ?Field {
while (r.key()) |k| {
if (k.major == .pos and k.arg <= std.math.maxInt(@typeInfo(ItemKey).@"enum".tag_type)) return .{
if (k.major == .pos and k.arg <= std.math.maxInt(@typeInfo(ItemKey).Enum.tag_type)) return .{
.key = @enumFromInt(k.arg),
.val = r.r.next(),
} else {
@ -504,9 +504,7 @@ pub fn import() void {
pub fn open(fd: std.fs.File) !void {
global.fd = fd;
// Do not use fd.getEndPos() because that requires newer kernels supporting statx() #261.
try fd.seekFromEnd(0);
const size = try fd.getPos();
const size = try fd.getEndPos();
if (size < 16) return error.EndOfStream;
// Read index block

View file

@ -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.ArrayListUnmanaged(model.Ref) = .empty;
var dir_parents = std.ArrayList(model.Ref).init(main.allocator);
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.ArrayListUnmanaged(struct { ptr: *model.Dir, ref: u64 }) = .empty;
var dir_refs = std.ArrayList(struct { ptr: *model.Dir, ref: u64 }).init(main.allocator);
// 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.ArrayListUnmanaged(?*model.Entry) = .empty;
var dir_items = std.ArrayList(?*model.Entry).init(main.allocator);
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(main.allocator, null) catch unreachable;
dir_items.append(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(main.allocator, e) catch unreachable;
dir_items.append(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(main.allocator, .{ .ptr = d, .ref = ref.ref }) catch unreachable;
if (main.config.binreader) dir_refs.append(.{ .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(main.allocator, .{ .ref = ref }) catch unreachable;
dir_parents.append(.{ .ref = ref }) catch unreachable;
} else {
dir_parent = model.root;
dir_parents.append(main.allocator, .{ .ptr = &dir_parent.entry }) catch unreachable;
dir_parents.append(.{ .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(main.allocator, .{ .ref = ref }) catch unreachable;
dir_parents.append(.{ .ref = ref }) catch unreachable;
} else {
dir_parent = e;
dir_parents.append(main.allocator, .{ .ptr = &e.entry }) catch unreachable;
dir_parents.append(.{ .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.ArrayListUnmanaged(*model.Link) = null;
var links: ?std.ArrayList(*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(main.allocator);
if (links) |*l| l.deinit();
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.ArrayListUnmanaged(*model.Link) = .empty;
var list = std.ArrayList(*model.Link).init(main.allocator);
var l = e.?.link().?;
while (true) {
list.append(main.allocator, l) catch unreachable;
list.append(l) catch unreachable;
l = l.next;
if (&l.entry == e)
break;

View file

@ -6,9 +6,6 @@ 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;
@ -71,57 +68,6 @@ 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)
@ -136,46 +82,30 @@ pub fn delete() ?*model.Entry {
if (it.* == entry)
break;
var path: std.ArrayListUnmanaged(u8) = .empty;
defer path.deinit(main.allocator);
parent.fmtPath(main.allocator, true, &path);
var path = std.ArrayList(u8).init(main.allocator);
defer path.deinit();
parent.fmtPath(true, &path);
if (path.items.len == 0 or path.items[path.items.len-1] != '/')
path.append(main.allocator, '/') catch unreachable;
path.appendSlice(main.allocator, entry.name()) catch unreachable;
path.append('/') catch unreachable;
path.appendSlice(entry.name()) catch unreachable;
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.*;
}
_ = deleteItem(std.fs.cwd(), util.arrayListBufZ(&path), it);
model.inodes.addAllStats();
return if (it.* == e) e else next_sel;
}
fn drawConfirm() void {
browser.draw();
const box = ui.Box.create(6, 60, "Confirm delete");
box.move(1, 2);
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('"');
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?");
}
box.move(4, 15);
@ -189,25 +119,20 @@ 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.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;
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;
// 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, main.allocator)), 56));
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path)), 56));
box.move(4, 41);
ui.addstr("Press ");
ui.style(.key);
@ -217,16 +142,16 @@ fn drawProgress() void {
}
fn drawErr() void {
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;
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;
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, main.allocator)), 41));
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path)), 41));
box.move(2, 4);
ui.addstr(ui.errorString(error_code));

View file

@ -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) = .empty,
wild: std.ArrayListUnmanaged(*const Pattern) = .{},
// 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

View file

@ -73,7 +73,7 @@ pub const Writer = struct {
dir_entry_open: bool = false,
fn flush(ctx: *Writer, bytes: usize) void {
@branchHint(.unlikely);
@setCold(true);
// This can only really happen when the root path exceeds PATH_MAX,
// in which case we would probably have error'ed out earlier anyway.
if (bytes > ctx.buf.len) ui.die("Error writing JSON export: path too long.\n", .{});
@ -126,14 +126,14 @@ pub const Writer = struct {
var index: usize = buf.len;
while (a >= 100) : (a = @divTrunc(a, 100)) {
index -= 2;
buf[index..][0..2].* = std.fmt.digits2(@as(u8, @intCast(a % 100)));
buf[index..][0..2].* = std.fmt.digits2(@as(usize, @intCast(a % 100)));
}
if (a < 10) {
index -= 1;
buf[index] = '0' + @as(u8, @intCast(a));
} else {
index -= 2;
buf[index..][0..2].* = std.fmt.digits2(@as(u8, @intCast(a)));
buf[index..][0..2].* = std.fmt.digits2(@as(usize, @intCast(a)));
}
ctx.write(buf[index..]);
}

View file

@ -83,6 +83,7 @@ const Parser = struct {
}
fn fill(p: *Parser) void {
@setCold(true);
p.rdoff = 0;
p.rdsize = (if (p.zstd) |z| z.read(p.rd, &p.buf) else p.rd.read(&p.buf)) catch |e| switch (e) {
error.IsDir => p.die("not a file"), // should be detected at open() time, but no flag for that...
@ -97,7 +98,6 @@ const Parser = struct {
// (Returning a '?u8' here is nicer but kills performance by about +30%)
fn nextByte(p: *Parser) u8 {
if (p.rdoff == p.rdsize) {
@branchHint(.unlikely);
p.fill();
if (p.rdsize == 0) return 0;
}
@ -133,7 +133,7 @@ const Parser = struct {
}
fn stringContentSlow(p: *Parser, buf: []u8, head: u8, off: usize) []u8 {
@branchHint(.unlikely);
@setCold(true);
var b = head;
var n = off;
while (true) {

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
pub const program_version = "2.9.2";
pub const program_version = "2.7";
const std = @import("std");
const model = @import("model.zig");
@ -41,7 +41,7 @@ test "imports" {
// This allocator never returns an error, it either succeeds or causes ncdu to quit.
// (Which means you'll find a lot of "catch unreachable" sprinkled through the code,
// they look scarier than they are)
fn wrapAlloc(_: *anyopaque, len: usize, ptr_alignment: std.mem.Alignment, return_address: usize) ?[*]u8 {
fn wrapAlloc(_: *anyopaque, len: usize, ptr_alignment: u8, return_address: usize) ?[*]u8 {
while (true) {
if (std.heap.c_allocator.vtable.alloc(undefined, len, ptr_alignment, return_address)) |r|
return r
@ -56,20 +56,18 @@ pub const allocator = std.mem.Allocator{
.alloc = wrapAlloc,
// AFAIK, all uses of resize() to grow an allocation will fall back to alloc() on failure.
.resize = std.heap.c_allocator.vtable.resize,
.remap = std.heap.c_allocator.vtable.remap,
.free = std.heap.c_allocator.vtable.free,
},
};
// Custom panic impl to reset the terminal before spewing out an error message.
pub const panic = std.debug.FullPanic(struct {
pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
@branchHint(.cold);
ui.deinit();
std.debug.defaultPanic(msg, first_trace_addr);
}
}.panicFn);
pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
@setCold(true);
ui.deinit();
std.debug.panicImpl(error_return_trace, ret_addr orelse @returnAddress(), msg);
}
pub const config = struct {
pub const SortCol = enum { name, blocks, size, items, mtime };
@ -80,6 +78,7 @@ 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;
@ -113,14 +112,10 @@ 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,
@ -129,7 +124,6 @@ const Args = struct {
last_arg: ?[:0]const u8 = null, // In the case of --option=<arg>
shortbuf: [2]u8 = undefined,
argsep: bool = false,
ignerror: bool = false,
const Self = @This();
const Option = struct {
@ -159,27 +153,22 @@ const Args = struct {
return .{ .opt = true, .val = &self.shortbuf };
}
pub fn die(self: *const Self, comptime msg: []const u8, args: anytype) !noreturn {
if (self.ignerror) return error.InvalidArg;
ui.die(msg, args);
}
/// Return the next option or positional argument.
/// 'opt' indicates whether it's an option or positional argument,
/// 'val' will be either -x, --something or the argument.
pub fn next(self: *Self) !?Option {
if (self.last_arg != null) try self.die("Option '{s}' does not expect an argument.\n", .{ self.last.? });
pub fn next(self: *Self) ?Option {
if (self.last_arg != null) ui.die("Option '{s}' does not expect an argument.\n", .{ self.last.? });
if (self.short) |s| return self.shortopt(s);
const val = self.pop() orelse return null;
if (self.argsep or val.len == 0 or val[0] != '-') return Option{ .opt = false, .val = val };
if (val.len == 1) try self.die("Invalid option '-'.\n", .{});
if (val.len == 1) ui.die("Invalid option '-'.\n", .{});
if (val.len == 2 and val[1] == '-') {
self.argsep = true;
return self.next();
}
if (val[1] == '-') {
if (std.mem.indexOfScalar(u8, val, '=')) |sep| {
if (sep == 2) try self.die("Invalid option '{s}'.\n", .{val});
if (sep == 2) ui.die("Invalid option '{s}'.\n", .{val});
self.last_arg = val[sep+1.. :0];
self.last = val[0..sep];
return Option{ .opt = true, .val = self.last.? };
@ -191,7 +180,7 @@ const Args = struct {
}
/// Returns the argument given to the last returned option. Dies with an error if no argument is provided.
pub fn arg(self: *Self) ![:0]const u8 {
pub fn arg(self: *Self) [:0]const u8 {
if (self.short) |a| {
defer self.short = null;
return a;
@ -201,11 +190,11 @@ const Args = struct {
return a;
}
if (self.pop()) |o| return o;
try self.die("Option '{s}' requires an argument.\n", .{ self.last.? });
ui.die("Option '{s}' requires an argument.\n", .{ self.last.? });
}
};
fn argConfig(args: *Args, opt: Args.Option, infile: bool) !void {
fn argConfig(args: *Args, opt: Args.Option, infile: bool) bool {
if (opt.is("-q") or opt.is("--slow-ui-updates")) config.update_delay = 2*std.time.ns_per_s
else if (opt.is("--fast-ui-updates")) config.update_delay = 100*std.time.ns_per_ms
else if (opt.is("-x") or opt.is("--one-file-system")) config.same_fs = true
@ -235,13 +224,13 @@ fn argConfig(args: *Args, opt: Args.Option, infile: bool) !void {
else if (opt.is("--enable-natsort")) config.sort_natural = true
else if (opt.is("--disable-natsort")) config.sort_natural = false
else if (opt.is("--graph-style")) {
const val = try args.arg();
const val = args.arg();
if (std.mem.eql(u8, val, "hash")) config.graph_style = .hash
else if (std.mem.eql(u8, val, "half-block")) config.graph_style = .half
else if (std.mem.eql(u8, val, "eighth-block") or std.mem.eql(u8, val, "eigth-block")) config.graph_style = .eighth
else try args.die("Unknown --graph-style option: {s}.\n", .{val});
else ui.die("Unknown --graph-style option: {s}.\n", .{val});
} else if (opt.is("--sort")) {
var val: []const u8 = try args.arg();
var val: []const u8 = args.arg();
var ord: ?config.SortOrder = null;
if (std.mem.endsWith(u8, val, "-asc")) {
val = val[0..val.len-4];
@ -265,13 +254,13 @@ fn argConfig(args: *Args, opt: Args.Option, infile: bool) !void {
} else if (std.mem.eql(u8, val, "mtime")) {
config.sort_col = .mtime;
config.sort_order = ord orelse .asc;
} else try args.die("Unknown --sort option: {s}.\n", .{val});
} else ui.die("Unknown --sort option: {s}.\n", .{val});
} else if (opt.is("--shared-column")) {
const val = try args.arg();
const val = args.arg();
if (std.mem.eql(u8, val, "off")) config.show_shared = .off
else if (std.mem.eql(u8, val, "shared")) config.show_shared = .shared
else if (std.mem.eql(u8, val, "unique")) config.show_shared = .unique
else try args.die("Unknown --shared-column option: {s}.\n", .{val});
else ui.die("Unknown --shared-column option: {s}.\n", .{val});
} else if (opt.is("--apparent-size")) config.show_blocks = false
else if (opt.is("--disk-usage")) config.show_blocks = true
else if (opt.is("-0")) config.scan_ui = .none
@ -282,13 +271,13 @@ fn argConfig(args: *Args, opt: Args.Option, infile: bool) !void {
else if (opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true
else if (opt.is("--no-follow-symlinks")) config.follow_symlinks = false
else if (opt.is("--exclude")) {
const arg = if (infile) (util.expanduser(try args.arg(), allocator) catch unreachable) else try args.arg();
const arg = if (infile) (util.expanduser(args.arg(), allocator) catch unreachable) else args.arg();
defer if (infile) allocator.free(arg);
exclude.addPattern(arg);
} else if (opt.is("-X") or opt.is("--exclude-from")) {
const arg = if (infile) (util.expanduser(try args.arg(), allocator) catch unreachable) else try args.arg();
const arg = if (infile) (util.expanduser(args.arg(), allocator) catch unreachable) else args.arg();
defer if (infile) allocator.free(arg);
readExcludeFile(arg) catch |e| try args.die("Error reading excludes from {s}: {s}.\n", .{ arg, ui.errorString(e) });
readExcludeFile(arg) catch |e| ui.die("Error reading excludes from {s}: {s}.\n", .{ arg, ui.errorString(e) });
} else if (opt.is("--exclude-caches")) config.exclude_caches = true
else if (opt.is("--include-caches")) config.exclude_caches = false
else if (opt.is("--exclude-kernfs")) config.exclude_kernfs = true
@ -296,30 +285,29 @@ fn argConfig(args: *Args, opt: Args.Option, infile: bool) !void {
else if (opt.is("-c") or opt.is("--compress")) config.compress = true
else if (opt.is("--no-compress")) config.compress = false
else if (opt.is("--compress-level")) {
const val = try args.arg();
const num = std.fmt.parseInt(u8, val, 10) catch try args.die("Invalid number for --compress-level: {s}.\n", .{val});
if (num <= 0 or num > 20) try args.die("Invalid number for --compress-level: {s}.\n", .{val});
config.complevel = num;
const val = args.arg();
config.complevel = std.fmt.parseInt(u8, val, 10) catch ui.die("Invalid number for --compress-level: {s}.\n", .{val});
if (config.complevel <= 0 or config.complevel > 20) ui.die("Invalid number for --compress-level: {s}.\n", .{val});
} else if (opt.is("--export-block-size")) {
const val = try args.arg();
const num = std.fmt.parseInt(u14, val, 10) catch try args.die("Invalid number for --export-block-size: {s}.\n", .{val});
if (num < 4 or num > 16000) try args.die("Invalid number for --export-block-size: {s}.\n", .{val});
const val = args.arg();
const num = std.fmt.parseInt(u14, val, 10) catch ui.die("Invalid number for --export-block-size: {s}.\n", .{val});
if (num < 4 or num > 16000) ui.die("Invalid number for --export-block-size: {s}.\n", .{val});
config.export_block_size = @as(usize, num) * 1024;
} else if (opt.is("--confirm-quit")) config.confirm_quit = true
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();
const val = args.arg();
if (std.mem.eql(u8, val, "off")) config.ui_color = .off
else if (std.mem.eql(u8, val, "dark")) config.ui_color = .dark
else if (std.mem.eql(u8, val, "dark-bg")) config.ui_color = .darkbg
else try args.die("Unknown --color option: {s}.\n", .{val});
else ui.die("Unknown --color option: {s}.\n", .{val});
} else if (opt.is("-t") or opt.is("--threads")) {
const val = try args.arg();
config.threads = std.fmt.parseInt(u8, val, 10) catch try args.die("Invalid number of --threads: {s}.\n", .{val});
} else return error.UnknownOption;
const val = args.arg();
config.threads = std.fmt.parseInt(u8, val, 10) catch ui.die("Invalid number of --threads: {s}.\n", .{val});
} else return false;
return true;
}
fn tryReadArgsFile(path: [:0]const u8) void {
@ -330,117 +318,148 @@ fn tryReadArgsFile(path: [:0]const u8) void {
};
defer f.close();
var arglist = std.ArrayList([:0]const u8).init(allocator);
var rd_ = std.io.bufferedReader(f.reader());
const rd = rd_.reader();
var line_buf: [4096]u8 = undefined;
var line_rd = util.LineReader.init(f, &line_buf);
var line_fbs = std.io.fixedBufferStream(&line_buf);
const line_writer = line_fbs.writer();
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;
var arglist: [2][:0]const u8 = .{ "", "" };
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();
var line = std.mem.trim(u8, line_, &std.ascii.whitespace);
if (line.len > 0 and line[0] == '@') {
ignerror = true;
line = line[1..];
}
if (line.len == 0 or line[0] == '#') continue;
if (std.mem.indexOfAny(u8, line, " \t=")) |i| {
arglist[argc] = allocator.dupeZ(u8, line[0..i]) catch unreachable;
argc += 1;
arglist.append(allocator.dupeZ(u8, line[0..i]) catch unreachable) catch unreachable;
line = std.mem.trimLeft(u8, line[i+1..], &std.ascii.whitespace);
}
arglist[argc] = allocator.dupeZ(u8, line) catch unreachable;
argc += 1;
var args = Args.init(arglist[0..argc]);
args.ignerror = ignerror;
while (args.next() catch null) |opt| {
if (argConfig(&args, opt, true)) |_| {}
else |_| {
if (ignerror) break;
ui.die("Unrecognized option in config file '{s}': {s}.\nRun with --ignore-config to skip reading config files.\n", .{path, opt.val});
}
}
allocator.free(arglist[0]);
if (argc == 2) allocator.free(arglist[1]);
arglist.append(allocator.dupeZ(u8, line) catch unreachable) catch unreachable;
}
var args = Args.init(arglist.items);
while (args.next()) |opt| {
if (!argConfig(&args, opt, true))
ui.die("Unrecognized option in config file '{s}': {s}.\nRun with --ignore-config to skip reading config files.\n", .{path, opt.val});
}
for (arglist.items) |i| allocator.free(i);
arglist.deinit();
}
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>
\\
\\Mode selection:
\\ -h, --help This help message
\\ -v, -V, --version Print version
\\Options:
\\ -h,--help This help message
\\ -q Quiet mode, refresh interval 2 seconds
\\ -v,-V,--version Print version
\\ -x Same filesystem
\\ -e Enable extended information
\\ -t NUM Number of threads to use
\\ -r Read only
\\ -o FILE Export scanned directory to FILE
\\ -f FILE Import scanned directory from FILE
\\ -o FILE Export scanned directory to FILE in JSON format
\\ -O FILE Export scanned directory to FILE in binary format
\\ -e, --extended Enable extended information
\\ --ignore-config Don't load config files
\\
\\Scan options:
\\ -x, --one-file-system Stay on the same filesystem
\\ -0,-1,-2 UI to use when scanning (0=none,2=full ncurses)
\\ --si Use base 10 (SI) prefixes instead of base 2
\\ --exclude PATTERN Exclude files that match PATTERN
\\ -X, --exclude-from FILE Exclude files that match any pattern in FILE
\\ --exclude-caches Exclude directories containing CACHEDIR.TAG
\\ -L, --follow-symlinks Follow symbolic links (excluding directories)
\\ --exclude-caches Exclude directories containing CACHEDIR.TAG
\\ --exclude-kernfs Exclude Linux pseudo filesystems (procfs,sysfs,cgroup,...)
\\ -t NUM Scan with NUM threads
\\ --confirm-quit Confirm quitting ncdu
\\ --color SCHEME Set color scheme (off/dark/dark-bg)
\\ --ignore-config Don't load config files
\\
\\Export options:
\\ -c, --compress Use Zstandard compression with `-o`
\\ --compress-level NUM Set compression level
\\ --export-block-size KIB Set export block size with `-O`
\\
\\Interface options:
\\ -0, -1, -2 UI to use when scanning (0=none,2=full ncurses)
\\ -q, --slow-ui-updates "Quiet" mode, refresh interval 2 seconds
\\ --enable-shell Enable/disable shell spawning feature
\\ --enable-delete Enable/disable file deletion feature
\\ --enable-refresh Enable/disable directory refresh feature
\\ -r Read only (--disable-delete)
\\ -rr Read only++ (--disable-delete & --disable-shell)
\\ --si Use base 10 (SI) prefixes instead of base 2
\\ --apparent-size Show apparent size instead of disk usage by default
\\ --hide-hidden Hide "hidden" or excluded files by default
\\ --show-itemcount Show item count column by default
\\ --show-mtime Show mtime column by default (requires `-e`)
\\ --show-graph Show graph column by default
\\ --show-percent Show percent column by default
\\ --graph-style STYLE hash / half-block / eighth-block
\\ --shared-column off / shared / unique
\\ --sort COLUMN-(asc/desc) disk-usage / name / apparent-size / itemcount / mtime
\\ --enable-natsort Use natural order when sorting by name
\\ --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.
\\Refer to `man ncdu` for the full list of options.
\\
) catch {};
std.process.exit(0);
}
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_rd = util.LineReader.init(f, &line_buf);
while (try line_rd.read()) |line| {
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();
if (line.len > 0)
exclude.addPattern(line);
}
@ -448,12 +467,12 @@ fn readExcludeFile(path: [:0]const u8) !void {
fn readImport(path: [:0]const u8) !void {
const fd =
if (std.mem.eql(u8, "-", path)) stdin
if (std.mem.eql(u8, "-", path)) std.io.getStdIn()
else try std.fs.cwd().openFileZ(path, .{});
errdefer fd.close();
var buf: [8]u8 = undefined;
if (8 != try fd.readAll(&buf)) return error.EndOfStream;
try fd.reader().readNoEof(&buf);
if (std.mem.eql(u8, &buf, bin_export.SIGNATURE)) {
try bin_reader.open(fd);
config.binreader = true;
@ -507,8 +526,8 @@ pub fn main() void {
const arglist = std.process.argsAlloc(allocator) catch unreachable;
defer std.process.argsFree(allocator, arglist);
var args = Args.init(arglist);
_ = args.next() catch unreachable; // program name
while (args.next() catch unreachable) |opt| {
_ = args.next(); // program name
while (args.next()) |opt| {
if (!opt.opt) {
// XXX: ncdu 1.x doesn't error, it just silently ignores all but the last argument.
if (scan_dir != null) ui.die("Multiple directories given, see ncdu -h for help.\n", .{});
@ -518,15 +537,15 @@ pub fn main() void {
if (opt.is("-h") or opt.is("-?") or opt.is("--help")) help()
else if (opt.is("-v") or opt.is("-V") or opt.is("--version")) version()
else if (opt.is("-o") and (export_json != null or export_bin != null)) ui.die("The -o flag can only be given once.\n", .{})
else if (opt.is("-o")) export_json = allocator.dupeZ(u8, args.arg() catch unreachable) catch unreachable
else if (opt.is("-o")) export_json = allocator.dupeZ(u8, args.arg()) catch unreachable
else if (opt.is("-O") and (export_json != null or export_bin != null)) ui.die("The -O flag can only be given once.\n", .{})
else if (opt.is("-O")) export_bin = allocator.dupeZ(u8, args.arg() catch unreachable) catch unreachable
else if (opt.is("-O")) export_bin = allocator.dupeZ(u8, args.arg()) catch unreachable
else if (opt.is("-f") and import_file != null) ui.die("The -f flag can only be given once.\n", .{})
else if (opt.is("-f")) import_file = allocator.dupeZ(u8, args.arg() catch unreachable) catch unreachable
else if (opt.is("-f")) import_file = allocator.dupeZ(u8, args.arg()) catch unreachable
else if (opt.is("--ignore-config")) {}
else if (opt.is("--quit-after-scan")) quit_after_scan = true // undocumented feature to help with benchmarking scan/import
else if (argConfig(&args, opt, false)) |_| {}
else |_| ui.die("Unrecognized option '{s}'.\n", .{opt.val});
else if (argConfig(&args, opt, false)) {}
else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
}
}
@ -535,6 +554,8 @@ 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) {
@ -572,7 +593,7 @@ pub fn main() void {
if (config.binreader and (export_json != null or export_bin != null))
bin_reader.import();
} else {
var buf: [std.fs.max_path_bytes+1]u8 = @splat(0);
var buf = [_]u8{0} ** (std.fs.MAX_PATH_BYTES+1);
const path =
if (std.posix.realpathZ(scan_dir orelse ".", buf[0..buf.len-1])) |p| buf[0..p.len:0]
else |_| (scan_dir orelse ".");
@ -592,10 +613,10 @@ pub fn main() void {
while (true) {
switch (state) {
.refresh => {
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 {
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 {
sink.global.last_error = allocator.dupeZ(u8, full_path.items) catch unreachable;
sink.global.state = .err;
while (state == .refresh) handleEvent(true, true);
@ -604,18 +625,13 @@ pub fn main() void {
browser.loadDir(0);
},
.shell => {
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);
spawnShell();
state = .browse;
},
.delete => {
const next = delete.delete();
if (state != .refresh) {
state = .browse;
browser.loadDir(if (next) |n| n.nameHash() else 0);
}
state = .browse;
browser.loadDir(if (next) |n| n.nameHash() else 0);
},
else => handleEvent(true, false)
}
@ -665,13 +681,13 @@ test "argument parser" {
const T = struct {
a: Args,
fn opt(self: *@This(), isopt: bool, val: []const u8) !void {
const o = (self.a.next() catch unreachable).?;
const o = self.a.next().?;
try std.testing.expectEqual(isopt, o.opt);
try std.testing.expectEqualStrings(val, o.val);
try std.testing.expectEqual(o.is(val), isopt);
}
fn arg(self: *@This(), val: []const u8) !void {
try std.testing.expectEqualStrings(val, self.a.arg() catch unreachable);
try std.testing.expectEqualStrings(val, self.a.arg());
}
};
var t = T{ .a = Args.init(&lst) };

View file

@ -17,24 +17,6 @@ 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,
@ -125,7 +107,21 @@ pub const Dir = struct {
}
const e = self.getEntry(t, stat.etype, main.config.extended and !stat.ext.isEmpty(), name);
statToEntry(stat, e, self.dir);
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;
return e;
}

View file

@ -56,14 +56,14 @@ fn rec(ctx: *Ctx, dir: *sink.Dir, entry: *model.Entry) void {
pub fn run(d: *model.Dir) void {
const sink_threads = sink.createThreads(1);
var ctx: Ctx = .{
var ctx = .{
.sink = &sink_threads[0],
.stat = toStat(&d.entry),
};
var buf: std.ArrayListUnmanaged(u8) = .empty;
d.fmtPath(main.allocator, true, &buf);
var buf = std.ArrayList(u8).init(main.allocator);
d.fmtPath(true, &buf);
const root = sink.createRoot(buf.items, &ctx.stat);
buf.deinit(main.allocator);
buf.deinit();
var it = d.sub.ptr;
while (it) |e| : (it = e.next.ptr) rec(&ctx, root, e);

View file

@ -109,8 +109,7 @@ 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) {
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
if (allocator.allocWithOptions(u8, size, 1, null)) |p| break :blk p
else |_| {}
ui.oom();
};
@ -218,20 +217,19 @@ pub const Dir = extern struct {
suberr: bool = false,
};
pub fn fmtPath(self: *const @This(), alloc: std.mem.Allocator, withRoot: bool, out: *std.ArrayListUnmanaged(u8)) void {
pub fn fmtPath(self: *const @This(), withRoot: bool, out: *std.ArrayList(u8)) void {
if (!withRoot and self.parent == null) return;
var components: std.ArrayListUnmanaged([:0]const u8) = .empty;
defer components.deinit(main.allocator);
var components = std.ArrayList([:0]const u8).init(main.allocator);
defer components.deinit();
var it: ?*const @This() = self;
while (it) |e| : (it = e.parent)
if (withRoot or e.parent != null)
components.append(main.allocator, e.entry.name()) catch unreachable;
components.append(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(main.allocator, '/') catch unreachable;
out.appendSlice(alloc, 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('/') catch unreachable;
out.appendSlice(components.items[i]) catch unreachable;
if (i == 0) break;
i -= 1;
}
@ -273,11 +271,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.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;
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;
}
// Add this link to the inodes map and mark it as 'uncounted'.
@ -352,7 +350,7 @@ pub const Ext = extern struct {
pub const devices = struct {
var lock = std.Thread.Mutex{};
// id -> dev
pub var list: std.ArrayListUnmanaged(u64) = .empty;
pub var list = std.ArrayList(u64).init(main.allocator);
// dev -> id
var lookup = std.AutoHashMap(u64, DevId).init(main.allocator);
@ -363,7 +361,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(main.allocator, dev) catch unreachable;
list.append(dev) catch unreachable;
}
return d.value_ptr.*;
}

View file

@ -46,24 +46,14 @@ fn truncate(comptime T: type, comptime field: anytype, x: anytype) std.meta.fiel
}
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);
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);
return sink.Stat{
.etype =
if (std.c.S.ISDIR(stat.mode)) .dir
if (std.posix.S.ISDIR(stat.mode)) .dir
else if (stat.nlink > 1) .link
else if (!std.c.S.ISREG(stat.mode)) .nonreg
else if (!std.posix.S.ISREG(stat.mode)) .nonreg
else .reg,
.blocks = clamp(sink.Stat, .blocks, stat.blocks),
.size = clamp(sink.Stat, .size, stat.size),
@ -77,7 +67,7 @@ pub fn statAt(parent: std.fs.Dir, name: [:0]const u8, follow: bool, symlink: ?*b
.hasgid = true,
.hasmode = true,
},
.mtime = clamp(model.Ext, .mtime, stat.mtime().sec),
.mtime = clamp(model.Ext, .mtime, stat.mtime().tv_sec),
.uid = truncate(model.Ext, .uid, stat.uid),
.gid = truncate(model.Ext, .gid, stat.gid),
.mode = truncate(model.Ext, .mode, stat.mode),
@ -91,7 +81,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.readAll(&buf) catch return false;
const len = f.reader().readAll(&buf) catch return false;
return len == sig.len and std.mem.eql(u8, &buf, sig);
}
@ -184,7 +174,7 @@ const Thread = struct {
thread_num: usize,
sink: *sink.Thread,
state: *State,
stack: std.ArrayListUnmanaged(*Dir) = .empty,
stack: std.ArrayList(*Dir) = std.ArrayList(*Dir).init(main.allocator),
thread: std.Thread = undefined,
namebuf: [4096]u8 = undefined,
@ -265,13 +255,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(main.allocator, ndir) catch unreachable;
t.stack.append(ndir) catch unreachable;
}
fn run(t: *Thread) void {
defer t.stack.deinit(main.allocator);
defer t.stack.deinit();
while (t.state.waitPop()) |dir| {
t.stack.append(main.allocator, dir) catch unreachable;
t.stack.append(dir) catch unreachable;
while (t.stack.items.len > 0) {
const d = t.stack.items[t.stack.items.len - 1];
@ -286,7 +276,7 @@ const Thread = struct {
if (entry) |e| t.scanOne(d, e.name)
else {
t.sink.setDir(null);
t.stack.pop().?.destroy(t);
t.stack.pop().destroy(t);
}
}
}

View file

@ -140,21 +140,20 @@ pub const Dir = struct {
}
fn path(d: *Dir) [:0]u8 {
var components: std.ArrayListUnmanaged([]const u8) = .empty;
defer components.deinit(main.allocator);
var components = std.ArrayList([]const u8).init(main.allocator);
defer components.deinit();
var it: ?*Dir = d;
while (it) |e| : (it = e.parent) components.append(main.allocator, e.name) catch unreachable;
while (it) |e| : (it = e.parent) components.append(e.name) catch unreachable;
var out: std.ArrayListUnmanaged(u8) = .empty;
var out = std.ArrayList(u8).init(main.allocator);
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(main.allocator, '/') catch unreachable;
out.appendSlice(main.allocator, 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('/') catch unreachable;
out.appendSlice(components.items[i]) catch unreachable;
if (i == 0) break;
i -= 1;
}
return out.toOwnedSliceSentinel(main.allocator, 0) catch unreachable;
return out.toOwnedSliceSentinel(0) catch unreachable;
}
fn ref(d: *Dir) void {
@ -163,7 +162,7 @@ pub const Dir = struct {
pub fn unref(d: *Dir, t: *Thread) void {
if (d.refcnt.fetchSub(1, .release) != 1) return;
_ = d.refcnt.load(.acquire);
d.refcnt.fence(.acquire);
switch (d.out) {
.mem => |*m| m.final(if (d.parent) |p| &p.out.mem else null),
@ -292,7 +291,7 @@ fn drawConsole() void {
var ansi: ?bool = null;
var lines_written: usize = 0;
};
const stderr = if (@hasDecl(std.io, "getStdErr")) std.io.getStdErr() else std.fs.File.stderr();
const stderr = std.io.getStdErr();
const ansi = st.ansi orelse blk: {
const t = stderr.supportsAnsiEscapeCodes();
st.ansi = t;
@ -451,28 +450,25 @@ pub fn draw() void {
switch (main.config.scan_ui.?) {
.none => {},
.line => drawConsole(),
.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(),
}
.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(),
},
}
}

View file

@ -17,7 +17,8 @@ pub var cols: u32 = undefined;
pub fn die(comptime fmt: []const u8, args: anytype) noreturn {
deinit();
std.debug.print(fmt, args);
const stderr = std.io.getStdErr();
stderr.writer().print(fmt, args) catch {};
std.process.exit(1);
}
@ -26,8 +27,6 @@ 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
@ -38,17 +37,18 @@ const sleep = if (@hasDecl(std.time, "sleep")) std.time.sleep else std.Thread.sl
// no clue if ncurses will consistently report OOM, but we're not handling that
// right now.
pub fn oom() void {
@branchHint(.cold);
@setCold(true);
if (main_thread == std.Thread.getCurrentId()) {
const haveui = inited;
deinit();
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);
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);
if (haveui)
init();
} else {
_ = oom_threads.fetchAdd(1, .monotonic);
sleep(std.time.ns_per_s);
std.time.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.ArrayListUnmanaged(u8) = .empty;
var to_utf8_buf = std.ArrayList(u8).init(main.allocator);
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(main.allocator, in[i .. i + cp_len]) catch unreachable;
to_utf8_buf.appendSlice(in[i .. i + cp_len]) catch unreachable;
i += cp_len;
continue;
} else |_| {}
}
} else |_| {}
to_utf8_buf.writer(main.allocator).print("\\x{X:0>2}", .{in[i]}) catch unreachable;
to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]}) catch unreachable;
i += 1;
}
return util.arrayListBufZ(&to_utf8_buf, main.allocator);
return util.arrayListBufZ(&to_utf8_buf);
}
var shorten_buf: std.ArrayListUnmanaged(u8) = .empty;
var shorten_buf = std.ArrayList(u8).init(main.allocator);
// 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(main.allocator, in[0..prefix_end]) catch unreachable;
shorten_buf.appendSlice(main.allocator, "...") catch unreachable;
shorten_buf.appendSlice(in[0..prefix_end]) catch unreachable;
shorten_buf.appendSlice("...") 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(main.allocator, in[start_len..]) catch unreachable;
shorten_buf.appendSlice(in[start_len..]) catch unreachable;
break;
}
}
return util.arrayListBufZ(&shorten_buf, main.allocator);
return util.arrayListBufZ(&shorten_buf);
}
fn shortenTest(in: [:0]const u8, max_width: u32, out: [:0]const u8) !void {
@ -288,7 +288,7 @@ pub const Style = lbl: {
};
}
break :lbl @Type(.{
.@"enum" = .{
.Enum = .{
.tag_type = u8,
.fields = &fields,
.decls = &[_]std.builtin.Type.Declaration{},
@ -335,7 +335,8 @@ 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.
std.debug.print("\x1b[J", .{});
const stderr = std.io.getStdErr();
stderr.writeAll("\x1b[J") catch {};
}
pub fn init() void {
@ -418,7 +419,7 @@ pub const FmtSize = struct {
pub fn fmt(v: u64) FmtSize {
if (main.config.si) {
if (v < 1000) { return FmtSize.init(" B", v, 10, 1); }
else if (v < 999_950) { return FmtSize.init(" kB", v, 1, 100); }
else if (v < 999_950) { return FmtSize.init(" KB", v, 1, 100); }
else if (v < 999_950_000) { return FmtSize.init(" MB", v, 1, 100_000); }
else if (v < 999_950_000_000) { return FmtSize.init(" GB", v, 1, 100_000_000); }
else if (v < 999_950_000_000_000) { return FmtSize.init(" TB", v, 1, 100_000_000_000); }
@ -451,11 +452,11 @@ test "fmtsize" {
main.config.si = true;
try FmtSize.fmt( 0).testEql(" 0.0 B");
try FmtSize.fmt( 999).testEql("999.0 B");
try FmtSize.fmt( 1000).testEql(" 1.0 kB");
try FmtSize.fmt( 1049).testEql(" 1.0 kB");
try FmtSize.fmt( 1050).testEql(" 1.1 kB");
try FmtSize.fmt( 999_899).testEql("999.9 kB");
try FmtSize.fmt( 999_949).testEql("999.9 kB");
try FmtSize.fmt( 1000).testEql(" 1.0 KB");
try FmtSize.fmt( 1049).testEql(" 1.0 KB");
try FmtSize.fmt( 1050).testEql(" 1.1 KB");
try FmtSize.fmt( 999_899).testEql("999.9 KB");
try FmtSize.fmt( 999_949).testEql("999.9 KB");
try FmtSize.fmt( 999_950).testEql(" 1.0 MB");
try FmtSize.fmt( 1000_000).testEql(" 1.0 MB");
try FmtSize.fmt( 999_850_009).testEql("999.9 MB");
@ -633,7 +634,7 @@ pub fn getch(block: bool) i32 {
}
if (ch == c.ERR) {
if (!block) return 0;
sleep(10*std.time.ns_per_ms);
std.time.sleep(10*std.time.ns_per_ms);
continue;
}
return ch;
@ -641,50 +642,3 @@ 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();
}
}

View file

@ -18,8 +18,8 @@ pub fn castClamp(comptime T: type, x: anytype) T {
// Cast any integer type to the target type, truncating if necessary.
pub fn castTruncate(comptime T: type, x: anytype) T {
const Ti = @typeInfo(T).int;
const Xi = @typeInfo(@TypeOf(x)).int;
const Ti = @typeInfo(T).Int;
const Xi = @typeInfo(@TypeOf(x)).Int;
const nx: std.meta.Int(Ti.signedness, Xi.bits) = @bitCast(x);
return if (Xi.bits > Ti.bits) @truncate(nx) else nx;
}
@ -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.ArrayListUnmanaged(u8), alloc: std.mem.Allocator) [:0]const u8 {
buf.append(alloc, 0) catch unreachable;
pub fn arrayListBufZ(buf: *std.ArrayList(u8)) [:0]const u8 {
buf.append(0) catch unreachable;
defer buf.items.len -= 1;
return buf.items[0..buf.items.len-1:0];
}
@ -196,54 +196,5 @@ 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.mem.concatWithSentinel(alloc, u8, &.{ home, path[len..] }, 0);
return try std.fmt.allocPrintZ(alloc, "{s}{s}", .{ home, path[len..] });
}
// 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];
}
};