Terminal-top — a TUI dashboard where each panel is authored as a Nix domain file

Sharing a flake I’ve been working on: it produces a generic terminal dashboard (Haskell/Brick) whose panels are authored as .nix files. The renderer is fixed; the meaning — URLs, JSON paths, sections, thresholds — lives in the nix.

Nix-side interesting bits:

lib/loadDomains.nix is a small readDir helper that walks ./domains/ and imports every .nix file, so adding a domain is “drop a file” with no registration elsewhere.

lib/sections.nix exposes section constructors (headline, caption, stat, sparkline, table, articles, legend, groupCount) so authors write with lib.sections; [ (headline "…") (stat { … }) ] instead of { type = "stat"; … } by hand.

lib/mkDomain.nix validates at eval time — missing required keys and unknown attrs both throw with a path-aware error: mkDomain: domain 'my-api' → source [0] (https://…) → panel [1] 'Overview' → section [2] (stat) is missing required key(s): label. Authoring a broken domain fails the build instead of silently rendering nothing.

The flake evaluates every domain to one JSON blob via pkgs.writeText "terminal-top-domains.json" (builtins.toJSON (import ./lib/loadDomains.nix ./domains)), and the wrapper bakes TERMINAL_TOP_DOMAINS_JSON=${domains-json} onto the binary. Pure eval (no --impure, no builtins.getEnv); zero Nix dependency at runtime.

Env-var interpolation (${VAR} / ${VAR:-default}) inside URLs and headers is expanded at fetch time by the Haskell side rather than at nix eval — this lets a domain ship a working shared default (e.g. an API token) while still honouring the user’s own environment.

Built-in domains:

Gaza / West Bank casualties (Tech for Palestine / Gaza MoH / OCHA), Sudan IPC food crisis (FEWS NET), UCDP conflict events (Uppsala University), Climate TRACE emissions, NOAA space weather. Each ships with the upstream codebook’s context alongside the numbers.

Live demo in any browser:

Runs on a Raspberry Pi 4B over Tailscale Funnel (uplink is still a bit flaky at the moment, so give it another try if the first load doesn’t come through):

Or locally:

nix run gitlab:hunorg/terminal-top

Source:

https://gitlab.com/hunorg/terminal-top

Interested in feedback on the TUI itself — rendering polish, the section vocabulary (stat / sparkline / table / articles / legend / groupCount), layout edge cases on narrow terminals, and ideas for new built-in domains worth adding.

Screenshots:

4 Likes

Very morbid, what are you using this for?

That aside, surprising that it was possible to make a schema that was flexible enough for ingesting all these various APIs. Regarding your question, could be that what you wanted was just to wrap what you have right now into module options for the types and formalizing the schema. But honestly, not sure if I’d personally bother unless you have tooling that is aware of it. Otherwise, it would likely just be a layer of indirection that could make this slow if you weren’t careful.

Unrelated: might also be worth considering Unicode dot graphs for the spark lines, could help make it easier to see, since vertical compression likely makes days with what would still seem like a significant amount (even one I imagine) comparatively small. That or logarithmic scaling.

1 Like

On the morbid question — that was my starting point, not the project’s scope. The name riffs on top/htop: the “top” I wanted to see was what’s most consequential in the world right now, not what’s busiest on the CPU. The data I cared about (Gaza casualties, Sudan famine, UCDP events, fossil-fuel emissions) tends to live behind heavily-branded dashboards or in academic CSV archives, and a plain TUI felt like the more honest medium for it.

Worth saying: the platform is deliberately subject-neutral. A domain is just “URL + JSON path + a typed list of sections” in a .nix file; the built-in humanitarian panels are the ones I care about, but anyone can point it at whatever JSON they find consequential — HN feeds, weather stations, build telemetry, self-hosted APIs — without touching the Haskell.

On module options — agreed. mkDomain.nix already does the typing and path-aware validation at eval time, so lib.types.submodule would mainly buy nixos-option / repl introspection, and that’s not worth the indirection if nothing downstream reads it.

On sparklines — fair, and something I’ve half-avoided. The renderer currently maps values to the eight stacked block glyphs against a linear min-max scale, so a one-fatality day next to a 7,000-killed one reads as the bottom bar. Braille patterns plus optional log scaling would fix both the resolution and the dynamic-range problem; worth shipping. Thanks for flagging.

1 Like