aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/companion/README.md
blob: 4fbe887b7ac1b5921e83dc7808ae865340f42b56 (plain)
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
100
101
102
103
104
105
106
107
108
109
110
# 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, per-app and per-artist 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.
- What is playing is `modules/sound/Player.qml`, which already drops the
  playerctld and browser-integration duplicates. A new artist+title while
  playing makes her DJ for `Mood.T.djHold`, 6 s (`dj` and `dj-scratch`
  cycling), and say the artist's line if one matches: lowercased keys in
  `Lines.artists`, matched as a substring, edited on Settings > Companion
  artists into `Prefs.companionArtists`. Pause and resume of the same
  track is not a change; a shell start with music on DJs once. While
  music plays, the first idle stage is a groove instead of bored:
  `whistle`, `headbang` (`headbang-up`/`-down` cycling) or `airguitar`,
  picked at random, with a `groove` line; yawning and lying down are
  unchanged.
- 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