Skip to content

Releases: pandasir/gander

v0.6.7

Choose a tag to compare

@github-actions github-actions released this 23 Oct 16:28

htop-style task counters on macOS

The Tasks header now uses htop's compact text layout:

Tasks: 540, 2386 thr, 0 kthr; 3 running

The change fixes the counters behind the text as well as the presentation.
Previously, every process whose task information was unreadable contributed an
assumed thread. On macOS, where ordinary users cannot inspect many system
processes, that inflated the thread count. The sampler now reads
PROC_PIDTASKINFO, as htop does, and sums pti_threadnum only for successful reads.

Running means running threads on macOS, not processes with a running thread.
The header sums pti_numrunning and caps the result at the CPU count. PID 0
(kernel_task) is included in the process list. The Darwin backend reports
0 kthr, matching htop's backend, which does not maintain a separate kernel
thread counter.

The extra permission messages and "running procs" wording are gone. The header
keeps zero-valued fields and uses the existing theme colors for labels,
threads, kernel threads, and running tasks. Narrow headers retain a shorter
summary instead of clipping a word. Linux and FreeBSD supply separate kernel
thread counts for the same display.

Counters are collected from process task information, independently of expanded
thread rows. The regression tests compare counts within a single snapshot:
threads may exit between two samples, so a test that requires the second count
to be greater was not a valid test of duplicate counting.

Validation: full macOS workspace tests, including live task-info reads and
PID 0; header text/color/narrow-width tests; FreeBSD cross-target checking;
clippy with warnings denied.


Install

tar -xzf gander-0.6.7-<platform>.tar.gz
./gander-0.6.7-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux x86_64 (glibc)
x86_64-unknown-linux-musl Linux x86_64 (static, any distro)
aarch64-unknown-linux-gnu Linux arm64 (glibc)
aarch64-unknown-linux-musl Linux arm64 (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.

Debian, Ubuntu

curl -fsSL https://pandasir.github.io/gander/gander.gpg \
  | sudo gpg --dearmor -o /usr/share/keyrings/gander.gpg
echo "deb [signed-by=/usr/share/keyrings/gander.gpg] https://pandasir.github.io/gander stable main" \
  | sudo tee /etc/apt/sources.list.d/gander.list
sudo apt update && sudo apt install gander

Fedora, RHEL, Rocky

sudo rpm --import https://pandasir.github.io/gander/RPM-GPG-KEY-gander
sudo curl -fsSL https://pandasir.github.io/gander/gander.repo \
  -o /etc/yum.repos.d/gander.repo
sudo dnf install gander

The .deb and .rpm files are also attached below for a one-off
apt install ./file.deb; SHA256SUMS covers all of them.

From source: cargo install --path crates/bin (Rust 1.89+).

Full history: commits

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.6.6

Choose a tag to compare

@github-actions github-actions released this 23 Oct 15:20

a 128-core header that is a header

On a machine with many cores the htop layout's header was taller than the
terminal. A 128-core box wanted 65 rows per header column, the header was clamped
to the screen, and the process list — the reason the program exists — got whatever
was left. The fix is not a cap or a scrollbar: it is what htop actually does.

The ladder is htop's

Meter::rows() had no width parameter, so a meter could not say how tall it was
without knowing how wide it would be. It takes the column width now, and
CpuMeter lays its cores out in a grid. meters_impl::cpu_layout is htop's
Settings_defaultMeters, transcribed:

<= 4     one CPU row
5-8      LeftCPUs / RightCPUs, one core per line
9-16     LeftCPUs2 / RightCPUs2, two per line
17-32    LeftCPUs4 / RightCPUs4, four per line
33-128   LeftCPUs8 / RightCPUs8, eight per line
> 128    one CPU row

128 cores now cost nine rows instead of 65, so the whole header is 13. Above 128
htop gives up and shows the average, on the grounds that a header taller than the
screen is not a header; that is copied too.

The geometry inside a meter is htop's as well, not just the row count

Cores fill down a column and then across, each column is (w - ncol) / ncol + 1
cells so there is a one-cell gap between neighbours, and the cells the integer
division drops are handed out one each to the first columns
(CPUMeterCommonDraw). Each cell is drawn by htop's BarMeterMode_draw: a fixed
three-cell caption, [, the bar, ], and the percentage right-aligned inside
the bar. That last part is what makes 0 [48.2] fit in ten cells; without it,
eight cores across a 100-cell column have room for a bar or a number, not both.

The percentage is the only part that gives way in a narrow cell: it drops to an
integer before it drops out, because 48 is a rounded 48.2 while htop's truncation
would show .2 and read as a different number.

Two bugs the change exposed

  • F2 -> Header could not select the grid shapes, and the htoprc importer
    dropped LeftCPUs8 and friends as unknown names — so an htoprc written by
    htop's own setup screen imported as one core per row. cpu_family now parses
    the whole family and is the only place that knows the naming scheme.
  • The setup screen tested "already has a CPU meter" with contains("CPU").
    That happens to work for nine names and would block adding any future CPU
    meter; it asks cpu_family instead.

Deliberately not copied

When a header half is too narrow for even a three-cell caption and two bar
borders per core, gander merges columns and grows the header where htop draws the
fragments. A grid of unidentifiable numbers is not a CPU meter. That needs a half
under 40 cells at eight across — an 80-column terminal.

Two documentation claims were wrong once this landed and are corrected rather
than left standing: LAYOUT.md argued that htop "spends a row per core" (it does
not above eight), and PORTABILITY.md documented the one-row-per-core behaviour
as the design.

Tests: the ladder's boundaries including both sides of 128; the name parser; a
128-core header is 13 rows rather than 66; every core gets a non-overlapping
cell, filled down then across; the aggregate meter is one row on any machine; a
suffixed name in the config is honoured; CPU stays the aggregate. 182 TUI,
34 config, 146 core tests, clippy -D warnings clean.


Install

tar -xzf gander-0.6.6-<platform>.tar.gz
./gander-0.6.6-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux x86_64 (glibc)
x86_64-unknown-linux-musl Linux x86_64 (static, any distro)
aarch64-unknown-linux-gnu Linux arm64 (glibc)
aarch64-unknown-linux-musl Linux arm64 (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.

Debian, Ubuntu

curl -fsSL https://pandasir.github.io/gander/gander.gpg \
  | sudo gpg --dearmor -o /usr/share/keyrings/gander.gpg
echo "deb [signed-by=/usr/share/keyrings/gander.gpg] https://pandasir.github.io/gander stable main" \
  | sudo tee /etc/apt/sources.list.d/gander.list
sudo apt update && sudo apt install gander

Fedora, RHEL, Rocky

sudo rpm --import https://pandasir.github.io/gander/RPM-GPG-KEY-gander
sudo curl -fsSL https://pandasir.github.io/gander/gander.repo \
  -o /etc/yum.repos.d/gander.repo
sudo dnf install gander

The .deb and .rpm files are also attached below for a one-off
apt install ./file.deb; SHA256SUMS covers all of them.

From source: cargo install --path crates/bin (Rust 1.89+).

Full history: commits

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.6.5

Choose a tag to compare

@github-actions github-actions released this 23 Oct 08:11

crates.io metadata that never reached the registry

A patch release with no behaviour change. Its only content is the metadata fixed
in 74e2ba0, which crates.io could not show because a crate's search fields are
frozen at upload time.

What changed on crates.io:

  • keywords lead with htop. They used to be ['monitor','process','system','tui'],
    none of which is the word someone looking for this category types. htop is.
  • gander-bin's description said "Executable entry point for gander", which
    describes the crate's role in the workspace and not the program. It now says
    what it is: a cross-platform interactive process viewer for Linux, macOS and
    FreeBSD.
  • homepage points at the project site rather than back at the repository.

The installed command is still gander; the crates.io package is still
gander-bin, because gander on crates.io is an unrelated crate.

Why this is worth a version of its own: before it, searching crates.io for "htop"
did not surface the package in the first twenty results, in a category where the
packages that do appear are mostly single-platform or low-download. The archive of
0.6.5 is otherwise byte-for-byte the behaviour of 0.6.4 — if you are on 0.6.4 and
installed it from a release archive or a package, there is nothing here to upgrade
for.

Nothing else changed. git diff v0.6.4..v0.6.5 -- crates/ touches only the version
string.


Install

tar -xzf gander-0.6.5-<platform>.tar.gz
./gander-0.6.5-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux x86_64 (glibc)
x86_64-unknown-linux-musl Linux x86_64 (static, any distro)
aarch64-unknown-linux-gnu Linux arm64 (glibc)
aarch64-unknown-linux-musl Linux arm64 (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.

Debian, Ubuntu

curl -fsSL https://pandasir.github.io/gander/gander.gpg \
  | sudo gpg --dearmor -o /usr/share/keyrings/gander.gpg
echo "deb [signed-by=/usr/share/keyrings/gander.gpg] https://pandasir.github.io/gander stable main" \
  | sudo tee /etc/apt/sources.list.d/gander.list
sudo apt update && sudo apt install gander

Fedora, RHEL, Rocky

sudo rpm --import https://pandasir.github.io/gander/RPM-GPG-KEY-gander
sudo curl -fsSL https://pandasir.github.io/gander/gander.repo \
  -o /etc/yum.repos.d/gander.repo
sudo dnf install gander

The .deb and .rpm files are also attached below for a one-off
apt install ./file.deb; SHA256SUMS covers all of them.

From source: cargo install --path crates/bin (Rust 1.89+).

Full history: commits

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.6.4

Choose a tag to compare

@github-actions github-actions released this 22 Oct 11:53

a 128-core machine no longer reports itself as 64 cores

A display bug, not a data bug. snapshot.cpu_count() read all 128 cores
correctly all along — linux_live.rs asserts it against
sysconf(_SC_NPROCESSORS_ONLN) — and then the system column drew 64 of them and
printed "64 cores" next to the grid. The number was the grid's capacity
presented as a fact about the hardware, so there was nothing on screen to suggest
a setting was responsible, and nothing in the docs or in F2 setup to change.

Three limits, each of which hid the others

Config::system_cores_max defaulted to 64, and the label printed the capped
count rather than the machine's. Raising it to 0, which its own doc comment
described as "render every core", changed nothing: the grid was also capped at
16 cells per row times four rows, so 64 glyphs was the ceiling no matter what
the config said. And under layout = "htop" the same machine rendered no
header at all
— draw_column_indices skipped any meter taller than the rows
that were left, and 128 cores want 65 rows per column, so the first meter
failed the test and the entire header went blank.

Fixing any one of the three alone would have left the other two in place, which
is why the report read as "the core count is wrong" rather than as "the grid is
capped".

What the column does now

The grid takes its shape from the terminal: up to 16 cores per row, and as many
rows as the height leaves after reserving room for MEM, SWP, LOAD, TASKS, RUN
and THR. A 128-core box draws eight rows on a 40-row terminal, and the figures
below the grid stay where they were.

The label reports what is actually drawn, and names the total when they differ:

128 cores          everything fits
64/128 cores       a short terminal or a configured cap stopped the grid

A truncated grid is fine. Reporting the truncation as the machine's core count
was the bug, and 64/128 is the difference between a limit and a lie.

system_cores_max is now reachable, and defaults to "all"

Default 0. F2 -> Display cycles 0 / 16 / 32 / 64 / 128, which is how the
setting should have been reachable from the start: a config-file-only value
that silently changes a displayed figure is indistinguishable from a wrong
figure. 0 renders as "all cores" rather than "0 cores", because the sentinel
means the opposite of what it says.

An existing config.toml with system_cores_max = 64 keeps its 64, and now
shows 64/128 cores instead of 64 cores. That is the honest reading of a
setting someone chose.

The htop header clips instead of disappearing

A meter taller than the remaining rows is drawn as far as it goes. The
renderers already stopped at area.bottom(); only the caller's break was
discarding the whole thing. A one-row slice of a split CPU meter relabels
itself CPU instead of keeping Avg, which asks "the average of what?" once
the core rows are gone.

Tests: 128 cores render 128 glyphs; a 16-row terminal labels itself N/128; a
configured cap labels itself 64/128; the figures below the grid survive; the
htop header is non-blank at 128 cores on 50- and 24-row terminals; the grid-cap
ladder wraps and recovers from a hand-edited value. 173 TUI, 146 core, 33 config
tests, clippy -D warnings clean, and the release binary reports 0.6.4.


Install

tar -xzf gander-0.6.4-<platform>.tar.gz
./gander-0.6.4-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux x86_64 (glibc)
x86_64-unknown-linux-musl Linux x86_64 (static, any distro)
aarch64-unknown-linux-gnu Linux arm64 (glibc)
aarch64-unknown-linux-musl Linux arm64 (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.

Debian, Ubuntu

curl -fsSL https://pandasir.github.io/gander/gander.gpg \
  | sudo gpg --dearmor -o /usr/share/keyrings/gander.gpg
echo "deb [signed-by=/usr/share/keyrings/gander.gpg] https://pandasir.github.io/gander stable main" \
  | sudo tee /etc/apt/sources.list.d/gander.list
sudo apt update && sudo apt install gander

Fedora, RHEL, Rocky

sudo rpm --import https://pandasir.github.io/gander/RPM-GPG-KEY-gander
sudo curl -fsSL https://pandasir.github.io/gander/gander.repo \
  -o /etc/yum.repos.d/gander.repo
sudo dnf install gander

The .deb and .rpm files are also attached below for a one-off
apt install ./file.deb; SHA256SUMS covers all of them.

From source: cargo install --path crates/bin (Rust 1.89+).

Full history: commits

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.6.3

Choose a tag to compare

@github-actions github-actions released this 22 Oct 06:49

installable with apt, dnf and Homebrew, and a signed repository

A packaging release. Nothing about the process table changed; what changed is
that installing gander no longer means downloading a tarball and copying a
binary into ~/.local/bin by hand.

Debian, Ubuntu, Fedora, RHEL and Rocky now have a real package manager path

https://pandasir.github.io/gander/ is a signed apt repository and a signed
yum/dnf repository, served from a repo branch by GitHub Pages:

sudo apt install gander
sudo dnf install gander

Upgrades arrive with the rest of the system. apt verifies InRelease, dnf runs
with gpgcheck=1 and repo_gpgcheck=1, and the RPMs are signed as well as the
metadata. The .deb files are not signed individually: apt authenticates the
index, which carries each package's SHA-256, so signing the package too would
protect nothing that the index does not.

The packages declare no dependencies, and that is the whole product

Both are built from the static musl binary, so they install on Debian 12,
Ubuntu 24.04, Rocky 9 and CentOS 7 alike. check-packages.sh asserts both
halves of the claim -- the control/Requires fields are empty and readelf shows
no NEEDED entries -- because either one can be made true while the other
regresses. A package that quietly picked up libc6 >= 2.34 would install on the
build machine and refuse to start on the user's.

Linux arm64, at last

aarch64-unknown-linux-gnu and aarch64-unknown-linux-musl are built on
GitHub's hosted arm64 runners, so the arm64 packages and the Homebrew Linux
ARM install both stop compiling from source. v0.6.2 shipped no arm64 archive,
which is why the formula still builds there; that becomes a prebuilt download
in the next formula update.

What a live client taught the implementation

Every one of these was found by running the thing rather than by reading it,
and none would have been caught by a green CI run of the code alone. The
orphan-branch path in the publish job had no origin remote, so the first
release would have failed to push -- a path that runs once and is therefore
never exercised again. sign-packages.sh checked the SIGPGP/SIGGPG tags, but
rpm 4.14+ stores a header signature in RSAHEADER, so it reported "none" for a
correctly signed package and would have passed one whose signing silently
failed. check-packages.sh passed a relative RPM path through a chdir, which
every local run missed because local runs used absolute paths. rpmbuild from
Debian refuses to build for a foreign architecture at all, so each package is
now built on a runner of its own architecture. linux-install echoed an
undefined ${IMAGE} on its last line, failing after the archive had already
downloaded, verified and run. gh release upload failed with "not a git
repository" in a job with no checkout. The repository's client check could not
resolve host.docker.internal, a Docker Desktop convenience absent on a Linux
runner; and once it could, the arm64 client died with "exec format error"
because the x86_64 runner had no QEMU handlers registered.

Verified against a local HTTP server with real clients: apt install gander on
debian:12 amd64 and arm64, dnf install gander on rockylinux:9 amd64 and arm64,
a tampered .deb rejected with "Hash Sum mismatch", and a tampered repomd.xml
rejected with "Bad GPG signature".

Signing key

docs/SIGNING.md is the key's whole lifecycle. It is created once by a
maintainer and stored as two repository secrets; nothing is generated in CI,
because a key that changed every release would invalidate every user's
imported copy. It has no expiry on purpose -- an expired key breaks every
apt update on the expiry date, which is a worse failure than a long-lived
key in a secret store.

Tests: 146 core, 33 config, 160 TUI, plus integration and doc tests;
cargo clippy --workspace --all-targets -- -D warnings is clean. The new packages
job runs on every pull request, and packaging/test-repo.sh exercises the whole
repository chain with a throwaway key.


Install

tar -xzf gander-0.6.3-<platform>.tar.gz
./gander-0.6.3-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux x86_64 (glibc)
x86_64-unknown-linux-musl Linux x86_64 (static, any distro)
aarch64-unknown-linux-gnu Linux arm64 (glibc)
aarch64-unknown-linux-musl Linux arm64 (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.

Debian, Ubuntu

curl -fsSL https://pandasir.github.io/gander/gander.gpg \
  | sudo gpg --dearmor -o /usr/share/keyrings/gander.gpg
echo "deb [signed-by=/usr/share/keyrings/gander.gpg] https://pandasir.github.io/gander stable main" \
  | sudo tee /etc/apt/sources.list.d/gander.list
sudo apt update && sudo apt install gander

Fedora, RHEL, Rocky

sudo rpm --import https://pandasir.github.io/gander/RPM-GPG-KEY-gander
sudo dnf config-manager --add-repo https://pandasir.github.io/gander/gander.repo
sudo dnf install gander

The .deb and .rpm files are also attached below for a one-off
apt install ./file.deb; SHA256SUMS covers all of them.

From source: cargo install --path crates/bin (Rust 1.89+).

Full history: commits

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.6.2

Choose a tag to compare

@github-actions github-actions released this 21 Oct 07:15

v0.6.2 — v0.6.2 — distributable source, and a lockfile worth trusting

This is a packaging release: no behaviour changed, but the project can now be installed from a source tarball without resolving a moving dependency graph.

crates.io-ready workspace

The internal gander dependencies now carry a version alongside their path, which cargo requires before it will package any of the four crates. The executable is published as gander-bin because the gander crate name was taken by an unrelated, yanked project in 2024; cargo install gander-bin still installs a binary named gander.

Cargo.lock is committed. The v0.6.1 source archive did not contain one, so cargo build --locked failed before compiling a line and Homebrew or AUR recipes could not pin the dependency graph. The same archive now builds reproducibly with the lockfile that CI and local tests use.

README and docs/RELEASING.md now describe the four-crate publish order and the lockfile rule, so the next release cannot silently regress either one.

Packaging recipes

packaging/ contains a Homebrew formula, an AUR PKGBUILD with .SRCINFO, and the checksums and commands needed to submit them. All point at the annotated v0.6.2 tag; the distribution metadata lives with the source rather than only in a tap or an AUR repository.

Tests: 145 core, 33 config, 151 TUI, plus integration and doc tests; cargo clippy --workspace --all-targets -- -D warnings is clean. The full suite runs outside the macOS sandbox, which denies KERN_PROC_ALL.


Install

tar -xzf gander-0.6.2-<platform>.tar.gz
./gander-0.6.2-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux (glibc)
x86_64-unknown-linux-musl Linux (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.
From source: cargo install --path crates/bin (Rust 1.89+).

Full history: commits

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.6.1

Choose a tag to compare

@github-actions github-actions released this 20 Oct 07:15

cleanup — dead code deleted, docs corrected

No new features. Three things that were lying about the code, and one small fix.

Dead code

capability had ten constants and four were read by nothing but their own doc
comments. PROC_IO was the worst: true on Linux and macOS, claiming
per-process I/O counters were available, while no column existed to be gated by
it. Removed along with IOPRIO, SMAPS, PSI, DELAYACCT, THREADS, LIVE.

Darwin's PrevRow carried disk_read/disk_written behind
#[allow(dead_code)] since M1, and the proc_pid_rusage call that filled them
ran once per process per tick with the result thrown away. The attribute silenced
the compiler, not the syscall. Both gone; scan::rusage() stays, tested and
documented as unwired, for whoever builds the I/O columns.

Docs

DESIGN.md §2 listed sort.rs, filter.rs and a criterion dependency that do
not exist, and omitted the darwin/ and freebsd/ backends. §4.1 restated the
Field enum from memory, including two variants deleted a milestone ago. §3 said
"the two platforms" about memory accounting when there are three.

These are now pointers to the source of truth plus the reasoning that is not in
the code. A doc that restates code is a doc that will be wrong within a milestone.

s reports the actual ptrace_scope

It said tracing "may" be forbidden. With the common ptrace_scope = 1 the attach
fails with EPERM for every pid on the machine including your own, which is not
something "may" lets you debug. The panel now reads the file, prints the value,
says what it means and how to change it. macOS names SIP, FreeBSD names truss.

Three test bugs, all the same mistake

Asserting the machine instead of the contract: devname(...) == "ttys000" (broken
CI for two pushes), and rusage → non-zero (a page-cached test binary has done no
block I/O, so zero is correct). Both now check that the call succeeds and the data
is readable, which holds on any host.

376 tests, zero clippy warnings, the Linux path re-run in a rust:1.89-slim
container, all five CI jobs green including the FreeBSD VM.


Install

tar -xzf gander-0.6.1-<platform>.tar.gz
./gander-0.6.1-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux (glibc)
x86_64-unknown-linux-musl Linux (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.
From source: cargo install --path crates/bin (Rust 1.89+).

Full history: commits

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 06:14

M4 complete — FreeBSD, the third backend

A third implementation of PlatformSampler, on a system with no /proc. The
whole port is one sysctl away from being cheap and several traps away from being
wrong; what follows is mostly the second part.

Why FreeBSD needed its own model rather than another /proc reader

FreeBSD has no /proc. The data comes from sysctl and libkvm, the same pair
htop's freebsd/ uses. One sysctl(KERN_PROC_PROC) returns the whole table with
CPU time, memory, RSS, thread count and the BSD identity fields, so the default
column set costs a single syscall — fewer than Linux's one openat per process.

It was written and verified on FreeBSD 14.5-RELEASE/arm64 in QEMU: the whole
workspace suite runs inside the guest, plus tests/freebsd_live.rs, twenty tests
that compare gander's figures against sysctl(8), ps(1), swapinfo(8) and
netstat(1) in the same test, so a changing system cannot explain a
discrepancy.

The five things a live run found that review had not

SSLEEP is three states, not one. ki_stat is not a state letter and is not even
a function of the state: SIDL (1) means "being created by fork" and ps prints
R for it; SSLEEP splits into D (uninterruptible, ki_tdflags & TDF_SINTR
clear), I (interruptible and asleep for MAXSLP = 20 s), and S; SLOCK is
L. Mapping 1..7 onto I R S T Z W ? arithmetically reported I for a
brand-new process and S for every sleeper — and collapsing D into S hides
exactly the state that explains a machine that feels hung, since a process
blocked on disk cannot be signalled away.

comm_buf truncated to empty. ArrayString::try_push_str rejects a string that
does not fit and leaves the buffer as it was, so a name over 64 bytes produced a
blank COMMAND rather than a shortened one. The test's own fully qualified path
triggered it every time.

ELAPSED subtracted the boot time twice: uptime - (start - boot) is 0 for every
process on a machine whose clock is newer than its boot, which is all of them.

cwd was None for every process. KERN_PROC_CWD returns a struct kinfo_file,
not a path, and a PATH_MAX buffer is too small for it (ENOMEM); reading the
buffer as a string yields a NUL byte, which is indistinguishable from a permission
refusal.

KERN_PROC_FILEDESC's records are variable-length. libc's kinfo_file is 1392
bytes; the kernel truncates each record after kf_path and reports 376-408.
Guarding on stride < size_of — the natural check — rejects every record the
kernel sends, so l reported "cannot read file descriptors" for processes whose
descriptors were fine. The stride is the record's own kf_structsize and
kf_path sits at a fixed offset of 368, because everything past the truncation
point was never copied out.

Three more that came from comparing against the system's own tools

hw.physmem exceeds v_page_count * pagesize by the kernel's early allocations,
so a bar scaled to it can never fill. kvm_getswapinfo reports pages, not bytes
(htop's FreeBSD backend multiplies by 1024 and labels the result bytes).
devstat's pass(4) devices double-count every byte next to the da/ada
device they front.

The rest is wiring

A third Sampler alias, capability::PLATFORM/HOST_DISK_IO/AFFINITY arms,
cpuset_* for the affinity panel (real read/write, unlike macOS's scheduler
hint), devname_r for TTY, and e/l mapped onto
KERN_PROC_ENV/KERN_PROC_FILEDESC instead of /proc. x says why FreeBSD has
no per-process lock table.

Memory is its own model again: the five page queues partition physical memory, so
used = active + wired + laundry, cache = inactive + page cache, and the
segments can be checked against v_page_count. laundry counts as used for the
same reason macOS's compressor does — those pages cannot be reused until the
write completes.

CI

A cross-compile check (cheap, catches a forgotten cfg arm) and a vmactions VM
job running the real suite, because "it compiles" is worth little here. Releases
build FreeBSD inside the VM: cross-linking wants FreeBSD's ld and -lkvm, and
faking it with a sysroot produces a binary that links and then fails at run time.

Also in this release

The post-M3 audit's nine restored identity columns. Session, Tty, Tpgid,
StartTime, Tgid, SchedPolicy, ProcComm, ProcExe and Cwd were declared
in Field with no FieldMeta, so Field::name() returned "UNKNOWN", the htoprc
importer silently dropped them, sort_key= fell through to PERCENT_CPU, and they
never appeared in the setup screen. TTY and TPGID were also never parsed, so
even with metadata they would have rendered a constant. s gained a screen that
explains why it is not implemented, instead of doing nothing at all.

M4 was already on main, untagged

This tag is the first that can build FreeBSD binaries, which is what the version
number is for.

Tests

374 on macOS, 363 in the FreeBSD guest, zero clippy warnings on both.

Two bugs this tag itself exposed

A test asserted devname(0x10000000) == "ttys000". devname(3) answers from the
live /dev table, so that is true on a Mac with open terminals and false on every
CI runner, which has none. The renderer was right in both cases — it falls back to
the raw number — so the test was asserting a property of the host rather than of
the code. It now asks devname what the answer should be and compares, so it
checks agreement with the platform instead of agreement with one machine.

The FreeBSD archive was never attached to a release. release declared
needs: build, and freebsd is a sibling job, so download-artifact could not
see its artifact: the job built the tarball, uploaded it, and the file expired
unused while the release body listed x86_64-unknown-freebsd in its platform
table. Every release so far was missing it and nothing was red. It took FreeBSD
being the headline feature for the absence to be the first thing anyone looked
for, which is the risk of a job that succeeds at doing nothing.

Both are fixed in this tag's history, and the CI run that gated it is green on
all five jobs including the FreeBSD VM.


Install

tar -xzf gander-0.6.0-<platform>.tar.gz
./gander-0.6.0-*/gander
Archive Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux (glibc)
x86_64-unknown-linux-musl Linux (static, any distro)
x86_64-unknown-freebsd FreeBSD 14 and later

Every archive has a .sha256 beside it: shasum -a 256 -c <file>.sha256.
From source: cargo install --path crates/bin (Rust 1.89+).

Full diff and commit list: v0.5.0...v0.6.0

Every release is explained at length — the reasoning, the alternatives, and what a
live kernel or VM taught the implementation — in
docs/ROADMAP.md. The notes above say
what shipped; ROADMAP says why.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 11:42

Download the archive for your platform, verify it, and run:

tar -xzf gander-*.tar.gz
./gander-*/gander
File Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux (glibc)
x86_64-unknown-linux-musl Linux (static, any distro)

Every archive has a .sha256 checksum next to it.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 11:03

Download the archive for your platform, verify it, and run:

tar -xzf gander-*.tar.gz
./gander-*/gander
File Platform
aarch64-apple-darwin macOS, Apple Silicon
x86_64-apple-darwin macOS, Intel
x86_64-unknown-linux-gnu Linux (glibc)
x86_64-unknown-linux-musl Linux (static, any distro)

Every archive has a .sha256 checksum next to it.