1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
# companion
A chibi 2B in the bottom-right corner of the focused monitor, on SUPER+\.
The design, with what was measured on the way, is
`docs/superpowers/specs/2026-10-23-companion-design.md`.
## Files
| File | |
| --- | --- |
| `Companion.qml` | the window, inputs, timers, speech, IPC (`companion toggle/show/hide`) |
| `Figure.qml` | draws a frame: crossfade, bob, drop out and back, explosion |
| `Mood.js` | which state wins, from a snapshot; all the timings are its `T` |
| `Lines.js` | her reaction and per-app lines, and the opening monologue |
| `Admired.qml` | the wallpaper list she blushes at, and what wallp has on each monitor |
| `ListFile.js` | that list as plain text: parse and toggle |
| `prep-frames` | generated images to display frames |
| `gen-frames`, `frames.txt` | the generated images, through sd-server |
`Figure.qml` is not `Sprite.qml` because QtQuick has a `Sprite` type, and
it wins over the directory's own file (see AGENTS.md).
Whether she is shown is `companion` in `Prefs.qml`, so it survives a
restart: SUPER+\ and the switch on Settings > Companion
(`desktop/modules/companion/`) both set it.
A hide by SUPER+\ or that switch plays a goodbye first: she waves (`wave`,
`wave-l` and `wave-r` cycling) and then drops out. One hide in four she also
says a `bye` line, under the usual 30 s line cooldown; with a line she holds
`Mood.T.goodbye` (2 s, enough to read it), silently only
`Mood.T.goodbyeSilent` (0.5 s). Fullscreen, the lock screen and the monitor
hop keep their own exits and do not wave.
## Art
Not in the repo: the character is Square Enix's and this repo is GPLv2.
It lives in `~/.local/share/keystone/companion/2b/`:
dress/<name>.png, leotard/<name>.png generated, 1024x1024 on white
frames/<outfit>/<name>.png what she shows, from prep-frames
recipe.txt how each was made, and what failed
Run `prep-frames` after adding or replacing any generated image. A missing
frame falls back to its outfit's `base`.
## Inputs
- Typing and BackSpace come from `sections/companion.lua` in the
hypr-theme repo, as `custom>>keystone-typing` and
`custom>>keystone-delete` on socket2. Nothing else about the keyboard
reaches her. The BackSpace bind is `non_consuming`; checked by hand that
it still deletes in a terminal and in the browser.
- The typing pulse skips modifiers and any key pressed with Ctrl, Alt or
Super held, so a shortcut is not typing; Shift and AltGr still count.
She watches only from the second pulse within 2.5 s, so a lone key is
not typing either.
- She is on the Overlay layer and remaps whenever another layer surface
opens (Hyprland's `openlayer` event: the assistant's dim, rofi, the
keybind reminder), vanishing and hopping back in, because Hyprland
stacks a later surface above an earlier one on a layer, and 0.56.2 has
no layer rule to order them (no `order` key in the binary; an unknown
key raises no config error).
- The program in front of her is Hyprland's `activewindow` event,
`class,title`. It also fires on every title change, so she reacts
only when the class changes, a third of the time, under the 30 s line
cooldown. Lines are by lowercased class in `Lines.js`; a class with
none says nothing. Settings > Companion > Companion phrases edits them
into `Prefs.companionApps`, which then replaces the defaults.
- A listed wallpaper (Settings > Companion for the file, the
heart in the wallpaper picker for entries) on an empty workspace: she
turns to it with `watch` and blushes with `blush3`, at most every 3
minutes. Checked on `workspacev2` and `closewindow`, and whenever she
stops being away. Her monitor's wallpaper comes from wallp's
`wallp.conf` outputs and `wall_h`/`wall_v`.
- Sony headphones connecting are the island's rule (`island/StatusPill.qml`:
`WH-`/`WF-`/`LinkBuds` by name, since BlueZ has no headphones class): she
sings once for `Mood.T.singHold`, 4 s, and says a line if the usual 30 s
cooldown allows it. A shell start with them already on is not a connection:
Bluetooth reads empty for ~2 s, then populates.
- She only feels the pointer while it is over her: Quickshell has no
global pointer and Hyprland sends no motion events. Hence the 0.6 s
dodge rather than moving away as the pointer approaches.
- Idle stages are 20, 40 and 70% of wscreensavers' `idleTimeoutMinutes`,
read live from its config, so she lies down before the saver starts.
- `IdleMonitor.enabled` read false on creation when left out, so each of
the three sets it. It also reads false at `Component.onCompleted` even
when set, and true a moment later; do not trust that first reading.
- The monitors ignore inhibitors. No lock holds an idle inhibitor for as
long as it is on, and with `respectInhibitors: true` she never idled
(measured: a 5 s monitor respecting them never fired, one ignoring them
did). Presentation and gaming mode keep her awake by name instead.
- Fullscreen is `lastIpcObject.fullscreen === 2`. Maximised is 1, and
most windows here are maximised.
- The lock screen covers every layer, so locking needs no code.
## Check
node desktop/companion/test_mood.js
node desktop/companion/test_lines.js
|