summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--ChangeLog257
-rw-r--r--Makefile42
-rw-r--r--README.md4
-rw-r--r--build.zig55
-rw-r--r--ncdu.1515
-rw-r--r--ncdu.pod454
-rw-r--r--src/browser.zig217
-rw-r--r--src/delete.zig15
-rw-r--r--src/exclude.zig322
-rw-r--r--src/main.zig549
-rw-r--r--src/model.zig288
-rw-r--r--src/ncurses_refs.c30
-rw-r--r--src/scan.zig222
-rw-r--r--src/ui.zig254
-rw-r--r--src/util.zig123
16 files changed, 1950 insertions, 1398 deletions
diff --git a/.gitignore b/.gitignore
index fcf65ca..b5ae571 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,6 @@
*.swp
*~
-ncdu.1
ncurses
static-*/
zig-cache/
diff --git a/ChangeLog b/ChangeLog
index 52ea325..c6a8804 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,67 @@
-# SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+# SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
# SPDX-License-Identifier: MIT
+2.3 - 2023-08-04
+ - Now requires Zig 0.11
+ - Add --(enable|disable)-natsort options
+ - Add indicator to apparent size/disk usage selection in the footer
+ - Fix build on armv7l (hopefully)
+ - Minor build system additions
+
+2.2.2 - 2023-01-19
+ - Now requires Zig 0.10 or 0.10.1
+ - That's it, pretty much.
+
+2.2.1 - 2022-10-25
+ - Still requires Zig 0.9.0 or 0.9.1
+ - Fix bug with 'dark' and 'off' color themes on FreeBSD and MacOS
+
+2.2 - 2022-10-17
+ - Still requires Zig 0.9.0 or 0.9.1
+ - (breaking) Wildcards in exclude patterns don't cross directory boundary anymore
+ - Improve exclude pattern matching performance
+ - Set full background in default dark-bg color scheme
+ - Fix broken JSON export when a filename contains control characters below 0x10
+
+2.1.2 - 2022-04-28
+ - Still requires Zig 0.9.0 or 0.9.1
+ - Fix possible crash on shortening file names with unicode variation
+ selectors or combining marks
+
+2.1.1 - 2022-03-25
+ - Still requires Zig 0.9.0 or 0.9.1
+ - Fix potential crash when refreshing
+ - Fix typo in --graph-style=eighth-block
+ - Revert default --graph-style to hash characters
+
+2.1 - 2022-02-07
+ - Still requires Zig 0.9.0
+ - Use natural sort order when sorting by file name
+ - Use Unicode box drawing characters for the file size bar
+ - Add --graph-style option to change drawing style for the file size bar
+ - Fix early exit if a configuration directory does not exist
+ - Fix display glitch for long file names
+ - Fix display glitch with drawing unique/shared size column
+
+2.0.1 - 2022-01-01
+ - Still requires Zig 0.9.0
+ - Fix build failure to find 'wcwidth' on some systems
+ - Add ZIG_FLAGS option to Makefile
+
+2.0 - 2021-12-21
+ - Requires Zig 0.9.0
+ - That's the only change.
+
+2.0-beta3 - 2021-11-09
+ - Requires Zig 0.8 or 0.8.1
+ - Add lots of new CLI flags to configure ncdu
+ - Add configuration file support
+ - Add 'dark-bg' color scheme and use that by default
+ - Fix not enabling -x by default
+ - Fix export feature
+ - Fix import of "special" dirs and files
+ - Fix double-slash display in file browser
+
2.0-beta2 - 2021-07-31
- Requires Zig 0.8
- Significantly reduce memory usage for hard links
@@ -10,199 +71,7 @@
2.0-beta1 - 2021-07-22
- Full release announcement: https://dev.yorhel.nl/doc/ncdu2
- Requires Zig 0.8
+ - Features and UI based on ncdu 1.16
- Lower memory use in most scenarios (except with many hard links)
- Improved performance of hard link counting
- Extra column for shared/unique directory sizes
-
-1.16 - 2021-07-02
- - Increase width of size bar depending on terminal size (Christian Göttsche)
- - Set/increment $NCDU_LEVEL variable when spawning a shell
- - Indicate whether apparent size or disk usage is being displayed
- - Display setuid, setgid and sticky bits in file flags in extended mode
- - Fix error handling while reading --exclude-from file
- - Improve JSON import to allow for several future extensions to the format
- - Export link count in JSON dumps
- - Don't export inode in JSON dumps for non-hardlinks
-
-1.15.1 - 2020-06-10
- - (Linux) Fix build on older Linux systems (Christian Göttsche)
- - (MacOS) Revert "Exclude firmlinks by default" behavior (until we have a better solution)
- - (MacOS) Add --exclude-firmlinks option to opt-in to the above behavior
-
-1.15 - 2020-05-30
- - (Linux) Add --exclude-kernfs option to exclude pseudo filesystems (Christian Göttsche)
- - (MacOS) Exclude firmlinks by default (Saagar Jha)
- - (MacOS) Add --follow-firmlinks option to follow firmlinks (Saagar Jha)
- - Fix bug in calculating the apparent size of directories containing hardlinks
- - Fix integer overflow with directories containing >2GiB worth of file names
- - Fix yet another possible 100% CPU bug when losing terminal
-
-1.14.2 - 2020-02-10
- - Fix compilation with GCC 10 (-fno-common)
- - Fix minor display issue when scanning 10M+ files
- - Slightly reduce memory usage for hard link detection
-
-1.14.1 - 2019-08-05
- - Fix occasional early exit on OS X
- - Fix --exclude-caches
- - Improve handling of out-of-memory situations
-
-1.14 - 2019-02-04
- - Add mtime display and sorting (Alex Wilson)
- - Add (limited) --follow-symlinks option (Simon Doppler)
- - Display larger file counts in browser UI
- - Add -V, --version, and --help alias flags
- - Fix crash when attempting to sort an empty directory
- - Fix 100% CPU bug when ncdu loses the terminal
- - Fix '--color=off' flag
- - Fix some typos
-
-1.13 - 2018-01-29
- - Add "extended information" mode and -e flag
- - Add file mode, modification time and uid/gid to info window with -e
- - Add experimental color support and --color flag
- - Add -rr option to disable shell spawning
- - Remove directory nesting limit on file import
- - Fix handling of interrupts during file import
- - Fix undefined behavior that triggered crash on OS X
-
-1.12 - 2016-08-24
- - Add NCDU_SHELL environment variable
- - Add --confirm-quit flag
- - Fix compilation due to missing sys/wait.h include
-
-1.11 - 2015-04-05
- - Added 'b' key to spawn shell in the current directory
- - Support scanning (and refreshing) of empty directories
- - Added --si flag for base 10 prefixes
- - Fix toggle dirs before files
-
-1.10 - 2013-05-09
- - Added 'c' key to display item counts
- - Added 'C' key to order by item counts
- - Added CACHEDIR.TAG support and --exclude-caches option
- - Use locale-dependent thousand separator
- - Use pkg-config to detect ncurses
- - Clip file/dir sizes to 8 EiB minus one byte
- - Fix buffer overflow when formatting huge file sizes
-
-1.9 - 2012-09-27
- - Added option to dump scanned directory information to a file (-o)
- - Added option to load scanned directory information from a file (-f)
- - Added multiple scan and load interfaces (-0,-1,-2)
- - Fit loading and error windows to the terminal width (#13)
- - Fix symlink resolving bug (#18)
- - Fix path display when scanning an empty directory (#15)
- - Fix hang when terminal is resized to a too small size while loading
- - Use top-level automake build
- - Remove useless AUTHORS, INSTALL and NEWS files
- - ncdu.1 now uses POD as source format
-
-1.8 - 2011-11-03
- - Use hash table to speed up hard link detection
- - Added read-only option (-r)
- - Use KiB instead of kiB (#3399279)
-
-1.7 - 2010-08-13
- - List the detected hard links in file info window
- - Count the size of a hard linked file once for each directory it appears in
- - Fixed crash on browsing dirs with a small window size (#2991787)
- - Fixed buffer overflow when some directories can't be scanned (#2981704)
- - Fixed segfault when launched on a nonexistent directory (#3012787)
- - Fixed segfault when root dir only contains hidden files
- - Improved browsing performance
- - More intuitive multi-page browsing
- - Display size graph by default
- - Various minor fixes
-
-1.6 - 2009-10-23
- - Implemented hard link detection
- - Properly select the next item after deletion
- - Removed reliance of dirfd()
- - Fixed non-void return in void delete_process()
- - Fixed several tiny memory leaks
- - Return to previously opened directory on failed recalculation
- - Properly display MiB units instead of MB (IEEE 1541 - bug #2831412)
- - Link to ncursesw when available
- - Improved support for non-ASCII characters
- - VIM keybindings for browsing through the tree (#2788249, #1880622)
-
-1.5 - 2009-05-02
- - Fixed incorrect apparent size on directory refresh
- - Browsing keys now work while file info window is displayed
- - Current directory is assumed when no directory is specified
- - Size graph uses the apparent size if that is displayed
- - Items are ordered by displayed size rather than disk usage
- - Removed switching between powers of 1000/1024
- - Don't rely on the availability of suseconds_t
- - Correctly handle paths longer than PATH_MAX
- - Fixed various bugs related to rpath()
- - Major code rewrite
- - Fixed line width when displaying 100%
-
-1.4 - 2008-09-10
- - Removed the startup window
- - Filenames ending with a tidle (~) will now also
- be hidden with the 'h'-key
- - Fixed buffer overflow when supplying a path longer
- than PATH_MAX (patch by Tobias Stoeckmann)
- - Used S_BLKSIZE instead of a hardcoded block size of 512
- - Fixed display of disk usage and apparent sizes
- - Updated ncdu -h
- - Included patches for Cygwin
- - Cursor now follows the selected item
- - Added spaces around path (debian #472194)
- - Fixed segfault on empty directory (debian #472294)
- - A few code rewrites and improvements
-
-1.3 - 2007-08-05
- - Added 'r'-key to refresh the current directory
- - Removed option to calculate apparent size: both
- the disk usage and the apparent size are calculated.
- - Added 'a'-key to switch between showing apparent
- size and disk usage.
- - Added 'i'-key to display information about the
- selected item.
- - Small performance improvements
- - configure checks for ncurses.h (bug #1764304)
-
-1.2 - 2007-07-24
- - Fixed some bugs on cygwin
- - Added du-like exclude patterns
- - Fixed bug #1758403: large directories work fine now
- - Rewrote a large part of the code
- - Fixed a bug with wide characters
- - Performance improvements when browsing large dirs
-
-1.1 - 2007-04-30
- - Deleting files and directories is now possible from
- within ncdu.
- - The key for sorting directories between files has
- changed to 't' instead of 'd'. The 'd'-key is now
- used for deleting files.
-
-1.0 - 2007-04-06
- - First stable release
- - Small code cleanup
- - Added a key to toggle between sorting dirs before
- files and dirs between files
- - Added graphs and percentages to the directory
- browser (can be enabled or disabled with the 'g'-key)
-
-0.3 - 2007-03-04
- - When browsing back to the previous directory, the
- directory you're getting back from will be selected.
- - Added directory scanning in quiet mode to save
- bandwidth on remote connections.
-
-0.2 - 2007-02-26
- - Fixed POSIX compliance: replaced realpath() with my
- own implementation, and gettimeofday() is not
- required anymore (but highly recommended)
- - Added a warning for terminals smaller than 60x16
- - Mountpoints (or any other directory pointing to
- another filesystem) are now considered to be
- directories rather than files.
-
-0.1 - 2007-02-21
- - Initial version
diff --git a/Makefile b/Makefile
index 0761fb7..bc14e76 100644
--- a/Makefile
+++ b/Makefile
@@ -1,39 +1,37 @@
-# SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+# SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
# SPDX-License-Identifier: MIT
# Optional semi-standard Makefile with some handy tools.
# Ncdu itself can be built with just the zig build system.
+ZIG ?= zig
+
PREFIX ?= /usr/local
BINDIR ?= ${PREFIX}/bin
MANDIR ?= ${PREFIX}/share/man/man1
+ZIG_FLAGS ?= -Doptimize=ReleaseFast
NCDU_VERSION=$(shell grep 'program_version = "' src/main.zig | sed -e 's/^.*"\(.\+\)".*$$/\1/')
+.PHONY: build
+build: release
+
release:
- zig build -Drelease-fast
+ $(ZIG) build ${ZIG_FLAGS}
debug:
- zig build
+ $(ZIG) build
clean:
rm -rf zig-cache zig-out
-distclean: clean
- rm -f ncdu.1
-
-doc: ncdu.1
-
-ncdu.1: ncdu.pod src/main.zig
- pod2man --center "ncdu manual" --release "ncdu-${NCDU_VERSION}" ncdu.pod >ncdu.1
-
install: install-bin install-doc
install-bin: release
mkdir -p ${BINDIR}
install -m0755 zig-out/bin/ncdu ${BINDIR}/
-install-doc: doc
+install-doc:
mkdir -p ${MANDIR}
install -m0644 ncdu.1 ${MANDIR}/
@@ -46,10 +44,10 @@ uninstall-bin:
uninstall-doc:
rm -f ${MANDIR}/ncdu.1
-dist: doc
+dist:
rm -f ncdu-${NCDU_VERSION}.tar.gz
mkdir ncdu-${NCDU_VERSION}
- for f in ncdu.1 `git ls-files | grep -v ^\.gitignore`; do mkdir -p ncdu-${NCDU_VERSION}/`dirname $$f`; ln -s "`pwd`/$$f" ncdu-${NCDU_VERSION}/$$f; done
+ for f in `git ls-files | grep -v ^\.gitignore`; do mkdir -p ncdu-${NCDU_VERSION}/`dirname $$f`; ln -s "`pwd`/$$f" ncdu-${NCDU_VERSION}/$$f; done
tar -cophzf ncdu-${NCDU_VERSION}.tar.gz --sort=name ncdu-${NCDU_VERSION}
rm -rf ncdu-${NCDU_VERSION}
@@ -64,25 +62,25 @@ static-%.tar.gz:
--without-gpm --without-sysmouse --enable-widec --with-default-terminfo-dir=/usr/share/terminfo\
--with-terminfo-dirs=/usr/share/terminfo:/lib/terminfo:/usr/local/share/terminfo\
--with-fallbacks="screen linux vt100 xterm xterm-256color" --host=$*\
- CC="zig cc --target=$*"\
- LD="zig cc --target=$*"\
- AR="zig ar" RANLIB="zig ranlib"\
+ CC="${ZIG} cc --target=$*"\
+ LD="${ZIG} cc --target=$*"\
+ AR="${ZIG} ar" RANLIB="${ZIG} ranlib"\
CPPFLAGS=-D_GNU_SOURCE && make && make install.libs
@# zig-build - cleaner approach but doesn't work, results in a dynamically linked binary.
@#cd static-$* && PKG_CONFIG_LIBDIR="`pwd`/inst/pkg" zig build -Dtarget=$*
@# --build-file ../build.zig --search-prefix inst/ --cache-dir zig -Drelease-fast=true
@# Alternative approach, bypassing zig-build
- cd static-$* && zig build-exe -target $*\
+ cd static-$* && ${ZIG} build-exe -target $*\
-Iinst/include -Iinst/include/ncursesw -lc inst/lib/libncursesw.a\
- --cache-dir zig-cache -static --strip -O ReleaseFast ../src/main.zig ../src/ncurses_refs.c
+ --cache-dir zig-cache -static -fstrip -O ReleaseFast ../src/main.zig
cd static-$* && mv main ncdu && tar -czf ../static-$*.tar.gz ncdu
rm -rf static-$*
static-linux-x86_64: static-x86_64-linux-musl.tar.gz
mv $< ncdu-${NCDU_VERSION}-linux-x86_64.tar.gz
-static-linux-i386: static-i386-linux-musl.tar.gz
- mv $< ncdu-${NCDU_VERSION}-linux-i386.tar.gz
+static-linux-x86: static-x86-linux-musl.tar.gz
+ mv $< ncdu-${NCDU_VERSION}-linux-x86.tar.gz
static-linux-aarch64: static-aarch64-linux-musl.tar.gz
mv $< ncdu-${NCDU_VERSION}-linux-aarch64.tar.gz
@@ -92,6 +90,6 @@ static-linux-arm: static-arm-linux-musleabi.tar.gz
static:\
static-linux-x86_64 \
- static-linux-i386 \
+ static-linux-x86 \
static-linux-aarch64 \
static-linux-arm
diff --git a/README.md b/README.md
index 1fa29af..050a639 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
SPDX-License-Identifier: MIT
-->
@@ -19,7 +19,7 @@ C version (1.x).
## Requirements
-- Zig 0.8
+- Zig 0.11.0
- Some sort of POSIX-like OS
- ncurses libraries and header files
diff --git a/build.zig b/build.zig
index e16eb2d..00c6dbe 100644
--- a/build.zig
+++ b/build.zig
@@ -1,21 +1,31 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
const std = @import("std");
-pub fn build(b: *std.build.Builder) void {
+pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
- const mode = b.standardReleaseOptions();
+ const optimize = b.standardOptimizeOption(.{});
- const exe = b.addExecutable("ncdu", "src/main.zig");
- exe.setTarget(target);
- exe.setBuildMode(mode);
- exe.addCSourceFile("src/ncurses_refs.c", &[_][]const u8{});
- exe.linkLibC();
- exe.linkSystemLibrary("ncursesw");
- exe.install();
+ const pie = b.option(bool, "pie", "Build with PIE support (by default false)") orelse false;
- const run_cmd = exe.run();
+ const exe = b.addExecutable(.{
+ .name = "ncdu",
+ .root_source_file = .{ .path = "src/main.zig" },
+ .target = target,
+ .optimize = optimize,
+ });
+
+ // https://github.com/ziglang/zig/blob/b52be973dfb7d1408218b8e75800a2da3dc69108/build.zig#L551-L554
+ if (exe.target.isDarwin()) {
+ // useful for package maintainers
+ exe.headerpad_max_install_names = true;
+ }
+ linkNcurses(exe);
+ exe.pie = pie;
+ b.installArtifact(exe);
+
+ const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
@@ -24,10 +34,21 @@ pub fn build(b: *std.build.Builder) void {
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
- const tst = b.addTest("src/main.zig");
- tst.linkLibC();
- tst.linkSystemLibrary("ncursesw");
- tst.addCSourceFile("src/ncurses_refs.c", &[_][]const u8{});
- const tst_step = b.step("test", "Run tests");
- tst_step.dependOn(&tst.step);
+ const unit_tests = b.addTest(.{
+ .root_source_file = .{ .path = "src/main.zig" },
+ .target = target,
+ .optimize = optimize,
+ });
+ linkNcurses(unit_tests);
+ unit_tests.pie = pie;
+
+ const run_unit_tests = b.addRunArtifact(unit_tests);
+
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_unit_tests.step);
+}
+
+pub fn linkNcurses(compile_step: *std.Build.CompileStep) void {
+ compile_step.linkSystemLibrary("ncursesw");
+ compile_step.linkLibC();
}
diff --git a/ncdu.1 b/ncdu.1
new file mode 100644
index 0000000..cfadc8b
--- /dev/null
+++ b/ncdu.1
@@ -0,0 +1,515 @@
+.\" SPDX-FileCopyrightText: Yoran Heling <projects@yorhel.nl>
+.\" SPDX-License-Identifier: MIT
+.Dd January 21, 2024
+.Dt NCDU 1
+.Os
+.Sh NAME
+.Nm ncdu
+.Nd NCurses Disk Usage
+.Sh SYNOPSIS
+.Nm
+.Op Fl f Ar file
+.Op Fl o Ar file
+.Op Fl e , \-extended , \-no\-extended
+.Op Fl \-ignore\-config
+.Op Fl x , \-one\-file\-system , \-cross\-file\-system
+.Op Fl \-exclude Ar pattern
+.Op Fl X , \-exclude\-from Ar file
+.Op Fl \-include\-caches , \-exclude\-caches
+.Op Fl L , \-follow\-symlinks , \-no\-follow\-symlinks
+.Op Fl \-include\-kernfs , \-exclude\-kernfs
+.Op Fl 0 , 1 , 2
+.Op Fl q , \-slow\-ui\-updates , \-fast\-ui\-updates
+.Op Fl \-enable\-shell , \-disable\-shell
+.Op Fl \-enable\-delete , \-disable\-delete
+.Op Fl \-enable\-refresh , \-disable\-refresh
+.Op Fl r
+.Op Fl \-si , \-no\-si
+.Op Fl \-disk\-usage , \-apparent\-size
+.Op Fl \-show\-hidden , \-hide\-hidden
+.Op Fl \-show\-itemcount , \-hide\-itemcount
+.Op Fl \-show\-mtime , \-hide\-mtime
+.Op Fl \-show\-graph , \-hide\-graph
+.Op Fl \-show\-percent , \-hide\-percent
+.Op Fl \-graph\-style Ar hash | half\-block | eighth\-block
+.Op Fl \-shared\-column Ar off | shared | unique
+.Op Fl \-sort Ar column
+.Op Fl \-enable\-natsort , \-disable\-natsort
+.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 \-color Ar off | dark | dark-bg
+.Op Ar path
+.Nm
+.Op Fl h , \-help
+.Nm
+.Op Fl v , V , \-version
+.Sh DESCRIPTION
+.Nm
+(NCurses Disk Usage) is an interactive curses-based version of the well-known
+.Xr du 1 ,
+and provides a fast way to see what directories are using your disk space.
+.Sh OPTIONS
+.Ss Mode Selection
+.Bl -tag -width Ds
+.It Fl h , \-help
+Print a short help message and quit.
+.It Fl v , V , \-version
+Print version and quit.
+.It Fl f Ar file
+Load the given file, which has earlier been created with the
+.Fl o
+flag.
+If
+.Ar file
+is equivalent to '\-', the file is read from standard input.
+.Pp
+For the sake of preventing a screw-up, the current version of
+.Nm
+will assume that the directory information in the imported file does not
+represent the filesystem on which the file is being imported.
+That is, the refresh, file deletion and shell spawning options in the browser
+will be disabled.
+.It Ar dir
+Scan the given directory.
+.It Fl o Ar file
+Export all necessary information to
+.Ar file
+instead of opening the browser interface.
+If
+.Ar file
+is '\-', the data is written to standard output.
+See the examples section below for some handy use cases.
+.Pp
+Be warned that the exported data may grow quite large when exporting a
+directory with many files.
+10.000 files will get you an export in the order of 600 to 700 KiB
+uncompressed, or a little over 100 KiB when compressed with gzip.
+This scales linearly, so be prepared to handle a few tens of megabytes when
+dealing with millions of files.
+.It Fl e , \-extended , \-no\-extended
+Enable/disable extended information mode.
+This will, in addition to the usual file information, also read the ownership,
+permissions and last modification time for each file.
+This will result in higher memory usage (by roughly ~30%) and in a larger
+output file when exporting.
+.Pp
+When using the file export/import function, this flag should be added both when
+exporting (to make sure the information is added to the export) and when
+importing (to read this extra information in memory).
+This flag has no effect when importing a file that has been exported without
+the extended information.
+.Pp
+This enables viewing and sorting by the latest child mtime, or modified time,
+using 'm' and 'M', respectively.
+.It Fl \-ignore\-config
+Do not attempt to load any configuration files.
+.El
+.Ss Scan Options
+These options affect the scanning progress, they have no effect when importing
+directory information from a file.
+.Bl -tag -width Ds
+.It Fl x , \-one\-file\-system
+Do not cross filesystem boundaries, i.e. only count files and directories on
+the same filesystem as the directory being scanned.
+.It Fl \-cross\-file\-system
+Do cross filesystem boundaries.
+This is the default, but can be specified to overrule a previously configured
+.Fl x .
+.It Fl \-exclude Ar pattern
+Exclude files that match
+.Ar pattern .
+The files are still displayed by default, but are not counted towards the disk
+usage statistics.
+This argument can be added multiple times to add more patterns.
+.It Fl X , \-exclude\-from Ar file
+Exclude files that match any pattern in
+.Ar file .
+Patterns should be separated by a newline.
+.It Fl \-include\-caches , \-exclude\-caches
+Include (default) or exclude directories containing
+.Pa CACHEDIR.TAG .
+Excluded cache directories are still displayed, but their contents will not be
+scanned or counted towards the disk usage statistics.
+.Lk https://bford.info/cachedir/
+.It Fl L , \-follow\-symlinks , \-no\-follow\-symlinks
+Follow (or not) symlinks and count the size of the file they point to.
+This option does not follow symlinks to directories and will cause each
+symlinked file to count as a unique file.
+This is different from how hard links are handled.
+The exact counting behavior of this flag is subject to change in the future.
+.It Fl \-include\-kernfs , \-exclude\-kernfs
+(Linux only) Include (default) or exclude Linux pseudo filesystems such as
+.Pa /proc
+(procfs) and
+.Pa /sys
+(sysfs).
+.Pp
+The complete list of currently known pseudo filesystems is: binfmt, bpf, cgroup,
+cgroup2, debug, devpts, proc, pstore, security, selinux, sys, trace.
+.El
+.Ss Interface Options
+.Bl -tag -width Ds
+.It Fl 0
+Don't give any feedback while scanning a directory or importing a file, except
+when a fatal error occurs.
+Ncurses will not be initialized until the scan is complete.
+When exporting the data with
+.Fl o ,
+ncurses will not be initialized at all.
+This option is the default when exporting to standard output.
+.It Fl 1
+Similar to
+.Fl 0 ,
+but does give feedback on the scanning progress with a single line of output.
+This option is the default when exporting to a file.
+.Pp
+In some cases, the ncurses browser interface which you'll see after the
+scan/import is complete may look garbled when using this option.
+If you're not exporting to a file,
+.Fl 2
+is usually a better choice.
+.It Fl 2
+Show a full-screen ncurses interface while scanning a directory or importing
+a file.
+This is the only interface that provides feedback on any non-fatal errors while
+scanning.
+.It Fl q , \-slow\-ui\-updates , \-fast\-ui\-updates
+Change the UI update interval while scanning or importing.
+.Nm
+updates the screen 10 times a second by default (with
+.Fl \-fast\-ui\-updates
+), this can be decreased to once every 2 seconds with
+.Fl q
+or
+.Fl \-slow\-ui\-updates .
+This option can be used to save bandwidth over remote connections.
+This option has no effect in combination with
+.Fl 0 .
+.It Fl \-enable\-shell , \-disable\-shell
+Enable or disable shell spawning from the file browser.
+This feature is enabled by default when scanning a live directory and disabled
+when importing from file.
+.It Fl \-enable\-delete , \-disable\-delete
+Enable or disable the built-in file deletion feature.
+This feature is enabled by default when scanning a live directory and disabled
+when importing from file.
+Explicitly disabling the deletion feature can work as a safeguard to prevent
+accidental data loss.
+.It Fl \-enable\-refresh , \-disable\-refresh
+Enable or disable directory refreshing from the file browser.
+This feature is enabled by default when scanning a live directory and disabled
+when importing from file.
+.It Fl r
+Read-only mode.
+When given once, this is an alias for
+.Fl \-disable\-delete ,
+when given twice it will also add
+.Fl \-disable\-shell ,
+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
+defined in the International System of Units (SI), instead of the usual base 2
+prefixes (KiB, MiB, etc).
+.It Fl \-disk\-usage , \-apparent\-size
+Select whether to display disk usage (default) or apparent sizes.
+Can also be toggled in the file browser with the 'a' key.
+.It Fl \-show\-hidden , \-hide\-hidden
+Show (default) or hide "hidden" and excluded files.
+Can also be toggled in the file browser with the 'e' key.
+.It Fl \-show\-itemcount , \-hide\-itemcount
+Show or hide (default) the item counts column.
+Can also be toggled in the file browser with the 'c' key.
+.It Fl \-show\-mtime , \-hide\-mtime
+Show or hide (default) the last modification time column.
+Can also be toggled in the file browser with the 'm' key.
+This option is ignored when not in extended mode, see
+.Fl e .
+.It Fl \-show\-graph , \-hide\-graph
+Show (default) or hide the relative size bar column.
+Can also be toggled in the file browser with the 'g' key.
+.It Fl \-show\-percent , \-hide\-percent
+Show (default) or hide the relative size percent column.
+Can also be toggled in the file browser with the 'g' key.
+.It Fl \-graph\-style Ar hash | half\-block | eighth\-block
+Change the way that the relative size bar column is drawn.
+Recognized values are
+.Ar hash
+to draw ASCII '#' characters (default and most portable),
+.Ar half\-block
+to use half-block drawing characters or
+.Ar eighth\-block
+to use eighth-block drawing characters.
+Eighth-block characters are the most precise but may not render correctly in
+all terminals.
+.It Fl \-shared\-column Ar off | shared | unique
+Set to
+.Ar off
+to disable the shared size column for directories,
+.Ar shared
+(default) to display shared directory sizes as a separate column or
+.Ar unique
+to display unique directory sizes as a separate column.
+These options can also be cycled through in the file browser with the 'u' key.
+.It Fl \-sort Ar column
+Change the default column to sort on.
+Accepted values are
+.Ar disk\-usage
+(the default),
+.Ar name , apparent\-size , itemcount
+or
+.Ar mtime .
+The latter only makes sense in extended mode, see
+.Fl e .
+.Pp
+The column name can be suffixed with
+.Li \-asc
+or
+.Li \-desc
+to change the order to ascending or descending, respectively.
+For example,
+.Li \-\-sort=name\-desc
+to sort by name in descending order.
+.It Fl \-enable\-natsort , \-disable\-natsort
+Enable (default) or disable natural sort when sorting by file name.
+.It Fl \-group\-directories\-first , \-no\-group\-directories\-first
+Sort (or not) directories before files.
+.It Fl \-confirm\-quit , \-no\-confirm\-quit
+Require a confirmation before quitting ncdu.
+Can be helpful when you accidentally press 'q' during or after a very long scan.
+.It Fl \-confirm\-delete , \-no\-confirm\-delete
+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 \-color Ar off | dark | dark-bg
+Set the color scheme.
+The following schemes are recognized:
+.Ar off
+to disable colors,
+.Ar dark
+for a color scheme intended for dark backgrounds and
+.Ar dark\-bg
+for a variation of the
+.Ar dark
+color scheme that also works in terminals with a light background.
+.Pp
+The default is
+.Ar dark\-bg
+unless the
+.Ev NO_COLOR
+environment variable is set.
+.El
+.Sh CONFIGURATION
+.Nm
+can be configured by placing command-line options in
+.Pa /etc/ncdu.conf
+or
+.Pa $HOME/.config/ncdu/config .
+If both files exist, the system configuration will be loaded before the user
+configuration, allowing users to override options set in the system
+configuration.
+Options given on the command line will override options set in the
+configuration files.
+The files will not be read at all when
+.Fl \-ignore\-config
+is given on the command line.
+.Pp
+The configuration file format is simply one command line option per line.
+Lines starting with '#' are ignored.
+Example configuration file:
+.Bd -literal -offset indent
+# Always enable extended mode
+\-e
+
+# Disable file deletion
+\-\-disable\-delete
+
+# Exclude .git directories
+\-\-exclude .git
+.Ed
+.Sh KEYS
+.Bl -tag -width Ds
+.It ?
+Open help + keys + about screen
+.It up , down , j , k
+Cycle through the items
+.It right, enter, l
+Open selected directory
+.It left, <, h
+Go to parent directory
+.It n
+Order by filename (press again for descending order)
+.It s
+Order by filesize (press again for descending order)
+.It C
+Order by number of items (press again for descending order)
+.It a
+Toggle between showing disk usage and showing apparent size.
+.It M
+Order by latest child mtime, or modified time (press again for descending
+order).
+Requires the
+.Fl e
+flag.
+.It d
+Delete the selected file or directory.
+An error message will be shown when the contents of the directory do not match
+or do not exist anymore on the filesystem.
+.It t
+Toggle dirs before files when sorting.
+.It g
+Toggle between showing percentage, graph, both, or none.
+Percentage is relative to the size of the current directory, graph is relative
+to the largest item in the current directory.
+.It u
+Toggle display of the shared / unique size column for directories that share
+hard links.
+This column is only visible if the current listing contains directories with
+shared hard links.
+.It c
+Toggle display of child item counts.
+.It m
+Toggle display of latest child mtime, or modified time.
+Requires the
+.Fl e
+flag.
+.It e
+Show/hide 'hidden' or 'excluded' files and directories.
+Be aware that even if you can't see the hidden files and directories, they are
+still there and they are still included in the directory sizes.
+If you suspect that the totals shown at the bottom of the screen are not
+correct, make sure you haven't enabled this option.
+.It i
+Show information about the current selected item.
+.It r
+Refresh/recalculate the current directory.
+.It b
+Spawn shell in current directory.
+.Pp
+.Nm
+determines your preferred shell from the
+.Ev NCDU_SHELL
+or
+.Ev SHELL
+environment variable (in that order), or calls
+.Pa /bin/sh
+if neither are set.
+This allows you to also configure another command to be run when he 'b' key is
+pressed.
+For example, to spawn the
+.Xr vifm 1
+file manager instead of a shell, run
+.Nm
+as follows:
+.Dl NCDU_SHELL=vifm ncdu
+The
+.Ev NCDU_LEVEL
+environment variable is set or incremented before spawning the shell, allowing
+you to detect if your shell is running from within
+.Nm .
+This can be useful to avoid nesting multiple instances, although
+.Nm
+itself does not (currently) warn about or prevent this situation.
+.It q
+Quit
+.El
+.Sh FILE FLAGS
+Entries in the browser interface may be prefixed by a one\-character flag.
+These flags have the following meaning:
+.Bl -tag -width Ds
+.It !
+An error occurred while reading this directory.
+.It \.
+An error occurred while reading a subdirectory, so the indicated size may not
+be correct.
+.It <
+File or directory is excluded from the statistics by using exclude patterns.
+.It >
+Directory is on another filesystem.
+.It ^
+Directory is excluded from the statistics due to being a Linux pseudo
+filesystem.
+.It @
+This is neither a file nor a folder (symlink, socket, ...).
+.It H
+Same file was already counted (hard link).
+.It e
+Empty directory.
+.El
+.Sh EXAMPLES
+To scan and browse the directory you're currently in, all you need is a simple:
+.Dl ncdu
+If you want to scan a full filesystem, for example your root filesystem, then
+you'll want to use
+.Fl x :
+.Dl ncdu \-x /
+.Pp
+Since scanning a large directory may take a while, you can scan a directory and
+export the results for later viewing:
+.Bd -literal -offset indent
+ncdu \-1xo\- / | gzip >export.gz
+# ...some time later:
+zcat export.gz | ncdu \-f\-
+.Ed
+To export from a cron job, make sure to replace
+.Fl 1
+with
+.Fl 0
+to suppress any unnecessary output.
+.Pp
+You can also export a directory and browse it once scanning is done:
+.Dl ncdu \-o\- | tee export.file | ./ncdu \-f\-
+The same is possible with gzip compression, but is a bit kludgey:
+.Dl ncdu \-o\- | gzip | tee export.gz | gunzip | ./ncdu \-f\-
+.Pp
+To scan a system remotely, but browse through the files locally:
+.Dl ssh \-C user@system ncdu \-o\- / | ./ncdu \-f\-
+The
+.Fl C
+option to ssh enables compression, which will be very useful over slow links.
+Remote scanning and local viewing has two major advantages when
+compared to running
+.Nm
+directly on the remote system: You can browse through the scanned directory on
+the local system without any network latency, and
+.Nm
+does not keep the entire directory structure in memory when exporting, so this
+won't consume much memory on the remote system.
+.Sh BUGS
+Directory hard links and firmlinks (MacOS) are not supported.
+They are not detected as being hard links, and will thus get scanned and
+counted multiple times.
+.Pp
+Some minor glitches may appear when displaying filenames that contain multibyte
+or multicolumn characters.
+.Pp
+The unique and shared directory sizes are calculated based on the assumption
+that the link count of hard links does not change during a filesystem scan or
+in between refreshes.
+If this does happen, for example when a hard link is deleted, then these
+numbers will be very much incorrect and a full refresh by restarting ncdu is
+needed to get correct numbers again.
+.Pp
+All sizes are internally represented as a signed 64bit integer.
+If you have a directory larger than 8 EiB minus one byte, ncdu will clip its
+size to 8 EiB minus one byte.
+When deleting or refreshing items in a directory with a clipped size, the
+resulting sizes will be incorrect.
+Likewise, item counts are stored in a 32-bit integer, so will be incorrect in
+the unlikely event that you happen to have more than 4 billion items in a
+directory.
+.Pp
+Please report any other bugs you may find at the bug tracker, which can be
+found on the web site at
+.Lk https://dev.yorhel.nl/ncdu
+.Sh SEE ALSO
+.Xr du 1 ,
+.Xr tree 1 .
+.Pp
+.Nm
+has a website:
+.Lk https://dev.yorhel.nl/ncdu
+.Sh AUTHORS
+Written by
+.An Yorhel Aq Mt projects@yorhel.nl
diff --git a/ncdu.pod b/ncdu.pod
deleted file mode 100644
index caa9812..0000000
--- a/ncdu.pod
+++ /dev/null
@@ -1,454 +0,0 @@
-SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
-SPDX-License-Identifier: MIT
-
-=head1 NAME
-
-B<ncdu> - NCurses Disk Usage
-
-
-=head1 SYNOPSIS
-
-B<ncdu> [I<options>] I<dir>
-
-
-=head1 DESCRIPTION
-
-ncdu (NCurses Disk Usage) is a curses-based version of the well-known 'du', and
-provides a fast way to see what directories are using your disk space.
-
-
-=head1 OPTIONS
-
-=head2 Mode Selection
-
-=over
-
-=item -h, --help
-
-Print a short help message and quit.
-
-=item -v, -V, --version
-
-Print ncdu version and quit.
-
-=item -f I<FILE>
-
-Load the given file, which has earlier been created with the C<-o> option. If
-I<FILE> is equivalent to C<->, the file is read from standard input.
-
-For the sake of preventing a screw-up, the current version of ncdu will assume
-that the directory information in the imported file does not represent the
-filesystem on which the file is being imported. That is, the refresh, file
-deletion and shell spawning options in the browser will be disabled.
-
-=item I<dir>
-
-Scan the given directory.
-
-=item -o I<FILE>
-
-Export all necessary information to I<FILE> instead of opening the browser
-interface. If I<FILE> is C<->, the data is written to standard output. See the
-examples section below for some handy use cases.
-
-Be warned that the exported data may grow quite large when exporting a
-directory with many files. 10.000 files will get you an export in the order of
-600 to 700 KiB uncompressed, or a little over 100 KiB when compressed with
-gzip. This scales linearly, so be prepared to handle a few tens of megabytes
-when dealing with millions of files.
-
-=item -e
-
-Enable extended information mode. This will, in addition to the usual file
-information, also read the ownership, permissions and last modification time
-for each file. This will result in higher memory usage (by roughly ~30%) and in
-a larger output file when exporting.
-
-When using the file export/import function, this flag will need to be added
-both when exporting (to make sure the information is added to the export), and
-when importing (to read this extra information in memory). This flag has no
-effect when importing a file that has been exported without the extended
-information.
-
-This enables viewing and sorting by the latest child mtime, or modified time,
-using 'm' and 'M', respectively.
-
-=back
-
-=head2 Interface options
-
-=over
-
-=item -0
-
-Don't give any feedback while scanning a directory or importing a file, other
-than when a fatal error occurs. Ncurses will not be initialized until the scan
-is complete. When exporting the data with C<-o>, ncurses will not be
-initialized at all. This option is the default when exporting to standard
-output.
-
-=item -1
-
-Similar to C<-0>, but does give feedback on the scanning progress with a single
-line of output. This option is the default when exporting to a file.
-
-In some cases, the ncurses browser interface which you'll see after the
-scan/import is complete may look garbled when using this option. If you're not
-exporting to a file, C<-2> is probably a better choice.
-
-=item -2
-
-Provide a full-screen ncurses interface while scanning a directory or importing
-a file. This is the only interface that provides feedback on any non-fatal
-errors while scanning.
-
-=item -q
-
-Quiet mode. While scanning or importing the directory, ncdu will update the
-screen 10 times a second by default, this will be decreased to once every 2
-seconds in quiet mode. Use this feature to save bandwidth over remote
-connections. This option has no effect when C<-0> is used.
-
-=item -r
-
-Read-only mode. This will disable the built-in file deletion feature. This
-option has no effect when C<-o> is used, because there will not be a browser
-interface in that case. It has no effect when C<-f> is used, either, because
-the deletion feature is disabled in that case anyway.
-
-WARNING: This option will only prevent deletion through the file browser. It is
-still possible to spawn a shell from ncdu and delete or modify files from
-there. To disable that feature as well, pass the C<-r> option twice (see
-C<-rr>).
-
-=item -rr
-
-In addition to C<-r>, this will also disable the shell spawning feature of the
-file browser.
-
-=item --si
-
-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, that is, powers of 1024 (KiB, MiB, etc).
-
-=item --confirm-quit
-
-Requires a confirmation before quitting ncdu. Very helpful when you
-accidentally press 'q' during or after a very long scan.
-
-=item --color I<SCHEME>
-
-Select a color scheme. Currently only two schemes are recognized: I<off> to
-disable colors (the default) and I<dark> for a color scheme intended for dark
-backgrounds.
-
-=back
-
-=head2 Scan Options
-
-These options affect the scanning progress, and have no effect when importing
-directory information from a file.
-
-=over
-
-=item -x
-
-Do not cross filesystem boundaries, i.e. only count files and directories on
-the same filesystem as the directory being scanned.
-
-=item --exclude I<PATTERN>
-
-Exclude files that match I<PATTERN>. The files will still be displayed by
-default, but are not counted towards the disk usage statistics. This argument
-can be added multiple times to add more patterns.
-
-=item -X I<FILE>, --exclude-from I<FILE>
-
-Exclude files that match any pattern in I<FILE>. Patterns should be separated
-by a newline.
-
-=item --exclude-caches
-
-Exclude directories containing CACHEDIR.TAG. The directories will still be
-displayed, but not their content, and they are not counted towards the disk
-usage statistics.
-See http://www.brynosaurus.com/cachedir/
-
-=item -L, --follow-symlinks
-
-Follow symlinks and count the size of the file they point to. As of ncdu 1.14,
-this option will not follow symlinks to directories and will count each
-symlinked file as a unique file (i.e. unlike how hard links are handled). This
-is subject to change in later versions.
-
-=item --exclude-firmlinks
-
-(MacOS only) Exclude firmlinks.
-
-=item --exclude-kernfs
-
-(Linux only) Exclude Linux pseudo filesystems, e.g. /proc (procfs), /sys (sysfs).
-
-The complete list of currently known pseudo filesystems is: binfmt, bpf, cgroup,
-cgroup2, debug, devpts, proc, pstore, security, selinux, sys, trace.
-
-=back
-
-
-=head1 KEYS
-
-=over
-
-=item ?
-
-Show help + keys + about screen
-
-=item up, down j, k
-
-Cycle through the items
-
-=item right, enter, l
-
-Open selected directory
-
-=item left, <, h
-
-Go to parent directory
-
-=item n
-
-Order by filename (press again for descending order)
-
-=item s
-
-Order by filesize (press again for descending order)
-
-=item C
-
-Order by number of items (press again for descending order)
-
-=item a
-
-Toggle between showing disk usage and showing apparent size.
-
-=item M
-
-Order by latest child mtime, or modified time. (press again for descending order)
-Requires the -e flag.
-
-=item d
-
-Delete the selected file or directory. An error message will be shown when the
-contents of the directory do not match or do not exist anymore on the
-filesystem.
-
-=item t
-
-Toggle dirs before files when sorting.
-
-=item g
-
-Toggle between showing percentage, graph, both, or none. Percentage is relative
-to the size of the current directory, graph is relative to the largest item in
-the current directory.
-
-=item c
-
-Toggle display of child item counts.
-
-=item m
-
-Toggle display of latest child mtime, or modified time. Requires the -e flag.
-
-=item e
-
-Show/hide 'hidden' or 'excluded' files and directories. Please note that even
-though you can't see the hidden files and directories, they are still there and
-they are still included in the directory sizes. If you suspect that the totals
-shown at the bottom of the screen are not correct, make sure you haven't
-enabled this option.
-
-=item i
-
-Show information about the current selected item.
-
-=item r
-
-Refresh/recalculate the current directory.
-
-=item b
-
-Spawn shell in current directory.
-
-Ncdu will determine your preferred shell from the C<NCDU_SHELL> or C<SHELL>
-variable (in that order), or will call C</bin/sh> if neither are set. This
-allows you to also configure another command to be run when he 'b' key is
-pressed. For example, to spawn the L<vifm(1)> file manager instead of a shell,
-run ncdu as follows:
-
- export NCDU_SHELL=vifm
- ncdu
-
-Ncdu will set the C<NCDU_LEVEL> environment variable or increment it before
-spawning the shell. This variable allows you to detect when your shell is
-running from within ncdu, which can be useful to avoid nesting multiple
-instances of ncdu. Ncdu itself does not (currently) warn when attempting to run
-nested instances.
-
-=item q
-
-Quit
-
-=back
-
-
-=head1 FILE FLAGS
-
-Entries in the browser interface may be prefixed by a one-character flag. These
-flags have the following meaning:
-
-=over
-
-=item !
-
-An error occurred while reading this directory.
-
-=item .
-
-An error occurred while reading a subdirectory, so the indicated size may not be
-correct.
-
-=item <
-
-File or directory is excluded from the statistics by using exclude patterns.
-
-=item >
-
-Directory is on another filesystem.
-
-=item ^
-
-Directory is excluded from the statistics due to being a Linux pseudo filesystem.
-
-=item @
-
-This is neither a file nor a folder (symlink, socket, ...).
-
-=item H
-
-Same file was already counted (hard link).
-
-=item e
-
-Empty directory.
-
-=back
-
-
-=head1 EXAMPLES
-
-To scan and browse the directory you're currently in, all you need is a simple:
-
- ncdu
-
-If you want to scan a full filesystem, your root filesystem, for example, then
-you'll want to use C<-x>:
-
- ncdu -x /
-
-Since scanning a large directory may take a while, you can scan a directory and
-export the results for later viewing:
-
- ncdu -1xo- / | gzip >export.gz
- # ...some time later:
- zcat export.gz | ncdu -f-
-
-To export from a cron job, make sure to replace C<-1> with C<-0> to suppress
-any unnecessary output.
-
-You can also export a directory and browse it once scanning is done:
-
- ncdu -o- | tee export.file | ./ncdu -f-
-
-The same is possible with gzip compression, but is a bit kludgey:
-
- ncdu -o- | gzip | tee export.gz | gunzip | ./ncdu -f-
-
-To scan a system remotely, but browse through the files locally:
-
- ssh -C user@system ncdu -o- / | ./ncdu -f-
-
-The C<-C> option to ssh enables compression, which will be very useful over
-slow links. Remote scanning and local viewing has two major advantages when
-compared to running ncdu directly on the remote system: You can browse through
-the scanned directory on the local system without any network latency, and ncdu
-does not keep the entire directory structure in memory when exporting, so you
-won't consume much memory on the remote system.
-
-
-=head1 HARD LINKS
-
-Every disk usage analysis utility has its own way of (not) counting hard links.
-There does not seem to be any universally agreed method of handling hard links,
-and it is even inconsistent among different versions of ncdu. This section
-explains what each version of ncdu does.
-
-ncdu 1.5 and below does not support any hard link detection at all: each link
-is considered a separate inode and its size is counted for every link. This
-means that the displayed directory sizes are incorrect when analyzing
-directories which contain hard links.
-
-ncdu 1.6 has basic hard link detection: When a link to a previously encountered
-inode is detected, the link is considered to have a file size of zero bytes.
-Its size is not counted again, and the link is indicated in the browser
-interface with a 'H' mark. The displayed directory sizes are only correct when
-all links to an inode reside within that directory. When this is not the case,
-the sizes may or may not be correct, depending on which links were considered
-as "duplicate" and which as "original". The indicated size of the topmost
-directory (that is, the one specified on the command line upon starting ncdu)
-is always correct.
-
-ncdu 1.7 and later has improved hard link detection. Each file that has more
-than two links has the "H" mark visible in the browser interface. Each hard
-link is counted exactly once for every directory it appears in. The indicated
-size of each directory is therefore, correctly, the sum of the sizes of all
-unique inodes that can be found in that directory. Note, however, that this may
-not always be same as the space that will be reclaimed after deleting the
-directory, as some inodes may still be accessible from hard links outside it.
-
-
-=head1 BUGS
-
-Directory hard links and firmlinks (MacOS) are not supported. They will not be
-detected as being hard links, and may thus be scanned and counted multiple
-times.
-
-Some minor glitches may appear when displaying filenames that contain multibyte
-or multicolumn characters.
-
-The unique and shared directory sizes are calculated based on the assumption
-that the link count of hard links does not change during a filesystem scan or
-in between refreshes. If it does, for example after deleting a hard link, then
-these numbers will be very much incorrect and a full refresh by restarting ncdu
-is needed to get correct numbers again.
-
-All sizes are internally represented as a signed 64bit integer. If you have a
-directory larger than 8 EiB minus one byte, ncdu will clip its size to 8 EiB
-minus one byte. When deleting or refreshing items in a directory with a clipped
-size, the resulting sizes will be incorrect. Likewise, item counts are stored
-in a 32-bit integer, so will be incorrect in the unlikely event that you happen
-to have more than 4 billion items in a directory.
-
-Please report any other bugs you may find at the bug tracker, which can be
-found on the web site at https://dev.yorhel.nl/ncdu
-
-
-=head1 AUTHOR
-
-Written by Yoran Heling <projects@yorhel.nl>.
-
-
-=head1 SEE ALSO
-
-L<du(1)>
diff --git a/src/browser.zig b/src/browser.zig
index b85d46a..44fe9f2 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
const std = @import("std");
@@ -8,7 +8,7 @@ const scan = @import("scan.zig");
const delete = @import("delete.zig");
const ui = @import("ui.zig");
const c = @cImport(@cInclude("time.h"));
-usingnamespace @import("util.zig");
+const util = @import("util.zig");
// Currently opened directory.
pub var dir_parent: *model.Dir = undefined;
@@ -44,15 +44,15 @@ const View = struct {
fn save(self: *@This()) void {
self.cursor_hash = if (dir_items.items.len == 0) 0
else hashEntry(dir_items.items[cursor_idx]);
- opened_dir_views.put(@ptrToInt(dir_parent), self.*) catch {};
+ opened_dir_views.put(@intFromPtr(dir_parent), self.*) catch {};
}
// Should be called after dir_parent or dir_items has changed, will load the last saved view and find the proper cursor_idx.
fn load(self: *@This(), sel: ?*const model.Entry) void {
- if (opened_dir_views.get(@ptrToInt(dir_parent))) |v| self.* = v
+ if (opened_dir_views.get(@intFromPtr(dir_parent))) |v| self.* = v
else self.* = @This(){};
cursor_idx = 0;
- for (dir_items.items) |e, i| {
+ for (dir_items.items, 0..) |e, i| {
if (if (sel != null) e == sel else self.cursor_hash == hashEntry(e)) {
cursor_idx = i;
break;
@@ -64,7 +64,7 @@ const View = struct {
var current_view = View{};
// Directories the user has browsed to before, and which item was last selected.
-// The key is the @ptrToInt() of the opened *Dir; An int because the pointer
+// The key is the @intFromPtr() of the opened *Dir; An int because the pointer
// itself may have gone stale after deletion or refreshing. They're only for
// lookups, not dereferencing.
var opened_dir_views = std.AutoHashMap(usize, View).init(main.allocator);
@@ -83,31 +83,30 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
switch (main.config.sort_col) {
.name => {}, // name sorting is the fallback
.blocks => {
- if (sortIntLt(a.blocks, b.blocks)) |r| return r;
+ if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
if (sortIntLt(a.size, b.size)) |r| return r;
},
.size => {
if (sortIntLt(a.size, b.size)) |r| return r;
- if (sortIntLt(a.blocks, b.blocks)) |r| return r;
+ if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
},
.items => {
const ai = if (a.dir()) |d| d.items else 0;
const bi = if (b.dir()) |d| d.items else 0;
if (sortIntLt(ai, bi)) |r| return r;
- if (sortIntLt(a.blocks, b.blocks)) |r| return r;
+ if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
if (sortIntLt(a.size, b.size)) |r| return r;
},
.mtime => {
- if (!a.isext or !b.isext) return a.isext;
+ if (!a.pack.isext or !b.pack.isext) return a.pack.isext;
if (sortIntLt(a.ext().?.mtime, b.ext().?.mtime)) |r| return r;
},
}
- // TODO: Unicode-aware sorting might be nice (and slow)
- const an = a.name();
- const bn = b.name();
- return if (main.config.sort_order == .asc) std.mem.lessThan(u8, an, bn)
- else std.mem.lessThan(u8, bn, an);
+ const an = (if (main.config.sort_order == .asc) a else b).name();
+ const bn = (if (main.config.sort_order == .asc) b else a).name();
+ return if (main.config.sort_natural) util.strnatcmp(an, bn) == .lt
+ else std.mem.lessThan(u8, an, bn);
}
// Should be called when:
@@ -118,7 +117,7 @@ fn sortDir(next_sel: ?*const model.Entry) void {
// No need to sort the first item if that's the parent dir reference,
// excluding that allows sortLt() to ignore null values.
const lst = dir_items.items[(if (dir_items.items.len > 0 and dir_items.items[0] == null) @as(usize, 1) else 0)..];
- std.sort.sort(?*model.Entry, lst, @as(void, undefined), sortLt);
+ std.mem.sort(?*model.Entry, lst, {}, sortLt);
current_view.load(next_sel);
}
@@ -135,11 +134,11 @@ pub fn loadDir(next_sel: ?*const model.Entry) void {
if (dir_parent != model.root)
dir_items.append(null) catch unreachable;
var it = dir_parent.sub;
- while (it) |e| {
- if (e.blocks > dir_max_blocks) dir_max_blocks = e.blocks;
+ while (it) |e| : (it = e.next) {
+ if (e.pack.blocks > dir_max_blocks) dir_max_blocks = e.pack.blocks;
if (e.size > dir_max_size) dir_max_size = e.size;
const shown = main.config.show_hidden or blk: {
- const excl = if (e.file()) |f| f.excluded else false;
+ const excl = if (e.file()) |f| f.pack.excluded else false;
const name = e.name();
break :blk !excl and name[0] != '.' and name[name.len-1] != '~';
};
@@ -147,7 +146,6 @@ pub fn loadDir(next_sel: ?*const model.Entry) void {
dir_items.append(e) catch unreachable;
if (e.dir()) |d| if (d.shared_blocks > 0 or d.shared_size > 0) { dir_has_shared = true; };
}
- it = e.next;
}
sortDir(next_sel);
}
@@ -165,14 +163,14 @@ const Row = struct {
const item = self.item orelse return;
const ch: u7 = ch: {
if (item.file()) |f| {
- if (f.err) break :ch '!';
- if (f.excluded) break :ch '<';
- if (f.other_fs) break :ch '>';
- if (f.kernfs) break :ch '^';
- if (f.notreg) break :ch '@';
+ if (f.pack.err) break :ch '!';
+ if (f.pack.excluded) break :ch '<';
+ if (f.pack.other_fs) break :ch '>';
+ if (f.pack.kernfs) break :ch '^';
+ if (f.pack.notreg) break :ch '@';
} else if (item.dir()) |d| {
- if (d.err) break :ch '!';
- if (d.suberr) break :ch '.';
+ if (d.pack.err) break :ch '!';
+ if (d.pack.suberr) break :ch '.';
if (d.sub == null) break :ch 'e';
} else if (item.link()) |_| break :ch 'H';
return;
@@ -188,13 +186,13 @@ const Row = struct {
width += 2 + width;
defer self.col += width;
const item = self.item orelse return;
- const siz = if (main.config.show_blocks) blocksToSize(item.blocks) else item.size;
- var shr = if (item.dir()) |d| (if (main.config.show_blocks) blocksToSize(d.shared_blocks) else d.shared_size) else 0;
- if (main.config.show_shared == .unique) shr = saturateSub(siz, shr);
+ const siz = if (main.config.show_blocks) util.blocksToSize(item.pack.blocks) else item.size;
+ var shr = if (item.dir()) |d| (if (main.config.show_blocks) util.blocksToSize(d.shared_blocks) else d.shared_size) else 0;
+ if (main.config.show_shared == .unique) shr = siz -| shr;
ui.move(self.row, self.col);
ui.addsize(self.bg, siz);
- if (shr > 0 and main.config.show_shared != .off) {
+ if (dir_has_shared and shr > 0 and main.config.show_shared != .off) {
self.bg.fg(.flag);
ui.addstr(if (main.config.show_shared == .unique) " U " else " S ");
ui.addsize(self.bg, shr);
@@ -202,39 +200,46 @@ const Row = struct {
}
fn graph(self: *Self) void {
- if (main.config.show_graph == .off or self.col + 20 > ui.cols) return;
-
- const bar_size = std.math.max(ui.cols/7, 10);
- defer self.col += switch (main.config.show_graph) {
- .off => unreachable,
- .graph => bar_size + 3,
- .percent => 9,
- .both => bar_size + 10,
- };
+ if ((!main.config.show_graph and !main.config.show_percent) or self.col + 20 > ui.cols) return;
+
+ const bar_size = @max(ui.cols/7, 10);
+ defer self.col += 3
+ + (if (main.config.show_graph) bar_size else 0)
+ + (if (main.config.show_percent) @as(u32, 6) else 0)
+ + (if (main.config.show_graph and main.config.show_percent) @as(u32, 1) else 0);
const item = self.item orelse return;
ui.move(self.row, self.col);
self.bg.fg(.default);
ui.addch('[');
- if (main.config.show_graph == .both or main.config.show_graph == .percent) {
+ if (main.config.show_percent) {
self.bg.fg(.num);
- ui.addprint("{d:>5.1}", .{ 100*
- if (main.config.show_blocks) @intToFloat(f32, item.blocks) / @intToFloat(f32, std.math.max(1, dir_parent.entry.blocks))
- else @intToFloat(f32, item.size) / @intToFloat(f32, std.math.max(1, dir_parent.entry.size))
+ ui.addprint("{d:>5.1}", .{ 100 *
+ if (main.config.show_blocks) @as(f32, @floatFromInt(item.pack.blocks)) / @as(f32, @floatFromInt(@max(1, dir_parent.entry.pack.blocks)))
+ else @as(f32, @floatFromInt(item.size)) / @as(f32, @floatFromInt(@max(1, dir_parent.entry.size)))
});
self.bg.fg(.default);
ui.addch('%');
}
- if (main.config.show_graph == .both) ui.addch(' ');
- if (main.config.show_graph == .both or main.config.show_graph == .graph) {
- const perblock = std.math.divFloor(u64, if (main.config.show_blocks) dir_max_blocks else dir_max_size, bar_size) catch unreachable;
- const num = if (main.config.show_blocks) item.blocks else item.size;
- var i: u32 = 0;
- var siz: u64 = 0;
+ if (main.config.show_graph and main.config.show_percent) ui.addch(' ');
+ if (main.config.show_graph) {
+ var max = if (main.config.show_blocks) dir_max_blocks else dir_max_size;
+ var num = if (main.config.show_blocks) item.pack.blocks else item.size;
+ if (max < bar_size) {
+ max *= bar_size;
+ num *= bar_size;
+ }
+
+ const perblock = std.math.divFloor(u64, max, bar_size) catch unreachable;
self.bg.fg(.graph);
- while (i < bar_size) : (i += 1) {
- siz = saturateAdd(siz, perblock);
- ui.addch(if (siz <= num) '#' else ' ');
+ for (0..bar_size) |_| {
+ const frac = @min(@as(usize, 8), (num *| 8) / perblock);
+ ui.addstr(switch (main.config.graph_style) {
+ .hash => ([_][:0]const u8{ " ", " ", " ", " ", " ", " ", " ", " ", "#" })[frac],
+ .half => ([_][:0]const u8{ " ", " ", " ", " ", "▌", "▌", "▌", "▌", "█" })[frac],
+ .eighth => ([_][:0]const u8{ " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█" })[frac],
+ });
+ num -|= perblock;
}
}
self.bg.fg(.default);
@@ -255,11 +260,11 @@ const Row = struct {
} else if (n < 100_000)
ui.addnum(self.bg, n)
else if (n < 1000_000) {
- ui.addprint("{d:>5.1}", .{ @intToFloat(f32, n) / 1000 });
+ ui.addprint("{d:>5.1}", .{ @as(f32, @floatFromInt(n)) / 1000 });
self.bg.fg(.default);
ui.addch('k');
} else if (n < 1000_000_000) {
- ui.addprint("{d:>5.1}", .{ @intToFloat(f32, n) / 1000_000 });
+ ui.addprint("{d:>5.1}", .{ @as(f32, @floatFromInt(n)) / 1000_000 });
self.bg.fg(.default);
ui.addch('M');
} else {
@@ -284,9 +289,9 @@ const Row = struct {
fn name(self: *Self) void {
ui.move(self.row, self.col);
if (self.item) |i| {
- self.bg.fg(if (i.etype == .dir) .dir else .default);
+ self.bg.fg(if (i.pack.etype == .dir) .dir else .default);
ui.addch(if (i.isDirectory()) '/' else ' ');
- ui.addstr(ui.shorten(ui.toUtf8(i.name()), saturateSub(ui.cols, self.col + 1)));
+ ui.addstr(ui.shorten(ui.toUtf8(i.name()), ui.cols -| self.col -| 1));
} else {
self.bg.fg(.dir);
ui.addstr("/..");
@@ -344,8 +349,8 @@ const info = struct {
var links_idx: usize = 0;
fn lt(_: void, a: *model.Link, b: *model.Link) bool {
- var pa = a.path(false);
- var pb = b.path(false);
+ const pa = a.path(false);
+ const pb = b.path(false);
defer main.allocator.free(pa);
defer main.allocator.free(pb);
return std.mem.lessThan(u8, pa, pb);
@@ -378,19 +383,18 @@ const info = struct {
// TODO: Zig's sort() implementation is type-generic and not very
// small. I suspect we can get a good save on our binary size by using
// a smaller or non-generic sort. This doesn't have to be very fast.
- std.sort.sort(*model.Link, list.items, @as(void, undefined), lt);
- for (list.items) |n,i| if (&n.entry == e) { links_idx = i; };
+ std.mem.sort(*model.Link, list.items, {}, lt);
+ for (list.items, 0..) |n,i| if (&n.entry == e) { links_idx = i; };
links = list;
}
}
fn drawLinks(box: ui.Box, row: *u32, rows: u32, cols: u32) void {
- const numrows = saturateSub(rows, 4);
+ const numrows = rows -| 4;
if (links_idx < links_top) links_top = links_idx;
if (links_idx >= links_top + numrows) links_top = links_idx - numrows + 1;
- var i: u32 = 0;
- while (i < numrows) : (i += 1) {
+ for (0..numrows) |i| {
if (i + links_top >= links.?.items.len) break;
const e = links.?.items[i+links_top];
ui.style(if (i+links_top == links_idx) .sel else .default);
@@ -398,7 +402,7 @@ const info = struct {
ui.addch(if (&e.entry == entry) '*' else ' ');
const path = e.path(false);
defer main.allocator.free(path);
- ui.addstr(ui.shorten(ui.toUtf8(path), saturateSub(cols, 5)));
+ ui.addstr(ui.shorten(ui.toUtf8(path), cols -| 5));
row.* += 1;
}
ui.style(.default);
@@ -422,7 +426,7 @@ const info = struct {
if (shared > 0) {
ui.style(.default);
drawSizeRow(box, row, " > shared: ", shared);
- drawSizeRow(box, row, " > unique: ", saturateSub(size, shared));
+ drawSizeRow(box, row, " > unique: ", size -| shared);
}
}
@@ -454,7 +458,7 @@ const info = struct {
} else {
ui.addstr("Type: ");
ui.style(.default);
- ui.addstr(if (e.isDirectory()) "Directory" else if (if (e.file()) |f| f.notreg else false) "Other" else "File");
+ ui.addstr(if (e.isDirectory()) "Directory" else if (if (e.file()) |f| f.pack.notreg else false) "Other" else "File");
}
row.* += 1;
@@ -468,8 +472,8 @@ const info = struct {
}
// Disk usage & Apparent size
- drawSize(box, row, " Disk usage: ", blocksToSize(e.blocks), if (e.dir()) |d| blocksToSize(d.shared_blocks) else 0);
- drawSize(box, row, "Apparent size: ", e.size, if (e.dir()) |d| d.shared_size else 0);
+ drawSize(box, row, " Disk usage: ", util.blocksToSize(e.pack.blocks), if (e.dir()) |d| util.blocksToSize(d.shared_blocks) else 0);
+ drawSize(box, row, "Apparent size: ", e.size, if (e.dir()) |d| d.shared_size else 0);
// Number of items
if (e.dir()) |d| {
@@ -516,7 +520,7 @@ const info = struct {
var row: u32 = 2;
// Tabs
- if (e.etype == .link) {
+ if (e.pack.etype == .link) {
box.tab(cols-19, tab == .info, 1, "Info");
box.tab(cols-10, tab == .links, 2, "Links");
}
@@ -537,7 +541,7 @@ const info = struct {
}
fn keyInput(ch: i32) bool {
- if (entry.?.etype == .link) {
+ if (entry.?.pack.etype == .link) {
switch (ch) {
'1', 'h', ui.c.KEY_LEFT => { set(entry, .info); return true; },
'2', 'l', ui.c.KEY_RIGHT => { set(entry, .links); return true; },
@@ -554,7 +558,7 @@ const info = struct {
set(null, .info);
}
}
- if (keyInputSelection(ch, &cursor_idx, dir_items.items.len, saturateSub(ui.rows, 3))) {
+ if (keyInputSelection(ch, &cursor_idx, dir_items.items.len, ui.rows -| 3)) {
set(dir_items.items[cursor_idx], .info);
return true;
}
@@ -567,7 +571,6 @@ const info = struct {
};
const help = struct {
- // TODO: Document 'u' key... once I have something final for it.
const keys = [_][:0]const u8{
"up, k", "Move cursor up",
"down, j", "Move cursor down",
@@ -580,6 +583,7 @@ const help = struct {
"d", "Delete selected file or directory",
"t", "Toggle dirs before files when sorting",
"g", "Show percentage and/or graph",
+ "u", "Show/hide hard link shared sizes",
"a", "Toggle between apparent size and disk usage",
"c", "Toggle display of child item counts",
"m", "Toggle display of latest mtime (-e flag)",
@@ -619,7 +623,7 @@ const help = struct {
var i = offset*2;
while (i < (offset + keylines)*2) : (i += 2) {
line += 1;
- box.move(line, 13 - @intCast(u32, keys[i].len));
+ box.move(line, 13 - @as(u32, @intCast(keys[i].len)));
ui.style(.key);
ui.addstr(keys[i]);
ui.style(.default);
@@ -651,15 +655,12 @@ const help = struct {
}
fn drawAbout(box: ui.Box) void {
- for (logo) |s, n| {
- box.move(@intCast(u32, n)+3, 12);
+ for (logo, 0..) |s, n| {
+ box.move(@as(u32, @intCast(n+3)), 12);
var i: u5 = 28;
- while (true) {
+ while (i != 0) : (i -= 1) {
ui.style(if (s & (@as(u29,1)<<i) > 0) .sel else .default);
ui.addch(' ');
- if (i == 0)
- break;
- i -= 1;
}
}
ui.style(.default);
@@ -727,10 +728,10 @@ pub fn draw() void {
ui.style(.hd);
ui.addstr(" for help");
if (main.config.imported) {
- ui.move(0, saturateSub(ui.cols, 10));
+ ui.move(0, ui.cols -| 10);
ui.addstr("[imported]");
- } else if (main.config.read_only) {
- ui.move(0, saturateSub(ui.cols, 10));
+ } else if (!main.config.can_delete.?) {
+ ui.move(0, ui.cols -| 10);
ui.addstr("[readonly]");
}
@@ -743,13 +744,13 @@ pub fn draw() void {
var pathbuf = std.ArrayList(u8).init(main.allocator);
dir_parent.fmtPath(true, &pathbuf);
- ui.addstr(ui.shorten(ui.toUtf8(arrayListBufZ(&pathbuf)), saturateSub(ui.cols, 5)));
+ ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&pathbuf)), ui.cols -| 5));
pathbuf.deinit();
ui.style(.default);
ui.addch(' ');
- const numrows = saturateSub(ui.rows, 3);
+ const numrows = ui.rows -| 3;
if (cursor_idx < current_view.top) current_view.top = cursor_idx;
if (cursor_idx >= current_view.top + numrows) current_view.top = cursor_idx - numrows + 1;
@@ -769,14 +770,17 @@ pub fn draw() void {
ui.style(.hd);
ui.move(ui.rows-1, 0);
ui.hline(' ', ui.cols);
- ui.move(ui.rows-1, 1);
+ ui.move(ui.rows-1, 0);
+ ui.addch(if (main.config.show_blocks) '*' else ' ');
ui.style(if (main.config.show_blocks) .bold_hd else .hd);
ui.addstr("Total disk usage: ");
- ui.addsize(.hd, blocksToSize(dir_parent.entry.blocks));
+ ui.addsize(.hd, util.blocksToSize(dir_parent.entry.pack.blocks));
ui.style(if (main.config.show_blocks) .hd else .bold_hd);
- ui.addstr(" Apparent size: ");
+ ui.addstr(" ");
+ ui.addch(if (main.config.show_blocks) ' ' else '*');
+ ui.addstr("Apparent size: ");
ui.addsize(.hd, dir_parent.entry.size);
- ui.addstr(" Items: ");
+ ui.addstr(" Items: ");
ui.addnum(.hd, dir_parent.items);
switch (state) {
@@ -812,9 +816,9 @@ fn keyInputSelection(ch: i32, idx: *usize, len: usize, page: u32) bool {
if (idx.* > 0) idx.* -= 1;
},
ui.c.KEY_HOME => idx.* = 0,
- ui.c.KEY_END, ui.c.KEY_LL => idx.* = saturateSub(len, 1),
- ui.c.KEY_PPAGE => idx.* = saturateSub(idx.*, page),
- ui.c.KEY_NPAGE => idx.* = std.math.min(saturateSub(len, 1), idx.* + page),
+ ui.c.KEY_END, ui.c.KEY_LL => idx.* = len -| 1,
+ ui.c.KEY_PPAGE => idx.* = idx.* -| page,
+ ui.c.KEY_NPAGE => idx.* = @min(len -| 1, idx.* + page),
else => return false,
}
return true;
@@ -840,27 +844,23 @@ pub fn keyInput(ch: i32) void {
'?' => state = .help,
'i' => if (dir_items.items.len > 0) info.set(dir_items.items[cursor_idx], .info),
'r' => {
- if (main.config.imported)
- message = "Directory imported from file, refreshing is disabled."
+ if (!main.config.can_refresh.?)
+ message = "Directory refresh feature disabled."
else {
main.state = .refresh;
scan.setupRefresh(dir_parent);
}
},
'b' => {
- if (main.config.imported)
- message = "Shell feature not available for imported directories."
- else if (!main.config.can_shell)
- message = "Shell feature disabled in read-only mode."
+ if (!main.config.can_shell.?)
+ message = "Shell feature disabled."
else
main.state = .shell;
},
'd' => {
if (dir_items.items.len == 0) {
- } else if (main.config.imported)
- message = "Deletion feature not available for imported directories."
- else if (main.config.read_only)
- message = "Deletion feature disabled in read-only mode."
+ } else if (!main.config.can_delete.?)
+ message = "Deletion feature disabled."
else if (dir_items.items[cursor_idx]) |e| {
main.state = .delete;
const next =
@@ -924,19 +924,18 @@ pub fn keyInput(ch: i32) void {
// Display settings
'c' => main.config.show_items = !main.config.show_items,
'm' => if (main.config.extended) { main.config.show_mtime = !main.config.show_mtime; },
- 'g' => main.config.show_graph = switch (main.config.show_graph) {
- .off => .graph,
- .graph => .percent,
- .percent => .both,
- .both => .off,
+ 'g' => {
+ if (!main.config.show_graph and !main.config.show_percent) { main.config.show_graph = true; main.config.show_percent = false; }
+ else if ( main.config.show_graph and !main.config.show_percent) { main.config.show_graph = false; main.config.show_percent = true; }
+ else if (!main.config.show_graph and main.config.show_percent) { main.config.show_graph = true; main.config.show_percent = true; }
+ else if ( main.config.show_graph and main.config.show_percent) { main.config.show_graph = false; main.config.show_percent = false; }
},
- // TODO: This key binding is not final! I'd rather add a menu selection thing for advanced settings rather than risk running out of more keys.
'u' => main.config.show_shared = switch (main.config.show_shared) {
.off => .shared,
.shared => .unique,
.unique => .off,
},
- else => _ = keyInputSelection(ch, &cursor_idx, dir_items.items.len, saturateSub(ui.rows, 3)),
+ else => _ = keyInputSelection(ch, &cursor_idx, dir_items.items.len, ui.rows -| 3),
}
}
diff --git a/src/delete.zig b/src/delete.zig
index b9e6851..c59aca8 100644
--- a/src/delete.zig
+++ b/src/delete.zig
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
const std = @import("std");
@@ -6,7 +6,7 @@ const main = @import("main.zig");
const model = @import("model.zig");
const ui = @import("ui.zig");
const browser = @import("browser.zig");
-usingnamespace @import("util.zig");
+const util = @import("util.zig");
var parent: *model.Dir = undefined;
var entry: *model.Entry = undefined;
@@ -45,8 +45,7 @@ fn deleteItem(dir: std.fs.Dir, path: [:0]const u8, ptr: *align(1) ?*model.Entry)
return true;
if (entry.dir()) |d| {
- var fd = dir.openDirZ(path, .{ .access_sub_paths = true, .iterate = false })
- catch |e| return err(e);
+ var fd = dir.openDirZ(path, .{.no_follow = true}, false) catch |e| return err(e);
var it = &d.sub;
parent = d;
defer parent = parent.parent.?;
@@ -89,7 +88,7 @@ pub fn delete() ?*model.Entry {
path.append('/') catch unreachable;
path.appendSlice(entry.name()) catch unreachable;
- _ = deleteItem(std.fs.cwd(), arrayListBufZ(&path), it);
+ _ = deleteItem(std.fs.cwd(), util.arrayListBufZ(&path), it);
model.inodes.addAllStats();
return if (it.* == e) e else next_sel;
}
@@ -101,7 +100,7 @@ fn drawConfirm() void {
ui.addstr("Are you sure you want to delete \"");
ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 21));
ui.addch('"');
- if (entry.etype != .dir)
+ if (entry.pack.etype != .dir)
ui.addch('?')
else {
box.move(2, 18);
@@ -132,7 +131,7 @@ fn drawProgress() void {
const box = ui.Box.create(6, 60, "Deleting...");
box.move(2, 2);
- ui.addstr(ui.shorten(ui.toUtf8(arrayListBufZ(&path)), 56));
+ ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path)), 56));
box.move(4, 41);
ui.addstr("Press ");
ui.style(.key);
@@ -151,7 +150,7 @@ fn drawErr() void {
const box = ui.Box.create(6, 60, "Error");
box.move(1, 2);
ui.addstr("Error deleting ");
- ui.addstr(ui.shorten(ui.toUtf8(arrayListBufZ(&path)), 41));
+ ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&path)), 41));
box.move(2, 4);
ui.addstr(ui.errorString(error_code));
diff --git a/src/exclude.zig b/src/exclude.zig
new file mode 100644
index 0000000..ca37d11
--- /dev/null
+++ b/src/exclude.zig
@@ -0,0 +1,322 @@
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
+// SPDX-License-Identifier: MIT
+
+const std = @import("std");
+const main = @import("main.zig");
+const c = @cImport(@cInclude("fnmatch.h"));
+
+// Reference:
+// https://manned.org/glob.7
+// https://manned.org/man.b4c7391e/rsync#head17
+// https://manned.org/man.401d6ade/arch/gitignore#head4
+// Patterns:
+// Single component (none of these patterns match a '/'):
+// * -> match any character sequence
+// ? -> match single character
+// [abc] -> match a single character in the given list
+// [a-c] -> match a single character in the given range
+// [!a-c] -> match a single character not in the given range
+// # (these are currently still handled by calling libc fnmatch())
+// Anchored patterns:
+// /pattern
+// /dir/pattern
+// /dir/subdir/pattern
+// # In both rsync and gitignore, anchored patterns are relative to the
+// # directory under consideration. In ncdu they are instead anchored to
+// # the filesystem root (i.e. matched against the absolute path).
+// Non-anchored patterns:
+// somefile
+// subdir/foo
+// sub*/bar
+// # In .gitignore, non-anchored patterns with a slash are implicitely anchored,
+// # in rsync they can match anywhere in a path. We follow rsync here.
+// Dir patterns (trailing '/' matches only dirs):
+// /pattern/
+// somedir/
+// subdir/pattern/
+//
+// BREAKING CHANGE:
+// ncdu < 2.2 single-component matches may cross directory boundary, e.g.
+// 'a*b' matches 'a/b'. This is an old bug, the fix breaks compatibility with
+// old exlude patterns.
+
+const Pattern = struct {
+ isdir: bool = undefined,
+ isliteral: bool = undefined,
+ pattern: [:0]const u8,
+ sub: ?*const Pattern = undefined,
+
+ fn isLiteral(str: []const u8) bool {
+ for (str) |chr| switch (chr) {
+ '[', '*', '?', '\\' => return false,
+ else => {},
+ };
+ return true;
+ }
+
+ fn parse(pat_: []const u8) *const Pattern {
+ var pat = std.mem.trimLeft(u8, pat_, "/");
+ const top = main.allocator.create(Pattern) catch unreachable;
+ var tail = top;
+ tail.sub = null;
+ while (std.mem.indexOfScalar(u8, pat, '/')) |idx| {
+ tail.pattern = main.allocator.dupeZ(u8, pat[0..idx]) catch unreachable;
+ tail.isdir = true;
+ tail.isliteral = isLiteral(tail.pattern);
+ pat = pat[idx+1..];
+ if (std.mem.allEqual(u8, pat, '/')) return top;
+
+ const next = main.allocator.create(Pattern) catch unreachable;
+ tail.sub = next;
+ tail = next;
+ tail.sub = null;
+ }
+ tail.pattern = main.allocator.dupeZ(u8, pat) catch unreachable;
+ tail.isdir = false;
+ tail.isliteral = isLiteral(tail.pattern);
+ return top;
+ }
+};
+
+test "parse" {
+ const t1 = Pattern.parse("");
+ try std.testing.expectEqualStrings(t1.pattern, "");
+ try std.testing.expectEqual(t1.isdir, false);
+ try std.testing.expectEqual(t1.isliteral, true);
+ try std.testing.expectEqual(t1.sub, null);
+
+ const t2 = Pattern.parse("//a//");
+ try std.testing.expectEqualStrings(t2.pattern, "a");
+ try std.testing.expectEqual(t2.isdir, true);
+ try std.testing.expectEqual(t2.isliteral, true);
+ try std.testing.expectEqual(t2.sub, null);
+
+ const t3 = Pattern.parse("foo*/bar.zig");
+ try std.testing.expectEqualStrings(t3.pattern, "foo*");
+ try std.testing.expectEqual(t3.isdir, true);
+ try std.testing.expectEqual(t3.isliteral, false);
+ try std.testing.expectEqualStrings(t3.sub.?.pattern, "bar.zig");
+ try std.testing.expectEqual(t3.sub.?.isdir, false);
+ try std.testing.expectEqual(t3.sub.?.isliteral, true);
+ try std.testing.expectEqual(t3.sub.?.sub, null);
+
+ const t4 = Pattern.parse("/?/sub/dir/");
+ try std.testing.expectEqualStrings(t4.pattern, "?");
+ try std.testing.expectEqual(t4.isdir, true);
+ try std.testing.expectEqual(t4.isliteral, false);
+ try std.testing.expectEqualStrings(t4.sub.?.pattern, "sub");
+ try std.testing.expectEqual(t4.sub.?.isdir, true);
+ try std.testing.expectEqual(t4.sub.?.isliteral, true);
+ try std.testing.expectEqualStrings(t4.sub.?.sub.?.pattern, "dir");
+ try std.testing.expectEqual(t4.sub.?.sub.?.isdir, true);
+ try std.testing.expectEqual(t4.sub.?.sub.?.isliteral, true);
+ try std.testing.expectEqual(t4.sub.?.sub.?.sub, null);
+}
+
+
+// List of patterns to be matched at one particular level.
+// There are 2 different types of lists: those where all patterns have a
+// sub-pointer (where the pattern only matches directories at this level, and
+// the match result is only used to construct the PatternList of the
+// subdirectory) and patterns without a sub-pointer (where the match result
+// determines whether the file/dir at this level should be included or not).
+fn PatternList(comptime withsub: bool) type {
+ return struct {
+ literals: std.HashMapUnmanaged(*const Pattern, Val, Ctx, 80) = .{},
+ 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
+ // list would be nicer, but that involves mutable Patterns, which in
+ // turn prevents multithreaded scanning. An alternative would be a
+ // sorted array + binary search, but that slows down lookups. Perhaps a
+ // custom hashmap with support for duplicate keys?
+ const Val = if (withsub) std.ArrayListUnmanaged(*const Pattern) else void;
+
+ const Ctx = struct {
+ pub fn hash(_: Ctx, p: *const Pattern) u64 {
+ return std.hash.Wyhash.hash(0, p.pattern);
+ }
+ pub fn eql(_: Ctx, a: *const Pattern, b: *const Pattern) bool {
+ return std.mem.eql(u8, a.pattern, b.pattern);
+ }
+ };
+
+ const Self = @This();
+
+ fn append(self: *Self, pat: *const Pattern) void {
+ std.debug.assert((pat.sub != null) == withsub);
+ if (pat.isliteral) {
+ const e = self.literals.getOrPut(main.allocator, pat) catch unreachable;
+ if (!e.found_existing) {
+ e.key_ptr.* = pat;
+ e.value_ptr.* = if (withsub) .{} else {};
+ }
+ if (!withsub and !pat.isdir and e.key_ptr.*.isdir) e.key_ptr.* = pat;
+ if (withsub) {
+ if (pat.sub) |s| e.value_ptr.*.append(main.allocator, s) catch unreachable;
+ }
+
+ } else self.wild.append(main.allocator, pat) catch unreachable;
+ }
+
+ fn match(self: *const Self, name: [:0]const u8) ?bool {
+ var ret: ?bool = null;
+ if (self.literals.getKey(&.{ .pattern = name })) |p| ret = p.isdir;
+ for (self.wild.items) |p| {
+ if (ret == false) return ret;
+ if (c.fnmatch(p.pattern.ptr, name.ptr, 0) == 0) ret = p.isdir;
+ }
+ return ret;
+ }
+
+ fn enter(self: *const Self, out: *Patterns, name: [:0]const u8) void {
+ if (self.literals.get(&.{ .pattern = name })) |lst| for (lst.items) |sub| out.append(sub);
+ for (self.wild.items) |p| if (c.fnmatch(p.pattern.ptr, name.ptr, 0) == 0) out.append(p.sub.?);
+ }
+
+ fn deinit(self: *Self) void {
+ if (withsub) {
+ var it = self.literals.valueIterator();
+ while (it.next()) |e| e.deinit(main.allocator);
+ }
+ self.literals.deinit(main.allocator);
+ self.wild.deinit(main.allocator);
+ self.* = undefined;
+ }
+ };
+}
+
+// List of all patterns that should be matched at one level.
+pub const Patterns = struct {
+ nonsub: PatternList(false) = .{},
+ sub: PatternList(true) = .{},
+ isroot: bool = false,
+
+ fn append(self: *Patterns, pat: *const Pattern) void {
+ if (pat.sub == null) self.nonsub.append(pat)
+ else self.sub.append(pat);
+ }
+
+ // Matches patterns in this level plus unanchored patterns.
+ // Returns null if nothing matches, otherwise whether the given item should
+ // only be exluced if it's a directory.
+ // (Should not be called on root_unanchored)
+ pub fn match(self: *const Patterns, name: [:0]const u8) ?bool {
+ const a = self.nonsub.match(name);
+ if (a == false) return false;
+ const b = root_unanchored.nonsub.match(name);
+ if (b == false) return false;
+ return a orelse b;
+ }
+
+ // Construct the list of patterns for a subdirectory.
+ pub fn enter(self: *const Patterns, name: [:0]const u8) Patterns {
+ var ret = Patterns{};
+ self.sub.enter(&ret, name);
+ root_unanchored.sub.enter(&ret, name);
+ return ret;
+ }
+
+ pub fn deinit(self: *Patterns) void {
+ // getPatterns() result should be deinit()ed, except when it returns the root,
+ // let's simplify that and simply don't deinit root.
+ if (self.isroot) return;
+ self.nonsub.deinit();
+ self.sub.deinit();
+ self.* = undefined;
+ }
+};
+
+// Unanchored patterns that should be checked at every level
+var root_unanchored: Patterns = .{};
+
+// Patterns anchored at the root
+var root: Patterns = .{ .isroot = true };
+
+pub fn addPattern(pattern: []const u8) void {
+ if (pattern.len == 0) return;
+ const p = Pattern.parse(pattern);
+ if (pattern[0] == '/') root.append(p)
+ else root_unanchored.append(p);
+}
+
+// Get the patterns for the given (absolute) path, assuming the given path
+// itself hasn't been excluded. This function is slow, directory walking code
+// should use Patterns.enter() instead.
+pub fn getPatterns(path_: []const u8) Patterns {
+ var path = std.mem.trim(u8, path_, "/");
+ if (path.len == 0) return root;
+ var pat = root;
+ defer pat.deinit();
+ while (std.mem.indexOfScalar(u8, path, '/')) |idx| {
+ const name = main.allocator.dupeZ(u8, path[0..idx]) catch unreachable;
+ defer main.allocator.free(name);
+ path = path[idx+1..];
+
+ const sub = pat.enter(name);
+ pat.deinit();
+ pat = sub;
+ }
+
+ const name = main.allocator.dupeZ(u8, path) catch unreachable;
+ defer main.allocator.free(name);
+ return pat.enter(name);
+}
+
+
+fn testfoo(p: *const Patterns) !void {
+ try std.testing.expectEqual(p.match("root"), null);
+ try std.testing.expectEqual(p.match("bar"), false);
+ try std.testing.expectEqual(p.match("qoo"), false);
+ try std.testing.expectEqual(p.match("xyz"), false);
+ try std.testing.expectEqual(p.match("okay"), null);
+ try std.testing.expectEqual(p.match("somefile"), false);
+ var s = p.enter("okay");
+ try std.testing.expectEqual(s.match("bar"), null);
+ try std.testing.expectEqual(s.match("xyz"), null);
+ try std.testing.expectEqual(s.match("notokay"), false);
+ s.deinit();
+}
+
+test "Matching" {
+ addPattern("/foo/bar");
+ addPattern("/foo/qoo/");
+ addPattern("/foo/qoo");
+ addPattern("/foo/qoo/");
+ addPattern("/f??/xyz");
+ addPattern("/f??/xyz/");
+ addPattern("/*o/somefile");
+ addPattern("/a??/okay");
+ addPattern("/roo?");
+ addPattern("/root/");
+ addPattern("excluded");
+ addPattern("somefile/");
+ addPattern("o*y/not[o]kay");
+
+ var a0 = getPatterns("/");
+ try std.testing.expectEqual(a0.match("a"), null);
+ try std.testing.expectEqual(a0.match("excluded"), false);
+ try std.testing.expectEqual(a0.match("somefile"), true);
+ try std.testing.expectEqual(a0.match("root"), false);
+ var a1 = a0.enter("foo");
+ a0.deinit();
+ try testfoo(&a1);
+ a1.deinit();
+
+ var b0 = getPatterns("/somedir/somewhere");
+ try std.testing.expectEqual(b0.match("a"), null);
+ try std.testing.expectEqual(b0.match("excluded"), false);
+ try std.testing.expectEqual(b0.match("root"), null);
+ try std.testing.expectEqual(b0.match("okay"), null);
+ var b1 = b0.enter("okay");
+ b0.deinit();
+ try std.testing.expectEqual(b1.match("excluded"), false);
+ try std.testing.expectEqual(b1.match("okay"), null);
+ try std.testing.expectEqual(b1.match("notokay"), false);
+ b1.deinit();
+
+ var c0 = getPatterns("/foo/");
+ try testfoo(&c0);
+ c0.deinit();
+}
diff --git a/src/main.zig b/src/main.zig
index 3e14f7e..cd3e6d8 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,7 +1,7 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
-pub const program_version = "2.0-beta2";
+pub const program_version = "2.3";
const std = @import("std");
const model = @import("model.zig");
@@ -9,39 +9,49 @@ const scan = @import("scan.zig");
const ui = @import("ui.zig");
const browser = @import("browser.zig");
const delete = @import("delete.zig");
+const util = @import("util.zig");
+const exclude = @import("exclude.zig");
const c = @cImport(@cInclude("locale.h"));
+test "imports" {
+ _ = model;
+ _ = scan;
+ _ = 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(alloc: *std.mem.Allocator, len: usize, alignment: u29, len_align: u29, return_address: usize) error{OutOfMemory}![]u8 {
+fn wrapAlloc(_: *anyopaque, len: usize, ptr_alignment: u8, return_address: usize) ?[*]u8 {
while (true) {
- if (std.heap.c_allocator.allocFn(alloc, len, alignment, len_align, return_address)) |r|
+ if (std.heap.c_allocator.vtable.alloc(undefined, len, ptr_alignment, return_address)) |r|
return r
- else |_| {}
+ else {}
ui.oom();
}
}
-fn wrapResize(alloc: *std.mem.Allocator, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, return_address: usize) std.mem.Allocator.Error!usize {
- // AFAIK, all uses of resizeFn to grow an allocation will fall back to allocFn on failure.
- return std.heap.c_allocator.resizeFn(alloc, buf, buf_align, new_len, len_align, return_address);
-}
-
-var allocator_state = std.mem.Allocator{
- .allocFn = wrapAlloc,
- .resizeFn = wrapResize,
+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,
+ },
};
-pub const allocator = &allocator_state;
-//var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
-//pub const allocator = &general_purpose_allocator.allocator;
+
pub const config = struct {
pub const SortCol = enum { name, blocks, size, items, mtime };
pub const SortOrder = enum { asc, desc };
- pub var same_fs: bool = true;
+ pub var same_fs: bool = false;
pub var extended: bool = false;
pub var follow_symlinks: bool = false;
pub var exclude_caches: bool = false;
@@ -49,10 +59,10 @@ pub const config = struct {
pub var exclude_patterns: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(allocator);
pub var update_delay: u64 = 100*std.time.ns_per_ms;
- pub var scan_ui: enum { none, line, full } = .full;
+ 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 } = .off;
+ pub var ui_color: enum { off, dark, darkbg } = .off;
pub var thousands_sep: []const u8 = ",";
pub var show_hidden: bool = true;
@@ -60,14 +70,18 @@ pub const config = struct {
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: enum { off, graph, percent, both } = .graph;
+ 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;
+ pub var sort_natural: bool = true;
- pub var read_only: bool = false;
pub var imported: bool = false;
- pub var can_shell: bool = true;
+ 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;
@@ -76,106 +90,256 @@ pub const config = struct {
pub var state: enum { scan, browse, refresh, shell, delete } = .scan;
// Simple generic argument parser, supports getopt_long() style arguments.
-// T can be any type that has a 'fn next(T) ?[:0]const u8' method, e.g.:
-// var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init());
-fn Args(T: anytype) type {
- return struct {
- it: T,
- 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);
- }
- };
-
- fn init(it: T) Self {
- return Self{ .it = it };
+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);
}
+ };
- 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 };
- }
+ fn init(lst: []const [:0]const u8) Self {
+ return Self{ .lst = lst };
+ }
- /// 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.it.next() 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();
- }
- 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.? };
- }
- self.last = val;
- return Option{ .opt = true, .val = val };
+ fn pop(self: *Self) ?[:0]const u8 {
+ if (self.lst.len == 0) return null;
+ defer self.lst = self.lst[1..];
+ return self.lst[0];
+ }
+
+ 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();
+ }
+ 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.? };
}
- return self.shortopt(val[1..:0]);
+ self.last = val;
+ return Option{ .opt = true, .val = val };
}
+ 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.it.next()) |o| return o;
- ui.die("Option '{s}' requires an argument.\n", .{ self.last.? });
+ /// 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.? });
+ }
+};
+
+fn argConfig(args: *Args, opt: Args.Option) 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
+ 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
+ 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")) {
+ 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
+ else if (opt.is("--exclude")) exclude.addPattern(args.arg())
+ 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 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);
+ 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});
+ }
+ for (arglist.items) |i| allocator.free(i);
+ arglist.deinit();
}
fn version() noreturn {
- std.io.getStdOut().writer().writeAll("ncdu " ++ program_version ++ "\n") catch {};
+ const stdout = std.io.getStdOut();
+ stdout.writeAll("ncdu " ++ program_version ++ "\n") catch {};
std.process.exit(0);
}
fn help() noreturn {
- std.io.getStdOut().writer().writeAll(
- "ncdu <options> <directory>\n\n"
- ++ " -h,--help This help message\n"
- ++ " -q Quiet mode, refresh interval 2 seconds\n"
- ++ " -v,-V,--version Print version\n"
- ++ " -x Same filesystem\n"
- ++ " -e Enable extended information\n"
- ++ " -r Read only\n"
- ++ " -o FILE Export scanned directory to FILE\n"
- ++ " -f FILE Import scanned directory from FILE\n"
- ++ " -0,-1,-2 UI to use when scanning (0=none,2=full ncurses)\n"
- ++ " --si Use base 10 (SI) prefixes instead of base 2\n"
- ++ " --exclude PATTERN Exclude files that match PATTERN\n"
- ++ " -X, --exclude-from FILE Exclude files that match any pattern in FILE\n"
- ++ " -L, --follow-symlinks Follow symbolic links (excluding directories)\n"
- ++ " --exclude-caches Exclude directories containing CACHEDIR.TAG\n"
- ++ " --exclude-kernfs Exclude Linux pseudo filesystems (procfs,sysfs,cgroup,...)\n"
- ++ " --confirm-quit Confirm quitting ncdu\n"
- ++ " --color SCHEME Set color scheme (off/dark)\n"
+ 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
+ \\ -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 {};
std.process.exit(0);
}
@@ -194,7 +358,7 @@ fn spawnShell() void {
// 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' => @as([]const u8, &.{l[0]+1}),
+ '0'...'8' => |d| &[1] u8{d+1},
'9' => "9",
else => "1"
}) catch unreachable
@@ -202,18 +366,19 @@ fn spawnShell() void {
env.put("NCDU_LEVEL", "1") catch unreachable;
const shell = std.os.getenvZ("NCDU_SHELL") orelse std.os.getenvZ("SHELL") orelse "/bin/sh";
- var child = std.ChildProcess.init(&.{shell}, allocator) catch unreachable;
- defer child.deinit();
+ var child = std.process.Child.init(&.{shell}, allocator);
child.cwd = path.items;
child.env_map = &env;
+ const stdin = std.io.getStdIn();
+ const stderr = std.io.getStdErr();
const term = child.spawnAndWait() catch |e| blk: {
- _ = std.io.getStdErr().writer().print(
+ stderr.writer().print(
"Error spawning shell: {s}\n\nPress enter to continue.\n",
.{ ui.errorString(e) }
) catch {};
- _ = std.io.getStdIn().reader().skipUntilDelimiterOrEof('\n') catch unreachable;
- break :blk std.ChildProcess.Term{ .Exited = 0 };
+ stdin.reader().skipUntilDelimiterOrEof('\n') catch unreachable;
+ break :blk std.process.Child.Term{ .Exited = 0 };
};
if (term != .Exited) {
const n = switch (term) {
@@ -228,24 +393,33 @@ fn spawnShell() void {
.Stopped => |v| v,
.Unknown => |v| v,
};
- _ = std.io.getStdErr().writer().print(
+ stderr.writer().print(
"Shell returned with {s} code {}.\n\nPress enter to continue.\n", .{ n, v }
) catch {};
- _ = std.io.getStdIn().reader().skipUntilDelimiterOrEof('\n') catch unreachable;
+ stdin.reader().skipUntilDelimiterOrEof('\n') catch unreachable;
}
}
-fn readExcludeFile(path: []const u8) !void {
- const f = try std.fs.cwd().openFile(path, .{});
+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()).reader();
- var buf = std.ArrayList(u8).init(allocator);
- while (true) {
- rd.readUntilDelimiterArrayList(&buf, '\n', 4096)
- catch |e| if (e != error.EndOfStream) return e else if (buf.items.len == 0) break;
- if (buf.items.len > 0)
- config.exclude_patterns.append(buf.toOwnedSliceSentinel(0) catch unreachable) catch unreachable;
+
+ 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);
}
}
@@ -254,66 +428,76 @@ pub fn main() void {
_ = c.setlocale(c.LC_ALL, "");
if (c.localeconv()) |locale| {
if (locale.*.thousands_sep) |sep| {
- const span = std.mem.spanZ(sep);
+ const span = std.mem.sliceTo(sep, 0);
if (span.len > 0)
config.thousands_sep = span;
}
}
+ if (std.os.getenvZ("NO_COLOR") == null) config.ui_color = .darkbg;
+
+ 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;
+ };
+
+ if (loadConf) {
+ tryReadArgsFile("/etc/ncdu.conf");
+
+ if (std.os.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.os.getenvZ("HOME")) |p| {
+ const path = std.fs.path.joinZ(allocator, &.{p, ".config", "ncdu", "config"}) catch unreachable;
+ defer allocator.free(path);
+ tryReadArgsFile(path);
+ }
+ }
- var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init());
var scan_dir: ?[]const u8 = null;
var import_file: ?[:0]const u8 = null;
var export_file: ?[:0]const u8 = null;
- var has_scan_ui = false;
- _ = 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 = opt.val;
- continue;
+ 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_file != null) ui.die("The -o flag can only be given once.\n", .{})
+ else if (opt.is("-o")) export_file = 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
+ else if (argConfig(&args, opt)) {}
+ else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
}
- 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("-q")) config.update_delay = 2*std.time.ns_per_s
- else if(opt.is("-x")) config.same_fs = true
- else if(opt.is("-e")) config.extended = true
- else if(opt.is("-r") and config.read_only) config.can_shell = false
- else if(opt.is("-r")) config.read_only = true
- else if(opt.is("-0")) { has_scan_ui = true; config.scan_ui = .none; }
- else if(opt.is("-1")) { has_scan_ui = true; config.scan_ui = .line; }
- else if(opt.is("-2")) { has_scan_ui = true; config.scan_ui = .full; }
- else if(opt.is("-o") and export_file != null) ui.die("The -o flag can only be given once.\n", .{})
- else if(opt.is("-o")) export_file = args.arg()
- 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 = args.arg()
- else if(opt.is("--si")) config.si = true
- else if(opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true
- else if(opt.is("--exclude")) config.exclude_patterns.append(args.arg()) catch unreachable
- 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("--exclude-kernfs")) config.exclude_kernfs = true
- else if(opt.is("--confirm-quit")) config.confirm_quit = true
- 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 ui.die("Unknown --color option: {s}.\n", .{val});
- } else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
}
- if (std.builtin.os.tag != .linux and config.exclude_kernfs)
- ui.die("The --exclude-kernfs tag is currently only supported on Linux.\n", .{});
+ if (@import("builtin").os.tag != .linux and config.exclude_kernfs)
+ ui.die("The --exclude-kernfs flag is currently only supported on Linux.\n", .{});
- const out_tty = std.io.getStdOut().isTty();
- const in_tty = std.io.getStdIn().isTty();
- if (!has_scan_ui) {
+ 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) {
if (export_file) |f| {
if (!out_tty or std.mem.eql(u8, f, "-")) config.scan_ui = .none
else config.scan_ui = .line;
- }
+ } else config.scan_ui = .full;
}
if (!in_tty and import_file == null and export_file == null)
ui.die("Standard input is not a TTY. Did you mean to import a file using '-f -'?\n", .{});
@@ -322,8 +506,8 @@ pub fn main() void {
event_delay_timer = std.time.Timer.start() catch unreachable;
defer ui.deinit();
- var out_file = if (export_file) |f| (
- if (std.mem.eql(u8, f, "-")) std.io.getStdOut()
+ const out_file = if (export_file) |f| (
+ 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)})
) else null;
@@ -333,7 +517,11 @@ pub fn main() void {
config.imported = true;
} else scan.scanRoot(scan_dir orelse ".", out_file)
catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
- if (out_file != null) return;
+ if (quit_after_scan or out_file != null) return;
+
+ 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;
config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
ui.init();
@@ -385,7 +573,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
var firstblock = block;
while (true) {
- var ch = ui.getch(firstblock);
+ const ch = ui.getch(firstblock);
if (ch == 0) return;
if (ch == -1) return handleEvent(firstblock, true);
switch (state) {
@@ -398,21 +586,10 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
}
}
-
test "argument parser" {
- const L = struct {
- lst: []const [:0]const u8,
- idx: usize = 0,
- fn next(s: *@This()) ?[:0]const u8 {
- if (s.idx == s.lst.len) return null;
- defer s.idx += 1;
- return s.lst[s.idx];
- }
- };
const lst = [_][:0]const u8{ "a", "-abcd=e", "--opt1=arg1", "--opt2", "arg2", "-x", "foo", "", "--", "--arg", "", "-", };
- const l = L{ .lst = &lst };
const T = struct {
- a: Args(L),
+ a: Args,
fn opt(self: *@This(), isopt: bool, val: []const u8) !void {
const o = self.a.next().?;
try std.testing.expectEqual(isopt, o.opt);
@@ -423,7 +600,7 @@ test "argument parser" {
try std.testing.expectEqualStrings(val, self.a.arg());
}
};
- var t = T{ .a = Args(L).init(l) };
+ var t = T{ .a = Args.init(&lst) };
try t.opt(false, "a");
try t.opt(true, "-a");
try t.opt(true, "-b");
diff --git a/src/model.zig b/src/model.zig
index 42f2a78..feb589a 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -1,10 +1,10 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
const std = @import("std");
const main = @import("main.zig");
const ui = @import("ui.zig");
-usingnamespace @import("util.zig");
+const util = @import("util.zig");
// While an arena allocator is optimimal for almost all scenarios in which ncdu
// is used, it doesn't allow for re-using deleted nodes after doing a delete or
@@ -12,11 +12,12 @@ usingnamespace @import("util.zig");
// will leak memory, but I'd say that's worth the efficiency gains.
// TODO: Can still implement a simple bucketed free list on top of this arena
// allocator to reuse nodes, if necessary.
-var allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+var allocator_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+const allocator = allocator_state.allocator();
-pub const EType = packed enum(u2) { dir, link, file };
+pub const EType = enum(u2) { dir, link, file };
-// Type for the Entry.blocks field. Smaller than a u64 to make room for flags.
+// Type for the Entry.Packed.blocks field. Smaller than a u64 to make room for flags.
pub const Blocks = u60;
// Memory layout:
@@ -30,93 +31,94 @@ pub const Blocks = u60;
// These are all packed structs and hence do not have any alignment, which is
// great for saving memory but perhaps not very great for code size or
// performance.
-// (TODO: What are the aliassing rules for Zig? There is a 'noalias' keyword,
-// but does that mean all unmarked pointers are allowed to alias?)
-pub const Entry = packed struct {
- etype: EType,
- isext: bool,
- // Whether or not this entry's size has been counted in its parents.
- // Counting of Link entries is deferred until the scan/delete operation has
- // completed, so for those entries this flag indicates an intention to be
- // counted.
- counted: bool,
- blocks: Blocks, // 512-byte blocks
- size: u64,
- next: ?*Entry,
+pub const Entry = extern struct {
+ pack: Packed align(1),
+ size: u64 align(1) = 0,
+ next: ?*Entry align(1) = null,
+
+ pub const Packed = packed struct(u64) {
+ etype: EType,
+ isext: bool,
+ // Whether or not this entry's size has been counted in its parents.
+ // Counting of Link entries is deferred until the scan/delete operation has
+ // completed, so for those entries this flag indicates an intention to be
+ // counted.
+ counted: bool = false,
+ blocks: Blocks = 0, // 512-byte blocks
+ };
const Self = @This();
pub fn dir(self: *Self) ?*Dir {
- return if (self.etype == .dir) @ptrCast(*Dir, self) else null;
+ return if (self.pack.etype == .dir) @ptrCast(self) else null;
}
pub fn link(self: *Self) ?*Link {
- return if (self.etype == .link) @ptrCast(*Link, self) else null;
+ return if (self.pack.etype == .link) @ptrCast(self) else null;
}
pub fn file(self: *Self) ?*File {
- return if (self.etype == .file) @ptrCast(*File, self) else null;
+ return if (self.pack.etype == .file) @ptrCast(self) else null;
}
// Whether this entry should be displayed as a "directory".
// Some dirs are actually represented in this data model as a File for efficiency.
pub fn isDirectory(self: *Self) bool {
- return if (self.file()) |f| f.other_fs or f.kernfs else self.etype == .dir;
+ return if (self.file()) |f| f.pack.other_fs or f.pack.kernfs else self.pack.etype == .dir;
}
- fn nameOffset(etype: EType) usize {
- return switch (etype) {
- .dir => @byteOffsetOf(Dir, "name"),
- .link => @byteOffsetOf(Link, "name"),
- .file => @byteOffsetOf(File, "name"),
+ pub fn name(self: *const Self) [:0]const u8 {
+ const self_name = switch (self.pack.etype) {
+ .dir => &@as(*const Dir, @ptrCast(self)).name,
+ .link => &@as(*const Link, @ptrCast(self)).name,
+ .file => &@as(*const File, @ptrCast(self)).name,
};
+ const name_ptr: [*:0]const u8 = @ptrCast(self_name);
+ return std.mem.sliceTo(name_ptr, 0);
}
- pub fn name(self: *const Self) [:0]const u8 {
- const ptr = @ptrCast([*:0]const u8, self) + nameOffset(self.etype);
- return ptr[0..std.mem.lenZ(ptr) :0];
+ pub fn ext(self: *Self) ?*Ext {
+ if (!self.pack.isext) return null;
+ return @ptrCast(@as([*]Ext, @ptrCast(self)) - 1);
}
- pub fn ext(self: *Self) ?*Ext {
- if (!self.isext) return null;
- return @ptrCast(*Ext, @ptrCast([*]Ext, self) - 1);
+ fn alloc(comptime T: type, etype: EType, isext: bool, ename: []const u8) *Entry {
+ const size = (if (isext) @as(usize, @sizeOf(Ext)) else 0) + @sizeOf(T) + ename.len + 1;
+ var ptr = blk: while (true) {
+ if (allocator.allocWithOptions(u8, size, 1, null)) |p| break :blk p
+ else |_| {}
+ ui.oom();
+ };
+ if (isext) {
+ @as(*Ext, @ptrCast(ptr)).* = .{};
+ ptr = ptr[@sizeOf(Ext)..];
+ }
+ const e: *T = @ptrCast(ptr);
+ e.* = .{ .entry = .{ .pack = .{ .etype = etype, .isext = isext } } };
+ const n = @as([*]u8, @ptrCast(&e.name))[0..ename.len+1];
+ @memcpy(n[0..ename.len], ename);
+ n[ename.len] = 0;
+ return &e.entry;
}
pub fn create(etype: EType, isext: bool, ename: []const u8) *Entry {
- const extsize = if (isext) @as(usize, @sizeOf(Ext)) else 0;
- const size = nameOffset(etype) + ename.len + 1 + extsize;
- var ptr = blk: {
- while (true) {
- if (allocator.allocator.allocWithOptions(u8, size, std.math.max(@alignOf(Ext), @alignOf(Entry)), null)) |p|
- break :blk p
- else |_| {}
- ui.oom();
- }
+ return switch (etype) {
+ .dir => alloc(Dir, etype, isext, ename),
+ .file => alloc(File, etype, isext, ename),
+ .link => alloc(Link, etype, isext, ename),
};
- std.mem.set(u8, ptr, 0); // kind of ugly, but does the trick
- var e = @ptrCast(*Entry, ptr.ptr + extsize);
- e.etype = etype;
- e.isext = isext;
- var name_ptr = @ptrCast([*]u8, e) + nameOffset(etype);
- std.mem.copy(u8, name_ptr[0..ename.len], ename);
- return e;
}
- // Set the 'err' flag on Dirs and Files, propagating 'suberr' to parents.
- pub fn setErr(self: *Self, parent: *Dir) void {
- if (self.dir()) |d| d.err = true
- else if (self.file()) |f| f.err = true
- else unreachable;
- var it: ?*Dir = if (&parent.entry == self) parent.parent else parent;
- while (it) |p| : (it = p.parent) {
- if (p.suberr) break;
- p.suberr = true;
- }
+ fn hasErr(self: *Self) bool {
+ return
+ if (self.file()) |f| f.pack.err
+ else if (self.dir()) |d| d.pack.err or d.pack.suberr
+ else false;
}
pub fn addStats(self: *Entry, parent: *Dir, nlink: u31) void {
- if (self.counted) return;
- self.counted = true;
+ if (self.pack.counted) return;
+ self.pack.counted = true;
// Add link to the inode map, but don't count its size (yet).
if (self.link()) |l| {
@@ -124,7 +126,7 @@ pub const Entry = packed struct {
var d = inodes.map.getOrPut(l) catch unreachable;
if (!d.found_existing) {
d.value_ptr.* = .{ .counted = false, .nlink = nlink };
- inodes.total_blocks = saturateAdd(inodes.total_blocks, self.blocks);
+ inodes.total_blocks +|= self.pack.blocks;
l.next = l;
} else {
inodes.setStats(.{ .key_ptr = d.key_ptr, .value_ptr = d.value_ptr }, false);
@@ -142,10 +144,10 @@ pub const Entry = packed struct {
if (self.ext()) |e|
if (p.entry.ext()) |pe|
if (e.mtime > pe.mtime) { pe.mtime = e.mtime; };
- p.items = saturateAdd(p.items, 1);
- if (self.etype != .link) {
- p.entry.size = saturateAdd(p.entry.size, self.size);
- p.entry.blocks = saturateAdd(p.entry.blocks, self.blocks);
+ p.items +|= 1;
+ if (self.pack.etype != .link) {
+ p.entry.size +|= self.size;
+ p.entry.pack.blocks +|= self.pack.blocks;
}
}
}
@@ -164,8 +166,8 @@ pub const Entry = packed struct {
// anymore, meaning that delStats() followed by addStats() with the same
// data may cause information to be lost.
pub fn delStats(self: *Entry, parent: *Dir) void {
- if (!self.counted) return;
- defer self.counted = false; // defer, to make sure inodes.setStats() still sees it as counted.
+ if (!self.pack.counted) return;
+ defer self.pack.counted = false; // defer, to make sure inodes.setStats() still sees it as counted.
if (self.link()) |l| {
var d = inodes.map.getEntry(l).?;
@@ -174,7 +176,7 @@ pub const Entry = packed struct {
if (l.next == l) {
_ = inodes.map.remove(l);
_ = inodes.uncounted.remove(l);
- inodes.total_blocks = saturateSub(inodes.total_blocks, self.blocks);
+ inodes.total_blocks -|= self.pack.blocks;
} else {
if (d.key_ptr.* == l)
d.key_ptr.* = l.next;
@@ -193,10 +195,10 @@ pub const Entry = packed struct {
var it: ?*Dir = parent;
while(it) |p| : (it = p.parent) {
- p.items = saturateSub(p.items, 1);
- if (self.etype != .link) {
- p.entry.size = saturateSub(p.entry.size, self.size);
- p.entry.blocks = saturateSub(p.entry.blocks, self.blocks);
+ p.items -|= 1;
+ if (self.pack.etype != .link) {
+ p.entry.size -|= self.size;
+ p.entry.pack.blocks -|= self.pack.blocks;
}
}
}
@@ -211,32 +213,35 @@ pub const Entry = packed struct {
}
};
-const DevId = u30; // Can be reduced to make room for more flags in Dir.
+const DevId = u30; // Can be reduced to make room for more flags in Dir.Packed.
-pub const Dir = packed struct {
+pub const Dir = extern struct {
entry: Entry,
- sub: ?*Entry,
- parent: ?*Dir,
+ sub: ?*Entry align(1) = null,
+ parent: ?*Dir align(1) = null,
// entry.{blocks,size}: Total size of all unique files + dirs. Non-shared hardlinks are counted only once.
// (i.e. the space you'll need if you created a filesystem with only this dir)
// shared_*: Unique hardlinks that still have references outside of this directory.
// (i.e. the space you won't reclaim by deleting this dir)
// (space reclaimed by deleting a dir =~ entry. - shared_)
- shared_blocks: u64,
- shared_size: u64,
- items: u32,
-
- // Indexes into the global 'devices.list' array
- dev: DevId,
+ shared_blocks: u64 align(1) = 0,
+ shared_size: u64 align(1) = 0,
+ items: u32 align(1) = 0,
- err: bool,
- suberr: bool,
+ pack: Packed align(1) = .{},
- // Only used to find the @byteOffsetOff, the name is written at this point as a 0-terminated string.
+ // Only used to find the @offsetOff, the name is written at this point as a 0-terminated string.
// (Old C habits die hard)
- name: u8,
+ name: [0]u8 = undefined,
+
+ pub const Packed = packed struct {
+ // Indexes into the global 'devices.list' array
+ dev: DevId = 0,
+ err: bool = false,
+ suberr: bool = false,
+ };
pub fn fmtPath(self: *const @This(), withRoot: bool, out: *std.ArrayList(u8)) void {
if (!withRoot and self.parent == null) return;
@@ -249,25 +254,38 @@ pub const Dir = packed struct {
var i: usize = components.items.len-1;
while (true) {
- if (i != components.items.len-1) out.append('/') 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;
}
}
+
+ // Only updates the suberr of this Dir, assumes child dirs have already
+ // been updated and does not propagate to parents.
+ pub fn updateSubErr(self: *@This()) void {
+ self.pack.suberr = false;
+ var sub = self.sub;
+ while (sub) |e| : (sub = e.next) {
+ if (e.hasErr()) {
+ self.pack.suberr = true;
+ break;
+ }
+ }
+ }
};
// File that's been hardlinked (i.e. nlink > 1)
-pub const Link = packed struct {
+pub const Link = extern struct {
entry: Entry,
- parent: *Dir,
- next: *Link, // Singly circular linked list of all *Link nodes with the same dev,ino.
+ parent: *Dir align(1) = undefined,
+ next: *Link align(1) = undefined, // Singly circular linked list of all *Link nodes with the same dev,ino.
// dev is inherited from the parent Dir
- ino: u64,
- name: u8,
+ ino: u64 align(1) = undefined,
+ name: [0]u8 = undefined,
// Return value should be freed with main.allocator.
- pub fn path(self: @This(), withRoot: bool) [:0]const u8 {
+ pub fn path(self: *const @This(), withRoot: bool) [:0]const u8 {
var out = std.ArrayList(u8).init(main.allocator);
self.parent.fmtPath(withRoot, &out);
out.append('/') catch unreachable;
@@ -277,40 +295,28 @@ pub const Link = packed struct {
};
// Anything that's not an (indexed) directory or hardlink. Excluded directories are also "Files".
-pub const File = packed struct {
+pub const File = extern struct {
entry: Entry,
-
- err: bool,
- excluded: bool,
- other_fs: bool,
- kernfs: bool,
- notreg: bool,
- _pad: u3,
-
- name: u8,
-
- pub fn resetFlags(f: *@This()) void {
- f.err = false;
- f.excluded = false;
- f.other_fs = false;
- f.kernfs = false;
- f.notreg = false;
- }
+ pack: Packed = .{},
+ name: [0]u8 = undefined,
+
+ pub const Packed = packed struct(u8) {
+ err: bool = false,
+ excluded: bool = false,
+ other_fs: bool = false,
+ kernfs: bool = false,
+ notreg: bool = false,
+ _pad: u3 = 0, // Make this struct "ABI sized" to allow inclusion in an extern struct
+ };
};
-pub const Ext = packed struct {
- mtime: u64 = 0,
- uid: u32 = 0,
- gid: u32 = 0,
- mode: u16 = 0,
+pub const Ext = extern struct {
+ mtime: u64 align(1) = 0,
+ uid: u32 align(1) = 0,
+ gid: u32 align(1) = 0,
+ mode: u16 align(1) = 0,
};
-comptime {
- std.debug.assert(@bitOffsetOf(Dir, "name") % 8 == 0);
- std.debug.assert(@bitOffsetOf(Link, "name") % 8 == 0);
- std.debug.assert(@bitOffsetOf(File, "name") % 8 == 0);
-}
-
// List of st_dev entries. Those are typically 64bits, but that's quite a waste
// of space when a typical scan won't cover many unique devices.
@@ -321,9 +327,9 @@ pub const devices = struct {
var lookup = std.AutoHashMap(u64, DevId).init(main.allocator);
pub fn getId(dev: u64) DevId {
- var d = lookup.getOrPut(dev) catch unreachable;
+ const d = lookup.getOrPut(dev) catch unreachable;
if (!d.found_existing) {
- d.value_ptr.* = @intCast(DevId, list.items.len);
+ d.value_ptr.* = @as(DevId, @intCast(list.items.len));
list.append(dev) catch unreachable;
}
return d.value_ptr.*;
@@ -366,13 +372,13 @@ pub const inodes = struct {
const HashContext = struct {
pub fn hash(_: @This(), l: *Link) u64 {
var h = std.hash.Wyhash.init(0);
- h.update(std.mem.asBytes(&@as(u32, l.parent.dev)));
+ h.update(std.mem.asBytes(&@as(u32, l.parent.pack.dev)));
h.update(std.mem.asBytes(&l.ino));
return h.final();
}
pub fn eql(_: @This(), a: *Link, b: *Link) bool {
- return a.ino == b.ino and a.parent.dev == b.parent.dev;
+ return a.ino == b.ino and a.parent.pack.dev == b.parent.pack.dev;
}
};
@@ -398,11 +404,11 @@ pub const inodes = struct {
defer dirs.deinit();
var it = entry.key_ptr.*;
while (true) {
- if (it.entry.counted) {
+ if (it.entry.pack.counted) {
nlink += 1;
var parent: ?*Dir = it.parent;
while (parent) |p| : (parent = p.parent) {
- var de = dirs.getOrPut(p) catch unreachable;
+ const de = dirs.getOrPut(p) catch unreachable;
if (de.found_existing) de.value_ptr.* += 1
else de.value_ptr.* = 1;
}
@@ -418,20 +424,20 @@ pub const inodes = struct {
var dir_iter = dirs.iterator();
if (add) {
while (dir_iter.next()) |de| {
- de.key_ptr.*.entry.blocks = saturateAdd(de.key_ptr.*.entry.blocks, entry.key_ptr.*.entry.blocks);
- de.key_ptr.*.entry.size = saturateAdd(de.key_ptr.*.entry.size, entry.key_ptr.*.entry.size);
+ de.key_ptr.*.entry.pack.blocks +|= entry.key_ptr.*.entry.pack.blocks;
+ de.key_ptr.*.entry.size +|= entry.key_ptr.*.entry.size;
if (de.value_ptr.* < nlink) {
- de.key_ptr.*.shared_blocks = saturateAdd(de.key_ptr.*.shared_blocks, entry.key_ptr.*.entry.blocks);
- de.key_ptr.*.shared_size = saturateAdd(de.key_ptr.*.shared_size, entry.key_ptr.*.entry.size);
+ de.key_ptr.*.shared_blocks +|= entry.key_ptr.*.entry.pack.blocks;
+ de.key_ptr.*.shared_size +|= entry.key_ptr.*.entry.size;
}
}
} else {
while (dir_iter.next()) |de| {
- de.key_ptr.*.entry.blocks = saturateSub(de.key_ptr.*.entry.blocks, entry.key_ptr.*.entry.blocks);
- de.key_ptr.*.entry.size = saturateSub(de.key_ptr.*.entry.size, entry.key_ptr.*.entry.size);
+ de.key_ptr.*.entry.pack.blocks -|= entry.key_ptr.*.entry.pack.blocks;
+ de.key_ptr.*.entry.size -|= entry.key_ptr.*.entry.size;
if (de.value_ptr.* < nlink) {
- de.key_ptr.*.shared_blocks = saturateSub(de.key_ptr.*.shared_blocks, entry.key_ptr.*.entry.blocks);
- de.key_ptr.*.shared_size = saturateSub(de.key_ptr.*.shared_size, entry.key_ptr.*.entry.size);
+ de.key_ptr.*.shared_blocks -|= entry.key_ptr.*.entry.pack.blocks;
+ de.key_ptr.*.shared_size -|= entry.key_ptr.*.entry.size;
}
}
}
@@ -456,8 +462,8 @@ pub var root: *Dir = undefined;
test "entry" {
- var e = Entry.create(.file, false, "hello") catch unreachable;
- std.debug.assert(e.etype == .file);
- std.debug.assert(!e.isext);
- std.testing.expectEqualStrings(e.name(), "hello");
+ var e = Entry.create(.file, false, "hello");
+ try std.testing.expectEqual(e.pack.etype, .file);
+ try std.testing.expect(!e.pack.isext);
+ try std.testing.expectEqualStrings(e.name(), "hello");
}
diff --git a/src/ncurses_refs.c b/src/ncurses_refs.c
deleted file mode 100644
index b458efb..0000000
--- a/src/ncurses_refs.c
+++ /dev/null
@@ -1,30 +0,0 @@
-/* SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
- * SPDX-License-Identifier: MIT
- */
-
-#include <curses.h>
-
-/* Zig @cImport() has problems with the ACS_* macros. Two, in fact:
- *
- * 1. Naively using the ACS_* macros results in:
- *
- * error: cannot store runtime value in compile time variable
- * return acs_map[NCURSES_CAST(u8, c)];
- * ^
- * That error doesn't make much sense to me, but it might be
- * related to https://github.com/ziglang/zig/issues/5344?
- *
- * 2. The 'acs_map' extern variable isn't being linked correctly?
- * Haven't investigated this one deeply enough yet, but attempting
- * to dereference acs_map from within Zig leads to a segfault;
- * its pointer value doesn't make any sense.
- */
-chtype ncdu_acs_ulcorner() { return ACS_ULCORNER; }
-chtype ncdu_acs_llcorner() { return ACS_LLCORNER; }
-chtype ncdu_acs_urcorner() { return ACS_URCORNER; }
-chtype ncdu_acs_lrcorner() { return ACS_LRCORNER; }
-chtype ncdu_acs_hline() { return ACS_VLINE ; }
-chtype ncdu_acs_vline() { return ACS_HLINE ; }
-
-/* https://github.com/ziglang/zig/issues/8947 */
-void ncdu_init_pair(short a,b,c) { init_pair(a,b,c); }
diff --git a/src/scan.zig b/src/scan.zig
index 38b9a2e..4608a4f 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -1,13 +1,13 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
const ui = @import("ui.zig");
-usingnamespace @import("util.zig");
+const util = @import("util.zig");
+const exclude = @import("exclude.zig");
const c_statfs = @cImport(@cInclude("sys/vfs.h"));
-const c_fnmatch = @cImport(@cInclude("fnmatch.h"));
// Concise stat struct for fields we're interested in, with the types used by the model.
@@ -23,26 +23,26 @@ const Stat = struct {
symlink: bool = false,
ext: model.Ext = .{},
- fn clamp(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
- return castClamp(std.meta.fieldInfo(T, field).field_type, x);
+ fn clamp(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).type {
+ return util.castClamp(std.meta.fieldInfo(T, field).type, x);
}
- fn truncate(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
- return castTruncate(std.meta.fieldInfo(T, field).field_type, x);
+ fn truncate(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).type {
+ return util.castTruncate(std.meta.fieldInfo(T, field).type, x);
}
fn read(parent: std.fs.Dir, name: [:0]const u8, follow: bool) !Stat {
- const stat = try std.os.fstatatZ(parent.fd, name, if (follow) 0 else std.os.AT_SYMLINK_NOFOLLOW);
+ const stat = try std.os.fstatatZ(parent.fd, name, if (follow) 0 else std.os.AT.SYMLINK_NOFOLLOW);
return Stat{
.blocks = clamp(Stat, .blocks, stat.blocks),
.size = clamp(Stat, .size, stat.size),
.dev = truncate(Stat, .dev, stat.dev),
.ino = truncate(Stat, .ino, stat.ino),
.nlink = clamp(Stat, .nlink, stat.nlink),
- .hlinkc = stat.nlink > 1 and !std.os.system.S_ISDIR(stat.mode),
- .dir = std.os.system.S_ISDIR(stat.mode),
- .reg = std.os.system.S_ISREG(stat.mode),
- .symlink = std.os.system.S_ISLNK(stat.mode),
+ .hlinkc = stat.nlink > 1 and !std.os.system.S.ISDIR(stat.mode),
+ .dir = std.os.system.S.ISDIR(stat.mode),
+ .reg = std.os.system.S.ISREG(stat.mode),
+ .symlink = std.os.system.S.ISLNK(stat.mode),
.ext = .{
.mtime = clamp(model.Ext, .mtime, stat.mtime().tv_sec),
.uid = truncate(model.Ext, .uid, stat.uid),
@@ -60,7 +60,7 @@ fn isKernfs(dir: std.fs.Dir, dev: u64) bool {
if (kernfs_cache.get(dev)) |e| return e;
var buf: c_statfs.struct_statfs = undefined;
if (c_statfs.fstatfs(dir.fd, &buf) != 0) return false; // silently ignoring errors isn't too nice.
- const iskern = switch (buf.f_type) {
+ const iskern = switch (util.castTruncate(u32, buf.f_type)) {
// These numbers are documented in the Linux 'statfs(2)' man page, so I assume they're stable.
0x42494e4d, // BINFMTFS_MAGIC
0xcafe4a11, // BPF_FS_MAGIC
@@ -96,7 +96,7 @@ fn writeJsonString(wr: anytype, s: []const u8) !void {
0xC => try wr.writeAll("\\f"),
'\\' => try wr.writeAll("\\\\"),
'"' => try wr.writeAll("\\\""),
- 0...7, 0xB, 0xE...0x1F, 127 => try wr.print("\\u00{x:02}", .{ch}),
+ 0...7, 0xB, 0xE...0x1F, 127 => try wr.print("\\u00{x:0>2}", .{ch}),
else => try wr.writeByte(ch)
}
}
@@ -141,7 +141,7 @@ const ScanDir = struct {
var count: Map.Size = 0;
var it = dir.sub;
while (it) |e| : (it = e.next) count += 1;
- self.entries.ensureCapacity(count) catch unreachable;
+ self.entries.ensureUnusedCapacity(count) catch unreachable;
it = dir.sub;
while (it) |e| : (it = e.next)
@@ -156,14 +156,14 @@ const ScanDir = struct {
// in-place conversion to a File entry. That's more efficient,
// but also more code. I don't expect this to happen often.
var e = entry.key_ptr.*.?;
- if (e.etype == .file) {
- if (e.size > 0 or e.blocks > 0) {
+ if (e.pack.etype == .file) {
+ if (e.size > 0 or e.pack.blocks > 0) {
e.delStats(self.dir);
e.size = 0;
- e.blocks = 0;
+ e.pack.blocks = 0;
e.addStats(self.dir, 0);
}
- e.file().?.resetFlags();
+ e.file().?.pack = .{};
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
break :blk e;
} else e.delStatsRec(self.dir);
@@ -176,10 +176,10 @@ const ScanDir = struct {
};
var f = e.file().?;
switch (t) {
- .err => e.setErr(self.dir),
- .other_fs => f.other_fs = true,
- .kernfs => f.kernfs = true,
- .excluded => f.excluded = true,
+ .err => f.pack.err = true,
+ .other_fs => f.pack.other_fs = true,
+ .kernfs => f.pack.kernfs = true,
+ .excluded => f.pack.excluded = true,
}
}
@@ -192,9 +192,9 @@ const ScanDir = struct {
// XXX: In-place conversion may also be possible here.
var e = entry.key_ptr.*.?;
// changes of dev/ino affect hard link counting in a way we can't simply merge.
- const samedev = if (e.dir()) |d| d.dev == model.devices.getId(stat.dev) else true;
+ const samedev = if (e.dir()) |d| d.pack.dev == model.devices.getId(stat.dev) else true;
const sameino = if (e.link()) |l| l.ino == stat.ino else true;
- if (e.etype == etype and samedev and sameino) {
+ if (e.pack.etype == etype and samedev and sameino) {
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
break :blk e;
} else e.delStatsRec(self.dir);
@@ -209,19 +209,16 @@ const ScanDir = struct {
// entire subtree, which, in turn, would break all shared hardlink
// sizes. The current approach may result in incorrect sizes after
// refresh, but I expect the difference to be fairly minor.
- if (!(e.etype == .dir and e.counted) and (e.blocks != stat.blocks or e.size != stat.size)) {
+ if (!(e.pack.etype == .dir and e.pack.counted) and (e.pack.blocks != stat.blocks or e.size != stat.size)) {
e.delStats(self.dir);
- e.blocks = stat.blocks;
+ e.pack.blocks = stat.blocks;
e.size = stat.size;
}
if (e.dir()) |d| {
d.parent = self.dir;
- d.dev = model.devices.getId(stat.dev);
- }
- if (e.file()) |f| {
- f.resetFlags();
- f.notreg = !stat.dir and !stat.reg;
+ d.pack.dev = model.devices.getId(stat.dev);
}
+ if (e.file()) |f| f.pack = .{ .notreg = !stat.dir and !stat.reg };
if (e.link()) |l| l.ino = stat.ino;
if (e.ext()) |ext| {
if (ext.mtime > stat.ext.mtime)
@@ -234,16 +231,17 @@ const ScanDir = struct {
}
fn final(self: *Self) void {
- if (self.entries.count() == 0) // optimization for the common case
- return;
- var it = &self.dir.sub;
- while (it.*) |e| {
- if (self.entries.contains(e)) {
- e.delStatsRec(self.dir);
- it.* = e.next;
- } else
- it = &e.next;
+ if (self.entries.count() > 0) {
+ var it = &self.dir.sub;
+ while (it.*) |e| {
+ if (self.entries.contains(e)) {
+ e.delStatsRec(self.dir);
+ it.* = e.next;
+ } else
+ it = &e.next;
+ }
}
+ self.dir.updateSubErr();
}
fn deinit(self: *Self) void {
@@ -263,7 +261,8 @@ const ScanDir = struct {
//
const Context = struct {
// When scanning to RAM
- parents: ?std.ArrayList(ScanDir) = std.ArrayList(ScanDir).init(main.allocator),
+ parents: ?std.ArrayList(ScanDir) = null,
+ refreshing: ?*model.Dir = null,
// When scanning to a file
wr: ?*Writer = null,
@@ -296,20 +295,30 @@ const Context = struct {
wr.print("{d}", .{std.time.timestamp()}) catch |e| writeErr(e);
wr.writeByte('}') catch |e| writeErr(e);
- var self = main.allocator.create(Self) catch unreachable;
+ const self = main.allocator.create(Self) catch unreachable;
self.* = .{ .wr = buf };
return self;
}
fn initMem(dir: ?*model.Dir) *Self {
var self = main.allocator.create(Self) catch unreachable;
- self.* = .{ .parents = std.ArrayList(ScanDir).init(main.allocator) };
+ self.* = .{
+ .parents = std.ArrayList(ScanDir).init(main.allocator),
+ .refreshing = dir,
+ };
if (dir) |d| self.parents.?.append(ScanDir.init(d)) catch unreachable;
return self;
}
fn final(self: *Self) void {
- if (self.parents) |_| model.inodes.addAllStats();
+ if (self.parents) |_| {
+ counting_hardlinks = true;
+ defer counting_hardlinks = false;
+ main.handleEvent(false, true);
+ model.inodes.addAllStats();
+ var p = self.refreshing;
+ while (p) |d| : (p = d.parent) d.updateSubErr();
+ }
if (self.wr) |wr| {
wr.writer().writeByte(']') catch |e| writeErr(e);
wr.flush() catch |e| writeErr(e);
@@ -345,13 +354,13 @@ const Context = struct {
}
fn pathZ(self: *Self) [:0]const u8 {
- return arrayListBufZ(&self.path);
+ return util.arrayListBufZ(&self.path);
}
// Set a flag to indicate that there was an error listing file entries in the current directory.
// (Such errors are silently ignored when exporting to a file, as the directory metadata has already been written)
fn setDirlistError(self: *Self) void {
- if (self.parents) |*p| p.items[p.items.len-1].dir.entry.setErr(p.items[p.items.len-1].dir);
+ if (self.parents) |*p| p.items[p.items.len-1].dir.pack.err = true;
}
const Special = enum { err, other_fs, kernfs, excluded };
@@ -374,8 +383,6 @@ const Context = struct {
// Insert the current path as a special entry (i.e. a file/dir that is not counted)
// Ignores self.stat except for the 'dir' option.
fn addSpecial(self: *Self, t: Special) void {
- std.debug.assert(self.items_seen > 0); // root item can't be a special
-
if (t == .err) {
if (self.last_error) |p| main.allocator.free(p);
self.last_error = main.allocator.dupeZ(u8, self.path.items) catch unreachable;
@@ -396,7 +403,7 @@ const Context = struct {
try w.writeAll("{\"name\":");
try writeJsonString(w, self.name);
if (self.stat.size > 0) try w.print(",\"asize\":{d}", .{ self.stat.size });
- if (self.stat.blocks > 0) try w.print(",\"dsize\":{d}", .{ blocksToSize(self.stat.blocks) });
+ if (self.stat.blocks > 0) try w.print(",\"dsize\":{d}", .{ util.blocksToSize(self.stat.blocks) });
if (self.stat.dir and self.stat.dev != dir_dev) try w.print(",\"dev\":{d}", .{ self.stat.dev });
if (self.stat.hlinkc) try w.print(",\"ino\":{d},\"hlnkc\":true,\"nlink\":{d}", .{ self.stat.ino, self.stat.nlink });
if (!self.stat.dir and !self.stat.reg) try w.writeAll(",\"notreg\":true");
@@ -412,11 +419,11 @@ const Context = struct {
var e = if (p.items.len == 0) blk: {
// Root entry
var e = model.Entry.create(.dir, main.config.extended, self.name);
- e.blocks = self.stat.blocks;
+ e.pack.blocks = self.stat.blocks;
e.size = self.stat.size;
if (e.ext()) |ext| ext.* = self.stat.ext;
model.root = e.dir().?;
- model.root.dev = model.devices.getId(self.stat.dev);
+ model.root.pack.dev = model.devices.getId(self.stat.dev);
break :blk e;
} else
p.items[p.items.len-1].addStat(self.name, &self.stat);
@@ -447,8 +454,8 @@ const Context = struct {
var active_context: *Context = undefined;
// Read and index entries of the given dir.
-fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
- var it = main.allocator.create(std.fs.Dir.Iterator) catch unreachable;
+fn scanDir(ctx: *Context, pat: *const exclude.Patterns, dir: std.fs.IterableDir, dir_dev: u64) void {
+ var it = main.allocator.create(std.fs.IterableDir.Iterator) catch unreachable;
defer main.allocator.destroy(it);
it.* = dir.iterate();
while(true) {
@@ -462,35 +469,23 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
defer ctx.popPath();
main.handleEvent(false, false);
- // XXX: This algorithm is extremely slow, can be optimized with some clever pattern parsing.
- const excluded = blk: {
- for (main.config.exclude_patterns.items) |pat| {
- var path = ctx.pathZ();
- while (path.len > 0) {
- if (c_fnmatch.fnmatch(pat, path, 0) == 0) break :blk true;
- if (std.mem.indexOfScalar(u8, path, '/')) |idx| path = path[idx+1..:0]
- else break;
- }
- }
- break :blk false;
- };
- if (excluded) {
+ const excluded = pat.match(ctx.name);
+ if (excluded == false) { // matched either a file or directory, so we can exclude this before stat()ing.
ctx.addSpecial(.excluded);
continue;
}
- ctx.stat = Stat.read(dir, ctx.name, false) catch {
+ ctx.stat = Stat.read(dir.dir, ctx.name, false) catch {
ctx.addSpecial(.err);
continue;
};
-
if (main.config.same_fs and ctx.stat.dev != dir_dev) {
ctx.addSpecial(.other_fs);
continue;
}
if (main.config.follow_symlinks and ctx.stat.symlink) {
- if (Stat.read(dir, ctx.name, true)) |nstat| {
+ if (Stat.read(dir.dir, ctx.name, true)) |nstat| {
if (!nstat.dir) {
ctx.stat = nstat;
// Symlink targets may reside on different filesystems,
@@ -500,21 +495,27 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
}
} else |_| {}
}
+ if (excluded) |e| if (e and ctx.stat.dir) {
+ ctx.addSpecial(.excluded);
+ continue;
+ };
var edir =
- if (ctx.stat.dir) dir.openDirZ(ctx.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch {
+ if (!ctx.stat.dir) null
+ else if (dir.dir.openDirZ(ctx.name, .{ .no_follow = true }, true)) |d| std.fs.IterableDir{.dir = d}
+ else |_| {
ctx.addSpecial(.err);
continue;
- } else null;
+ };
defer if (edir != null) edir.?.close();
- if (std.builtin.os.tag == .linux and main.config.exclude_kernfs and ctx.stat.dir and isKernfs(edir.?, ctx.stat.dev)) {
+ if (@import("builtin").os.tag == .linux and main.config.exclude_kernfs and ctx.stat.dir and isKernfs(edir.?.dir, ctx.stat.dev)) {
ctx.addSpecial(.kernfs);
continue;
}
if (main.config.exclude_caches and ctx.stat.dir) {
- if (edir.?.openFileZ("CACHEDIR.TAG", .{})) |f| {
+ if (edir.?.dir.openFileZ("CACHEDIR.TAG", .{})) |f| {
const sig = "Signature: 8a477f597d28d172789f06886806bc55";
var buf: [sig.len]u8 = undefined;
if (f.reader().readAll(&buf)) |len| {
@@ -527,7 +528,11 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
}
ctx.addStat(dir_dev);
- if (ctx.stat.dir) scanDir(ctx, edir.?, ctx.stat.dev);
+ if (ctx.stat.dir) {
+ var subpat = pat.enter(ctx.name);
+ defer subpat.deinit();
+ scanDir(ctx, &subpat, edir.?, ctx.stat.dev);
+ }
}
}
@@ -551,21 +556,24 @@ pub fn setupRefresh(parent: *model.Dir) void {
parent.fmtPath(true, &full_path);
active_context.pushPath(full_path.items);
active_context.stat.dir = true;
- active_context.stat.dev = model.devices.list.items[parent.dev];
+ active_context.stat.dev = model.devices.list.items[parent.pack.dev];
}
// To be called after setupRefresh() (or from scanRoot())
pub fn scan() void {
defer active_context.deinit();
- var dir = std.fs.cwd().openDirZ(active_context.pathZ(), .{ .access_sub_paths = true, .iterate = true }) catch |e| {
+ const dir_ = std.fs.cwd().openDirZ(active_context.pathZ(), .{}, true) catch |e| {
active_context.last_error = main.allocator.dupeZ(u8, active_context.path.items) catch unreachable;
active_context.fatal_error = e;
while (main.state == .refresh or main.state == .scan)
main.handleEvent(true, true);
return;
};
+ var dir = std.fs.IterableDir{.dir = dir_};
defer dir.close();
- scanDir(active_context, dir, active_context.stat.dev);
+ var pat = exclude.getPatterns(active_context.pathZ());
+ defer pat.deinit();
+ scanDir(active_context, &pat, dir, active_context.stat.dev);
active_context.popPath();
active_context.final();
}
@@ -614,7 +622,10 @@ const Import = struct {
return;
}
}
- self.ch = self.rdbuf[self.rdoff];
+ // Zig 0.10 copies the entire array to the stack in ReleaseSafe mode,
+ // work around that bug by indexing into a pointer to the array
+ // instead.
+ self.ch = (&self.rdbuf)[self.rdoff];
self.rdoff += 1;
self.byte += 1;
}
@@ -783,7 +794,7 @@ const Import = struct {
},
'd' => {
if (eq(u8, key, "dsize")) {
- self.ctx.stat.blocks = @intCast(model.Blocks, self.uint(u64)>>9);
+ self.ctx.stat.blocks = @intCast(self.uint(u64)>>9);
return;
}
if (eq(u8, key, "dev")) {
@@ -799,6 +810,7 @@ const Import = struct {
if (eq(u8, typ, "otherfs")) special.* = .other_fs
else if (eq(u8, typ, "kernfs")) special.* = .kernfs
else special.* = .excluded;
+ return;
}
},
'g' => {
@@ -899,7 +911,9 @@ const Import = struct {
fn item(self: *Self, dev: u64) void {
self.ctx.stat = .{};
+ var isdir = false;
if (self.ch == '[') {
+ isdir = true;
self.ctx.stat.dir = true;
self.con();
self.conws();
@@ -908,7 +922,7 @@ const Import = struct {
self.iteminfo(dev);
self.conws();
- if (self.ctx.stat.dir) {
+ if (isdir) {
const ndev = self.ctx.stat.dev;
while (self.ch == ',') {
self.con();
@@ -961,7 +975,7 @@ const Import = struct {
};
pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
- var fd = if (std.mem.eql(u8, "-", path)) std.io.getStdIn()
+ const fd = if (std.mem.eql(u8, "-", path)) std.io.getStdIn()
else std.fs.cwd().openFileZ(path, .{})
catch |e| ui.die("Error reading file: {s}.\n", .{ui.errorString(e)});
defer fd.close();
@@ -974,29 +988,37 @@ pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
}
var animation_pos: u32 = 0;
+var counting_hardlinks: bool = false;
var need_confirm_quit = false;
fn drawError(err: anyerror) void {
- const width = saturateSub(ui.cols, 5);
+ const width = ui.cols -| 5;
const box = ui.Box.create(7, width, "Scan error");
box.move(2, 2);
ui.addstr("Path: ");
- ui.addstr(ui.shorten(ui.toUtf8(active_context.last_error.?), saturateSub(width, 10)));
+ ui.addstr(ui.shorten(ui.toUtf8(active_context.last_error.?), width -| 10));
box.move(3, 2);
ui.addstr("Error: ");
- ui.addstr(ui.shorten(ui.errorString(err), saturateSub(width, 6)));
+ ui.addstr(ui.shorten(ui.errorString(err), width -| 6));
- box.move(5, saturateSub(width, 27));
+ box.move(5, width -| 27);
ui.addstr("Press any key to continue");
}
+fn drawCounting() void {
+ const box = ui.Box.create(4, 25, "Finalizing");
+ box.move(2, 2);
+ ui.addstr("Counting hardlinks...");
+}
+
fn drawBox() void {
ui.init();
const ctx = active_context;
if (ctx.fatal_error) |err| return drawError(err);
- const width = saturateSub(ui.cols, 5);
+ if (counting_hardlinks) return drawCounting();
+ const width = ui.cols -| 5;
const box = ui.Box.create(10, width, "Scanning...");
box.move(2, 2);
ui.addstr("Total items: ");
@@ -1006,12 +1028,12 @@ fn drawBox() void {
box.move(2, 30);
ui.addstr("size: ");
// TODO: Should display the size of the dir-to-be-refreshed on refreshing, not the root.
- ui.addsize(.default, blocksToSize(saturateAdd(model.root.entry.blocks, model.inodes.total_blocks)));
+ ui.addsize(.default, util.blocksToSize(model.root.entry.pack.blocks +| model.inodes.total_blocks));
}
box.move(3, 2);
ui.addstr("Current item: ");
- ui.addstr(ui.shorten(ui.toUtf8(ctx.pathZ()), saturateSub(width, 18)));
+ ui.addstr(ui.shorten(ui.toUtf8(ctx.pathZ()), width -| 18));
if (ctx.last_error) |path| {
box.move(5, 2);
@@ -1019,20 +1041,20 @@ fn drawBox() void {
ui.addstr("Warning: ");
ui.style(.default);
ui.addstr("error scanning ");
- ui.addstr(ui.shorten(ui.toUtf8(path), saturateSub(width, 28)));
+ ui.addstr(ui.shorten(ui.toUtf8(path), width -| 28));
box.move(6, 3);
ui.addstr("some directory sizes may not be correct.");
}
if (need_confirm_quit) {
- box.move(8, saturateSub(width, 20));
+ box.move(8, width -| 20);
ui.addstr("Press ");
ui.style(.key);
ui.addch('y');
ui.style(.default);
ui.addstr(" to confirm");
} else {
- box.move(8, saturateSub(width, 18));
+ box.move(8, width -| 18);
ui.addstr("Press ");
ui.style(.key);
ui.addch('q');
@@ -1045,9 +1067,8 @@ fn drawBox() void {
animation_pos += 1;
if (animation_pos >= txt.len*2) animation_pos = 0;
if (animation_pos < txt.len) {
- var i: u32 = 0;
box.move(8, 2);
- while (i <= animation_pos) : (i += 1) ui.addch(txt[i]);
+ for (txt[0..animation_pos + 1]) |t| ui.addch(t);
} else {
var i: u32 = txt.len-1;
while (i > animation_pos-txt.len) : (i -= 1) {
@@ -1059,24 +1080,27 @@ fn drawBox() void {
}
pub fn draw() void {
- if (active_context.fatal_error != null and main.config.scan_ui != .full)
+ if (active_context.fatal_error != null and main.config.scan_ui.? != .full)
ui.die("Error reading {s}: {s}\n", .{ active_context.last_error.?, ui.errorString(active_context.fatal_error.?) });
- switch (main.config.scan_ui) {
+ switch (main.config.scan_ui.?) {
.none => {},
.line => {
var buf: [256]u8 = undefined;
var line: []const u8 = undefined;
- if (active_context.parents == null) {
+ if (counting_hardlinks) {
+ line = "\x1b7\x1b[JCounting hardlinks...\x1b8";
+ } else if (active_context.parents == null) {
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8",
.{ ui.shorten(active_context.pathZ(), 63), active_context.items_seen }
) catch return;
} else {
- const r = ui.FmtSize.fmt(blocksToSize(model.root.entry.blocks));
+ const r = ui.FmtSize.fmt(util.blocksToSize(model.root.entry.pack.blocks));
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <51} {d:>9} files / {s}{s}\x1b8",
.{ ui.shorten(active_context.pathZ(), 51), active_context.items_seen, r.num(), r.unit }
) catch return;
}
- _ = std.io.getStdErr().write(line) catch {};
+ const stderr = std.io.getStdErr();
+ stderr.writeAll(line) catch {};
},
.full => drawBox(),
}
diff --git a/src/ui.zig b/src/ui.zig
index d9a7d74..2c14ce9 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -1,18 +1,18 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
// Ncurses wrappers and TUI helper functions.
const std = @import("std");
const main = @import("main.zig");
-usingnamespace @import("util.zig");
+const util = @import("util.zig");
pub const c = @cImport({
+ @cDefine("_XOPEN_SOURCE", "1");
@cInclude("stdio.h");
@cInclude("string.h");
@cInclude("curses.h");
@cInclude("time.h");
- @cDefine("_X_OPEN_SOURCE", "1");
@cInclude("wchar.h");
@cInclude("locale.h");
});
@@ -24,7 +24,8 @@ pub var cols: u32 = undefined;
pub fn die(comptime fmt: []const u8, args: anytype) noreturn {
deinit();
- _ = std.io.getStdErr().writer().print(fmt, args) catch {};
+ const stderr = std.io.getStdErr();
+ stderr.writer().print(fmt, args) catch {};
std.process.exit(1);
}
@@ -45,7 +46,8 @@ pub fn quit() noreturn {
pub fn oom() void {
const haveui = inited;
deinit();
- _ = std.io.getStdErr().writer().writeAll("\x1b7\x1b[JOut of memory, trying again in 1 second. Hit Ctrl-C to abort.\x1b8") catch {};
+ 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();
@@ -73,7 +75,7 @@ pub fn errorString(e: anyerror) [:0]const u8 {
error.ReadOnlyFilesystem => "Read-only filesystem",
error.SymlinkLoop => "Symlink loop",
error.SystemFdQuotaExceeded => "System file descriptor limit exceeded",
- else => @bitCast([:0]const u8, @errorName(e)), // XXX: The bitCast can be removed after a Zig >0.8 release.
+ else => @errorName(e),
};
}
@@ -113,7 +115,7 @@ pub fn toUtf8(in: [:0]const u8) [:0]const u8 {
to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]}) catch unreachable;
i += 1;
}
- return arrayListBufZ(&to_utf8_buf);
+ return util.arrayListBufZ(&to_utf8_buf);
}
var shorten_buf = std.ArrayList(u8).init(main.allocator);
@@ -128,20 +130,21 @@ pub fn shorten(in: [:0]const u8, max_width: u32) [:0] const u8 {
var total_width: u32 = 0;
var prefix_width: u32 = 0;
var prefix_end: u32 = 0;
+ var prefix_done = false;
var it = std.unicode.Utf8View.initUnchecked(in).iterator();
while (it.nextCodepoint()) |cp| {
// XXX: libc assumption: wchar_t is a Unicode point. True for most modern libcs?
// (The "proper" way is to use mbtowc(), but I'd rather port the musl wcwidth implementation to Zig so that I *know* it'll be Unicode.
// On the other hand, ncurses also use wcwidth() so that would cause duplicated code. Ugh)
const cp_width_ = c.wcwidth(cp);
- const cp_width = @intCast(u32, if (cp_width_ < 0) 1 else cp_width_);
+ const cp_width: u32 = @intCast(if (cp_width_ < 0) 0 else cp_width_);
const cp_len = std.unicode.utf8CodepointSequenceLength(cp) catch unreachable;
total_width += cp_width;
- if (prefix_width + cp_width <= @divFloor(max_width-1, 2)-1) {
+ if (!prefix_done and prefix_width + cp_width <= @divFloor(max_width-1, 2)-1) {
prefix_width += cp_width;
prefix_end += cp_len;
- continue;
- }
+ } else
+ prefix_done = true;
}
if (total_width <= max_width) return in;
@@ -154,7 +157,7 @@ pub fn shorten(in: [:0]const u8, max_width: u32) [:0] const u8 {
it = std.unicode.Utf8View.initUnchecked(in[prefix_end..]).iterator();
while (it.nextCodepoint()) |cp| {
const cp_width_ = c.wcwidth(cp);
- const cp_width = @intCast(u32, if (cp_width_ < 0) 1 else cp_width_);
+ const cp_width: u32 = @intCast(if (cp_width_ < 0) 0 else cp_width_);
const cp_len = std.unicode.utf8CodepointSequenceLength(cp) catch unreachable;
start_width += cp_width;
start_len += cp_len;
@@ -163,7 +166,7 @@ pub fn shorten(in: [:0]const u8, max_width: u32) [:0] const u8 {
break;
}
}
- return arrayListBufZ(&shorten_buf);
+ return util.arrayListBufZ(&shorten_buf);
}
fn shortenTest(in: [:0]const u8, max_width: u32, out: [:0]const u8) !void {
@@ -185,99 +188,109 @@ test "shorten" {
try t("AaBCDEFGH", 8, "A...H"); // could optimize this, but w/e
try t("ABCDEFGaH", 8, "A...aH");
try t("ABCDEFGH", 15, "ABC...FGH");
+ try t("❤︎a❤︎a❤︎a", 5, "❤︎...︎a"); // Variation selectors; not great, there's an additional U+FE0E before 'a'.
+ try t("ą́ą́ą́ą́ą́ą́", 5, "ą́...̨́ą́"); // Combining marks, similarly bad.
}
-// ncurses_refs.c
-extern fn ncdu_acs_ulcorner() c.chtype;
-extern fn ncdu_acs_llcorner() c.chtype;
-extern fn ncdu_acs_urcorner() c.chtype;
-extern fn ncdu_acs_lrcorner() c.chtype;
-extern fn ncdu_acs_hline() c.chtype;
-extern fn ncdu_acs_vline() c.chtype;
-extern fn ncdu_init_pair(idx: c_short, fg: c_short, bg: c_short) void;
-
const StyleAttr = struct { fg: i16, bg: i16, attr: u32 };
const StyleDef = struct {
name: []const u8,
off: StyleAttr,
dark: StyleAttr,
+ darkbg: StyleAttr,
fn style(self: *const @This()) StyleAttr {
return switch (main.config.ui_color) {
.off => self.off,
.dark => self.dark,
+ .darkbg => self.darkbg,
};
}
};
const styles = [_]StyleDef{
- .{ .name = "default",
- .off = .{ .fg = -1, .bg = -1, .attr = 0 },
- .dark = .{ .fg = -1, .bg = -1, .attr = 0 } },
- .{ .name = "bold",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
- .dark = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD } },
- .{ .name = "bold_hd",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD|c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_BLACK, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } },
- .{ .name = "box_title",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
- .dark = .{ .fg = c.COLOR_BLUE, .bg = -1, .attr = c.A_BOLD } },
- .{ .name = "hd", // header + footer
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_BLACK, .bg = c.COLOR_CYAN, .attr = 0 } },
- .{ .name = "sel",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_WHITE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } },
- .{ .name = "num",
- .off = .{ .fg = -1, .bg = -1, .attr = 0 },
- .dark = .{ .fg = c.COLOR_YELLOW, .bg = -1, .attr = c.A_BOLD } },
- .{ .name = "num_hd",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } },
- .{ .name = "num_sel",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } },
- .{ .name = "key",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
- .dark = .{ .fg = c.COLOR_YELLOW, .bg = -1, .attr = c.A_BOLD } },
- .{ .name = "key_hd",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD|c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } },
- .{ .name = "dir",
- .off = .{ .fg = -1, .bg = -1, .attr = 0 },
- .dark = .{ .fg = c.COLOR_BLUE, .bg = -1, .attr = c.A_BOLD } },
- .{ .name = "dir_sel",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_BLUE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } },
- .{ .name = "flag",
- .off = .{ .fg = -1, .bg = -1, .attr = 0 },
- .dark = .{ .fg = c.COLOR_RED, .bg = -1, .attr = 0 } },
- .{ .name = "flag_sel",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_RED, .bg = c.COLOR_GREEN, .attr = 0 } },
- .{ .name = "graph",
- .off = .{ .fg = -1, .bg = -1, .attr = 0 },
- .dark = .{ .fg = c.COLOR_MAGENTA, .bg = -1, .attr = 0 } },
- .{ .name = "graph_sel",
- .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
- .dark = .{ .fg = c.COLOR_MAGENTA, .bg = c.COLOR_GREEN, .attr = 0 } },
+ .{ .name = "default",
+ .off = .{ .fg = -1, .bg = -1, .attr = 0 },
+ .dark = .{ .fg = -1, .bg = -1, .attr = 0 },
+ .darkbg = .{ .fg = c.COLOR_WHITE, .bg = c.COLOR_BLACK, .attr = 0 } },
+ .{ .name = "bold",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
+ .dark = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_WHITE, .bg = c.COLOR_BLACK, .attr = c.A_BOLD } },
+ .{ .name = "bold_hd",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD|c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_BLACK, .bg = c.COLOR_CYAN, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_BLACK, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } },
+ .{ .name = "box_title",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
+ .dark = .{ .fg = c.COLOR_BLUE, .bg = -1, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_BLUE, .bg = c.COLOR_BLACK, .attr = c.A_BOLD } },
+ .{ .name = "hd", // header + footer
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_BLACK, .bg = c.COLOR_CYAN, .attr = 0 },
+ .darkbg = .{ .fg = c.COLOR_BLACK, .bg = c.COLOR_CYAN, .attr = 0 } },
+ .{ .name = "sel",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_WHITE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_WHITE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } },
+ .{ .name = "num",
+ .off = .{ .fg = -1, .bg = -1, .attr = 0 },
+ .dark = .{ .fg = c.COLOR_YELLOW, .bg = -1, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_BLACK, .attr = c.A_BOLD } },
+ .{ .name = "num_hd",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } },
+ .{ .name = "num_sel",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_GREEN, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } },
+ .{ .name = "key",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD },
+ .dark = .{ .fg = c.COLOR_YELLOW, .bg = -1, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_BLACK, .attr = c.A_BOLD } },
+ .{ .name = "key_hd",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD|c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } },
+ .{ .name = "dir",
+ .off = .{ .fg = -1, .bg = -1, .attr = 0 },
+ .dark = .{ .fg = c.COLOR_BLUE, .bg = -1, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_BLUE, .bg = c.COLOR_BLACK, .attr = c.A_BOLD } },
+ .{ .name = "dir_sel",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_BLUE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD },
+ .darkbg = .{ .fg = c.COLOR_BLUE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } },
+ .{ .name = "flag",
+ .off = .{ .fg = -1, .bg = -1, .attr = 0 },
+ .dark = .{ .fg = c.COLOR_RED, .bg = -1, .attr = 0 },
+ .darkbg = .{ .fg = c.COLOR_RED, .bg = c.COLOR_BLACK, .attr = 0 } },
+ .{ .name = "flag_sel",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_RED, .bg = c.COLOR_GREEN, .attr = 0 },
+ .darkbg = .{ .fg = c.COLOR_RED, .bg = c.COLOR_GREEN, .attr = 0 } },
+ .{ .name = "graph",
+ .off = .{ .fg = -1, .bg = -1, .attr = 0 },
+ .dark = .{ .fg = c.COLOR_MAGENTA, .bg = -1, .attr = 0 },
+ .darkbg = .{ .fg = c.COLOR_MAGENTA, .bg = c.COLOR_BLACK, .attr = 0 } },
+ .{ .name = "graph_sel",
+ .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE },
+ .dark = .{ .fg = c.COLOR_MAGENTA, .bg = c.COLOR_GREEN, .attr = 0 },
+ .darkbg = .{ .fg = c.COLOR_MAGENTA, .bg = c.COLOR_GREEN, .attr = 0 } },
};
pub const Style = lbl: {
- var fields: [styles.len]std.builtin.TypeInfo.EnumField = undefined;
- var decls = [_]std.builtin.TypeInfo.Declaration{};
- inline for (styles) |s, i| {
- fields[i] = .{
+ comptime var fields: [styles.len]std.builtin.Type.EnumField = undefined;
+ inline for (&fields, styles, 0..) |*field, s, i| {
+ field.* = .{
.name = s.name,
.value = i,
};
}
break :lbl @Type(.{
.Enum = .{
- .layout = .Auto,
.tag_type = u8,
.fields = &fields,
- .decls = &decls,
+ .decls = &[_]std.builtin.Type.Declaration{},
.is_exhaustive = true,
}
});
@@ -314,23 +327,24 @@ pub const Bg = enum {
fn updateSize() void {
// getmax[yx] macros are marked as "legacy", but Zig can't deal with the "proper" getmaxyx macro.
- rows = @intCast(u32, c.getmaxy(c.stdscr));
- cols = @intCast(u32, c.getmaxx(c.stdscr));
+ rows = @intCast(c.getmaxy(c.stdscr));
+ cols = @intCast(c.getmaxx(c.stdscr));
}
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.io.getStdErr().write("\x1b[J") catch {};
+ const stderr = std.io.getStdErr();
+ stderr.writeAll("\x1b[J") catch {};
}
pub fn init() void {
if (inited) return;
clearScr();
if (main.config.nc_tty) {
- var tty = c.fopen("/dev/tty", "r+");
- if (tty == null) die("Error opening /dev/tty: {s}.\n", .{ c.strerror(std.c.getErrno(-1)) });
- var term = c.newterm(null, tty, tty);
+ const tty = c.fopen("/dev/tty", "r+");
+ if (tty == null) die("Error opening /dev/tty: {s}.\n", .{ c.strerror(@intFromEnum(std.c.getErrno(-1))) });
+ const term = c.newterm(null, tty, tty);
if (term == null) die("Error initializing ncurses.\n", .{});
_ = c.set_term(term);
} else {
@@ -344,8 +358,8 @@ pub fn init() void {
_ = c.start_color();
_ = c.use_default_colors();
- for (styles) |s, i| _ = ncdu_init_pair(@intCast(i16, i+1), s.style().fg, s.style().bg);
-
+ for (styles, 0..) |s, i| _ = c.init_pair(@as(i16, @intCast(i+1)), s.style().fg, s.style().bg);
+ _ = c.bkgd(@intCast(c.COLOR_PAIR(@intFromEnum(Style.default)+1)));
inited = true;
}
@@ -361,18 +375,18 @@ pub fn deinit() void {
}
pub fn style(s: Style) void {
- _ = c.attr_set(styles[@enumToInt(s)].style().attr, @enumToInt(s)+1, null);
+ _ = c.attr_set(styles[@intFromEnum(s)].style().attr, @intFromEnum(s)+1, null);
}
pub fn move(y: u32, x: u32) void {
- _ = c.move(@intCast(i32, y), @intCast(i32, x));
+ _ = c.move(@as(i32, @intCast(y)), @as(i32, @intCast(x)));
}
// Wraps to the next line if the text overflows, not sure how to disable that.
// (Well, addchstr() does that, but not entirely sure I want to go that way.
// Does that even work with UTF-8? Or do I really need to go wchar madness?)
pub fn addstr(s: [:0]const u8) void {
- _ = c.addstr(s);
+ _ = c.addstr(s.ptr);
}
// Not to be used for strings that may end up >256 bytes.
@@ -396,7 +410,7 @@ pub const FmtSize = struct {
pub fn fmt(v: u64) @This() {
var r: @This() = undefined;
- var f = @intToFloat(f32, v);
+ var f: f32 = @floatFromInt(v);
if (main.config.si) {
if(f < 1000.0) { r.unit = " B"; }
else if(f < 1e6) { r.unit = " KB"; f /= 1e3; }
@@ -420,7 +434,7 @@ pub const FmtSize = struct {
}
pub fn num(self: *const @This()) [:0]const u8 {
- return std.mem.spanZ(&self.buf);
+ return std.mem.sliceTo(&self.buf, 0);
}
};
@@ -441,7 +455,7 @@ pub fn addnum(bg: Bg, v: u64) void {
const s = std.fmt.bufPrint(&buf, "{d}", .{v}) catch unreachable;
var f: [64:0]u8 = undefined;
var i: usize = 0;
- for (s) |digit, n| {
+ for (s, 0..) |digit, n| {
if (n != 0 and (s.len - n) % 3 == 0) {
for (main.config.thousands_sep) |ch| {
f[i] = ch;
@@ -459,14 +473,14 @@ pub fn addnum(bg: Bg, v: u64) void {
// Print a file mode, takes 10 columns
pub fn addmode(mode: u32) void {
- addch(switch (mode & std.os.S_IFMT) {
- std.os.S_IFDIR => 'd',
- std.os.S_IFREG => '-',
- std.os.S_IFLNK => 'l',
- std.os.S_IFIFO => 'p',
- std.os.S_IFSOCK => 's',
- std.os.S_IFCHR => 'c',
- std.os.S_IFBLK => 'b',
+ addch(switch (mode & std.os.S.IFMT) {
+ std.os.S.IFDIR => 'd',
+ std.os.S.IFREG => '-',
+ std.os.S.IFLNK => 'l',
+ std.os.S.IFIFO => 'p',
+ std.os.S.IFSOCK => 's',
+ std.os.S.IFCHR => 'c',
+ std.os.S.IFBLK => 'b',
else => '?'
});
addch(if (mode & 0o400 > 0) 'r' else '-');
@@ -477,12 +491,12 @@ pub fn addmode(mode: u32) void {
addch(if (mode & 0o2000 > 0) 's' else if (mode & 0o010 > 0) @as(u7, 'x') else '-');
addch(if (mode & 0o004 > 0) 'r' else '-');
addch(if (mode & 0o002 > 0) 'w' else '-');
- addch(if (mode & 0o1000 > 0) (if (std.os.S_ISDIR(mode)) @as(u7, 't') else 'T') else if (mode & 0o001 > 0) @as(u7, 'x') else '-');
+ addch(if (mode & 0o1000 > 0) (if (std.os.S.ISDIR(mode)) @as(u7, 't') else 'T') else if (mode & 0o001 > 0) @as(u7, 'x') else '-');
}
// Print a timestamp, takes 25 columns
pub fn addts(bg: Bg, ts: u64) void {
- const t = castClamp(c.time_t, ts);
+ const t = util.castClamp(c.time_t, ts);
var buf: [32:0]u8 = undefined;
const len = c.strftime(&buf, buf.len, "%Y-%m-%d %H:%M:%S %z", c.localtime(&t));
if (len > 0) {
@@ -495,7 +509,7 @@ pub fn addts(bg: Bg, ts: u64) void {
}
pub fn hline(ch: c.chtype, len: u32) void {
- _ = c.hline(ch, @intCast(i32, len));
+ _ = c.hline(ch, @as(i32, @intCast(len)));
}
// Draws a bordered box in the center of the screen.
@@ -507,26 +521,27 @@ pub const Box = struct {
pub fn create(height: u32, width: u32, title: [:0]const u8) Self {
const s = Self{
- .start_row = saturateSub(rows>>1, height>>1),
- .start_col = saturateSub(cols>>1, width>>1),
+ .start_row = (rows>>1) -| (height>>1),
+ .start_col = (cols>>1) -| (width>>1),
};
style(.default);
if (width < 6 or height < 3) return s;
- const ulcorner = ncdu_acs_ulcorner();
- const llcorner = ncdu_acs_llcorner();
- const urcorner = ncdu_acs_urcorner();
- const lrcorner = ncdu_acs_lrcorner();
- const acs_hline = ncdu_acs_hline();
- const acs_vline = ncdu_acs_vline();
+ const acs_map = @extern(*[128]c.chtype, .{ .name = "acs_map" });
+ const ulcorner = acs_map['l'];
+ const llcorner = acs_map['m'];
+ const urcorner = acs_map['k'];
+ const lrcorner = acs_map['j'];
+ const acs_hline = acs_map['q'];
+ const acs_vline = acs_map['x'];
var i: u32 = 0;
while (i < height) : (i += 1) {
s.move(i, 0);
- addch(if (i == 0) ulcorner else if (i == height-1) llcorner else acs_hline);
- hline(if (i == 0 or i == height-1) acs_vline else ' ', width-2);
+ addch(if (i == 0) ulcorner else if (i == height-1) llcorner else acs_vline);
+ hline(if (i == 0 or i == height-1) acs_hline else ' ', width-2);
s.move(i, width-1);
- addch(if (i == 0) urcorner else if (i == height-1) lrcorner else acs_hline);
+ addch(if (i == 0) urcorner else if (i == height-1) lrcorner else acs_vline);
}
s.move(0, 3);
@@ -563,20 +578,19 @@ pub fn getch(block: bool) i32 {
// In non-blocking mode, we can only assume that ERR means "no input yet".
// In blocking mode, give it 100 tries with a 10ms delay in between,
// then just give up and die to avoid an infinite loop and unresponsive program.
- var attempts: u8 = 0;
- while (attempts < 100) : (attempts += 1) {
- var ch = c.getch();
+ for (0..100) |_| {
+ const ch = c.getch();
if (ch == c.KEY_RESIZE) {
updateSize();
return -1;
}
if (ch == c.ERR) {
if (!block) return 0;
- std.os.nanosleep(0, 10*std.time.ns_per_ms);
+ std.time.sleep(10*std.time.ns_per_ms);
continue;
}
return ch;
}
die("Error reading keyboard input, assuming TTY has been lost.\n(Potentially nonsensical error message: {s})\n",
- .{ c.strerror(std.c.getErrno(-1)) });
+ .{ c.strerror(@intFromEnum(std.c.getErrno(-1))) });
}
diff --git a/src/util.zig b/src/util.zig
index 4c5c8a9..f0fc1d4 100644
--- a/src/util.zig
+++ b/src/util.zig
@@ -1,18 +1,8 @@
-// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
+// SPDX-FileCopyrightText: 2021-2023 Yoran Heling <projects@yorhel.nl>
// SPDX-License-Identifier: MIT
const std = @import("std");
-pub fn saturateAdd(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
- std.debug.assert(@typeInfo(@TypeOf(a)).Int.signedness == .unsigned);
- return std.math.add(@TypeOf(a), a, b) catch std.math.maxInt(@TypeOf(a));
-}
-
-pub fn saturateSub(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
- std.debug.assert(@typeInfo(@TypeOf(a)).Int.signedness == .unsigned);
- return std.math.sub(@TypeOf(a), a, b) catch std.math.minInt(@TypeOf(a));
-}
-
// Cast any integer type to the target type, clamping the value to the supported maximum if necessary.
pub fn castClamp(comptime T: type, x: anytype) T {
// (adapted from std.math.cast)
@@ -21,7 +11,7 @@ pub fn castClamp(comptime T: type, x: anytype) T {
} else if (std.math.minInt(@TypeOf(x)) < std.math.minInt(T) and x < std.math.minInt(T)) {
return std.math.minInt(T);
} else {
- return @intCast(T, x);
+ return @intCast(x);
}
}
@@ -29,13 +19,13 @@ pub fn castClamp(comptime T: type, x: anytype) T {
pub fn castTruncate(comptime T: type, x: anytype) T {
const Ti = @typeInfo(T).Int;
const Xi = @typeInfo(@TypeOf(x)).Int;
- const nx = if (Xi.signedness != Ti.signedness) @bitCast(std.meta.Int(Ti.signedness, Xi.bits), x) else x;
- return if (Xi.bits > Ti.bits) @truncate(T, nx) else nx;
+ const nx: std.meta.Int(Ti.signedness, Xi.bits) = @bitCast(x);
+ return if (Xi.bits > Ti.bits) @truncate(nx) else nx;
}
// Multiplies by 512, saturating.
pub fn blocksToSize(b: u64) u64 {
- return if (b & 0xFF80000000000000 > 0) std.math.maxInt(u64) else b << 9;
+ return b *| 512;
}
// Ensure the given arraylist buffer gets zero-terminated and returns a slice
@@ -46,3 +36,106 @@ pub fn arrayListBufZ(buf: *std.ArrayList(u8)) [:0]const u8 {
defer buf.items.len -= 1;
return buf.items[0..buf.items.len-1:0];
}
+
+// Straightforward Zig port of strnatcmp() from https://github.com/sourcefrog/natsort/
+// (Requiring nul-terminated strings is ugly, but we've got them anyway and it does simplify the code)
+pub fn strnatcmp(a: [:0]const u8, b: [:0]const u8) std.math.Order {
+ var ai: usize = 0;
+ var bi: usize = 0;
+ const isDigit = std.ascii.isDigit;
+ while (true) {
+ while (std.ascii.isWhitespace(a[ai])) ai += 1;
+ while (std.ascii.isWhitespace(b[bi])) bi += 1;
+
+ if (isDigit(a[ai]) and isDigit(b[bi])) {
+ if (a[ai] == '0' or b[bi] == '0') { // compare_left
+ while (true) {
+ if (!isDigit(a[ai]) and !isDigit(b[bi])) break;
+ if (!isDigit(a[ai])) return .lt;
+ if (!isDigit(b[bi])) return .gt;
+ if (a[ai] < b[bi]) return .lt;
+ if (a[ai] > b[bi]) return .gt;
+ ai += 1;
+ bi += 1;
+ }
+ } else { // compare_right - for right-aligned numbers
+ var bias = std.math.Order.eq;
+ while (true) {
+ if (!isDigit(a[ai]) and !isDigit(b[bi])) {
+ if (bias != .eq or (a[ai] == 0 and b[bi] == 0)) return bias
+ else break;
+ }
+ if (!isDigit(a[ai])) return .lt;
+ if (!isDigit(b[bi])) return .gt;
+ if (bias == .eq) {
+ if (a[ai] < b[bi]) bias = .lt;
+ if (a[ai] > b[bi]) bias = .gt;
+ }
+ ai += 1;
+ bi += 1;
+ }
+ }
+ }
+ if (a[ai] == 0 and b[bi] == 0) return .eq;
+ if (a[ai] < b[bi]) return .lt;
+ if (a[ai] > b[bi]) return .gt;
+ ai += 1;
+ bi += 1;
+ }
+}
+
+test "strnatcmp" {
+ // Test strings from https://github.com/sourcefrog/natsort/
+ // Includes sorted-words, sorted-dates and sorted-fractions.
+ const w = [_][:0]const u8{
+ "1-02",
+ "1-2",
+ "1-20",
+ "1.002.01",
+ "1.002.03",
+ "1.002.08",
+ "1.009.02",
+ "1.009.10",
+ "1.009.20",
+ "1.010.12",
+ "1.011.02",
+ "10-20",
+ "1999-3-3",
+ "1999-12-25",
+ "2000-1-2",
+ "2000-1-10",
+ "2000-3-23",
+ "fred",
+ "jane",
+ "pic01",
+ "pic02",
+ "pic02a",
+ "pic02000",
+ "pic05",
+ "pic2",
+ "pic3",
+ "pic4",
+ "pic 4 else",
+ "pic 5",
+ "pic 5 ",
+ "pic 5 something",
+ "pic 6",
+ "pic 7",
+ "pic100",
+ "pic100a",
+ "pic120",
+ "pic121",
+ "tom",
+ "x2-g8",
+ "x2-y08",
+ "x2-y7",
+ "x8-y8",
+ };
+ // Test each string against each other string, simple and thorough.
+ const eq = std.testing.expectEqual;
+ for (0..w.len) |i| {
+ try eq(strnatcmp(w[i], w[i]), .eq);
+ for (0..i) |j| try eq(strnatcmp(w[i], w[j]), .gt);
+ for (i+1..w.len) |j| try eq(strnatcmp(w[i], w[j]), .lt);
+ }
+}