Skip to content

Commit cb7e306

Browse files
feat: sync with upstream to 2.7
1 parent 76ad78f commit cb7e306

File tree

18 files changed

+288
-88
lines changed

18 files changed

+288
-88
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
zig-out/
2+
.zig-cache/
23
zig-cache/

ChangeLog

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# SPDX-FileCopyrightText: Yorhel <[email protected]>
22
# SPDX-License-Identifier: MIT
33

4+
2.7 - 2024-11-19
5+
- Still requires Zig 0.12 or 0.13
6+
- Support transparent reading/writing of zstandard-compressed JSON
7+
- Add `--compress` and `--export-block-size` options
8+
- Perform tilde expansion on paths in the config file
9+
- Fix JSON import of escaped UTF-16 surrogate pairs
10+
- Fix incorrect field in root item when exporting to the binary format
11+
- Add -Dstrip build flag
12+
413
2.6 - 2024-09-27
514
- Still requires Zig 0.12 or 0.13
615
- Add dependency on libzstd

Makefile

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ ZIG ?= zig
99
PREFIX ?= /usr/local
1010
BINDIR ?= ${PREFIX}/bin
1111
MANDIR ?= ${PREFIX}/share/man/man1
12-
ZIG_FLAGS ?= --release=fast
12+
ZIG_FLAGS ?= --release=fast -Dstrip
1313

1414
NCDU_VERSION=$(shell grep 'program_version = "' src/main.zig | sed -e 's/^.*"\(.\+\)".*$$/\1/')
1515

@@ -68,24 +68,25 @@ static-%.tar.gz:
6868
LD="${ZIG} cc --target=$*"\
6969
AR="${ZIG} ar" RANLIB="${ZIG} ranlib"
7070
cd static-$*/nc && ../../ncurses/configure --prefix="`pwd`/../inst"\
71-
--with-pkg-config-libdir="`pwd`/../inst/pkg"\
7271
--without-cxx --without-cxx-binding --without-ada --without-manpages --without-progs\
73-
--without-tests --enable-pc-files --without-pkg-config --without-shared --without-debug\
72+
--without-tests --disable-pc-files --without-pkg-config --without-shared --without-debug\
7473
--without-gpm --without-sysmouse --enable-widec --with-default-terminfo-dir=/usr/share/terminfo\
7574
--with-terminfo-dirs=/usr/share/terminfo:/lib/terminfo:/usr/local/share/terminfo\
7675
--with-fallbacks="screen linux vt100 xterm xterm-256color" --host=$*\
7776
CC="${ZIG} cc --target=$*"\
7877
LD="${ZIG} cc --target=$*"\
7978
AR="${ZIG} ar" RANLIB="${ZIG} ranlib"\
80-
CPPFLAGS=-D_GNU_SOURCE && make -j8 && make install.libs
79+
CPPFLAGS=-D_GNU_SOURCE && make -j8
8180
@# zig-build - cleaner approach but doesn't work, results in a dynamically linked binary.
8281
@#cd static-$* && PKG_CONFIG_LIBDIR="`pwd`/inst/pkg" zig build -Dtarget=$*
8382
@# --build-file ../build.zig --search-prefix inst/ --cache-dir zig -Drelease-fast=true
8483
@# Alternative approach, bypassing zig-build
8584
cd static-$* && ${ZIG} build-exe -target $*\
86-
-Iinst/include -Iinst/include/ncursesw -Izstd -lc inst/lib/libncursesw.a zstd/libzstd.a\
85+
-Inc/include -Izstd -lc nc/lib/libncursesw.a zstd/libzstd.a\
8786
--cache-dir zig-cache -static -fstrip -O ReleaseFast ../src/main.zig
88-
strip -R .eh_frame -R .eh_frame_hdr static-$*/main
87+
@# My system's strip can't deal with arm binaries and zig doesn't wrap a strip alternative.
88+
@# Whatever, just let it error for those.
89+
strip -R .eh_frame -R .eh_frame_hdr static-$*/main || true
8990
cd static-$* && mv main ncdu && tar -czf ../static-$*.tar.gz ncdu
9091
rm -rf static-$*
9192

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-License-Identifier: MIT
66
# ncdu-zig
77

88
## Description
9-
<img src="https://github.com/user-attachments/assets/f2b4b591-ac61-4751-8d59-fbaf4c747521" width="50%" />
9+
<img src="https://github.com/user-attachments/assets/a0235a3e-5071-406b-83ce-7343ab0268e1" width="50%" />
1010

1111
Ncdu is a disk usage analyzer with an ncurses interface. It is designed to find
1212
space hogs on a remote server where you don't have an entire graphical setup

build.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ pub fn build(b: *std.Build) void {
88
const optimize = b.standardOptimizeOption(.{});
99

1010
const pie = b.option(bool, "pie", "Build with PIE support (by default false)") orelse false;
11+
const strip = b.option(bool, "strip", "Strip debugging info (by default false)") orelse false;
1112

1213
const exe = b.addExecutable(.{
1314
.name = "ncdu",
1415
.root_source_file = b.path("src/main.zig"),
1516
.target = target,
1617
.optimize = optimize,
18+
.strip = strip,
1719
.link_libc = true,
1820
});
1921

@@ -44,6 +46,7 @@ pub fn build(b: *std.Build) void {
4446
});
4547
unit_tests.pie = pie;
4648
unit_tests.root_module.linkSystemLibrary("ncursesw", .{});
49+
unit_tests.root_module.linkSystemLibrary("libzstd", .{});
4750

4851
const run_unit_tests = b.addRunArtifact(unit_tests);
4952

ncdu.1

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.\" SPDX-FileCopyrightText: Yorhel <[email protected]>
22
.\" SPDX-License-Identifier: MIT
3-
.Dd September 27, 2024
3+
.Dd November 19, 2024
44
.Dt NCDU 1
55
.Os
66
.Sh NAME
@@ -21,7 +21,9 @@
2121
.Op Fl L , \-follow\-symlinks , \-no\-follow\-symlinks
2222
.Op Fl \-include\-kernfs , \-exclude\-kernfs
2323
.Op Fl t , \-threads Ar num
24+
.Op Fl c , \-compress , \-no\-compress
2425
.Op Fl \-compress\-level Ar num
26+
.Op Fl \-export\-block\-size Ar num
2527
.Op Fl 0 , 1 , 2
2628
.Op Fl q , \-slow\-ui\-updates , \-fast\-ui\-updates
2729
.Op Fl \-enable\-shell , \-disable\-shell
@@ -97,6 +99,11 @@ uncompressed, or a little over 100 KiB when compressed with gzip.
9799
This scales linearly, so be prepared to handle a few tens of megabytes when
98100
dealing with millions of files.
99101
.Pp
102+
Consider enabling
103+
.Fl c
104+
to output Zstandard-compressed JSON, which can significantly reduce size of the
105+
exported data.
106+
.Pp
100107
When running a multi-threaded scan or when scanning a directory tree that may
101108
not fit in memory, consider using
102109
.Fl O
@@ -187,12 +194,36 @@ The binary format (see
187194
.Fl O )
188195
does not have this problem and supports efficient exporting with any number of
189196
threads.
197+
.El
198+
.
199+
.Ss Export Options
200+
These options affect behavior when exporting to file with the
201+
.Fl o
202+
or
203+
.Fl O
204+
options.
205+
.Bl -tag -width Ds
206+
.It Fl c , \-compress , \-no\-compress
207+
Enable or disable Zstandard compression when exporting to JSON (see
208+
.Fl o ) .
190209
.It Fl \-compress\-level Ar num
191210
Set the Zstandard compression level when using
192211
.Fl O
193-
to create a binary export.
212+
or
213+
.Fl c .
194214
Valid values are 1 (fastest) to 19 (slowest).
195215
Defaults to 4.
216+
.It Fl \-export\-block\-size Ar num
217+
Set the block size, in kibibytes, for the binary export format (see
218+
.Fl O ) .
219+
Larger blocks require more memory but result in better compression efficiency.
220+
This option can be combined with a higher
221+
.Fl \-compress\-level
222+
for even better compression.
223+
.Pp
224+
Accepted values are between 4 and 16000.
225+
The defaults is to start at 64 KiB and then gradually increase the block size
226+
for large exports.
196227
.El
197228
.
198229
.Ss Interface Options
@@ -487,34 +518,28 @@ Empty directory.
487518
.Sh EXAMPLES
488519
To scan and browse the directory you're currently in, all you need is a simple:
489520
.Dl ncdu
490-
If you want to scan a full filesystem, for example your root filesystem, then
491-
you'll want to use
521+
To scan a full filesystem, for example your root filesystem, you'll want to use
492522
.Fl x :
493523
.Dl ncdu \-x /
494524
.Pp
495525
Since scanning a large directory may take a while, you can scan a directory and
496526
export the results for later viewing:
497527
.Bd -literal -offset indent
498-
ncdu \-1xo\- / | gzip >export.gz
528+
ncdu \-1xO export.ncdu /
499529
# ...some time later:
500-
zcat export.gz | ncdu \-f\-
530+
ncdu \-f export.ncdu
501531
.Ed
502532
To export from a cron job, make sure to replace
503533
.Fl 1
504534
with
505535
.Fl 0
506-
to suppress any unnecessary output.
536+
to suppress unnecessary progress output.
507537
.Pp
508538
You can also export a directory and browse it once scanning is done:
509-
.Dl ncdu \-o\- | tee export.file | ./ncdu \-f\-
510-
The same is possible with gzip compression, but is a bit kludgey:
511-
.Dl ncdu \-o\- | gzip | tee export.gz | gunzip | ./ncdu \-f\-
539+
.Dl ncdu \-co\- | tee export.json.zst | ./ncdu \-f\-
512540
.Pp
513541
To scan a system remotely, but browse through the files locally:
514-
.Dl ssh \-C user@system ncdu \-o\- / | ./ncdu \-f\-
515-
The
516-
.Fl C
517-
option to ssh enables compression, which will be very useful over slow links.
542+
.Dl ssh user@system ncdu \-co\- / | ./ncdu \-f\-
518543
Remote scanning and local viewing has two major advantages when
519544
compared to running
520545
.Nm

src/bin_export.zig

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ const model = @import("model.zig");
77
const sink = @import("sink.zig");
88
const util = @import("util.zig");
99
const ui = @import("ui.zig");
10-
11-
extern fn ZSTD_compress(dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize, compressionLevel: c_int) usize;
12-
extern fn ZSTD_isError(code: usize) c_uint;
10+
const c = @import("c.zig").c;
1311

1412
pub const global = struct {
1513
var fd: std.fs.File = undefined;
@@ -65,9 +63,6 @@ inline fn blockHeader(id: u4, len: u28) [4]u8 { return bigu32((@as(u32, id) << 2
6563

6664
inline fn cborByte(major: CborMajor, arg: u5) u8 { return (@as(u8, @intFromEnum(major)) << 5) | arg; }
6765

68-
// ZSTD_COMPRESSBOUND(), assuming input does not exceed ZSTD_MAX_INPUT_SIZE
69-
fn compressBound(size: usize) usize { return size + (size>>8) + (if (size < (128<<10)) ((128<<10) - size) >> 11 else 0); }
70-
7166

7267
// (Uncompressed) data block size.
7368
// Start with 64k, then use increasingly larger block sizes as the export file
@@ -77,7 +72,8 @@ fn compressBound(size: usize) usize { return size + (size>>8) + (if (size < (128
7772
fn blockSize(num: u32) usize {
7873
// block size uncompressed data in this num range
7974
// # mil # KiB # GiB
80-
return if (num < ( 1<<20)) 64<<10 // 64
75+
return main.config.export_block_size
76+
orelse if (num < ( 1<<20)) 64<<10 // 64
8177
else if (num < ( 2<<20)) 128<<10 // 128
8278
else if (num < ( 4<<20)) 256<<10 // 512
8379
else if (num < ( 8<<20)) 512<<10 // 2048
@@ -100,8 +96,8 @@ pub const Thread = struct {
10096

10197
fn compressZstd(in: []const u8, out: []u8) usize {
10298
while (true) {
103-
const r = ZSTD_compress(out.ptr, out.len, in.ptr, in.len, main.config.complevel);
104-
if (ZSTD_isError(r) == 0) return r;
99+
const r = c.ZSTD_compress(out.ptr, out.len, in.ptr, in.len, main.config.complevel);
100+
if (c.ZSTD_isError(r) == 0) return r;
105101
ui.oom(); // That *ought* to be the only reason the above call can fail.
106102
}
107103
}
@@ -110,7 +106,7 @@ pub const Thread = struct {
110106
var out = std.ArrayList(u8).init(main.allocator);
111107
if (t.block_num == std.math.maxInt(u32) or t.off == 0) return out;
112108

113-
out.ensureTotalCapacityPrecise(12 + compressBound(t.off)) catch unreachable;
109+
out.ensureTotalCapacityPrecise(12 + @as(usize, @intCast(c.ZSTD_COMPRESSBOUND(@as(c_int, @intCast(t.off)))))) catch unreachable;
114110
out.items.len = out.capacity;
115111
const bodylen = compressZstd(t.buf[0..t.off], out.items[8..]);
116112
out.items.len = 12 + bodylen;
@@ -395,7 +391,7 @@ pub const Dir = struct {
395391
p.last_sub = t.itemStart(.dir, p.last_sub, name);
396392
} else {
397393
d.countLinks(null);
398-
global.root_itemref = t.itemStart(.dir, 0, name);
394+
global.root_itemref = t.itemStart(.dir, null, name);
399395
}
400396
d.inodes.deinit();
401397

src/bin_reader.zig

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ const util = @import("util.zig");
88
const sink = @import("sink.zig");
99
const ui = @import("ui.zig");
1010
const bin_export = @import("bin_export.zig");
11-
12-
extern fn ZSTD_decompress(dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, compressedSize: usize) usize;
13-
extern fn ZSTD_getFrameContentSize(src: ?*const anyopaque, srcSize: usize) c_ulonglong;
11+
const c = @import("c.zig").c;
1412

1513

1614
const CborMajor = bin_export.CborMajor;
@@ -103,11 +101,11 @@ fn readBlock(num: u32) []const u8 {
103101
catch |e| ui.die("Error reading from file: {s}\n", .{ui.errorString(e)});
104102
if (rdlen != buf.len) die();
105103

106-
const rawlen = ZSTD_getFrameContentSize(buf.ptr, buf.len);
104+
const rawlen = c.ZSTD_getFrameContentSize(buf.ptr, buf.len);
107105
if (rawlen <= 0 or rawlen >= (1<<24)) die();
108106
block.data = main.allocator.alloc(u8, @intCast(rawlen)) catch unreachable;
109107

110-
const res = ZSTD_decompress(block.data.ptr, block.data.len, buf.ptr, buf.len);
108+
const res = c.ZSTD_decompress(block.data.ptr, block.data.len, buf.ptr, buf.len);
111109
if (res != block.data.len) ui.die("Error decompressing block {} (expected {} got {})\n", .{ num, block.data.len, res });
112110

113111
return block.data;

src/browser.zig

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const mem_sink = @import("mem_sink.zig");
99
const bin_reader = @import("bin_reader.zig");
1010
const delete = @import("delete.zig");
1111
const ui = @import("ui.zig");
12-
const c = @cImport(@cInclude("time.h"));
12+
const c = @import("c.zig").c;
1313
const util = @import("util.zig");
1414

1515
// Currently opened directory.
@@ -644,8 +644,8 @@ const info = struct {
644644
fn keyInput(ch: i32) bool {
645645
if (entry.?.pack.etype == .link) {
646646
switch (ch) {
647-
'1', 'h', ui.c.KEY_LEFT => { set(entry, .info); return true; },
648-
'2', 'l', ui.c.KEY_RIGHT => { set(entry, .links); return true; },
647+
'1', 'h', c.KEY_LEFT => { set(entry, .info); return true; },
648+
'2', 'l', c.KEY_RIGHT => { set(entry, .links); return true; },
649649
else => {},
650650
}
651651
}
@@ -802,17 +802,17 @@ const help = struct {
802802
'1' => tab = .keys,
803803
'2' => tab = .flags,
804804
'3' => tab = .about,
805-
'h', ui.c.KEY_LEFT => tab = if (tab == .about) .flags else .keys,
806-
'l', ui.c.KEY_RIGHT => tab = if (tab == .keys) .flags else .about,
807-
'j', ' ', ui.c.KEY_DOWN, ui.c.KEY_NPAGE => {
805+
'h', c.KEY_LEFT => tab = if (tab == .about) .flags else .keys,
806+
'l', c.KEY_RIGHT => tab = if (tab == .keys) .flags else .about,
807+
'j', ' ', c.KEY_DOWN, c.KEY_NPAGE => {
808808
const max = switch (tab) {
809809
.keys => keys.len/2 - keylines,
810810
else => @as(u32, 0),
811811
};
812812
if (offset < max)
813813
offset += 1;
814814
},
815-
'k', ui.c.KEY_UP, ui.c.KEY_PPAGE => { if (offset > 0) offset -= 1; },
815+
'k', c.KEY_UP, c.KEY_PPAGE => { if (offset > 0) offset -= 1; },
816816
else => state = .main,
817817
}
818818
}
@@ -917,16 +917,16 @@ fn sortToggle(col: main.config.SortCol, default_order: main.config.SortOrder) vo
917917

918918
fn keyInputSelection(ch: i32, idx: *usize, len: usize, page: u32) bool {
919919
switch (ch) {
920-
'j', ui.c.KEY_DOWN => {
920+
'j', c.KEY_DOWN => {
921921
if (idx.*+1 < len) idx.* += 1;
922922
},
923-
'k', ui.c.KEY_UP => {
923+
'k', c.KEY_UP => {
924924
if (idx.* > 0) idx.* -= 1;
925925
},
926-
ui.c.KEY_HOME => idx.* = 0,
927-
ui.c.KEY_END, ui.c.KEY_LL => idx.* = len -| 1,
928-
ui.c.KEY_PPAGE => idx.* = idx.* -| page,
929-
ui.c.KEY_NPAGE => idx.* = @min(len -| 1, idx.* + page),
926+
c.KEY_HOME => idx.* = 0,
927+
c.KEY_END, c.KEY_LL => idx.* = len -| 1,
928+
c.KEY_PPAGE => idx.* = idx.* -| page,
929+
c.KEY_NPAGE => idx.* = @min(len -| 1, idx.* + page),
930930
else => return false,
931931
}
932932
return true;
@@ -1017,7 +1017,7 @@ pub fn keyInput(ch: i32) void {
10171017
},
10181018

10191019
// Navigation
1020-
10, 'l', ui.c.KEY_RIGHT => {
1020+
10, 'l', c.KEY_RIGHT => {
10211021
if (dir_items.items.len == 0) {
10221022
} else if (dir_items.items[cursor_idx]) |e| {
10231023
if (e.dir()) |d| {
@@ -1032,7 +1032,7 @@ pub fn keyInput(ch: i32) void {
10321032
state = .main;
10331033
}
10341034
},
1035-
'h', '<', ui.c.KEY_BACKSPACE, ui.c.KEY_LEFT => {
1035+
'h', '<', c.KEY_BACKSPACE, c.KEY_LEFT => {
10361036
if (dir_parents.items.len > 1) {
10371037
//const h = dir_parent.entry.nameHash();
10381038
enterParent();

src/c.zig

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: Yorhel <[email protected]>
2+
// SPDX-License-Identifier: MIT
3+
4+
pub const c = @cImport({
5+
@cDefine("_XOPEN_SOURCE", "1"); // for wcwidth()
6+
@cInclude("stdio.h"); // fopen(), used to initialize ncurses
7+
@cInclude("string.h"); // strerror()
8+
@cInclude("time.h"); // strftime()
9+
@cInclude("wchar.h"); // wcwidth()
10+
@cInclude("locale.h"); // setlocale() and localeconv()
11+
@cInclude("fnmatch.h"); // fnmatch()
12+
@cInclude("unistd.h"); // getuid()
13+
@cInclude("sys/types.h"); // struct passwd
14+
@cInclude("pwd.h"); // getpwnam(), getpwuid()
15+
if (@import("builtin").os.tag == .linux) {
16+
@cInclude("sys/vfs.h"); // statfs()
17+
}
18+
@cInclude("curses.h");
19+
@cInclude("zstd.h");
20+
});

0 commit comments

Comments
 (0)