- Go 81.3%
- Nix 9.8%
- Shell 5.4%
- NSIS 3.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| cmd/file-utils | ||
| data | ||
| installer | ||
| internal | ||
| vendor | ||
| windows | ||
| .gitignore | ||
| alejandra.toml | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
file-utils
A small box of file tools in one GTK 4 window: a sidebar of categories, a toolbar of tools across the top of each, and a page per tool. Results always land in a new timestamped folder in your Downloads.
Everything the window offers is also a subcommand, because both are generated from the same declaration.
| category | tool | what it does |
|---|---|---|
| Split one into many | every page becomes its own file | |
| Join many into one | concatenate, in the order you arrange them | |
| Images | Convert | any of JPEG, PNG, WebP, HEIC to any other |
| Images | Compress | shrink in place, three strengths |
| Audio | — | nothing yet; the page is an empty state |
Two builds from one source tree: a native Linux binary and a Windows .exe
with an installer. Both are built from Linux, and both run the same GTK 4 and
libadwaita interface.
Using it
file-utils # open the window
file-utils report.pdf # open it with that file loaded
file-utils pdf split report.pdf # or do it headlessly
file-utils pdf join intro.pdf body.pdf # joined in argument order
file-utils images convert -to webp *.heic
file-utils images compress -level strong photos/*.jpg
file-utils -h # the whole tool tree
In the window, drop files anywhere on a tool's page or use Add Files. The Join page numbers its list and gives each row up and down buttons, because that order is the page order of the result.
The last line the CLI prints is always the output folder, so it pipes:
out=$(file-utils pdf split report.pdf | tail -1)
Shared flags: -o DIR puts the output folder somewhere other than Downloads,
-open shows the result when it finishes, -quiet prints only the folder.
Images
Conversion goes between JPEG, PNG, WebP and HEIC in any direction. EXIF orientation is applied to the pixels on the way in, so a phone photo comes out upright rather than carrying a tag the next program might ignore.
Compression keeps the format and offers three strengths:
| JPEG | WebP | HEIC | PNG | |
|---|---|---|---|---|
| minimal | q85 | q85 | q85 | lossless re-encode |
| medium | q70 | q70 | q65 | 256 colours |
| strong | q55 | q55 | q50 | 128 colours |
PNG is the odd one out: it has no quality dial, so the lossy levels reduce the palette instead, which is the only thing that meaningfully shrinks a PNG. The mapping is nearest-colour rather than dithered — dithering hides the banding but replaces it with per-pixel noise, and PNG compresses by predicting each pixel from its neighbours. Measured both ways, dithering made files six to eight times larger.
If a compressed result comes out bigger than its input, the original is kept and the run says so.
Building
nix build # -> result/bin/file-utils (Linux)
nix build .#windows-installer # -> result/file-utils-0.3.0-setup.exe (Windows)
nix build .#windows-dist # -> the unpacked Windows tree
nix flake check # the logic tests
nix run # run the Linux build
nix run .#windows # run the Windows build under wine
nix develop # go + gtk4 + libadwaita + mingw + nsis + wine
Inside nix develop:
go build -o file-utils ./cmd/file-utils
go test ./...
How the Windows build works
The Linux side is ordinary: GTK 4 and libadwaita from nixpkgs, cgo, and
wrapGAppsHook4 to put the icon theme, GSettings schemas and pixbuf loaders
on the binary's path.
Windows is the interesting half. nixpkgs cannot cross-compile GTK 4 to
mingw — the dependency chain breaks on a broken python3-mingw — so the GTK
runtime comes from MSYS2's prebuilt mingw-w64 packages instead, pinned by
SHA256 in windows/msys2.lock.json. The build then:
- fetches those 99 packages and unpacks them into a sysroot,
- cross-compiles with
pkgsCross.mingwW64andCGO_ENABLED=1, pointingpkg-configat the sysroot, - walks the exe's import table to copy in only the DLLs it actually needs, adds the gdk-pixbuf loaders (which are dlopened, so the walk cannot see them), compiles the GSettings schemas and copies the Adwaita icon theme,
- wraps the lot in an NSIS installer.
That comes out around 80 MB installed. Two things are worth knowing if you touch it:
- GStreamer cannot be dropped. It looks like dead weight for an app that
plays no media, but MSYS2 builds
libgtk-4-1.dllwith the media backend linked in rather than as a loadable module, so GTK will not load without it. mcfgthreadsmust be on the link path. nixpkgs builds its mingw gcc against it, and the final link fails on-lmcfgthreadotherwise.
When a pinned URL starts 404ing — MSYS2 eventually drops old revisions — or you want a newer GTK:
nix run .#update-msys2
It reads the repository database, which carries a SHA256 for every package, so the lock is regenerated without downloading any of them.
Installing on Windows
Run file-utils-0.3.0-setup.exe. It is a per-user install into
%LOCALAPPDATA%\Programs\file-utils — no admin rights, no UAC prompt — with
Start Menu and Desktop shortcuts and a normal Add/Remove Programs entry.
Because the installer is unsigned, SmartScreen will show a "Windows protected your PC" warning the first time: More info -> Run anyway.
The .exe is a GUI-subsystem binary so double-clicking never flashes a
console. Run from cmd/PowerShell with arguments and it borrows that console
to print; with none — a file dropped on the icon — it shows the result folder
instead and reports errors in a message box.
Layout
| path | what |
|---|---|
cmd/file-utils |
entry point: window, files, or subcommand |
internal/tools |
the registry — every tool declared as data |
internal/app |
the GTK 4 window, rendering whatever the registry declares |
internal/cli |
subcommands and flags, generated from the same registry |
internal/pdf |
split and join, via pdfcpu |
internal/img |
codecs, conversion, compression, quantizing, EXIF |
internal/run |
shared result types and the output folder |
internal/platform |
Downloads folder, file manager, console, notifications |
windows/ |
the MSYS2 lock, its updater, and the DLL bundler |
installer/ |
the NSIS script |
vendor/ |
committed Go deps, so the Nix builds are offline |
Adding a tool
Add one entry to internal/tools/registry.go. Declare its title, the
extensions it accepts, any options as Enum or Bool, and a Run function.
The window builds its page — file list, option rows, progress, results — and
the CLI grows a subcommand with matching flags. Neither package needs
touching.
Updating dependencies
nix develop
go get -u ./... && go mod tidy && go mod vendor
vendorHash is not used; the committed vendor/ directory is what the builds
read. gotk4's generated bindings are most of its 34 MB.