- Web-based key editor (Alpine.js SPA) - Actions: shell commands, scripts, built-in media/volume/brightness, page switching - Keyboard shortcut action (wtype/xdotool) - On-device display with periodic command output - Auto page switching (Hyprland, Sway, Niri, GNOME, KDE, X11) - Screensaver with idle detection - Config hot-reload - Multi-device support
162 lines
7.9 KiB
Markdown
162 lines
7.9 KiB
Markdown
# StreamController → Go: план портирования
|
||
|
||
## Цель
|
||
|
||
Портировать StreamController (Python/GTK4 GUI для Elgato Stream Deck) на Go.
|
||
Один статический бинарник, конфигурация через JSON, веб-интерфейс для настройки.
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
streamdeck-lets-go
|
||
├── main.go # CLI entry: "serve" → web, иначе → daemon
|
||
├── daemon.go # главный event loop (deck + autoswitch + screensaver)
|
||
├── config.go # JSON config types + load/save/validate
|
||
├── deck.go # обёртка над dh1tw/streamdeck + PID config map
|
||
├── page.go # страницы, ключи, рендер
|
||
├── action.go # exec.Command + built-in D-Bus экшены
|
||
├── autoswitch.go # детект активного окна (GNOME/Hyprland/Sway/KDE/X11)
|
||
├── screensaver.go # idle/lock детекция (logind D-Bus)
|
||
├── media.go # MPRIS D-Bus helpers
|
||
└── web/
|
||
├── server.go # HTTP API + embed static
|
||
└── static/ # index.html + htmx app.js
|
||
```
|
||
|
||
## Зависимости
|
||
|
||
| Пакет | Зачем |
|
||
|---|---|
|
||
| `github.com/dh1tw/streamdeck` | HID общение с Stream Deck (bearsh/hid) |
|
||
| `github.com/godbus/dbus/v5` | D-Bus (медиа, screensaver, GNOME) |
|
||
| stdlib (embed, image, net/http, os/exec, slog, flag) | — |
|
||
|
||
CGO требуется из-за bearsh/hid (libusb).
|
||
|
||
## Поддерживаемые девайсы
|
||
|
||
| Модель | PID | Keys | Key Size | Format |
|
||
|---|---|---|---|---|
|
||
| Mini | 0x60 | 3×2=6 | 80px | BMP |
|
||
| Original V1 | 0x6d | 5×3=15 | 72px | JPEG |
|
||
| Original V2 | 0x63 | 5×3=15 | 72px | BMP |
|
||
| MK.2 | 0x80 | 5×3=15 | 72px | JPEG |
|
||
| XL | 0x6c | 8×4=32 | 96px | JPEG |
|
||
|
||
## JSON Config
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"default_page": "main",
|
||
"log_level": "info",
|
||
"devices": [
|
||
{"serial": "AL12345", "brightness": 75}
|
||
],
|
||
"pages": [
|
||
{
|
||
"name": "main",
|
||
"icon": "icons/main.png",
|
||
"keys": [
|
||
{
|
||
"index": 0,
|
||
"icon": "icons/play.png",
|
||
"label": "Play/Pause",
|
||
"action": {"type": "builtin", "builtin": "media:playpause"}
|
||
}
|
||
],
|
||
"background": "wallpapers/bg.jpg",
|
||
"screensaver": {"image": "ss.png", "delay": 30, "brightness": 10}
|
||
}
|
||
],
|
||
"auto_switch": [
|
||
{
|
||
"wm_class": "firefox",
|
||
"title": ".*",
|
||
"page": "browser",
|
||
"devices": ["*"],
|
||
"stay": false
|
||
}
|
||
],
|
||
"screensaver": {
|
||
"enabled": true,
|
||
"idle_seconds": 30,
|
||
"image": "screensaver.png",
|
||
"brightness": 10
|
||
}
|
||
}
|
||
```
|
||
|
||
## Порядок реализации
|
||
|
||
1. **main.go** — CLI mux (daemon / serve / discover), signal handling
|
||
2. **config.go** — типы, Load, Save, Validate
|
||
3. **deck.go** — PID map, Deck wrapper (open, render, brightness, events)
|
||
4. **page.go** — PageManager, ActivatePage, Key render
|
||
5. **action.go** — ExecuteAction (command, builtin, script, page switch)
|
||
6. **daemon.go** — event loop (key press → action, reconnect, SIGHUP reload)
|
||
7. **autoswitch.go** — WindowDetector интерфейс, DE-specific реализации
|
||
8. **screensaver.go** — idle детекция, suspend/resume
|
||
9. **web/** — embed FS, REST API, HTMX frontend
|
||
|
||
## REST API (web)
|
||
|
||
| Endpoint | Method | Description |
|
||
|---|---|---|
|
||
| `/api/config` | GET | Получить config.json |
|
||
| `/api/config` | PUT | Сохранить config.json (и перезагрузить) |
|
||
| `/api/status` | GET | Состояние: девайс, страница, статистика |
|
||
| `/api/page/{name}` | POST | Переключить страницу |
|
||
| `/` | GET | index.html (SPA) |
|
||
|
||
## Что нужно доделать / Known Gaps
|
||
|
||
### P0 — Баги / неработающие фичи
|
||
|
||
#### P0.1 — Screensaver не вызывается ✅
|
||
|
||
**Проблема**: `ssTicker` создаётся в daemon loop (daemon.go:78), но в `select` нет `case <-ssTicker.C`. `Check()` никогда не вызывается. Кроме того, `NewScreensaver` не проставляет `ss.enabled` из конфига.
|
||
|
||
**Решение**:
|
||
1. `screensaver.go`: `NewScreensaver` — добавлен `ss.enabled = cfg.Enabled`
|
||
2. `screensaver.go`: добавлены методы `Activate(deck, cfg)` (гасит яркость, показывает image на панель) и `Deactivate(deck, pm)` (восстанавливает яркость)
|
||
3. `daemon.go`: добавлена переменная `ssCtrl`, добавлен `case <-ssTicker.C` в select, вызывающий `ssCtrl.Check()` → `ssCtrl.Activate()`
|
||
4. `daemon.go`: в обработчике key press вызывается `ssCtrl.NotifyInput()` и при необходимости `ssCtrl.Deactivate()`
|
||
5. `screensaver.go`: `Check()` теперь возвращает `true` только при переходе в active (не флудит Activate каждые 5с)
|
||
|
||
#### P0.2 — Brightness up/down — заглушка ✅
|
||
|
||
**Проблема**: `deckBuiltinAction` (action.go:210-221) всегда ставит 100 или 25, нет циклического переключения.
|
||
|
||
**Решение**:
|
||
1. `deck.go`: добавлено поле `brightness int` в `Deck`, инициализируется 75, обновляется в `SetBrightness`, добавлен геттер `Brightness()`
|
||
2. `action.go`: `deckBuiltinAction` — цикл 25→50→75→100→25 (up) и 100→75→50→25→100 (down)
|
||
3. `daemon.go`: добавлен `deviceBrightness(cfg)` helper, стартовый `SetBrightness` читает из `cfg.Devices[0].Brightness`
|
||
4. `daemon.go`: reconnectTicker заменён с `SetBrightness(75)` на `SetBrightness(deck.Brightness())` (no-op check) + `deviceBrightness(cfg)` после reconnect
|
||
5. `daemon.go`: configTicker при hot-reload теперь применяет `deviceBrightness(cfg)`
|
||
|
||
### P1 — Функциональные дыры
|
||
|
||
- [ ] **Multi-device**: `OpenDeck("")` игнорирует `devices[n].serial`. Первый девайс всегда открывается первым попавшимся HID.
|
||
- [ ] **Grid в UI жёстко 5×3**: Web UI не адаптируется под Mini (6 ключей) или XL (32). API возвращает всегда 15 слотов.
|
||
- [ ] **Нет проверки external зависимостей при старте**: `playerctl`, `wpctl`, `brightnessctl`, `rsvg-convert`, `xdotool` — если не установлены, ошибка всплывает в рантайме без понятного сообщения.
|
||
- [ ] **Конфиг хранит координаты клавиш (index)**, но UI не позволяет их перетаскивать — нет drag-and-drop, нет reorder.
|
||
- [ ] **No README**: ни описания установки, ни списка зависимостей, ни примера запуска.
|
||
|
||
### P2 — Качество кода / инфраструктура
|
||
|
||
- [ ] **Тесты**: только `autoswitch_test.go` (1 файл, 7 тестов). Нет покрытия для config, deck, page, web, action, render.
|
||
- [ ] **CI / сборка**: нет Makefile, Dockerfile, systemd unit, миграции конфига (только v1).
|
||
- [ ] **Паник-безопасность**: горутины display capture не имеют recover; одна паника убивает весь daemon.
|
||
- [ ] **Request logging**: HTTP сервер без middleware — нет access логов.
|
||
- [ ] **No WebSocket fallback**: Display outputs poll раз в 3с вместо push.
|
||
|
||
### Не в приоритете (зафиксировать)
|
||
|
||
- [ ] Folder/paging groups (вложенные страницы)
|
||
- [ ] Multi-action / макросы (chain команд)
|
||
- [ ] Поддержка SD+, SD Neo, SD LCD (touch + dials)
|
||
- [ ] Анимации (GIF/APNG на клавишах)
|
||
- [ ] HTTPS / auth
|
||
- [ ] D-Bus implementation (MPRIS, logind) вместо CLI тулов
|