- 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
7.9 KiB
7.9 KiB
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
{
"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
}
}
Порядок реализации
- main.go — CLI mux (daemon / serve / discover), signal handling
- config.go — типы, Load, Save, Validate
- deck.go — PID map, Deck wrapper (open, render, brightness, events)
- page.go — PageManager, ActivatePage, Key render
- action.go — ExecuteAction (command, builtin, script, page switch)
- daemon.go — event loop (key press → action, reconnect, SIGHUP reload)
- autoswitch.go — WindowDetector интерфейс, DE-specific реализации
- screensaver.go — idle детекция, suspend/resume
- 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 из конфига.
Решение:
screensaver.go:NewScreensaver— добавленss.enabled = cfg.Enabledscreensaver.go: добавлены методыActivate(deck, cfg)(гасит яркость, показывает image на панель) иDeactivate(deck, pm)(восстанавливает яркость)daemon.go: добавлена переменнаяssCtrl, добавленcase <-ssTicker.Cв select, вызывающийssCtrl.Check()→ssCtrl.Activate()daemon.go: в обработчике key press вызываетсяssCtrl.NotifyInput()и при необходимостиssCtrl.Deactivate()screensaver.go:Check()теперь возвращаетtrueтолько при переходе в active (не флудит Activate каждые 5с)
P0.2 — Brightness up/down — заглушка ✅
Проблема: deckBuiltinAction (action.go:210-221) всегда ставит 100 или 25, нет циклического переключения.
Решение:
deck.go: добавлено полеbrightness intвDeck, инициализируется 75, обновляется вSetBrightness, добавлен геттерBrightness()action.go:deckBuiltinAction— цикл 25→50→75→100→25 (up) и 100→75→50→25→100 (down)daemon.go: добавленdeviceBrightness(cfg)helper, стартовыйSetBrightnessчитает изcfg.Devices[0].Brightnessdaemon.go: reconnectTicker заменён сSetBrightness(75)наSetBrightness(deck.Brightness())(no-op check) +deviceBrightness(cfg)после reconnectdaemon.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 тулов