# 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 тулов