docs: design spec für Noctalia-Plugin loki/headsetcontrol

This commit is contained in:
loki committed 2026-08-31 11:25:41 +02:00
commit 85b048feef
1 file changed
+160
@@ -0,0 +1,160 @@
# Design: Noctalia-Plugin `loki/headsetcontrol`
Datum: 2026-08-31 · Status: genehmigt (abschnittsweise reviewed am 31.08.2026)
## Ziel
Ein Noctalia-v5-Plugin, das [HeadsetControl](https://github.com/Sapd/HeadsetControl) (CLI) ansteuert,
um Gaming-Headsets aus der Shell heraus zu überwachen und zu steuern. Referenzgerät ist das
**Audeze Maxwell** des Autors; das Plugin soll aber geräteübergreifend funktionieren, indem es die
vom Gerät gemeldeten Capabilities auswertet.
## Umgebung / Fakten (verifiziert)
- Noctalia v5.0.0 läuft, Plugin-System in Beta (Plugin APIs 3–28).
- HeadsetControl installiert unter `/usr/bin/headsetcontrol`, Ausgabe `-o json` (api_version 1.4).
- Detektiertes Gerät: `Audeze Maxwell` (`0x3329:0x4b19`), Capabilities:
`sidetone`, `battery`, `inactive time`, `chatmix`, `voice prompts`,
`equalizer preset` (10 Presets), `volume limiter`, `microphone noise filter`.
- Batterie aktuell `BATTERY_UNAVAILABLE` (Headset aus). Chatmix ist read-only (nur Anzeige).
- Ein kombinierter Aufruf `headsetcontrol -b -m -o json` liefert Batterie + Chatmix in einer JSON-Antwort.
## Architektur
**Ansatz A (gewählt): Service-zentrale Architektur.**
- Ein `[[service]]` pollt den Headset-Status im konfigurierbaren Intervall und publiziert ihn über
den Noctalia-State-Channel (`noctalia.state`).
- `[[widget]]` (Bar), `[[panel]]` (Steuerung) und `[[shortcut]]` (Control-Center-Tile) konsumieren
den State und senden Steuerbefehle.
- Das Panel sendet Befehle direkt per `noctalia.runAsync(argv, cb)` (argv-Array, `plugin_api = 24`,
kein Shell-Parsing bei dynamischen Werten).
- Capabilities werden einmalig beim Start per `--capabilities -o json` erkannt und über State geteilt.
- Battery-Warnung lebt im Service (läuft immer), dedupliziert mit Hysterese.
### Komponenten
| Komponente | Datei | Aufgabe |
|---|---|---|
| Manifest | `plugin.toml` | Identität `loki/headsetcontrol`, Entries, Settings-Schema |
| CLI-Wrapper | `lib/hsc.luau` | JSON-Parsing, Command-Builder (argv-Arrays), geteilt zwischen Entries |
| Service | `service.luau` | Capabilities lesen, Status pollen, Battery-Warnung, State publizieren |
| Widget | `widget.luau` | Bar-Widget: Batterie + Farben, Scroll = Sidetone, Klick = Panel öffnen |
| Panel | `panel.luau` | Steuerungs-UI (capabilities-gesteuert) |
| Shortcut | `shortcut.luau` | Control-Center-Tile: Sidetone an/aus |
| Translations | `translations/en.json`, `translations/de.json` | UI-Texte + Setting-Labels |
| Docs | `README.md`, `thumbnail.webp`, `LICENSE`, `catalog.toml` | Publishing-fertig |
## Manifest
```
id = "loki/headsetcontrol"
name = "Headset Control"
version = "1.0.0"
plugin_api = 24
author = "loki"
license = "MIT"
dependencies = ["headsetcontrol"]
```
Entries: `[[service]]`, `[[widget]]`, `[[panel]]`, `[[shortcut]]`.
### Settings (Plugin-Level, `[[setting]]`)
| Key | Typ | Default | Beschreibung |
|---|---|---|---|
| `poll_interval` | int (10–600) | 60 | Poll-Intervall in Sekunden |
| `battery_low` | int (5–50) | 20 | Warnschwelle Batterie in % |
| `sidetone_default` | int (0–128) | 64 | Startwert Sidetone |
| `device` | string | `""` | Leer = Auto-Detect; sonst `VID:PID` |
| `mock` | bool, advanced | `false` | Simuliert Audeze Maxwell (Entwicklung ohne Headset) |
## Datenfluss / State-Channel
Service publiziert (`noctalia.state`):
- `caps` — Liste unterstützter Features (`{"sidetone", "battery", ...}`) + `equalizer_presets_count`
- `battery` — `{ level, status }` (`status` ∈ `BATTERY_CHARGING/FULL/NORMAL/DISCHARGING/UNAVAILABLE`)
- `chatmix` — int (negativ = Game lauter, positiv = Chat lauter, 0 = Mitte)
- `device` — `{ name, vendor, product, id_vendor, id_product }`
- `sidetone` — letzter gesetzter Wert (Sidetone ist nicht auslesbar → lokaler Merker)
- `lastUpdate` — ms-Timestamp
- `error` — letzter Fehlerstring (oder `nil`)
Widget/Panel/Shortcut watch die Keys und re-rendern.
## Entries im Detail
### Service (`service.luau`)
- Top-Level: Capabilities einmalig lesen (respektiert `device`), in `state.caps`/`state.device` publizieren.
- `update()` (Intervall `poll_interval`): `headsetcontrol [-d V:P] -b -m -o json` → State publizieren.
- **Battery-Warnung:** Wenn `level <= battery_low` und nicht schon gewarnt → `noctalia.notifyError`
(dedupliziert; Re-Arm erst wenn Status charging/full oder `level > battery_low + 5`).
- `onConfigChanged()`: Intervall/Schwelle/Device live übernehmen.
- `onIpc("refresh", ...)`: manueller Re-Poll (für `noctalia msg plugin loki/headsetcontrol:service all refresh`).
- Fehlerzustand (Binary fehlt, kein Gerät, Command-Fehler) wird als `state.error` publiziert statt zu spammen.
### Widget (`widget.luau`) — imperative API
- Watcht `battery`/`chatmix`/`caps`/`error`.
- Anzeige: Glyph (Battery, Variante für Charging) + `%`-Text.
- Farben: `>= battery_low` normal; `< battery_low` `error`; `BATTERY_UNAVAILABLE`/kein Gerät gedimmt mit `–`.
- **Scroll (vertikal):** Sidetone ±4 (0–128), Command senden, neuer Wert kurz als Text anzeigen.
Startwert = `sidetone_default` bzw. letzter State-Wert.
- **Klick:** `noctalia.togglePanel("loki/headsetcontrol:panel")`.
- Tooltip: Gerätename + Status + Chatmix.
- `[widget.actions]`: keine festen Bindings (Skript-Callbacks behalten Vorrang).
### Panel (`panel.luau`) — deklarative `ui.*`-UI
- Manifest: `width = 400`, `height = 480`, `placement = "floating"`, `dismiss_on_outside_click = true`.
- Rendert in `onOpen(context)` und bei State-Updates/Commands.
- Kopf: Gerätename, Batterie-Progressbar + %, Chatmix-Balance-Anzeige.
- Controls (nur sichtbar bei unterstützter Capability):
- Sidetone: Slider 0–128 + Off-Button.
- EQ-Presets: `ui.select` mit `Preset 0..N-1` (N aus `equalizer_presets_count`).
- Inactive Time: `ui.select` (Nie, 1, 2, 5, 10, 15, 20, 30, 45, 60, 90 min).
- Noise Filter: 3 Buttons (Off/Low/High).
- Voice Prompts: `ui.toggle`.
- Volume Limiter: `ui.toggle`.
- Befehle: `noctalia.runAsync({"headsetcontrol", "-d", dev, <flag>, <wert>}, cb)`.
- `exitCode ~= 0` → `noctalia.notifyError` (stderr-Kurzfassung), optimistischen Wert zurücksetzen.
- Erfolg → State `sidetone` aktualisieren; Service-Poll bestätigt den realen Zustand.
### Shortcut (`shortcut.luau`)
- Tile „Sidetone": Toggle an = letzter Wert/`sidetone_default`, aus = 0.
- `setLabel` zeigt Batterie, `setActive` spiegelt Toggle-Zustand.
- `onConfigChanged()` nicht nötig (Shortcut ist Widget-artig, wird bei Settings-Änderung neu gebaut).
## Fehlerbehandlung
- `headsetcontrol` fehlt (`noctalia.commandExists` false) → Widget: Warn-Glyph + Tooltip,
Service loggt einmalig, keine Spam-Notifications.
- Kein Gerät / `BATTERY_UNAVAILABLE` → normaler Zustand: gedimmte Anzeige, keine Errors.
- Command-Fehler → `notifyError` + UI-Rollback des optimistischen Werts.
- Fehlende State-Keys (Service noch nicht gestartet) → Defaults, keine Crashes.
## Testing
1. `mock = true`: fake Maxwell-Daten, komplette UI ohne Headset testbar.
2. IPC: `noctalia msg plugin loki/headsetcontrol:service all refresh`.
3. Hot-Reload der Luau-Dateien während der Entwicklung.
4. Live-Verifikation realer Commands gegen das Maxwell im Terminal
(`-s`, `-p`, `-i`, `--noise-filter`, `-v`, `--volume-limiter`).
5. Dev-Install unter `~/.local/share/noctalia/plugins/headsetcontrol/`
(höchste Source-Priorität, übersteuert alle Quellen).
## Publishing (optional, später)
- Repo-Struktur: Plugin im Unterordner `headsetcontrol/`, `catalog.toml` am Repo-Root.
- PR gegen `noctalia-dev/community-plugins` (CI validiert Manifest + Katalog).
- Dev-Install bleibt über das `path`-Source-Override möglich.
## Nicht im Scope (YAGNI)
- Launcher-Provider, Desktop-Widget.
- Chatmix schreiben (HeadsetControl unterstützt das nicht).
- EQ-Preset-Namen (API liefert keine; nur `Preset N`).
- I18n über `en`/`de` hinaus.