# Ayn — the watcher
**Ayn** (عين, *"eye"*) is the smallest of the five programs, and optional. It is the
window manager's eyes on the rest of the machine. Whether a device is in use or a
switch is flipped is a fact about the machine, not about a window, so the window
manager does not read it; Ayn does, and hands each fact over as a
[context](configuration.md#contexts) the window manager holds while the fact is true.
Out of the box it watches the camera and the microphone, and supplies three facts:
`camera-in-use` and `microphone-in-use`, from the record Windows keeps of which
programs have a device open — the one the privacy indicator in the tray reads — and
`microphone-muted`, from the mute switch of the default communications microphone,
falling back to the default one — the switch the Sound settings toggle. Seven more
are there for the asking, off until the file names them: `screen-captured` (a program
is sharing or recording the screen, from the same record), `speaker-muted`,
`on-battery`, `battery-low`, `lid-closed`, `user-away` (Windows's own judgement that
nobody is at the keyboard — the one that dims the display) and `dark-theme`. Facts are
named `subject-state`, so the ones about one device sit together and the next device
slots in beside them. Three readings can also go to the bar as words rather than as
facts — the battery's percentage and the names of the default speaker and microphone;
see [Values](#values).
It is its own program, `ayn`, started by the window manager from the config
(`startup-command "ayn"`), and it reads the `ayn` section of the same file.
```kdl
contexts {
context "camera-in-use" { } // facts: nothing in the file sets them, ayn does
context "microphone-in-use" { }
context "microphone-muted" { }
context "meeting" { // policy: yours to write
when { context "microphone-in-use" }
bindings { bind "alt+shift+q" { } }
}
context "meeting-muted" { when { context "meeting"; context "microphone-muted" } }
}
ayn {
camera { in-use "camera-in-use" }
microphone { in-use "microphone-in-use"; muted "microphone-muted" }
settle 500 // ms a change of use must last; mute is instant
}
```
The names must be declared in `contexts`, and `shubbak check-config` says so when one
is not. `camera #false` says nothing about the camera at all; `muted #false` turns one
fact off. `settle` is how long a change of use must last before it is believed: a call
opens and closes the devices several times while it is setting up. The mute is never
settled, because a person who pressed the key wants the icon now; nor are the power
facts or the theme.
The rest of the section, all optional:
```kdl
contexts {
context "camera-in-use" { }
context "sharing" { }
context "in-a-call" { }
context "speaker-muted" { }
context "on-headset" { }
context "unplugged" { }
context "low-battery" { }
context "away" { }
context "lid-closed" { }
context "dark" { }
}
ayn {
camera {
in-use "camera-in-use"
ignore "obs64.exe" "Lens*" // these do not count as use
by "ms-teams.exe" "in-a-call" // a context of its own for one program
}
screen { captured "sharing" }
speaker {
muted "speaker-muted"
device "*barracuda*" "on-headset" // while the headset is what sound goes to
}
power { on-battery "unplugged"; battery-low "low-battery"; battery-low-at 15; user-away "away"; lid-closed "lid-closed" }
theme { dark "dark" }
renew 60 // re-assert every held context each minute
}
```
`ignore` names programs whose use of a device does not count — the recording tool
that keeps the camera open all day is not a meeting — and `by` gives one program's
use a context of its own, so a Teams call and an OBS stream can be told apart without
any context knowing the difference between programs. Both take the program's name as
it appears in `ayn --report`, with `*` and `?` as wildcards and no regard for case.
`microphone` and `screen` take the same two.
`device` is about which device is the default rather than who is using it: `speaker {
device "*barracuda*" "on-headset" }` holds `on-headset` while the default speaker is
one whose name matches, and lets go the moment Windows moves the default — to the
monitor when the headset is unplugged, say. `microphone` takes it too, for the webcam's
microphone against the headset's. The name is the one Windows shows in its sound
settings and `ayn --report` prints, with the same wildcards and no regard for case.
Nothing to settle here: plugging a headset in is one event, and the person who did it
is looking at the bar. A rule missing its name or its context is pointed out and
skipped (`AYN0011`).
`battery-low-at` is the percentage `battery-low` starts at; twenty unless said.
`renew` is off unless said: with it, every held context is asserted again that many
seconds apart with a time to live of twice that, so a watcher that is alive but stuck
— connection open, loop wedged — loses its pins too, a little after it stops renewing
them. It is off by default because a window manager stalled for longer than the time
to live would drop the context and take it back a moment later, running whatever the
file hangs on that.
Two facts naming one context are pointed out (`AYN0006`), because each hands the
context back when it goes false and takes the other's pin with it; the file wants two
contexts and a third composed from them. `camera "meeting"` is read as
`camera { in-use "meeting" }` and said so (`AYN0007`).
## Values
A fact is a yes or a no, and a context is what the file can hang behaviour on. The
battery's percentage is not that — nothing should change at 41 percent that did not
change at 42 — and yet the number is worth showing, and the watcher already read it
for `battery-low` and threw it away. Three readings can therefore go out as **values**:
words for the bar rather than contexts for the window manager, published as a
[signal](scripting.md#signals) the window manager carries without reading, and shown
by a `source ... kind="signal"` in the [bar's section](taj.md#sources).
```kdl
ayn {
power { battery-percent "battery" } // signal "battery" "41"; empty when there is none
speaker { device-name "speaker" } // signal "speaker" "Speakers (Realtek(R) Audio)"
microphone { device-name "microphone" } // the default microphone's name
}
bar {
source "battery" kind="signal"
source "speaker" kind="signal"
profile "default" {
zone "right" justify="end" {
text template="{{ battery }}%"
text template="{{ speaker | truncate:24 }}"
}
}
}
```
Each is off until named, like every fact added since the first three; `#true` names it
after its subject — `battery`, `speaker`, `microphone` — which is what the bar's source
would be called anyway. The name is a signal's, not a context's, so it is checked
against nothing; an empty one is pointed out and off (`AYN0012`). A value is said when
it changes and not otherwise, and said again whenever a bar asks with
`signal "announce"`, which a bar does when it connects with a signal source in its
file — so a bar that starts after the watcher is not blank until the battery next
moves. A value the file stops naming, or renames, is cleared once under its old name,
so no bar goes on showing the last number it heard. A device name that Windows spells
with a quote in it arrives as it is unless it holds both kinds, which the command
language cannot write; then its double quotes become the typographic kind rather
than the value being dropped.
## What it acts on
Two things: `signal "ayn" "microphone" "mute" | "unmute" | "toggle-mute"` from a
keybinding, the bar or the palette flips the system mute, and the endpoint's own
change notification turns that into the context — so a bar widget that reads
`{{ context.meeting-muted | then:\u{EC54} }}` with `on-click="signal ayn microphone
unmute"` is a mute button, and the window manager never learns the word. This is the
system's mute: a call's own mute button is the call's and invisible from here, but
Teams and its kind notice this one and say "muted by your system". `signal "ayn"
"speaker" "toggle-mute"` does the same for the default speaker.
## Why it is a separate program
The pins are made with `--lease`, so they die with Ayn's connection: a watcher that
crashes leaves nothing behind, and a window manager that restarts is told again within
a second. The file says what a meeting *does*; Ayn never needs to know. That division
is the point of it — Shubbak observes the desktop, not the applications, and whether
the camera is on is a fact about an application and a piece of hardware. Everything
of that kind lives out here rather than in the daemon, because "it is only a hundred
lines" is true of every device that would follow the camera, and is how a window
manager grows a weather widget. Ayn is where those lines go: the next device is
another subject in its section, not another thing the window manager knows. Anything
with the same shape that Ayn does not watch — Teams presence, OBS recording, a
calendar — is written the same way: `shubbak context --set --hold` from a
script that knows when it starts and ends, or a pipe connection of your own held open
around `context --set --lease`. See [Scripting](scripting.md#supplying-facts).
## Running it
`ayn --report` prints what Windows says about each device, the power and the theme
right now, which is the same reading the watcher acts on. It outlives a `wm-exit` and
reconnects when the window manager returns; `exit-all` takes it down with everything
else, as do `shubbak ayn-exit` on its own and `shubbak stop` from outside. Only one
watcher runs per account. It sleeps on registry notifications, Core Audio callbacks
and power notifications and holds no timer between changes — a microphone unplugged
or a speaker swapped is noticed the same way — so an idle watcher costs nothing.
Sources the file does not name are never opened, and a device the file starts naming
after a reload is opened then, without a restart.