ncdu-zig/src/main.zig

680 lines
27 KiB
Zig
Raw Normal View History

// SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
2021-07-18 01:36:05 -08:00
// SPDX-License-Identifier: MIT
2024-07-24 04:07:17 -08:00
pub const program_version = "2.5";
const std = @import("std");
const model = @import("model.zig");
const scan = @import("scan.zig");
const json_import = @import("json_import.zig");
const json_export = @import("json_export.zig");
const bin_export = @import("bin_export.zig");
const sink = @import("sink.zig");
const mem_src = @import("mem_src.zig");
const mem_sink = @import("mem_sink.zig");
const ui = @import("ui.zig");
const browser = @import("browser.zig");
const delete = @import("delete.zig");
2021-10-06 01:05:56 -08:00
const util = @import("util.zig");
Improve exclude pattern matching performance (and behavior, a bit) Behavioral changes: - A single wildcard ('*') does not cross directory boundary anymore. Previously 'a*b' would also match 'a/b', but no other tool that I am aware of matches paths that way. This change breaks compatibility with old exclude patterns but improves consistency with other tools. - Patterns with a trailing '/' now prevent recursing into the directory. Previously any directory excluded with such a pattern would show up as a regular directory with all its contents excluded, but now the directory entry itself shows up as excluded. - If the path given to ncdu matches one of the exclude patterns, the old implementation would exclude every file/dir being read, this new implementation would instead ignore the rule. Not quite sure how to best handle this case, perhaps just exit with an error message? Performance wise, I haven't yet found a scenario where this implementation is slower than the old one and it's *significantly* faster in some cases - in particular when using a large amount of patterns, especially with literal paths and file names. That's not to say this implementation is anywhere near optimal: - A list of relevant patterns is constructed for each directory being scanned. It may be possible to merge pattern lists that share the same prefix, which could both reduce memory use and the number of patterns that need to be matched upon entering a directory. - A hash table with dynamic arrays as values is just garbage from a memory allocation point of view. - This still uses libc fnmatch(), but there's an opportunity to precompile patterns for faster matching.
2022-08-09 23:46:36 -08:00
const exclude = @import("exclude.zig");
const c = @cImport(@cInclude("locale.h"));
test "imports" {
_ = model;
_ = scan;
_ = json_import;
_ = json_export;
_ = bin_export;
_ = sink;
_ = mem_src;
_ = mem_sink;
_ = ui;
_ = browser;
_ = delete;
_ = util;
_ = exclude;
}
// "Custom" allocator that wraps the libc allocator and calls ui.oom() on error.
// 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: u8, return_address: usize) ?[*]u8 {
while (true) {
if (std.heap.c_allocator.vtable.alloc(undefined, len, ptr_alignment, return_address)) |r|
return r
else {}
ui.oom();
}
}
2021-07-19 05:28:11 -08:00
pub const allocator = std.mem.Allocator{
.ptr = undefined,
.vtable = &.{
.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,
.free = std.heap.c_allocator.vtable.free,
},
};
2021-07-19 05:28:11 -08:00
pub const config = struct {
pub const SortCol = enum { name, blocks, size, items, mtime };
pub const SortOrder = enum { asc, desc };
pub var same_fs: bool = false;
pub var extended: bool = false;
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;
Add (temporary) compression support for the new export format This is mainly for testing and benchmarking, I plan to choose a single block size and compression library before release, to avoid bloating the ncdu binary too much. Currently this links against the system-provided zstd, zlib and lz4. ncdubinexp.pl doesn't support compressed files yet. Early benchmarks of `ncdu -f firefox-128.0.json` (407k files) with different block sizes and compression options: bin8k bin16k bin32k bin64k bin128k bin256k bin512k json algo size time size time size time size time size time size time size time size time none 16800 128 16760 126 16739 125 16728 124 16724 125 16722 124 16721 124 24835 127 lz4 7844 143 7379 141 7033 140 6779 140 6689 138 6626 139 6597 139 5850 179 zlib-1 6017 377 5681 310 5471 273 5345 262 5289 259 5257 256 5242 255 4415 164 zlib-2 5843 386 5496 319 5273 284 5136 276 5072 271 5037 270 5020 268 4164 168 zlib-3 5718 396 5361 339 5130 316 4977 321 4903 318 4862 324 4842 319 3976 196 zlib-4 5536 424 5153 372 4903 341 4743 339 4665 338 4625 340 4606 336 3798 212 zlib-5 5393 464 4993 419 4731 406 4561 414 4478 422 4434 426 4414 420 3583 261 zlib-6 5322 516 4902 495 4628 507 4450 535 4364 558 4318 566 4297 564 3484 352 zlib-7 5311 552 4881 559 4599 601 4417 656 4329 679 4282 696 4260 685 3393 473 zlib-8 5305 588 4864 704 4568 1000 4374 1310 4280 1470 4230 1530 4206 1550 3315 1060 zlib-9 5305 589 4864 704 4568 1030 4374 1360 4280 1510 4230 1600 4206 1620 3312 1230 zstd-1 5845 177 5426 169 5215 165 5030 160 4921 156 4774 157 4788 153 3856 126 zstd-2 5830 178 5424 170 5152 164 4963 161 4837 160 4595 162 4614 158 3820 134 zstd-3 5683 187 5252 177 5017 172 4814 168 4674 169 4522 169 4446 170 3664 145 zstd-4 5492 235 5056 230 4966 173 4765 170 4628 169 4368 222 4437 170 3656 145 zstd-5 5430 270 4988 266 4815 234 4616 229 4485 224 4288 241 4258 223 3366 189 zstd-6 5375 323 4928 322 4694 282 4481 279 4334 276 4231 275 4125 271 3234 235 zstd-7 5322 400 4866 420 4678 319 4464 314 4315 312 4155 300 4078 295 3173 269 zstd-8 5314 454 4848 689 4636 344 4420 346 4270 345 4137 350 4060 342 3082 330 zstd-9 5320 567 4854 615 4596 392 4379 398 4228 401 4095 408 4060 345 3057 385 zstd-10 5319 588 4852 662 4568 458 4350 466 4198 478 4066 491 4024 395 3005 489 zstd-11 5310 975 4857 1040 4543 643 4318 688 4164 743 4030 803 3999 476 2967 627 zstd-12 5171 1300 4692 1390 4539 699 4313 765 4154 854 4018 939 3999 478 2967 655 zstd-13 5128 1760 4652 1880 4556 1070 4341 1130 4184 1230 3945 1490 3980 705 2932 1090 zstd-14 5118 2040 4641 2180 4366 1540 4141 1620 3977 1780 3854 1810 3961 805 2893 1330 mzstd-1 5845 206 5426 195 5215 188 5030 180 4921 176 4774 175 4788 172 mzstd-2 5830 207 5424 196 5152 186 4963 183 4837 181 4765 178 4614 176 mzstd-3 5830 207 5424 196 5150 187 4960 183 4831 180 4796 181 4626 180 mzstd-4 5830 206 5427 196 5161 188 4987 185 4879 182 4714 180 4622 179 mzstd-5 5430 347 4988 338 5161 189 4987 185 4879 181 4711 180 4620 180 mzstd-6 5384 366 4939 359 4694 390 4481 391 4334 383 4231 399 4125 394 mzstd-7 5328 413 4873 421 4694 390 4481 390 4334 385 4155 442 4078 435 mzstd-8 5319 447 4854 577 4649 417 4434 421 4286 419 4155 440 4078 436 mzstd-9 5349 386 4900 385 4606 469 4390 478 4241 478 4110 506 4078 436 mzstd-10 5319 448 4853 597 4576 539 4360 560 4210 563 4079 597 4039 502 mzstd-11 5430 349 4988 339 4606 468 4390 478 4241 478 4110 506 4013 590 mzstd-12 5384 366 4939 361 4576 540 4360 556 4210 559 4079 597 4013 589 mzstd-13 5349 387 4900 388 4694 390 4481 392 4334 386 4155 439 4078 436 mzstd-14 5328 414 4873 420 4649 417 4434 424 4286 420 4155 444 4039 500 I'll need to do benchmarks on other directories, with hardlink support and in extended mode as well to get more varied samples. Another consideration in choosing a compression library is the size of its implementation: zlib: 100k lz4: 106k zstd: 732k (regular), 165k (ZSTD_LIB_MINIFY, "mzstd" above)
2024-07-31 01:46:15 -08:00
pub var compression: enum { none, zlib, zstd, lz4 } = .none;
pub var complevel: u8 = 5;
pub var blocksize: usize = 64*1024;
pub var update_delay: u64 = 100*std.time.ns_per_ms;
pub var scan_ui: ?enum { none, line, full } = null;
pub var si: bool = false;
pub var nc_tty: bool = false;
pub var ui_color: enum { off, dark, darkbg } = .off;
pub var thousands_sep: []const u8 = ",";
pub var show_hidden: bool = true;
pub var show_blocks: bool = true;
pub var show_shared: enum { off, shared, unique } = .shared;
pub var show_items: bool = false;
pub var show_mtime: bool = false;
pub var show_graph: bool = true;
pub var show_percent: bool = false;
pub var graph_style: enum { hash, half, eighth } = .hash;
pub var sort_col: SortCol = .blocks;
pub var sort_order: SortOrder = .desc;
pub var sort_dirsfirst: bool = false;
2023-03-04 22:31:31 -09:00
pub var sort_natural: bool = true;
pub var imported: bool = false;
pub var can_delete: ?bool = null;
pub var can_shell: ?bool = null;
pub var can_refresh: ?bool = null;
pub var confirm_quit: bool = false;
pub var confirm_delete: bool = true;
pub var ignore_delete_errors: bool = false;
};
pub var state: enum { scan, browse, refresh, shell, delete } = .scan;
2021-04-29 08:59:25 -08:00
// Simple generic argument parser, supports getopt_long() style arguments.
const Args = struct {
lst: []const [:0]const u8,
short: ?[:0]const u8 = null, // Remainder after a short option, e.g. -x<stuff> (which may be either more short options or an argument)
last: ?[]const u8 = null,
last_arg: ?[:0]const u8 = null, // In the case of --option=<arg>
shortbuf: [2]u8 = undefined,
argsep: bool = false,
const Self = @This();
const Option = struct {
opt: bool,
val: []const u8,
fn is(self: @This(), cmp: []const u8) bool {
return self.opt and std.mem.eql(u8, self.val, cmp);
2021-04-29 08:59:25 -08:00
}
};
fn init(lst: []const [:0]const u8) Self {
return Self{ .lst = lst };
2021-10-06 01:05:56 -08:00
}
fn pop(self: *Self) ?[:0]const u8 {
if (self.lst.len == 0) return null;
defer self.lst = self.lst[1..];
return self.lst[0];
2021-10-06 01:05:56 -08:00
}
fn shortopt(self: *Self, s: [:0]const u8) Option {
self.shortbuf[0] = '-';
self.shortbuf[1] = s[0];
self.short = if (s.len > 1) s[1.. :0] else null;
self.last = &self.shortbuf;
return .{ .opt = true, .val = &self.shortbuf };
}
/// 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) 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) ui.die("Invalid option '-'.\n", .{});
if (val.len == 2 and val[1] == '-') {
self.argsep = true;
return self.next();
2021-10-06 01:05:56 -08:00
}
if (val[1] == '-') {
if (std.mem.indexOfScalar(u8, val, '=')) |sep| {
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.? };
2021-10-06 01:05:56 -08:00
}
self.last = val;
return Option{ .opt = true, .val = val };
2021-10-06 01:05:56 -08:00
}
return self.shortopt(val[1..:0]);
}
/// 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 {
if (self.short) |a| {
defer self.short = null;
return a;
}
if (self.last_arg) |a| {
defer self.last_arg = null;
return a;
}
if (self.pop()) |o| return o;
ui.die("Option '{s}' requires an argument.\n", .{ self.last.? });
2021-10-06 01:05:56 -08:00
}
};
fn argConfig(args: *Args, opt: Args.Option) bool {
2021-10-06 01:05:56 -08:00
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
else if (opt.is("--cross-file-system")) config.same_fs = false
else if (opt.is("-e") or opt.is("--extended")) config.extended = true
else if (opt.is("--no-extended")) config.extended = false
else if (opt.is("-r") and !(config.can_delete orelse true)) config.can_shell = false
else if (opt.is("-r")) config.can_delete = false
else if (opt.is("--enable-shell")) config.can_shell = true
else if (opt.is("--disable-shell")) config.can_shell = false
else if (opt.is("--enable-delete")) config.can_delete = true
else if (opt.is("--disable-delete")) config.can_delete = false
else if (opt.is("--enable-refresh")) config.can_refresh = true
else if (opt.is("--disable-refresh")) config.can_refresh = false
else if (opt.is("--show-hidden")) config.show_hidden = true
else if (opt.is("--hide-hidden")) config.show_hidden = false
else if (opt.is("--show-itemcount")) config.show_items = true
else if (opt.is("--hide-itemcount")) config.show_items = false
else if (opt.is("--show-mtime")) config.show_mtime = true
else if (opt.is("--hide-mtime")) config.show_mtime = false
else if (opt.is("--show-graph")) config.show_graph = true
else if (opt.is("--hide-graph")) config.show_graph = false
else if (opt.is("--show-percent")) config.show_percent = true
else if (opt.is("--hide-percent")) config.show_percent = false
else if (opt.is("--group-directories-first")) config.sort_dirsfirst = true
else if (opt.is("--no-group-directories-first")) config.sort_dirsfirst = false
2023-03-04 22:31:31 -09:00
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 = 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 ui.die("Unknown --graph-style option: {s}.\n", .{val});
} else if (opt.is("--sort")) {
2021-10-06 01:05:56 -08:00
var val: []const u8 = args.arg();
var ord: ?config.SortOrder = null;
if (std.mem.endsWith(u8, val, "-asc")) {
val = val[0..val.len-4];
ord = .asc;
} else if (std.mem.endsWith(u8, val, "-desc")) {
val = val[0..val.len-5];
ord = .desc;
}
if (std.mem.eql(u8, val, "name")) {
config.sort_col = .name;
config.sort_order = ord orelse .asc;
} else if (std.mem.eql(u8, val, "disk-usage")) {
config.sort_col = .blocks;
config.sort_order = ord orelse .desc;
} else if (std.mem.eql(u8, val, "apparent-size")) {
config.sort_col = .size;
config.sort_order = ord orelse .desc;
} else if (std.mem.eql(u8, val, "itemcount")) {
config.sort_col = .items;
config.sort_order = ord orelse .desc;
} else if (std.mem.eql(u8, val, "mtime")) {
config.sort_col = .mtime;
config.sort_order = ord orelse .asc;
} else ui.die("Unknown --sort option: {s}.\n", .{val});
} else if (opt.is("--shared-column")) {
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 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
else if (opt.is("-1")) config.scan_ui = .line
else if (opt.is("-2")) config.scan_ui = .full
else if (opt.is("--si")) config.si = true
else if (opt.is("--no-si")) config.si = false
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
Improve exclude pattern matching performance (and behavior, a bit) Behavioral changes: - A single wildcard ('*') does not cross directory boundary anymore. Previously 'a*b' would also match 'a/b', but no other tool that I am aware of matches paths that way. This change breaks compatibility with old exclude patterns but improves consistency with other tools. - Patterns with a trailing '/' now prevent recursing into the directory. Previously any directory excluded with such a pattern would show up as a regular directory with all its contents excluded, but now the directory entry itself shows up as excluded. - If the path given to ncdu matches one of the exclude patterns, the old implementation would exclude every file/dir being read, this new implementation would instead ignore the rule. Not quite sure how to best handle this case, perhaps just exit with an error message? Performance wise, I haven't yet found a scenario where this implementation is slower than the old one and it's *significantly* faster in some cases - in particular when using a large amount of patterns, especially with literal paths and file names. That's not to say this implementation is anywhere near optimal: - A list of relevant patterns is constructed for each directory being scanned. It may be possible to merge pattern lists that share the same prefix, which could both reduce memory use and the number of patterns that need to be matched upon entering a directory. - A hash table with dynamic arrays as values is just garbage from a memory allocation point of view. - This still uses libc fnmatch(), but there's an opportunity to precompile patterns for faster matching.
2022-08-09 23:46:36 -08:00
else if (opt.is("--exclude")) exclude.addPattern(args.arg())
2021-10-06 01:05:56 -08:00
else if (opt.is("-X") or opt.is("--exclude-from")) {
const arg = args.arg();
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
else if (opt.is("--include-kernfs")) config.exclude_kernfs = false
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("--color")) {
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 ui.die("Unknown --color option: {s}.\n", .{val});
} else if (opt.is("-t") or opt.is("--threads")) {
const val = args.arg();
config.threads = std.fmt.parseInt(u8, val, 10) catch ui.die("Invalid number of --threads: {s}.\n", .{val});
2021-10-06 01:05:56 -08:00
} else return false;
return true;
}
fn tryReadArgsFile(path: [:0]const u8) void {
var f = std.fs.cwd().openFileZ(path, .{}) catch |e| switch (e) {
error.FileNotFound => return,
error.NotDir => return,
else => ui.die("Error opening {s}: {s}\nRun with --ignore-config to skip reading config files.\n", .{ path, ui.errorString(e) }),
};
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_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| 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 or line[0] == '#') continue;
if (std.mem.indexOfAny(u8, line, " \t=")) |i| {
arglist.append(allocator.dupeZ(u8, line[0..i]) catch unreachable) catch unreachable;
line = std.mem.trimLeft(u8, line[i+1..], &std.ascii.whitespace);
}
arglist.append(allocator.dupeZ(u8, line) catch unreachable) catch unreachable;
}
var args = Args.init(arglist.items);
2021-10-06 01:05:56 -08:00
while (args.next()) |opt| {
if (!argConfig(&args, opt))
ui.die("Unrecognized option in config file '{s}': {s}.\nRun with --ignore-config to skip reading config files.\n", .{path, opt.val});
2021-10-06 01:05:56 -08:00
}
for (arglist.items) |i| allocator.free(i);
arglist.deinit();
2021-10-06 01:05:56 -08:00
}
2021-04-29 08:59:25 -08:00
fn version() noreturn {
const stdout = std.io.getStdOut();
stdout.writeAll("ncdu " ++ program_version ++ "\n") catch {};
2021-04-29 08:59:25 -08:00
std.process.exit(0);
}
fn help() noreturn {
const stdout = std.io.getStdOut();
stdout.writeAll(
\\ncdu <options> <directory>
\\
\\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
\\ -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
\\ -L, --follow-symlinks Follow symbolic links (excluding directories)
\\ --exclude-caches Exclude directories containing CACHEDIR.TAG
\\ --exclude-kernfs Exclude Linux pseudo filesystems (procfs,sysfs,cgroup,...)
\\ --confirm-quit Confirm quitting ncdu
\\ --color SCHEME Set color scheme (off/dark/dark-bg)
\\ --ignore-config Don't load config files
\\
\\Refer to `man ncdu` for the full list of options.
\\
) catch {};
2021-04-29 08:59:25 -08:00
std.process.exit(0);
}
2021-07-14 01:20:27 -08:00
fn spawnShell() void {
ui.deinit();
defer ui.init();
var path = std.ArrayList(u8).init(allocator);
defer path.deinit();
browser.dir_parent.fmtPath(true, &path);
2021-07-14 01:20:27 -08:00
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},
2021-07-14 01:20:27 -08:00
'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);
2021-07-14 01:20:27 -08:00
child.cwd = path.items;
child.env_map = &env;
const stdin = std.io.getStdIn();
const stderr = std.io.getStdErr();
2021-07-14 01:20:27 -08:00
const term = child.spawnAndWait() catch |e| blk: {
stderr.writer().print(
2021-07-14 01:20:27 -08:00
"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 };
2021-07-14 01:20:27 -08:00
};
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(
2021-07-14 01:20:27 -08:00
"Shell returned with {s} code {}.\n\nPress enter to continue.\n", .{ n, v }
) catch {};
stdin.reader().skipUntilDelimiterOrEof('\n') catch unreachable;
2021-07-14 01:20:27 -08:00
}
}
2021-10-06 01:05:56 -08:00
fn readExcludeFile(path: [:0]const u8) !void {
const f = try std.fs.cwd().openFileZ(path, .{});
defer f.close();
var rd_ = std.io.bufferedReader(f.reader());
const rd = rd_.reader();
var line_buf: [4096]u8 = undefined;
var line_fbs = std.io.fixedBufferStream(&line_buf);
const line_writer = line_fbs.writer();
while (true) : (line_fbs.reset()) {
rd.streamUntilDelimiter(line_writer, '\n', line_buf.len) catch |err| switch (err) {
error.EndOfStream => if (line_fbs.getPos() catch unreachable == 0) break,
else => |e| return e,
};
const line = line_fbs.getWritten();
if (line.len > 0)
exclude.addPattern(line);
}
}
pub fn main() void {
2024-07-17 01:48:06 -08:00
ui.main_thread = std.Thread.getCurrentId();
// Grab thousands_sep from the current C locale.
_ = c.setlocale(c.LC_ALL, "");
if (c.localeconv()) |locale| {
if (locale.*.thousands_sep) |sep| {
2021-07-19 05:28:11 -08:00
const span = std.mem.sliceTo(sep, 0);
if (span.len > 0)
config.thousands_sep = span;
}
}
const loadConf = blk: {
var args = std.process.ArgIteratorPosix.init();
while (args.next()) |a|
if (std.mem.eql(u8, a, "--ignore-config"))
break :blk false;
break :blk true;
};
2021-10-06 01:05:56 -08:00
if (loadConf) {
tryReadArgsFile("/etc/ncdu.conf");
if (std.posix.getenvZ("XDG_CONFIG_HOME")) |p| {
const path = std.fs.path.joinZ(allocator, &.{p, "ncdu", "config"}) catch unreachable;
defer allocator.free(path);
tryReadArgsFile(path);
} else if (std.posix.getenvZ("HOME")) |p| {
const path = std.fs.path.joinZ(allocator, &.{p, ".config", "ncdu", "config"}) catch unreachable;
defer allocator.free(path);
tryReadArgsFile(path);
}
2021-10-06 01:05:56 -08:00
}
var scan_dir: ?[:0]const u8 = null;
var import_file: ?[:0]const u8 = null;
var export_json: ?[:0]const u8 = null;
var export_bin: ?[:0]const u8 = null;
var quit_after_scan = false;
{
const arglist = std.process.argsAlloc(allocator) catch unreachable;
defer std.process.argsFree(allocator, arglist);
var args = Args.init(arglist);
_ = 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", .{});
scan_dir = allocator.dupeZ(u8, opt.val) catch unreachable;
continue;
}
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
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
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
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
Add (temporary) compression support for the new export format This is mainly for testing and benchmarking, I plan to choose a single block size and compression library before release, to avoid bloating the ncdu binary too much. Currently this links against the system-provided zstd, zlib and lz4. ncdubinexp.pl doesn't support compressed files yet. Early benchmarks of `ncdu -f firefox-128.0.json` (407k files) with different block sizes and compression options: bin8k bin16k bin32k bin64k bin128k bin256k bin512k json algo size time size time size time size time size time size time size time size time none 16800 128 16760 126 16739 125 16728 124 16724 125 16722 124 16721 124 24835 127 lz4 7844 143 7379 141 7033 140 6779 140 6689 138 6626 139 6597 139 5850 179 zlib-1 6017 377 5681 310 5471 273 5345 262 5289 259 5257 256 5242 255 4415 164 zlib-2 5843 386 5496 319 5273 284 5136 276 5072 271 5037 270 5020 268 4164 168 zlib-3 5718 396 5361 339 5130 316 4977 321 4903 318 4862 324 4842 319 3976 196 zlib-4 5536 424 5153 372 4903 341 4743 339 4665 338 4625 340 4606 336 3798 212 zlib-5 5393 464 4993 419 4731 406 4561 414 4478 422 4434 426 4414 420 3583 261 zlib-6 5322 516 4902 495 4628 507 4450 535 4364 558 4318 566 4297 564 3484 352 zlib-7 5311 552 4881 559 4599 601 4417 656 4329 679 4282 696 4260 685 3393 473 zlib-8 5305 588 4864 704 4568 1000 4374 1310 4280 1470 4230 1530 4206 1550 3315 1060 zlib-9 5305 589 4864 704 4568 1030 4374 1360 4280 1510 4230 1600 4206 1620 3312 1230 zstd-1 5845 177 5426 169 5215 165 5030 160 4921 156 4774 157 4788 153 3856 126 zstd-2 5830 178 5424 170 5152 164 4963 161 4837 160 4595 162 4614 158 3820 134 zstd-3 5683 187 5252 177 5017 172 4814 168 4674 169 4522 169 4446 170 3664 145 zstd-4 5492 235 5056 230 4966 173 4765 170 4628 169 4368 222 4437 170 3656 145 zstd-5 5430 270 4988 266 4815 234 4616 229 4485 224 4288 241 4258 223 3366 189 zstd-6 5375 323 4928 322 4694 282 4481 279 4334 276 4231 275 4125 271 3234 235 zstd-7 5322 400 4866 420 4678 319 4464 314 4315 312 4155 300 4078 295 3173 269 zstd-8 5314 454 4848 689 4636 344 4420 346 4270 345 4137 350 4060 342 3082 330 zstd-9 5320 567 4854 615 4596 392 4379 398 4228 401 4095 408 4060 345 3057 385 zstd-10 5319 588 4852 662 4568 458 4350 466 4198 478 4066 491 4024 395 3005 489 zstd-11 5310 975 4857 1040 4543 643 4318 688 4164 743 4030 803 3999 476 2967 627 zstd-12 5171 1300 4692 1390 4539 699 4313 765 4154 854 4018 939 3999 478 2967 655 zstd-13 5128 1760 4652 1880 4556 1070 4341 1130 4184 1230 3945 1490 3980 705 2932 1090 zstd-14 5118 2040 4641 2180 4366 1540 4141 1620 3977 1780 3854 1810 3961 805 2893 1330 mzstd-1 5845 206 5426 195 5215 188 5030 180 4921 176 4774 175 4788 172 mzstd-2 5830 207 5424 196 5152 186 4963 183 4837 181 4765 178 4614 176 mzstd-3 5830 207 5424 196 5150 187 4960 183 4831 180 4796 181 4626 180 mzstd-4 5830 206 5427 196 5161 188 4987 185 4879 182 4714 180 4622 179 mzstd-5 5430 347 4988 338 5161 189 4987 185 4879 181 4711 180 4620 180 mzstd-6 5384 366 4939 359 4694 390 4481 391 4334 383 4231 399 4125 394 mzstd-7 5328 413 4873 421 4694 390 4481 390 4334 385 4155 442 4078 435 mzstd-8 5319 447 4854 577 4649 417 4434 421 4286 419 4155 440 4078 436 mzstd-9 5349 386 4900 385 4606 469 4390 478 4241 478 4110 506 4078 436 mzstd-10 5319 448 4853 597 4576 539 4360 560 4210 563 4079 597 4039 502 mzstd-11 5430 349 4988 339 4606 468 4390 478 4241 478 4110 506 4013 590 mzstd-12 5384 366 4939 361 4576 540 4360 556 4210 559 4079 597 4013 589 mzstd-13 5349 387 4900 388 4694 390 4481 392 4334 386 4155 439 4078 436 mzstd-14 5328 414 4873 420 4649 417 4434 424 4286 420 4155 444 4039 500 I'll need to do benchmarks on other directories, with hardlink support and in extended mode as well to get more varied samples. Another consideration in choosing a compression library is the size of its implementation: zlib: 100k lz4: 106k zstd: 732k (regular), 165k (ZSTD_LIB_MINIFY, "mzstd" above)
2024-07-31 01:46:15 -08:00
else if (opt.is("--binfmt")) { // Experimental, for benchmarking
const a = args.arg();
config.compression = switch (a[0]) {
'z' => .zlib,
's','S' => .zstd,
'l' => .lz4,
else => .none,
};
config.complevel = (a[1] - '0') + (if (a[0] == 'S') @as(u8, 10) else 0);
config.blocksize = @as(usize, 8*1024) << @intCast(a[2] - '0'); // 0 = 8k, 1 16k, 2 32k, 3 64k, 4 128k, 5 256k, 6 512k
} else if (argConfig(&args, opt)) {}
else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
2021-04-29 08:59:25 -08:00
}
}
if (config.threads == 0) config.threads = std.Thread.getCpuCount() catch 1;
2021-07-19 05:28:11 -08:00
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();
2021-10-06 01:05:56 -08:00
if (config.scan_ui == null) {
if (export_json orelse export_bin) |f| {
if (!out_tty or std.mem.eql(u8, f, "-")) config.scan_ui = .none
2021-05-09 10:58:17 -08:00
else config.scan_ui = .line;
2021-10-06 01:05:56 -08:00
} else config.scan_ui = .full;
2021-05-09 10:58:17 -08:00
}
if (!in_tty and import_file == null and export_json == null and export_bin == null and !quit_after_scan)
ui.die("Standard input is not a TTY. Did you mean to import a file using '-f -'?\n", .{});
config.nc_tty = !in_tty or (if (export_json orelse export_bin) |f| std.mem.eql(u8, f, "-") else false);
2021-05-09 10:58:17 -08:00
event_delay_timer = std.time.Timer.start() catch unreachable;
2021-05-09 10:58:17 -08:00
defer ui.deinit();
if (export_json) |f| {
const file =
if (std.mem.eql(u8, f, "-")) stdout
else std.fs.cwd().createFileZ(f, .{})
catch |e| ui.die("Error opening export file: {s}.\n", .{ui.errorString(e)});
json_export.setupOutput(file);
sink.global.sink = .json;
} else if (export_bin) |f| {
const file =
if (std.mem.eql(u8, f, "-")) stdout
else std.fs.cwd().createFileZ(f, .{})
catch |e| ui.die("Error opening export file: {s}.\n", .{ui.errorString(e)});
bin_export.setupOutput(file);
sink.global.sink = .bin;
}
if (import_file) |f| {
json_import.import(f);
config.imported = true;
} else {
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 ".");
scan.scan(path) catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
}
if (quit_after_scan or export_json != null or export_bin != null) return;
2021-10-06 01:05:56 -08:00
config.can_shell = config.can_shell orelse !config.imported;
config.can_delete = config.can_delete orelse !config.imported;
config.can_refresh = config.can_refresh orelse !config.imported;
2021-05-09 10:58:17 -08:00
config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
ui.init();
2021-05-09 10:58:17 -08:00
state = .browse;
browser.dir_parent = model.root;
browser.loadDir(null);
while (true) {
2021-07-14 01:20:27 -08:00
switch (state) {
.refresh => {
var full_path = std.ArrayList(u8).init(allocator);
defer full_path.deinit();
mem_sink.global.root.?.fmtPath(true, &full_path);
scan.scan(util.arrayListBufZ(&full_path)) catch {
sink.global.last_error = allocator.dupeZ(u8, full_path.items) catch unreachable;
sink.global.state = .err;
while (state == .refresh) handleEvent(true, true);
};
2021-07-14 01:20:27 -08:00
state = .browse;
browser.loadDir(null);
2021-07-14 01:20:27 -08:00
},
.shell => {
spawnShell();
state = .browse;
},
.delete => {
const next = delete.delete();
state = .browse;
browser.loadDir(next);
},
2021-07-14 01:20:27 -08:00
else => handleEvent(true, false)
}
}
}
var event_delay_timer: std.time.Timer = undefined;
// Draw the screen and handle the next input event.
// In non-blocking mode, screen drawing is rate-limited to keep this function fast.
pub fn handleEvent(block: bool, force_draw: bool) void {
2024-07-17 01:48:06 -08:00
while (ui.oom_threads.load(.monotonic) > 0) ui.oom();
if (block or force_draw or event_delay_timer.read() > config.update_delay) {
2021-05-09 10:58:17 -08:00
if (ui.inited) _ = ui.c.erase();
switch (state) {
.scan, .refresh => sink.draw(),
.browse => browser.draw(),
.delete => delete.draw(),
2021-07-14 01:20:27 -08:00
.shell => unreachable,
2021-05-09 10:58:17 -08:00
}
if (ui.inited) _ = ui.c.refresh();
event_delay_timer.reset();
}
2021-05-09 10:58:17 -08:00
if (!ui.inited) {
std.debug.assert(!block);
return;
}
var firstblock = block;
while (true) {
const ch = ui.getch(firstblock);
if (ch == 0) return;
if (ch == -1) return handleEvent(firstblock, true);
switch (state) {
.scan, .refresh => sink.keyInput(ch),
.browse => browser.keyInput(ch),
.delete => delete.keyInput(ch),
2021-07-14 01:20:27 -08:00
.shell => unreachable,
}
firstblock = false;
2021-05-09 10:58:17 -08:00
}
}
2021-04-29 08:59:25 -08:00
test "argument parser" {
const lst = [_][:0]const u8{ "a", "-abcd=e", "--opt1=arg1", "--opt2", "arg2", "-x", "foo", "", "--", "--arg", "", "-", };
const T = struct {
a: Args,
fn opt(self: *@This(), isopt: bool, val: []const u8) !void {
2021-04-29 08:59:25 -08:00
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);
2021-04-29 08:59:25 -08:00
}
fn arg(self: *@This(), val: []const u8) !void {
try std.testing.expectEqualStrings(val, self.a.arg());
2021-04-29 08:59:25 -08:00
}
};
var t = T{ .a = Args.init(&lst) };
try t.opt(false, "a");
try t.opt(true, "-a");
try t.opt(true, "-b");
try t.arg("cd=e");
try t.opt(true, "--opt1");
try t.arg("arg1");
try t.opt(true, "--opt2");
try t.arg("arg2");
try t.opt(true, "-x");
try t.arg("foo");
try t.opt(false, "");
try t.opt(false, "--arg");
try t.opt(false, "");
try t.opt(false, "-");
try std.testing.expectEqual(t.a.next(), null);
2021-04-29 08:59:25 -08:00
}