No description
  • Go 81.3%
  • Nix 9.8%
  • Shell 5.4%
  • NSIS 3.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-20 23:16:21 +02:00
cmd/file-utils Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
data Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
installer Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
internal Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
vendor Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
windows Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
.gitignore Ignore the new binary names 2026-09-20 23:13:48 +02:00
alejandra.toml files 2026-09-20 23:16:00 +02:00
flake.lock stuff 2026-09-09 14:36:50 +01:00
flake.nix files 2026-09-20 23:16:00 +02:00
go.mod Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
go.sum Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00
README.md Rebuild pdfsplit as file-utils: a multi-tool GTK 4 app 2026-09-20 22:31:43 +02:00

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
PDF Split one into many every page becomes its own file
PDF 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:

  1. fetches those 99 packages and unpacks them into a sysroot,
  2. cross-compiles with pkgsCross.mingwW64 and CGO_ENABLED=1, pointing pkg-config at the sysroot,
  3. 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,
  4. 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.dll with the media backend linked in rather than as a loadable module, so GTK will not load without it.
  • mcfgthreads must be on the link path. nixpkgs builds its mingw gcc against it, and the final link fails on -lmcfgthread otherwise.

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.