I Have a Problem With Installing Things to Try Them Once
If you use Nix, you know this move:
nix shell nixpkgs#cowsay
cowsay "I was never installed"
That shell gets cowsay. Close it and it's gone from your PATH. Nothing touched your system config, and the next garbage collection takes the files back.
I wanted that for desktop apps. Not a terminal, not a command: a menu I can open from anywhere, type "pinball" into, and be playing a minute later without having installed anything.
So I built one and bound it to Super+R. Here it is opening three games in a row, none of them installed:
How the Launcher Works
Step 1 of 3 happens before I ever press a key. When the launcher is built, it asks nix-index for every .desktop file in nixpkgs. A .desktop file is how a Linux app says "I belong in your app menu." That gives a list of every GUI app nixpkgs can build, baked into the launcher so the menu opens instantly.
Step 2 is the menu. Super+R opens rofi with that list, plus anything I already have installed, with my recent picks at the top. Each row says where it comes from, like Space Cadet Pinball [nixpkgs: space-cadet-pinball].
Step 3 is the run. An installed app opens normally. Anything else gets built from one pinned nixpkgs revision with nix build, which in practice means downloading it from the NixOS binary cache. A small progress box shows the elapsed seconds so I know it's working, and then nix run starts the app. If something fails, I get a desktop notification with the last lines of the error instead of silence.
That's the whole trick. It's nix shell with a menu on the front. Its sibling on Super+Shift+R does the same for terminal commands through comma. Between the two, I never add a package to my config to try it once.
No NixOS? It Runs on Linux Mint Too
Nothing in the launcher needs NixOS. It needs Nix, the package manager, which installs on most Linux distros. A friend runs Linux Mint 22 (Cinnamon, built on Ubuntu), so I packaged the launcher as a tarball for them. You can have the same one: mint-nix-launcher.tar.gz (11.5 KiB). Three commands:
tar xf mint-nix-launcher.tar.gz
cd mint-nix-launcher
./install.sh
install.sh installs Nix if it's missing, builds the launcher from a pinned flake, and binds the keys in Cinnamon's keyboard settings. The first run takes a few minutes and a few hundred megabytes of downloads; after that it behaves exactly like mine. It's built for Cinnamon on X11, so stock Ubuntu with GNOME on Wayland would need its own key bindings and a different menu program.
The tarball is six text files and nothing compiled: the two .nix files that are the whole launcher, a flake.nix and flake.lock pinning the exact nixpkgs, nix-index-database and nixpkgs-multiverse revisions I run, a README, and install.sh. Read install.sh before you run it. It pipes the Determinate Systems Nix installer into sh, asks for sudo, and unbinds Cinnamon's built-in run dialog so Super+R is free. To check your download matches mine:
sha256sum mint-nix-launcher.tar.gz
# c9385e3d34a38d88355c36e15cc49b648c3fa6825efe78bef69fa71970737fbe
But It Only Knows About Today
Then I went looking for yuzu and got nothing.
Yuzu, the Switch emulator, left nixpkgs in early March 2024, right as its developers settled Nintendo's lawsuit and shut the project down. That's what started this. It makes a lousy example, though: without Nintendo's keys and a game, an emulator opens to an empty window.
So the example for the rest of this post is one you can play. Not Tetris 2 is Tetris with physics. Pieces land at any angle, pile up in a heap, and a line clears once it's full enough. Its author sums it up as "all the upsides of Tetris and all the downsides of physics."
nixpkgs dropped it in January 2022. The removal commit says it crashed on startup, and it went out in a cleanup that also removed LÖVE 0.7, the old game engine it ran on. Type nottetris2 into a launcher built from today's nixpkgs and nothing comes back. The launcher isn't broken. It's telling the truth about a tree that no longer contains the thing you asked for.
That's the part worth writing about, because it's a whole class of failure.
nix-index Describes One Revision, and Only One
nix-index builds its database from the build outputs of a single nixpkgs revision. My launchers run two nix-locate queries at build time: one for /bin/* against the small, bin-only database to get every runnable command, and one for /share/applications/*.desktop against the full database to get every GUI app. Both results get baked into store paths, so the menus open instantly instead of grinding through a database on every keystroke.
Which means a deleted package is not missing from my index. It's absent from the tree my index describes. Fuzzier matching won't recover it, and neither will a bigger database or better ranking. The information was never there.
I find this distinction useful for debugging search problems in general. Before improving a search, check whether the thing you're looking for is inside the corpus at all. Half the time you're searching the wrong corpus, and no amount of work on the search will help.
So I went looking for a different corpus.
nixpkgs-multiverse
nixpkgs-multiverse by Farid Zakaria indexes every (attribute, version) pair across 1,538 sampled nixos-unstable revisions, from 2012-07-05 to 2026-08-26. At the commit I have pinned, that's 31,987 attributes and 306,845 version pairs. He wrote up the background in nixpkgs-multiverse: every version that ever existed, and the whole index is browsable at nixmultiverse.com.
The design choice I like most is that the index ships as plain JSON inside the flake. There's no server to query and no cache directory to go stale. If you have the flake input, you have the data, and two people on the same rev get the same answers.
It also ships mvs, a Rust CLI that reads the index offline, plus a "fast path" that resolves attr@version straight to a /nix/store path from an index of what Hydra built. That matters more than it sounds. Per the project's README, the alternative is fetching and evaluating a ~378 MB nixpkgs tree; substituting a known store path from cache.nixos.org skips both. Running a two-year-old package becomes a download instead of a build.
The null Sentinel
Here's the part I enjoyed. index/versions.json looks like this (trimmed):
{
"attrs": {
"nottetris2": { "2.0": 716 },
"hello": { "2.12.2": 1486, "2.12.3": null }
}
}
Each integer is an index into revisions.json: the last revision that version was seen in. Revision 716 is the 2022-01-15 bump, the last one with Not Tetris 2 in it. And null means this version is still current at the tip.
So "which packages did nixpkgs delete?" has a one-line answer. An attribute is gone if no version under it carries a null. Nobody curates that list or files PRs to maintain it. It falls out of the index shape with a single jq pass.
I ran it. 7,012 of 31,987 attributes no longer exist in nixpkgs. Grouped by the year they were last seen:
2012: 58 2017: 262 2021: 387 2025: 1747
2013: 434 2018: 357 2022: 940 2026: 712
2016: 107 2019: 257 2023: 570
2020: 364 2024: 817
No removals land in 2014 or 2015, and 2025 is the biggest year by a wide margin. I haven't dug into why yet.
For the version picker I needed a second view: which versions an attribute ever had, newest first. That's the same file read the other way. For snapshot mode (below) I needed index/history.json, which stores [firstIdx, lastIdx] runs per version, sometimes a list of runs when a version came back after a gap. emacs 25.1 is my favourite example: present for 58 revisions, missing for one, then back for one more.
All four catalogs are generated in runCommand derivations at build time and pre-sorted there. Opening a picker costs one awk pass over an already-ordered file. Running the jq over 5.5 MB of JSON at keystroke time took a couple of seconds on my machine, which is the difference between a tool you use and a tool you remember you have.
The Gate
The tempting design is "if comma fails, try multiverse." I didn't do that, because comma failing is not evidence that a package is gone. Plenty of packages in nixpkgs are missing from the bin-only small index, and comma handles those correctly on its own.
So the divert is gated on proof. Here's the command launcher's routing:
Super+Shift+R -> you pick or type something
|
|- tagged "[multiverse: removed ...]" ------------> multiverse
|- contains "@" (e.g. ripgrep@13.0.0) ----------> multiverse
|- found in nix-index bin list -------------------> comma (unchanged)
|- NOT in nix-index AND removed per multiverse ---> multiverse
'- anything else ---------------------------------> comma (unchanged)
The GUI launcher on Super+R uses the same three divert conditions in front of its ordinary nix run path. Only an attribute the index proves is gone gets diverted. Everything that worked yesterday takes the identical path today.
I also splice the 7,012 removed attributes into the ordinary menus, tagged, so nottetris2 is findable in the bar I already use, with no dedicated mode to remember.
And mvs has its own fallback underneath mine. When the store-path index has no entry for that exact version, it drops from substitution to fetching and evaluating the revision. Slower, still works.
The Release-Mode Trap
My first idea for a third mode was "let me pick a nixpkgs release": 21.11, 24.05, that shape. It's the obvious feature. It's also wrong twice, and I only caught the second problem because I checked the data before writing the code.
First, a release selector moves. The multiverse docs say this plainly: backports land on release-26.05 for the channel's entire life, and the selector follows them, exactly as github:NixOS/nixpkgs/nixos-26.05 does. It's a channel tip. Only a date or a commit gives you something that can't drift.
Second, and this is the one I would have shipped a bug over: I diffed releases.json against revisions.json and none of the 25 release revisions appear in the index. Zero overlap. The index holds nixos-unstable channel bumps.
That kills the obvious workaround too. Approximating a release by the nearest indexed date isn't imprecise, it's incorrect:
REL ITS DATE NEAREST INDEXED BUMP
26.05 2026-08-27 2026-08-26 <- that bump is 26.11pre, a different tree
25.11 2026-06-30 2026-06-29
21.11 2022-08-02 2022-07-31
26.05's entry is dated 2026-08-27 because that's where its moving tip was when the index was built. The nearest unstable bump, the day before, is 26.11pre. Same week, different nixpkgs. A release mode built that way would confidently hand you the wrong tree and never say so.
So I dropped release mode and built snapshot mode: pick a date from the 1,538 indexed revisions, then pick a package as it existed that day. It's one awk range test over the run catalog (firstIdx <= R <= lastIdx), which works for any revision. At revision 621 (2021-07-18) that yields 13,432 attributes, Not Tetris 2 among them at version 2.0, six months before nixpkgs dropped it.
It answers the question I actually had, entirely from the index, and a date can't drift out from under me.
What You're Actually Running
I want to be straight about this part, because "run any package that ever existed" is a fun sentence that hides a real cost.
What you get is that revision's entire closure: its openssl, its curl, its glibc, as they were. Every CVE patched since then is unpatched in what you just launched. For a single-player game reading local files, that's a defensible trade. For anything that opens a socket or parses untrusted input, it's a bad one.
The launcher can't tell you which case you're in, because the index records when a package left nixpkgs, never why. Unmaintained upstream, an unfixable CVE, an EOL runtime, a legal withdrawal, a source tarball that 404s: all of them produce an identical menu row.
I considered a confirmation dialog and decided against it. A prompt that can't tell a legally withdrawn emulator from an unpatched CVE is a prompt you learn to click through, and then it's worse than nothing because it feels like a safeguard. Instead, the terminal that already opens prints the facts and gets out of the way:
! nixpkgs dropped this package after 2022-01-15.
You get that revision's exact closure: its openssl, curl and
glibc as they were then, with no patches applied since.
Nothing is installed; this run is ephemeral.
A few things genuinely aren't risks, and saying so makes the real one stand out.
- Nothing is installed.
mvs runtouches no profile, no generation, no PATH. - Store paths come from
cache.nixos.orgunder the official signing key. The new flake input supplies JSON and a CLI; it never becomes a substituter. - The vanished-tarball case limits itself. Those are usually fixed-output derivations whose hash no longer fetches, so they refuse to run instead of running stale.
The cost nobody warns you about is disk. Each old revision drags in its own stdenv closure. Not Tetris 2 itself is a 2.6 MiB download, but its 2022 closure is 128 store paths: 50.4 MiB to fetch, 221.4 MiB once unpacked. Run five packages from five different years and you're holding five near-complete base systems until nix-collect-garbage takes them back.
Where It Ended Up
Four keys, one implementation:
| Mode | Key | Question |
|---|---|---|
| GUI app | Super+R | unchanged |
| Command | Super+Shift+R | unchanged |
| Time machine | Super+Ctrl+R | "run Not Tetris 2" |
| Snapshot | Super+Alt+R | "what did nixpkgs have on 2021-07-18?" |
I run rofi on X11 (Cinnamon) and fuzzel on Hyprland, and I didn't want the routing logic living in two places where one copy quietly rots. The multiverse package takes the picker as an argument, a shell command that reads choices on stdin and prints the selection, so both desktops get the feature from one file:
multiverse = import ./nix-multiverse-launcher.nix {
inherit pkgs nixpkgs-multiverse;
picker = "${pkgs.fuzzel}/bin/fuzzel --dmenu --prompt='multiverse > '";
};
That's the entire abstraction. It cost one function parameter and saved a duplicate I would have found out of sync in six months.
No Nix? The question still has answers. nixmultiverse.com lets you browse every version of a package in a browser, lazamar's Nix package versions tells you which nixpkgs commit shipped a given version, and Debian users have snapshot.debian.org for the same "what did the archive look like on that day" trick. The lesson carries over to any package manager: check which snapshot your search is looking at before blaming the search.
The launcher that could only ever see the present now sees fourteen years.
Header photo by Niklas König on Unsplash.
Content on this blog was created using human and AI-assisted workflows described in my standards and workflow posts. Original ideas and editorial decisions by Justin Quaintance.