This is the Austral rewrite of fetch.c.
The goal here was not to make some cute alternate version. The goal was to get as close as possible to the real bfetch behavior, output, and style while keeping the fetch logic in Austral and rebuilding the missing low-level pieces ourselves.
Right now this version matches the C binary on the tested paths for:
- default output
--help--version--gentoo--bedrock--cachyos
It also keeps the same buffered output model. Everything is assembled in memory and written out in one shot.
bfetchaust/ is its own little subproject.
The high-level split is:
src/Fetch.aum- argument parsing
- mode selection
- exact fetch rendering
- exact help and version output
runtime/BfetchAust.Posix.auiandruntime/BfetchAust.Posix.aum- the raw OS boundary
- file, dir, env, uname, process, and libc imports that Austral does not ship with
runtime/BfetchAust.FastIO.auiandruntime/BfetchAust.FastIO.aum- byte buffer code
- buffered writes
- fast file reads
- a few small formatting helpers
runtime/BfetchAust.Sys.auiandruntime/BfetchAust.Sys.aum- all the actual probes for distro, kernel, uptime, WM, terminal, shell, packages, CPU, GPU, and memory
bfetchaust.c- generated C output from the Austral compiler
- not hand-written, not something you are meant to edit
Austral lives here:
This repo currently assumes you have that compiler checked out next to bfetch in ../austral and that you built austral.exe there. If your compiler lives somewhere else, edit AUSTRAL_BIN in bfetchaust/Makefile.
From the repo root:
eval $(opam env --switch=default --set-switch)
make -C bfetchaustTypecheck only:
eval $(opam env --switch=default --set-switch)
make -C bfetchaust tcRun it:
./bfetchaust/bfetchaust
./bfetchaust/bfetchaust --help
./bfetchaust/bfetchaust --cachyosInstall it somewhere on your path if you want:
install -Dm755 bfetchaust/bfetchaust ~/.local/bin/bfetchaustOr system-wide:
sudo install -Dm755 bfetchaust/bfetchaust /usr/local/bin/bfetchaustThe C version is just allowed to be direct.
It opens files, reads directories, uses mmap, pulls env vars, parses /proc, walks /sys/class/drm, pokes cpuid, and writes the final buffer. That is the whole point of fetch.c. It is a very small binary with basically no ceremony between the idea and the machine.
Austral is not like that.
Austral gives you strong rules about ownership and mutation, which is good, but it does not give you the Linux-facing standard library surface this project needs. So if you want to write a real system tool in it, you have to build that surface first.
That meant doing all of this before the port was even interesting:
- file reads for things like
/etc/os-release,/proc/cpuinfo,/proc/meminfo,/proc/uptime - directory walking for package managers
- environment access for
TERM,TERM_PROGRAM,SHELL,XDG_CURRENT_DESKTOP,DESKTOP_SESSION - process inspection for parent PID and terminal detection
- sysfs probing for DRM devices and GPU IDs
- uname access for kernel release
- one-buffer output helpers so the renderer could stay close to the C version
So the actual rewrite was not just "port fetch.c". It was "first build the missing chunk of a tiny Linux stdlib, then port fetch.c on top of it which nearly killed me".
I did not want this thing cheating, no.
So the rules were:
- no shelling out, same as fetch.c
- no subprocess-based probing
- no fake wrappers that hide the real work
- keep the real fetch logic in Austral
- match the C output as closely as possible instead of settling for "close enough"
The one place where reality still matters is the OS boundary. Austral without that boundary cannot even open a file. So the low-level imports live in runtime/BfetchAust.Posix.*, and the rest of the actual program logic stays above that in Austral.
Some parts ported cleanly. Some absolutely did not.
In C, fetch.c just calls libc and the kernel-facing interfaces it needs.
In Austral, I had to build:
- a byte buffer type for all formatted output and small string work
- file read helpers
- pointer-based path helpers
- directory iteration wrappers
- env wrappers
- formatting helpers for decimal and hex output
- probe helpers for
/procand/sys
Without that, there is no port.
The initial raw open path looked like the obvious thing to do. In practice it was the wrong path in this build, so the file runtime got rewritten around fopen, fread, and fclose instead. That made the I/O layer stable enough to keep moving.
The simple symlink-based idea was not reliable enough here. Reading /proc/<pid>/exe was not the right fit for the actual process chain in this shit.
So terminal detection was switched to /proc/<pid>/comm plus /proc/<pid>/stat, walking the parent process chain until it found the terminal instead of the shell wrapper sitting in front of it.
The point of bfetch is that it does not shell out, so the Austral version could not either.
That means package counting is still done natively:
- Pacman by directory counting
- Flatpak by filesystem inspection
- Snap by filesystem inspection
- DPKG by directory counting
- Nix by direct manifest scanning
- Gentoo by walking
var/db/pkg
The Nix path was especially annoying because the original C version leans on mmap substring search. The Austral runtime ended up doing deterministic byte scanning over the manifest files yah now so it can keep up.
Getting the vendor and device IDs is easy enough.
Matching the C output exactly is where the pain starts. The port had to reproduce the pci.ids lookup rules, the bracket stripping, and the vendor prefix heuristic so NVIDIA GeForce RTX 3070 Ti came out the same way instead of almost the same way, still don't know about vender revisions.
The C binary prints Usage: ./bfetch [OPTIONS].
That is not argv[0]. That is just what it prints.
So the Austral version prints the same literal string because the point here was parity, not doing what looks cleaner.
Austral is interesting because it is not trying to be Rust, and it is definitely not C with a nicer parser.
The big thing it cares about is linearity and regions.
That changes how you write code:
- mutation is explicit
- borrowing is explicit
- ownership mistakes get caught early
- branch cleanup has to be structurally consistent
- you cannot casually pass mutable state around and hope for shit to be sweet
That part is good.
The rough part is that you feel the lack of batteries constantly.
In Rust, if you want to build a tool like this, the language is strict but the ecosystem is huge. You have crates, helpers, wrappers, and a mature library story.
In C, nothing stops you, so you can go straight at the machine and be done fast if you know what you are doing.
Austral sits in a weird middle spot:
- stricter than C by a lot
- smaller and lower-level than Rust in practice
- much less ergonomic than mainstream memory-safe languages
- very little stdlib for real Linux systems work
So writing this felt like doing systems programming with one hand tied behind my back and another in my ass, but in a way that was still interesting because the language rules actually force discipline, and im a little kinkly like that.
These were the repeated gotchas during the rewrite:
- hex literals and some surface syntax that look fine in other languages were not accepted the way I first wrote them
- chained boolean expressions sometimes had to be flattened into explicit nested control flow
- region mismatches show up fast if one helper accidentally ties input and output to the same region when they should not be
- linear cleanup rules mean a casual early return can turn into a type error if one branch consumes a value and another does not
- a lot of simple C string work becomes explicit byte-buffer work
That sounds like complaining, but it is really the shape of the language. Austral wants you to make ownership and mutation obvious. If you do not, it pushes back.
The generated C file is useful if you want to understand what Austral is actually doing.
A few things jump out to me:
- the runtime support is right at the top
- options and other small algebraic types get monomorphized into generated structs and tags
- foreign imports become plain
externdeclarations in the generated C - the final output is a lot bigger and more mechanical than the source Austral code
So if you open bfetchaust.c, what you see is not "nice hand-written C". You see the compiler lowering Austral's model into a big C translation unit.
That file also made one performance issue obvious. Austral's default bin build path was invoking cc without real optimization flags, which was a bad fit for this project. That is why the Makefile here does not use the plain default binary mode anymore. It emits C first, then compiles that C with:
cc -O3 -march=native -fwrapv -Wno-builtin-declaration-mismatchThat one change cut a lot of the gap in peformance.
This was never going to beat the C version without a fight.
Current rough numbers on my machine after switching to the optimized generated-C build:
./bfetch: about1.9 ms./bfetchaust/bfetchaust: about3.2 ms
So the Austral version is still slower.
That matters, and I am not pretending otherwise and its unacceptable i know.
The good news is that the big stupid gap from the unoptimized default build path is gone. The bad news is that there is still real overhead left after that.
Right now the port is in a solid state(like an nvme):
- output parity is there on the tested paths
- no shelling out
- fetch logic is in Austral
- runtime support is split into sane modules
- the build is stable
- the generated C path is compiled with actual optimization flags
What is still not perfect:
- it is still slower than the C binary
- the runtime layer exists because Austral does not ship the Linux-facing stdlib this shi needs
- there is still more room to chase performance if I want to push harder on
mmapand other C-style shortcuts in the runtime
Start here:
src/Fetch.aumruntime/BfetchAust.FastIO.aumruntime/BfetchAust.Sys.aumruntime/BfetchAust.Posix.aui
That gets you the whole picture fast.