# Create a room (/docs/haus/rooms/creating)



Every room in the [catalogue](/docs/haus/rooms) is a nix-darwin module under
[`modules/`](https://github.com/hausfold/haus/tree/main/modules), plus three
lines of registry that put it in the option reference, the agent skill and the
generated host file at once.

Most of the time you want no room. The
[option reference](/docs/haus/reference/options) is generated, so search it
first; then [Customizing](/docs/haus/desktops/customizing), which has
`haus.roster` for an app, `haus.keys.leaderExtras` or `haus.launcher.items` for
a command, and `system.defaults.…` in your host file for a macOS setting haus
doesn't expose. A room earns its existence when a package, its launchd job, its
config file, its theming and the options steering all four have to move and
switch as one. One setting is not a room.

<Callout title="Your machine, or the layer?">
  If it only ever runs on your Mac, write a plain module, name its options
  &#x2A;*`haus.my.*`**, and set them from your **host file**, never a desktop, which
  is a file other people run without your room. haus promises never to ship a
  room under that prefix, and a flake check holds the promise. Under a plain
  name you race a future release, silently: two modules declaring different
  leaves under one namespace merge with no error, one room's switch steering the
  other's. Your Mac warns you if you do it anyway. Publishing drops the prefix
  for a plain `haus.<name>`, which is what a stranger writes in their host file
  once [`haus add --room --namespace <ns>`](/docs/haus/reference/haus#pinning-a-room)
  has pinned it.
</Callout>

## The two files [#the-two-files]

```
modules/kettle/
  options.nix     # what a host may say
  default.nix     # what happens when they say it
```

`options.nix` is a `{ lib, ... }` module. It may take `config`, which is how
one option's default tracks another, and such a default carries a `defaultText`
so nothing has to force it. It may never touch `pkgs`, or anything from darwin:
that keeps the option surface evaluable on any platform, which is what lets
Linux CI render one JSON into the reference page, the `haus` agent skill and a
fresh install's annotated host file.

`default.nix` takes `{ config, lib, pkgs, username, ... }`, binds
`cfg = config.haus.kettle`, and wraps its whole body in `lib.mkIf cfg.enable`.
A user-level config file goes to `home-manager.users.${username}.home.file`.

<Callout type="warn" title="Why the options can't live in default.nix">
  An option declared inside that gate vanishes exactly when the module system
  needs it to decide the condition. Hence the split, and hence
  `modules/options-modules.nix`, one list of every room's `options.nix` that the
  renderers and `modules/default.nix` all read.
</Callout>

## Three lines of registry [#three-lines-of-registry]

`modules/options-groups.nix` is the room registry, and it **fails closed**: the
`room-registry` flake check compares it against the evaluated option tree, so a
namespace or leaf it doesn't mention is an error. A room owes it:

1. **Every public leaf**, by exact path, under `optionPaths`. An inventory, not
   a glob, so a new option fails loudly. Identity, secrets, tokens and hardware
   ids are a host's: mark those leaves host-only and no shared desktop can set
   them.
2. **An owner**, in `roomOwners`. Two namespaces can share one room:
   `haus.bar` and `haus.menuBar` are both the Bar.
3. **A blurb** and an `order`, in `groups`: the sentence a catalogue is laid
   out from, and where the room sits in it.

Then add the options file to `modules/options-modules.nix` and the
implementation to `modules/default.nix`, and regenerate the committed metadata
that the `site-data-current` check compares against and this site renders from.
A room whose metadata was never regenerated is a room the docs cannot see.

```sh
out=$(nix build --no-link --print-out-paths .#site-data)
install -m644 "$out"/*.json docs/site-data/
```

## Rooms offer, never reach [#rooms-offer-never-reach]

The Kettle room wants a pill in the bar, a separate room with its own switch
that this Mac may not have on at all. The rule: &#x2A;*a room may offer something to
another room, and may never reach into one.** No room reads another's settings,
and none switches another on.

The receiving room declares a slot, an *extension point*, and says what may go
in it. Any other room may fill it. The receiver draws whatever it finds inside
its own enable gate:

```nix
# in the RECEIVING room's options.nix, here the Bar's
haus._contrib.bar.brewStatus = contrib.mkExtensionPoint {
  description = "A pill the Kettle room asks the bar to draw.";
  options = { … };
};
```

Read the path as an address: `bar` is who receives, `brewStatus` is what is
offered, and the room doing the asking is not in it. So switching the Bar off
takes the pill with it and the Kettle keeps boiling, and switching the Kettle
off leaves the Bar's own pills untouched. The middle segment names the
receiving **room**, not a namespace, so the Bar's slot is `_contrib.bar.*`
under both `haus.bar` and `haus.menuBar`. Nothing enforces that. The leading
`_` means wiring rather than a setting, and the renderers prune it.

## Default to off, and mean it [#default-to-off-and-mean-it]

An optional room's `enable` defaults to `false`, always. A
[desktop](/docs/haus/desktops/choosing) is the *complete* answer to what a Mac
should feel like, not a diff, so a `true` default grows silently into every
desktop that never heard of your room. `core` (macOS itself) and `terminal`
(the shell) have no switch on purpose: they are the floor, not furniture.

## Prove it [#prove-it]

```sh
nix flake check
nix eval .#darwinConfigurations.example.system.drvPath
```

The first runs the registry check, the desktop seam, the keymap collision check
and the rest; the second evaluates a whole machine, which is what catches an
option that type-checks and then contradicts another room. Neither activates,
and [`haus plan`](/docs/haus/reference/haus) shows what a real rebuild would
change without changing it.

<Callout type="warn" title="A room landing in haus owes a docs page">
  The layer and this site are two repositories. A room owes a page under
  `content/docs/haus/rooms/`, a row in that tree's `meta.json`, a card in the
  [catalogue](/docs/haus/rooms) and an icon, and the site's `rooms-drift` check
  goes red until it has them. A changed keybinding owes an edit to
  [Keys](/docs/haus/rooms/keys), and nothing catches that one. The reference page
  regenerates itself; the prose does not.
</Callout>

## What a room may not do [#what-a-room-may-not-do]

* **Hold a colour.** Hexes live in
  [nebelung](https://github.com/hausfold/nebelung); a room reads the palette.
* **Hold an app's own logic.** A room installs and configures the app; it is
  not the app.
* **Switch another room on.** A standalone `darwinModules.<room>` import is the
  bare foundation plus that one room, and stays true only because no room
  activates its neighbours.
