Apps
Every app, font and CLI tool on the machine in one list: where each one installs from, what key launches it, and the packs that bring a whole set at once.
Sooner or later you want Slack, or Discord, or ripgrep. Everything the machine
has goes in one list, haus.roster, and you never choose an option, only
which fields to set. A field says where the app comes from; another says what
key launches it; an entry with neither is still a declaration.
The quickest route doesn't involve editing anything: open pounce, run Install
App, and pick from the curated shelf or search Homebrew, the Mac App Store, or
your flake's pinned nixpkgs. It writes a small Nix module under
hosts/<hostname>/packages/, rebuilds, and commits that one file. There is no
roster.json behind it, just the same options you'd have written by hand.
By hand:
haus edit # opens ~/.config/nix/hosts/<hostname>/default.nix
haus rebuild # build, then switchEnable it
The Apps room is always present: the roster is a list, and an empty one is a machine with nothing extra on it. What you switch on are the curated extras:
haus.apps.videoPlayer.enable = true; # IINA, and the file types it claims
haus.apps.packs.writing.enable = true; # a saved collection, in one line
haus.appStore.install = true; # let a rebuild install appStoreId entriesWhere it comes from
Four source fields, one per package manager. Set the one that applies; setting two is a build error:
| Field | Installs from | Example |
|---|---|---|
cask | a Homebrew cask | cask = "slack"; |
brew | a Homebrew formula | brew = "ical-buddy"; |
package | your flake's pinned nixpkgs | package = pkgs.ripgrep; |
appStoreId | the Mac App Store | appStoreId = 497799835; |
Set none of them and the entry is pure metadata: right for something already on the machine (Safari, Music) that you still want a launcher key for.
Don't know a name? brew search --cask obsidian, or
nix search nixpkgs ripgrep.
An app you'll live in
A keyed roster entry gives an app a Caps-Lock letter, a cheatsheet row, and the install. A workspace entry naming it gives it a workspace, a bar pill, and the rule that sends its windows there:
haus.roster.slack = {
key = "s";
name = "Slack";
appId = "com.tinyspeck.slackmacgap";
cask = "slack";
};
haus.workspaces.S = {
key = "s";
icon = ":slack:";
apps = [ "slack" ];
};Rebuild, and ⇪ then S lands you on Slack, on workspace
S, every time.
Two fields everybody looks up:
appIdis the bundle id, which is how windows get sent to the right workspace.osascript -e 'id of app "Slack"'prints it.icongoes on the workspace, not the app: a ligature from the SketchyBar app font, like:slack:. Leave it out and the pill shows the workspace id.
Keeping them apart is what lets one workspace hold several apps (a comms
space with Slack, Mail and Messages) instead of one app per workspace.
An app you just want installed
Omit the launcher fields. No letter, no cheatsheet row, no pill, but still declared, which is what keeps a later Homebrew cleanup from removing it:
haus.roster = {
discord = { name = "Discord"; cask = "discord"; };
figma = { name = "Figma"; cask = "figma"; };
};Homebrew apps sit outside Nix's time machine
Casks are installed by the rebuild but aren't in Nix generations, so haus rollback neither uninstalls an app you added nor restores one you removed.
Deleting the line doesn't delete the app either; haus.homebrew.cleanup is
"none" on purpose. To actually remove one: brew uninstall --zap <cask>.
An App Store app
Recording the id is always safe; installing from it is opt-in, because it reaches the network and acts on your Apple Account:
haus.roster.xcode = {
name = "Xcode";
appStoreId = 497799835; # the digits in its App Store URL
};
haus.appStore.install = true;Three limits worth knowing before you rely on it. mas cannot sign in; do
that once in App Store.app. It can never buy a paid app; buy it once, then
any machine can install it. It can fetch a free app you've never had. An
unpurchased paid app is reported and skipped during a rebuild, never fatal.
A rebuild fetches these during activation, which is already running as root, so
mas get neither prompts nor wedges. Going through pounce's Install App
instead runs it in a visible floating terminal, because that path can be asked
for an Apple Account.
A CLI tool
Same list, no launcher fields:
haus.roster = {
ripgrep = { package = pkgs.ripgrep; };
ical-buddy = { brew = "ical-buddy"; }; # when nixpkgs hasn't got it
};scope applies to package only, and it is about reach, not privileges:
"user"(default): your profile. Right for anything you run as yourself."system": machine-wide, so it's onPATHfor root, launchd jobs and non-login shells. What a tool called by a daemon actually needs.
To try something without installing it, hearth wires up comma:
, cowsay "hello fog" # fetches it, runs it, keeps nothingReach for it twice and it has earned a roster entry.
Every field
| Field | What it's for |
|---|---|
key | The launcher letter, unique across the roster, and not one of launch mode's own: V E Z , ` - = / 1–4 Esc and the arrows are taken, and a rebuild refuses them. null (default) makes the entry install-only. |
name | The macOS app name, which is what open -a uses. Required when key is set. |
appId | The bundle id: auto-assigns windows to a workspace, and matches float. |
label | The cheatsheet caption. null uses name. |
float | Keep this app's windows floating instead of tiled, for a picker or status window that would reflow the workspace every time it opens. Needs appId. |
titleRegex | Scopes float to matching window titles instead of every window. |
cask · brew · package · appStoreId | Where it comes from. One of them, or none. |
scope | "user" (default) or "system", for package only. |
order | Lower numbers appear earlier. |
enable | false disables a default or imported entry by id. |
installedBy | Set by haus, not by you: for an app copied in by its own activation step, which no source field can describe. |
haus's own apps are entries too
You don't have to know what haus already installs. Each room declares its own: prowl brings AeroSpace, sill brings SketchyBar, den brings Ghostty. So giving one of them a launcher key means writing only the launcher half:
# perch's name, bundle id and installer come from haus.perch.
haus.roster.perch.key = "p";The rule underneath: whoever installs an app owns its source fields, whoever gives it a key owns its launcher fields. Two modules never fight over one field, and your host file never restates what haus is doing.
A handful of apps arrive because haus chose them for you rather than because a
room needs them; those live in haus.apps, each behind one switch:
haus.apps.videoPlayer.enable = false; # don't install IINA
haus.apps.videoPlayer.claimFileTypes = false; # install it, rebind nothingWhat it installs is an ordinary roster entry, so you can key it like any other. File-type claims are the same user default Finder's Get Info ▸ Change All writes, so nothing there is hard to undo. But only one haus-owned app may claim a type, or macOS asks you to pick a winner on every rebuild. If you add an extension to one list, check it against the other by UTI, not by spelling.
It may theme itself on the way in
If the id you key the entry under matches a tool nebelung has a port for (zed,
warp, xcode, gitui, kitty and a few dozen more), its theme lands where
that app looks for one on the next rebuild, in your flavor, contrast and accent.
See the Appearance room.
Then apply it
haus rebuild builds first, so a typo can't take down a running system. New
casks install, new tools land on PATH, and a new roster app gets its letter,
workspace and pill together. If it doesn't tile on first launch, tap
⇪ then ` to re-sort everything.
Your host file is a plain nix-darwin module: anything nix-darwin or home-manager accepts works there and merges with haus. The roster is the common case, not the only door; Making it yours is the rest of what that file can say.
Works with
- Windows: a roster id named in a workspace gets that workspace's pill and the rule that sends its windows there.
- Launcher: Install App writes a roster entry for you and rebuilds, so the common case needs no editor.
- Appearance: an app whose id nebelung has a port for is themed on the next rebuild.
Permissions and side effects
haus.appStore.install is off by default: it reaches the network and acts on
your Apple Account, and it can never be complete: mas cannot sign in and
cannot buy a paid app.
What a rebuild does to apps you didn't declare is
haus.homebrew.cleanup,
"none" by default, so nothing you installed by hand is ever removed.
Remove it
Delete the roster entry and rebuild. Whether the app itself goes depends on that
same cleanup policy: with the default "none" it stays installed and simply
stops being declared. A pack switches off as one line.
Options
Every setting, with types and defaults: Apps.
Share a desktop
Publishing a desktop someone can read before they run it: what the repository holds, what the README owes them, and the checklist before you hand it out.
Appearance
One silver-mist palette across every tool: the accent, the contrast, light mode, the generated wallpaper, and one line for bigger type.