# Notifications (/docs/haus/rooms/notifications)



Every banner this desktop draws goes through one command, `haus-notify&#x60;: the bar
reloading, a rebuild finishing, an agent lane blocking on you. That command hands
the event to &#x2A;*[trill](/docs/trill)** when trill answers on its socket, and falls
back to Apple's own banner when nothing does. It runs on every haus machine, so
no room waits on trill being installed.

Which makes this room the narrower question, and only that one: &#x2A;*does haus own
the bundle?** Switch it on and Trill.app is installed from its own flake, pinned
by your `flake.lock` like every other room, and kept at a path that does not
move.

<Callout type="info" title="There is no haus.notifications.enable">
  The missing name is deliberate. `haus.notifications.enable = false` would read as
  "this Mac draws no haus notifications", and that is false on every machine:
  `haus-notify` runs either way and Apple's banner is always the floor.
  `compositor` is the one question the room actually answers.
</Callout>

## Enable it [#enable-it]

```nix
haus.notifications.compositor = true;
```

```sh
haus rebuild
```

It is off by default, and not only out of taste. There is no `trill` cask, and
the `trill` command already resolves without this room (see [What it does not
do](#what-it-does-not-do)), so turning it on is a decision to let haus own the
bundle rather than a step you were missing.

## Why the path is fixed [#why-the-path-is-fixed]

The bundle is copied to &#x2A;*`/Applications/Trill.app`** and never run from the Nix
store. That is the room's whole design, and it is not tidiness.

trill's `trill doctor`, its System Mirror and its "Silence Native Banners" helper
all rest on a **Full Disk Access** grant, and macOS keys a TCC grant to an app's
*path* plus its signing identity. A store path changes on every version bump, so
a store-run trill would lose the grant on exactly the rebuild that installed the
fix you wanted. `/Applications/Trill.app` is where a cask or a drag install would
have put it, so a grant you have already given carries over instead of being
asked for again.

The other half of that key is the signature, and nothing here disturbs it: the
release `.app` is Developer-ID signed and notarized, and the copy preserves both
the signature and the stapled ticket. There is no re-signing step in your
rebuild.

## What a rebuild does [#what-a-rebuild-does]

trill is an ordinary background app rather than a launchd daemon. It draws no
Dock icon, and it registers *itself* as a login item the first time it runs, so
nothing but a login (or you) ever starts it. The rebuild has to pay for that:
swapping the bundle under a running compositor would leave the Mac drawing
Apple's banners for the rest of the session.

So a rebuild with a new trill to install stops the running one on purpose, swaps
the bundle, and starts it again in your GUI session without stealing focus. Then
it waits for trill's socket to answer, because a launch being accepted is not the
same thing as a compositor being up.

**A trill you quit on purpose stays quit.** The rebuild only brings back a
compositor that was running when it looked, or one this Mac has never had before,
and it tells you which happened:

| The line                                                                        | What it means                                                      |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `trill: installing /nix/store/… → /Applications/Trill.app`                      | a new version is going in                                          |
| `trill: compositor back up (its socket answers)`                                | it was running, and it is running again                            |
| `trill: left quit …`                                                            | it was not running when the rebuild looked, so nothing was started |
| `warning: trill: the compositor did not answer on its socket after three tries` | the bundle is in place, but nothing is behind the socket           |
| `trill: … was installed by another activation while this one waited`            | two rebuilds at once; the other one did the work                   |
| `trill: ditto failed; leaving any existing /Applications/Trill.app in place`    | the copy failed, and your old bundle was not touched               |

Two of those leave you without a compositor (`left quit`, and the socket
warning), and both have the same answer: `open -g /Applications/Trill.app`, or
leave it and it comes back at your next login.

<Callout type="warn" title="What a Mac with no compositor loses">
  `haus-notify` falls back to Apple's banner, so you still get told things. What
  goes quiet is everything trill adds: your `rules.json` stops applying, the bar's
  `trill` pill draws dim, and any question parked on trill's ledge sits
  unanswered. The ledge is drawn by the app, so while it is down there is nothing
  on screen to click.
</Callout>

## What it does not do [#what-it-does-not-do]

Three things, each left out on purpose:

* **It does not put `trill` on your PATH.** haus answers `trill` on every
  machine, room or no room: a small wrapper resolves `$TRILL_APP`, then
  `/Applications/Trill.app`, then `~/Applications/Trill.app`, and execs the
  binary inside whichever it finds. (Trill.app *is* the CLI. One signed
  executable serves the daemon and every verb.) The wrapper has to resolve at run
  time, because whether a bundle exists is a runtime fact and a build-time
  symlink into one that is missing would be a `trill` your shell finds and every
  call fails on. A second `bin/trill` from this room would collide with it rather
  than back it up.

  The bundle this room places is the one that wins, and that order is
  deliberate: `/Applications` is where a cask, a drag-install and this room all
  put the release, while `~/Applications` is where a build you were testing gets
  left. Home-first meant that build outranked the pinned one *forever* — the
  room rewrites its own path on every activation and can never displace a copy
  sitting in front of it, so the Mac went on calling an old daemon and no
  rebuild said a word. Point `$TRILL_APP` at a bundle to override the pair; it
  is still tried first, and it is how you run a branch build without touching
  the desktop.
* **It does not decide what happens to any one notification.** Routing, batching,
  quieting and dropping by `source` all live in `~/.config/trill/rules.json`,
  which trill reloads when you save it. Every `haus-notify` send carries a
  `--source` (`haus.bar`, `haus.lane`, one per room), so "stop telling me about
  the bar reloading" is a line in that file rather than a rebuild. haus
  deliberately puts no second dial in front of it.
* **It does not own `~/.config/trill/config.json`.** One key in that file is
  haus's: `fontFamily`, written from
  [`haus.fonts.sans.name`](/docs/haus/rooms/appearance#type), so the one option
  that sets the launcher's and the shelf's proportional family sets trill's too.
  The room merges that key and leaves the file a real, writable file rather than
  a symlink into the Nix store, so every other switch in it stays yours and your
  clicks survive a rebuild. The cost is the font row in trill's own Settings:
  trill only refuses a write it can see is generated, and this file is not, so a
  family you pick there holds until the next `haus rebuild` puts the desktop's
  back.

  Two shapes it steps over instead of rewriting: a config that is a symlink (any
  symlink, so a dotfiles link is safe, not only a store one) and a config that is
  not valid JSON. In both it writes nothing and says so on the rebuild's stderr,
  which is the only place it is said. If you keep that file linked, set trill's
  family in it yourself.

## New mail [#new-mail]

The room's other half is a source rather than a switch. `haus.mail` watches a
mailbox over IMAP and draws one card for each message that arrives.

Nothing polls. The connection is held open with IDLE, so the server pushes and a
card lands a second or two behind the mail itself. What draws it is `haus-notify`
again, which means a trill card where trill is installed and an Apple banner
where it is not.

```nix
haus.mail = {
  enable = true;
  address = "you@gmail.com";
  mailboxes = [ "INBOX" ];
};
```

`INBOX` is the one mailbox name IMAP standardises. Everything else is your
provider's own spelling, and Gmail's are bracketed (`[Gmail]/Starred`). Name the
ones you want rather than leaving the list empty: empty means every mailbox on
the account, which on Gmail is every label plus All Mail, so one message would
announce itself three times. haus refuses to build that way.

Then the password, once:

```sh
haus rebuild
haus-secret --check
```

`--check` asks for `MAIL_IMAP_PASSWORD` and keeps it wherever
[`haus.secrets.provider`](/docs/haus/rooms/security#secrets-your-rooms-need) points, which is your login
keychain by default. On a Google account the value is an [app
password](https://myaccount.google.com/apppasswords) rather than the password you
type at a login screen, because Google stopped accepting that one for IMAP.
Minting one needs 2-Step Verification turned on first.

<Callout type="warn" title="An app password is a real key to the mailbox">
  It is static and it can read everything in there, which is why it belongs in the
  keychain and never in your host file. The upside of that shape: revoking it on
  Google's own page is all it takes to cut this Mac off, and no other machine of
  yours notices.
</Callout>

Until there is a password the watcher parks and says so in
`~/Library/Logs/haus-mail.log`. The rebuild is fine either way; the watcher
simply is not up yet.

### What counts as new [#what-counts-as-new]

A mailbox nobody has watched before records where it is and announces nothing,
so switching this on does not draw a card for every unread message already
sitting there. After that a message has to be both unseen and newer than the
last one this Mac announced, which is what stops your laptop announcing mail you
read on your phone at breakfast.

More than five at once collapses into one card saying how many. That is the
morning case: mail that arrived while the Mac was shut is a count, not twenty
banners.

Cards from one mailbox share a trill thread, so a burst folds into a single card
you can hover to open into the list of what folded in. On Gmail each card also
carries an Open pill that goes to the message's own thread. Elsewhere it carries
none, because a link that opens the wrong thing is worse than no link.

The mailbox is opened read-only and only headers are fetched, so nothing here
marks your mail as read.

### Check it without waiting for mail [#check-it-without-waiting-for-mail]

```sh
haus-mail-announce --mailbox INBOX --host imap.gmail.com \
  --address you@gmail.com \
  --password-command "haus-secret MAIL_IMAP_PASSWORD" \
  --state-dir ~/.local/state/haus/mail --test
```

That draws a card for the newest message in the mailbox and leaves the watermark
where it was, so it proves the whole chain (login, fetch, decode, card) without
changing what gets announced next. Drop `--test` for `--dry-run` to print the
card instead of drawing it.

### What the mail half does not do [#what-the-mail-half-does-not-do]

* **No filter of its own.** Which mail deserves a card is already decided twice
  before haus sees it. Your account's filters decide what reaches the mailbox at
  all, so a Gmail filter that skips the inbox draws nothing, and
  `~/.config/trill/rules.json` decides what a `haus.mail` card then does. Quiet
  hours, dropped entirely, one sender silenced: all of it is a line in that file
  rather than a rebuild, which is the same reason `compositor` above has no
  second dial.
* **No OAuth.** Gmail's read scopes are restricted, and an app Google has not
  put through verification has its refresh token expired every seven days. That
  is a watcher which dies weekly, quietly, on a schedule nobody remembers. An
  app password you can revoke in one click is the smaller promise.
* **No message body.** The card carries who it is from and what it is about. A
  preview is three lines of somebody else's writing on your screen, and trill
  already drops a card's body while anything is being shared or recorded.

## Works with [#works-with]

* **[Bar](/docs/haus/rooms/bar)**: the `trill` pill opens the inbox in one click,
  and right-click (or ⌥-click) opens it filtered to the asks still waiting on
  you. That pill draws nothing at all unless Trill.app is installed, which is
  what this room guarantees.
* **[AI](/docs/haus/rooms/ai)**: with this room on, trill's own agent skill is
  installed beside `haus` and scruff's, so an agent asked to "tell me when this
  finishes" reaches for `trill` rather than guessing. The skill follows the
  *room*, not the app: a Mac with a hand-installed Trill.app and this room off
  gets no skill, because a skill for an app the machine may not have is worse
  than none.
* **[Apps](/docs/haus/rooms/apps)**: Trill joins `haus.roster` as an app haus
  installed, so `haus.roster.trill.key` gives it a launcher letter without your
  host file restating where the app came from.
* **[Focus](/docs/haus/rooms/focus)**: the other half of interruptions. Focus
  decides whether anything gets through; this room decides who draws what does.

## Remove it [#remove-it]

```nix
haus.notifications.compositor = false;
```

The room stops installing and updating it. The copy already at
`/Applications/Trill.app` stays until you delete it: a rebuild never takes an app
out of your Applications folder behind your back. `haus-notify` and the `trill`
command go on finding it there, which is what "the room is about ownership, not
about the app" means in practice. The `fontFamily` the room wrote into
`~/.config/trill/config.json` stays too, for the same reason, and stops being
re-asserted: it is an ordinary line in a file that is yours, so edit or drop it
whenever you like.

## Options [#options]

Every setting, with types and defaults:
[`haus.notifications` and `haus.mail`](/docs/haus/reference/options#notifications).
