# Create a desktop (/docs/haus/desktops/creating)



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: it cannot 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
[one](/docs/haus/desktops/choosing#the-gallery); this page is how you
write yours.

## The format [#the-format]

```nix
{
  haus = {
    developer.enable = false;
    windows.enable = false;
    keys.leader = "none";

    theme = {
      flavor = "latte";
      accent = "rosewater";
    };

    bar.items.weather = false;
  };
}
```

No `{ pkgs, lib, config, ... }:` header: this is data, not a module function. No
`system.defaults`, no `pkgs`, no activation hooks, no `imports`, and only the
[`haus.*` leaves marked safe for desktop data](/docs/haus/reference/options).
Someone selects it the way they select one of haus's own:

```nix
desktop = ./desktops/writer.nix;
```

`git add` it before you rebuild. Nix reads only a config's tracked files, so an
unstaged desktop fails with an error about a path that does not exist, naming a
file sitting right there in the directory.

## A desktop is the whole answer [#a-desktop-is-the-whole-answer]

A host runs **exactly one**, so yours is not a diff against hacker: whatever you
don't say, the rooms' own neutral defaults decide, which for every optional room
means **off**. Check that twice when you port a layer that was stacked on another
desktop, since what it inherited silently is what it now has to state. haus's own
`hacker` names `security.touchId.passwordlessRebuild` and `focus.enable` rather
than assuming them, for that reason.

Say what your desktop is **for** in a comment at the top, and 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 [#teach-it-your-own-first-lap]

The bar's tour teaches haus's core moves one hint at a time, and a desktop can
replace that lap with its own without leaving the data surface. One that turns
rooms off usually must: the built-in lap is three leader moves plus the palette,
so it has nothing left to teach without them.

```nix
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. **Write keys as `{palette}`, `{leader}` and `{leaderName}`, never a
literal chord**: they expand to what *that* machine resolved, and a hardcoded
`⌘Space` is wrong invisibly on a host that moved `keys.palette`, where you never
see the consumer's bar. A misspelled placeholder renders as typed, and the
rebuild warns and names it. `null` keeps the built-in lap, an **empty** list is a
type error, and the off switch is `tour.enable = false`.

<Callout type="warn" title="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 bar's own module, so a desktop that turns the bar off gets
  none of them. Check that pairing by eye.
</Callout>

## Prove the boundary [#prove-the-boundary]

Run the same structural check haus runs against its own desktops:

```sh
haus show ./writer.nix
```

It names the file's class, then either passes it or names what escaped: a stray
top-level key, an `imports`, a merge or priority instruction, a non-haus option,
or a host-only leaf. It also prints what the file sets grouped by room, and which
rooms it leaves alone, which a `true` never told you. It exits non-zero on
failure, so it is the line for your CI; [the
reference](/docs/haus/reference/haus#reading-a-desktop-or-room-before-you-trust-it)
has the exit codes and `--json`.

The rules underneath are `haus.lib.checkDesktop`'s, reachable with no haus
installed:

```sh
nix eval --impure --expr \
  '(builtins.getFlake "github:hausfold/haus").lib.checkDesktop ./writer.nix'
```

That prints `true` or throws. To prove the *values* build a machine, select it
from a real host and evaluate:

```sh
nix eval .#darwinConfigurations.myhost.system.drvPath
```

[`haus plan`](/docs/haus/reference/haus) is the stronger version: it prints what
a rebuild *would* change (packages, settings, files, launchd jobs, casks) and
changes nothing. It builds first, so it is slower and tells you more.

## Leave room for the host [#leave-room-for-the-host]

A consumer overrides anything you set with a plain assignment in their host file.
Your values arrive below a host's in the priority ladder, so nobody needs
`lib.mkForce` to disagree with you.

<Callout type="warn" title="A list you set is replaced whole, not added to">
  Every leaf arrives 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
  restates all four, so list what your desktop sets in its README.

  **Attribute sets are the middle case.** `roster`, `workspaces` and
  `launcher.items` are carried per key, so a host that names one app's field leaves
  the rest of yours standing.
</Callout>

The [options reference](/docs/haus/reference/options) says which is which per
option: *host-only* for the ones a desktop may never set, *desktop-safe per key*
for the containers whose keys a named rule decides.

## What doesn't belong in one [#what-doesnt-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 `pkgs` value, or a `/nix/store` path;
* activation hooks and shell commands;
* a display UUID, and a `shortcut:<uuid>` palette row: each names one Mac's
  hardware or one Mac's Shortcuts library. `haus.displays.main` and the other
  palette addresses (`cmd:`, `app:`, `setting:`, `mode:`) are free to set;
* a private room: `haus.my.*` is reserved for rooms that live on one Mac.

`checkDesktop` refuses every one of them, so this list is what the error message
will be about rather than a set of rules to remember.

Naming a *package* needs no `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 and stay data-only. A configuration that genuinely needs
`pkgs`, scripts or arbitrary nix-darwin options is a **power module**, and
carries a different trust model.

## Options [#options]

[Every setting, with its type, default and whether a desktop may set
it](/docs/haus/reference/options).
