hausfold

haus

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 switch

Enable 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 entries

Where it comes from

Four source fields, one per package manager. Set the one that applies; setting two is a build error:

FieldInstalls fromExample
caska Homebrew caskcask = "slack";
brewa Homebrew formulabrew = "ical-buddy";
packageyour flake's pinned nixpkgspackage = pkgs.ripgrep;
appStoreIdthe Mac App StoreappStoreId = 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:

  • appId is the bundle id, which is how windows get sent to the right workspace. osascript -e 'id of app "Slack"' prints it.
  • icon goes 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 on PATH for 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 nothing

Reach for it twice and it has earned a roster entry.

Every field

FieldWhat it's for
keyThe launcher letter, unique across the roster, and not one of launch mode's own: V E Z , ` - = / 14 Esc and the arrows are taken, and a rebuild refuses them. null (default) makes the entry install-only.
nameThe macOS app name, which is what open -a uses. Required when key is set.
appIdThe bundle id: auto-assigns windows to a workspace, and matches float.
labelThe cheatsheet caption. null uses name.
floatKeep 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.
titleRegexScopes float to matching window titles instead of every window.
cask · brew · package · appStoreIdWhere it comes from. One of them, or none.
scope"user" (default) or "system", for package only.
orderLower numbers appear earlier.
enablefalse disables a default or imported entry by id.
installedBySet 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 nothing

What 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.

On this page