mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-13 09:18:40 -09:00
Compare commits
No commits in common. "zig" and "v2.7" have entirely different histories.
19 changed files with 352 additions and 590 deletions
31
ChangeLog
31
ChangeLog
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
24
build.zig
24
build.zig
|
|
@ -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
34
ncdu.1
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
131
src/delete.zig
131
src/delete.zig
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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..]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
310
src/main.zig
310
src/main.zig
|
|
@ -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) };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.*;
|
||||
}
|
||||
|
|
|
|||
34
src/scan.zig
34
src/scan.zig
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
60
src/sink.zig
60
src/sink.zig
|
|
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
98
src/ui.zig
98
src/ui.zig
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
src/util.zig
59
src/util.zig
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue