Create a desktop
A desktop is one file whose only top-level key is haus. The closed format, what it may and may not set, and how to prove it before anyone else runs it.
A desktop is a single .nix file whose only top-level key is haus. That is
the entire format, and the smallness is the point: a desktop can't run a script,
reach a macOS setting haus doesn't already expose, or hand the build a package of
its own. A stranger can read one in a minute and know the worst it can do, which
is what makes running one from someone you've never met reasonable.
haus ships four: hacker, everyday, minimal and blank. This page is how you write yours.
The format
{
haus = {
developer.enable = false;
prowl.enable = false;
keys.leader = "none";
theme = {
flavor = "latte";
accent = "rosewater";
};
sill.items.weather = false;
};
}There is no { pkgs, lib, config, ... }: header; this is data, not a module
function. No system.defaults, no pkgs, no activation hooks, no imports.
Everything stays inside the documented
haus.* option surface, and only the leaves
marked safe for desktop data.
Someone selects it the same way they select one of haus's own:
desktop = ./desktops/writer.nix;A desktop is the whole answer
A host runs exactly one, so yours is not a diff against hacker; it is the complete selection. Whatever you don't say, the rooms' own neutral defaults decide, which for every optional room means off.
That is the one thing to check twice when you port something that used to be a
layer stacked on another desktop: the values it silently inherited are now
values it has to state. haus's own everyday is the worked example: it names
collar.passwordlessRebuild and hush.enable explicitly, because inheriting
them quietly was how the old preset worked and losing them quietly is how the
port would have failed.
Say what your desktop is for in a comment at the top, and say what it deliberately leaves out. The reason a room is off is the part a reader can't reconstruct.
Teach it your own first lap
The built-in tour teaches haus's core moves in the bar, one hint at a time. A desktop can replace that lap with its own and stay data-only. A desktop that turns rooms off usually must, because the built-in one is leader moves plus the launcher and has nothing left to teach without them:
haus.tour.steps = [
{
hint = "Press {palette}, type tour, then hit ↵";
detect = "palette";
}
{
hint = "Move to another workspace";
detect = "workspace";
}
];Steps run in order and advance on signals haus already emits, never on
keystrokes. detect takes one of launch, workspace, navigate, resize or
palette; the first four need prowl, and palette needs the launcher and a
palette key bound. A left click skips a step. Left at its default null,
tour.steps keeps the built-in lap; an empty list is a type error, and the
off switch is tour.enable = false.
Write keys as {palette}, {leader} and {leaderName}, never as a literal
chord. They expand to what that machine resolved, so a tour you share still
names the right keys on a host that moved keys.palette, where a hardcoded
⌘Space would be wrong invisibly, since you never see the consumer's bar. A
misspelled placeholder renders as typed, and the rebuild warns and names it.
The tour is drawn by the bar
A rebuild warns when a step needs a room the desktop switched off, but those warnings live inside sill's own module, so a desktop that turns the bar off gets none of them. Check that pairing by eye.
Prove the boundary
Run the same structural check haus runs against its own desktops:
nix eval --impure --expr \
'(builtins.getFlake "github:hausfold/haus").lib.checkDesktop ./writer.nix'It prints true, or throws naming exactly what escaped: a stray top-level key,
an imports, a merge or priority instruction, an option that isn't a haus
option, or a leaf that is host-only because it names a person or a piece of
hardware. Every rule has a fixture behind it, so the message names the file and
the option rather than failing somewhere deeper.
If the file is an app pack (a data-only file narrowed to haus.roster, the
apps on a machine), run lib.checkPack instead; same idea, one level in.
checkDesktop proves the trust boundary and the option names. To prove the
values build a machine, select it from a real host and evaluate:
nix eval .#darwinConfigurations.myhost.system.drvPathhaus plan is the stronger version of that last
step: it prints what a rebuild would change (packages, settings, files,
launchd jobs, casks) and changes nothing. It builds first, so it takes longer
than the eval above; it tells you correspondingly more.
Leave room for the host
Anything your desktop sets, a consumer overrides with a plain assignment in
their host file. That already works: the desktop's values arrive below a host's
in the priority ladder, so nobody needs lib.mkForce to disagree with you.
The exception is worth knowing before you set one:
A list you set is replaced whole, not added to
Every leaf a desktop sets is carried in at the desktop's priority, a list
included, so a host that names tour.steps, keys.leaderExtras or
snippets.matches at all replaces yours rather than appending to it. Someone
who wants three of your four entries has to restate all four.
That is the deliberate choice: a host says something, and the host's value is what you get. The alternative (lists concatenating) means a consumer can add to your list but can never drop one entry from it.
Attribute sets are the middle case. roster, workspaces and
pounce.items are carried per key, so a host that names one app's field leaves
the rest of yours standing.
List what your desktop sets in its README, so restating it is possible without reading the file.
What doesn't belong in one
Keep these in a private host file, or in something honestly labelled as more powerful:
- identity, secrets, tokens and personal account names;
- an app roster that reveals more than the desktop needs;
- raw macOS defaults outside
haus.*; - a
pkgsvalue, or a/nix/storepath; - activation hooks and shell commands;
- a display UUID:
haus.displays.mainsays "the screen this Mac is using" without naming anyone's hardware.
checkDesktop refuses every one of them, so this list is what the error message
will be about rather than a set of rules you have to remember.
Naming a package is fine and doesn't need pkgs: options that take one have a
packageName sibling taking the nixpkgs attribute as a string, so a desktop can
change the mono font or add a tool without leaving the data-only surface. A
configuration that genuinely needs pkgs, scripts, or arbitrary nix-darwin
options is a power module, and carries a different trust model.
Customize a desktop
One file holds every choice you make: the dials worth knowing, the rooms you can switch off, and how to beat anything your desktop set.
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.