1
0
Maksim Totmin 3ae704ec48 Initial commit: Stream Deck daemon with web UI
- 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
2026-06-16 22:30:38 +07:00

7.9 KiB
Raw Blame History

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
  }
}

Порядок реализации

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