@sirenodeck/sirenodeck 0.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/bin/_wrapper-shared.js +42 -0
- package/bin/dev.js +95 -0
- package/bin/sirenodeck.js +42 -0
- package/config/default-config.yml +117 -0
- package/config/starter-config.yml +54 -0
- package/dist/main.mjs +64702 -0
- package/package.json +140 -0
- package/src/action/executor.ts +156 -0
- package/src/action/index.ts +7 -0
- package/src/addon/api-types.ts +15 -0
- package/src/addon/api.ts +461 -0
- package/src/addon/check-runner.ts +84 -0
- package/src/addon/index.ts +5 -0
- package/src/addon/loader.ts +441 -0
- package/src/addon/registry.ts +230 -0
- package/src/addon/spec.ts +69 -0
- package/src/api/addon.ts +33 -0
- package/src/api/asset-url.ts +24 -0
- package/src/api/index.ts +50 -0
- package/src/api/protocol-internal.ts +656 -0
- package/src/api/protocol.ts +2 -0
- package/src/api/react/index.ts +11 -0
- package/src/api/react/registry.ts +91 -0
- package/src/api/react/use-addon-channel.ts +26 -0
- package/src/api/react/use-button-state.tsx +23 -0
- package/src/api/react/use-deck.ts +24 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/logo72x72.png +0 -0
- package/src/assets/logoFull.png +0 -0
- package/src/builtin-addons/brightness/README.md +51 -0
- package/src/builtin-addons/brightness/buttons/brightness/backend.ts +13 -0
- package/src/builtin-addons/brightness/buttons/brightness/config.ts +10 -0
- package/src/builtin-addons/brightness/buttons/brightness/frontend.tsx +50 -0
- package/src/builtin-addons/brightness/domain/macos.ts +46 -0
- package/src/builtin-addons/brightness/index.d.ts +5 -0
- package/src/builtin-addons/brightness/index.ts +20 -0
- package/src/builtin-addons/brightness/poller.ts +27 -0
- package/src/builtin-addons/brightness/providers/darwin.ts +107 -0
- package/src/builtin-addons/brightness/providers/index.ts +81 -0
- package/src/builtin-addons/brightness/providers/linux.ts +108 -0
- package/src/builtin-addons/brightness/providers/windows.ts +77 -0
- package/src/builtin-addons/brightness/sirenodeck.json +6 -0
- package/src/builtin-addons/coding-agents/assets/claude-code.svg +1 -0
- package/src/builtin-addons/coding-agents/assets/opencode-dark-square.svg +1 -0
- package/src/builtin-addons/coding-agents/buttons/agent/backend.ts +82 -0
- package/src/builtin-addons/coding-agents/buttons/agent/config.ts +13 -0
- package/src/builtin-addons/coding-agents/buttons/agent/frontend.tsx +130 -0
- package/src/builtin-addons/coding-agents/buttons/agent/pages.tsx +150 -0
- package/src/builtin-addons/coding-agents/buttons/summary/backend.ts +48 -0
- package/src/builtin-addons/coding-agents/buttons/summary/config.ts +11 -0
- package/src/builtin-addons/coding-agents/buttons/summary/frontend.tsx +254 -0
- package/src/builtin-addons/coding-agents/decks/agents.ts +69 -0
- package/src/builtin-addons/coding-agents/global/backend.ts +402 -0
- package/src/builtin-addons/coding-agents/global-entry.ts +7 -0
- package/src/builtin-addons/coding-agents/hook/script.ts +123 -0
- package/src/builtin-addons/coding-agents/index.ts +15 -0
- package/src/builtin-addons/coding-agents/manifest.ts +115 -0
- package/src/builtin-addons/coding-agents/providers/claude-code.ts +470 -0
- package/src/builtin-addons/coding-agents/providers/claude-instances.ts +184 -0
- package/src/builtin-addons/coding-agents/providers/opencode-instances.ts +108 -0
- package/src/builtin-addons/coding-agents/providers/opencode.ts +636 -0
- package/src/builtin-addons/coding-agents/providers/registry.ts +120 -0
- package/src/builtin-addons/coding-agents/providers/spawn.ts +112 -0
- package/src/builtin-addons/coding-agents/shared/claude-hook-events.ts +55 -0
- package/src/builtin-addons/coding-agents/shared/claude-status.ts +236 -0
- package/src/builtin-addons/coding-agents/shared/live-count.ts +39 -0
- package/src/builtin-addons/coding-agents/shared/notifier.ts +58 -0
- package/src/builtin-addons/coding-agents/shared/opencode-status.ts +82 -0
- package/src/builtin-addons/coding-agents/shared/snapshot.ts +66 -0
- package/src/builtin-addons/coding-agents/shared/state.ts +72 -0
- package/src/builtin-addons/coding-agents/sirenodeck.json +6 -0
- package/src/builtin-addons/coding-agents/types/types.ts +33 -0
- package/src/builtin-addons/core/README.md +89 -0
- package/src/builtin-addons/core/buttons/action/backend.ts +7 -0
- package/src/builtin-addons/core/buttons/action/config.ts +13 -0
- package/src/builtin-addons/core/buttons/action/frontend.tsx +16 -0
- package/src/builtin-addons/core/buttons/change-deck/backend.ts +18 -0
- package/src/builtin-addons/core/buttons/change-deck/config.ts +15 -0
- package/src/builtin-addons/core/buttons/change-deck/frontend.tsx +18 -0
- package/src/builtin-addons/core/buttons/media-sample/backend.ts +8 -0
- package/src/builtin-addons/core/buttons/media-sample/config.ts +7 -0
- package/src/builtin-addons/core/buttons/media-sample/frontend.tsx +2 -0
- package/src/builtin-addons/core/buttons/page-nav/backend.ts +28 -0
- package/src/builtin-addons/core/buttons/page-nav/config.ts +9 -0
- package/src/builtin-addons/core/buttons/page-nav/frontend.tsx +68 -0
- package/src/builtin-addons/core/buttons/toggle/backend.ts +119 -0
- package/src/builtin-addons/core/buttons/toggle/config.ts +57 -0
- package/src/builtin-addons/core/buttons/toggle/frontend.tsx +57 -0
- package/src/builtin-addons/core/buttons/toggle/global-service.ts +174 -0
- package/src/builtin-addons/core/index.ts +120 -0
- package/src/builtin-addons/core/sirenodeck.json +6 -0
- package/src/builtin-addons/date-time/README.md +73 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/config.ts +4 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/frontend.tsx +88 -0
- package/src/builtin-addons/date-time/buttons/custom/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/custom/config.ts +9 -0
- package/src/builtin-addons/date-time/buttons/custom/frontend.tsx +26 -0
- package/src/builtin-addons/date-time/buttons/date/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/date/config.ts +10 -0
- package/src/builtin-addons/date-time/buttons/date/frontend.tsx +42 -0
- package/src/builtin-addons/date-time/buttons/time/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/time/config.ts +9 -0
- package/src/builtin-addons/date-time/buttons/time/frontend.tsx +29 -0
- package/src/builtin-addons/date-time/index.d.ts +5 -0
- package/src/builtin-addons/date-time/index.ts +30 -0
- package/src/builtin-addons/date-time/poller.ts +11 -0
- package/src/builtin-addons/date-time/shared/format-date.ts +36 -0
- package/src/builtin-addons/date-time/shared/format.ts +105 -0
- package/src/builtin-addons/date-time/shared/use-now.ts +10 -0
- package/src/builtin-addons/date-time/sirenodeck.json +6 -0
- package/src/builtin-addons/emoji-selector/README.md +45 -0
- package/src/builtin-addons/emoji-selector/assets/launcher.svg +10 -0
- package/src/builtin-addons/emoji-selector/assets/smileys.svg +8 -0
- package/src/builtin-addons/emoji-selector/buttons/category/backend.ts +14 -0
- package/src/builtin-addons/emoji-selector/buttons/category/config.ts +8 -0
- package/src/builtin-addons/emoji-selector/buttons/category/frontend.tsx +19 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/backend.ts +7 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/config.ts +9 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/frontend.tsx +11 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/backend.ts +13 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/config.ts +6 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/frontend.tsx +31 -0
- package/src/builtin-addons/emoji-selector/data/categories.ts +1084 -0
- package/src/builtin-addons/emoji-selector/decks/index.ts +137 -0
- package/src/builtin-addons/emoji-selector/index.d.ts +5 -0
- package/src/builtin-addons/emoji-selector/index.ts +29 -0
- package/src/builtin-addons/emoji-selector/sirenodeck.json +6 -0
- package/src/builtin-addons/emoji-selector/support.ts +49 -0
- package/src/builtin-addons/index.ts +13 -0
- package/src/builtin-addons/internal-settings/README.md +26 -0
- package/src/builtin-addons/internal-settings/backend.ts +58 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/backend.ts +7 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/config.ts +13 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/frontend.tsx +18 -0
- package/src/builtin-addons/internal-settings/buttons/brightness/common/frontend.tsx +50 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/backend.ts +16 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/config.ts +6 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/frontend.tsx +8 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/backend.ts +16 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/config.ts +6 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/frontend.tsx +8 -0
- package/src/builtin-addons/internal-settings/decks/settings.ts +25 -0
- package/src/builtin-addons/internal-settings/index.d.ts +4 -0
- package/src/builtin-addons/internal-settings/index.ts +53 -0
- package/src/builtin-addons/internal-settings/logo72x72.png +0 -0
- package/src/builtin-addons/internal-settings/sirenodeck.json +6 -0
- package/src/builtin-addons/media/README.md +45 -0
- package/src/builtin-addons/media/backend.ts +143 -0
- package/src/builtin-addons/media/buttons/media-player/backend.ts +19 -0
- package/src/builtin-addons/media/buttons/media-player/components/MediaStatus.tsx +25 -0
- package/src/builtin-addons/media/buttons/media-player/components/MediaSurface.tsx +52 -0
- package/src/builtin-addons/media/buttons/media-player/components/ProgressBar.tsx +21 -0
- package/src/builtin-addons/media/buttons/media-player/components/status-meta.ts +36 -0
- package/src/builtin-addons/media/buttons/media-player/config.ts +4 -0
- package/src/builtin-addons/media/buttons/media-player/frontend.tsx +29 -0
- package/src/builtin-addons/media/buttons/volume/common/frontend.tsx +52 -0
- package/src/builtin-addons/media/buttons/volume/mute/backend.ts +13 -0
- package/src/builtin-addons/media/buttons/volume/mute/config.ts +4 -0
- package/src/builtin-addons/media/buttons/volume/mute/frontend.tsx +26 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/backend.ts +14 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/config.ts +6 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/frontend.tsx +10 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/backend.ts +14 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/config.ts +6 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/frontend.tsx +8 -0
- package/src/builtin-addons/media/checks.ts +104 -0
- package/src/builtin-addons/media/executor.ts +36 -0
- package/src/builtin-addons/media/index.d.ts +5 -0
- package/src/builtin-addons/media/index.ts +53 -0
- package/src/builtin-addons/media/progress.ts +16 -0
- package/src/builtin-addons/media/providers/darwin.ts +391 -0
- package/src/builtin-addons/media/providers/index.ts +27 -0
- package/src/builtin-addons/media/providers/linux.ts +196 -0
- package/src/builtin-addons/media/providers/types.ts +48 -0
- package/src/builtin-addons/media/providers/windows.ts +121 -0
- package/src/builtin-addons/media/sirenodeck.json +6 -0
- package/src/builtin-addons/media/state.ts +19 -0
- package/src/builtin-addons/register-builtins.ts +65 -0
- package/src/builtin-addons/session/README.md +14 -0
- package/src/builtin-addons/session/buttons/session-info/backend.ts +8 -0
- package/src/builtin-addons/session/buttons/session-info/config.ts +4 -0
- package/src/builtin-addons/session/buttons/session-info/frontend.tsx +2 -0
- package/src/builtin-addons/session/buttons/time/backend.ts +7 -0
- package/src/builtin-addons/session/buttons/time/config.ts +6 -0
- package/src/builtin-addons/session/buttons/time/frontend.tsx +2 -0
- package/src/builtin-addons/session/index.d.ts +5 -0
- package/src/builtin-addons/session/index.ts +18 -0
- package/src/builtin-addons/session/sirenodeck.json +6 -0
- package/src/builtin-addons/system-status/README.md +169 -0
- package/src/builtin-addons/system-status/buttons/_shared.ts +131 -0
- package/src/builtin-addons/system-status/buttons/system-status/frontend.tsx +224 -0
- package/src/builtin-addons/system-status/buttons/system-status/schemas.ts +67 -0
- package/src/builtin-addons/system-status/domain/catalog.ts +53 -0
- package/src/builtin-addons/system-status/domain/chart-sampler.ts +58 -0
- package/src/builtin-addons/system-status/domain/display-metrics.ts +128 -0
- package/src/builtin-addons/system-status/domain/index.ts +10 -0
- package/src/builtin-addons/system-status/domain/live-metrics.ts +1021 -0
- package/src/builtin-addons/system-status/domain/metric-ids.ts +0 -0
- package/src/builtin-addons/system-status/index.ts +16 -0
- package/src/builtin-addons/system-status/manifest.ts +105 -0
- package/src/builtin-addons/system-status/shared/metrics-catalog.ts +316 -0
- package/src/builtin-addons/system-status/sirenodeck.json +6 -0
- package/src/builtin-addons/test-buildin/decks/action-buttons-test.ts +56 -0
- package/src/builtin-addons/test-buildin/index.d.ts +5 -0
- package/src/builtin-addons/test-buildin/index.ts +16 -0
- package/src/builtin-addons/value-display/README.md +59 -0
- package/src/builtin-addons/value-display/backend.ts +86 -0
- package/src/builtin-addons/value-display/buttons/value-display/backend.ts +17 -0
- package/src/builtin-addons/value-display/buttons/value-display/config.ts +36 -0
- package/src/builtin-addons/value-display/buttons/value-display/frontend.tsx +54 -0
- package/src/builtin-addons/value-display/index.d.ts +5 -0
- package/src/builtin-addons/value-display/index.ts +23 -0
- package/src/builtin-addons/value-display/sirenodeck.json +6 -0
- package/src/builtin-addons/weather/README.md +46 -0
- package/src/builtin-addons/weather/backend.ts +121 -0
- package/src/builtin-addons/weather/buttons/weather/backend.ts +27 -0
- package/src/builtin-addons/weather/buttons/weather/config.ts +29 -0
- package/src/builtin-addons/weather/buttons/weather/frontend.tsx +63 -0
- package/src/builtin-addons/weather/buttons/weather/icons.tsx +88 -0
- package/src/builtin-addons/weather/buttons/weather/pages.tsx +109 -0
- package/src/builtin-addons/weather/domain/codes.ts +35 -0
- package/src/builtin-addons/weather/domain/fetch.ts +47 -0
- package/src/builtin-addons/weather/index.d.ts +5 -0
- package/src/builtin-addons/weather/index.ts +23 -0
- package/src/builtin-addons/weather/poller.ts +70 -0
- package/src/builtin-addons/weather/provider/city-key.ts +9 -0
- package/src/builtin-addons/weather/provider/codes.ts +35 -0
- package/src/builtin-addons/weather/provider/fetch.ts +97 -0
- package/src/builtin-addons/weather/provider/geocode.ts +67 -0
- package/src/builtin-addons/weather/provider/types.ts +42 -0
- package/src/builtin-addons/weather/sirenodeck.json +6 -0
- package/src/cli/coding-agents-onboarding.ts +434 -0
- package/src/cli/commands/addon-decks.ts +441 -0
- package/src/cli/commands/addon-registry.ts +639 -0
- package/src/cli/commands/emulator-mode.ts +289 -0
- package/src/cli/commands/install.ts +128 -0
- package/src/cli/commands/logs.ts +74 -0
- package/src/cli/commands/network-bind.ts +111 -0
- package/src/cli/commands/pipeline/helpers.ts +75 -0
- package/src/cli/commands/pipeline/preflight.ts +20 -0
- package/src/cli/commands/port-identity.ts +152 -0
- package/src/cli/commands/reload.ts +37 -0
- package/src/cli/commands/restart.ts +139 -0
- package/src/cli/commands/run.ts +2270 -0
- package/src/cli/commands/service/install.ts +121 -0
- package/src/cli/commands/service/render-template.ts +158 -0
- package/src/cli/commands/service-manager.ts +184 -0
- package/src/cli/commands/spawn-daemon.ts +251 -0
- package/src/cli/commands/start.ts +1061 -0
- package/src/cli/commands/status.ts +110 -0
- package/src/cli/commands/stop.ts +121 -0
- package/src/cli/commands/subprocess-supervisor.ts +203 -0
- package/src/cli/commands/system-requirements.ts +699 -0
- package/src/cli/cwd.ts +28 -0
- package/src/cli/first-run.ts +149 -0
- package/src/cli/http-server.ts +244 -0
- package/src/cli/index.ts +355 -0
- package/src/cli/install-root.ts +26 -0
- package/src/cli/main.ts +194 -0
- package/src/cli/package-manager.ts +175 -0
- package/src/cli/probe-deps.ts +45 -0
- package/src/cli/prompt.ts +22 -0
- package/src/cli/startup-display.ts +452 -0
- package/src/config/builtin-icons.ts +57 -0
- package/src/config/config-diff.ts +54 -0
- package/src/config/discovery.ts +64 -0
- package/src/config/icon-resolver.ts +90 -0
- package/src/config/include-resolver.ts +202 -0
- package/src/config/index.ts +9 -0
- package/src/config/loader.ts +187 -0
- package/src/config/mutation.ts +514 -0
- package/src/config/reference-expander.ts +84 -0
- package/src/config/schemas.ts +223 -0
- package/src/config/validation.ts +254 -0
- package/src/core/asset-id.ts +21 -0
- package/src/core/gesture-state.ts +344 -0
- package/src/core/icon-asset-registry.ts +127 -0
- package/src/core/icon-source.ts +86 -0
- package/src/core/index.ts +30 -0
- package/src/core/mime.ts +16 -0
- package/src/core/pagination.ts +90 -0
- package/src/core/pub-sub.ts +126 -0
- package/src/core/settings.ts +153 -0
- package/src/core/store.ts +88 -0
- package/src/core/watcher.ts +73 -0
- package/src/deck/addon-handler-bridge.ts +516 -0
- package/src/deck/deck-config.ts +271 -0
- package/src/deck/editor-surfaces.ts +87 -0
- package/src/deck/host-context.ts +18 -0
- package/src/deck/index.ts +102 -0
- package/src/deck/macro-parse.ts +182 -0
- package/src/deck/methods.ts +348 -0
- package/src/deck/paginate-deck.ts +98 -0
- package/src/deck/position-buttons.ts +57 -0
- package/src/deck/runtime/index.ts +5 -0
- package/src/deck/runtime/runtime.ts +1060 -0
- package/src/deck/runtime-subscriptions.ts +45 -0
- package/src/deck/system-back-injection.ts +62 -0
- package/src/deck/system-buttons/registry.tsx +97 -0
- package/src/deck/system-buttons/types.ts +14 -0
- package/src/device/black-frame.ts +73 -0
- package/src/device/index.ts +13 -0
- package/src/device/linux-udev.ts +36 -0
- package/src/device/models.ts +89 -0
- package/src/device/reconnecting.ts +193 -0
- package/src/device/registry.ts +38 -0
- package/src/device/stream-deck.ts +155 -0
- package/src/index.ts +74 -0
- package/src/outputClient/emulator.ts +357 -0
- package/src/outputClient/index.ts +26 -0
- package/src/outputClient/open-browser.ts +26 -0
- package/src/outputClient/real.ts +393 -0
- package/src/outputClient/types.ts +83 -0
- package/src/render/browser-renderer.ts +240 -0
- package/src/render/buffer-hash.ts +20 -0
- package/src/render/editor-handler.ts +197 -0
- package/src/render/emulator-server.ts +128 -0
- package/src/render/icon-source-resolver.ts +69 -0
- package/src/render/index.ts +16 -0
- package/src/render/protocol.ts +61 -0
- package/src/render/push-raw-image.ts +100 -0
- package/src/render/screenshot-cadence.ts +87 -0
- package/src/render/state-publisher.ts +110 -0
- package/src/render/vite-server.ts +138 -0
- package/src/render/ws-bridge.ts +315 -0
- package/src/system/device-selection.ts +80 -0
- package/src/system/glob-match.ts +74 -0
- package/src/system/providers/active-app/darwin.ts +117 -0
- package/src/system/providers/active-app/linux.ts +245 -0
- package/src/system/providers/active-app/wayland-gnome.ts +303 -0
- package/src/system/providers/active-app/windows.ts +94 -0
- package/src/system/providers/active-app.ts +97 -0
- package/src/system/providers/clipboard/darwin.ts +65 -0
- package/src/system/providers/clipboard/linux.ts +163 -0
- package/src/system/providers/clipboard/windows.ts +94 -0
- package/src/system/providers/clipboard.ts +84 -0
- package/src/system/providers/error.ts +16 -0
- package/src/system/providers/key-macro/darwin.ts +221 -0
- package/src/system/providers/key-macro/linux.ts +651 -0
- package/src/system/providers/key-macro/parser.ts +215 -0
- package/src/system/providers/key-macro/windows.ts +476 -0
- package/src/system/providers/key-macro.ts +79 -0
- package/src/system/providers/notification/darwin.ts +36 -0
- package/src/system/providers/notification/linux.ts +115 -0
- package/src/system/providers/notification/windows.ts +72 -0
- package/src/system/providers/notification.ts +75 -0
- package/src/system/providers/session/darwin.ts +82 -0
- package/src/system/providers/session/linux.ts +175 -0
- package/src/system/providers/session/windows.ts +73 -0
- package/src/system/providers/session.ts +89 -0
- package/src/system/providers/shared.ts +65 -0
- package/src/system/requirements.ts +235 -0
- package/src/system/setup-wizard/config-seed.ts +30 -0
- package/src/system/setup-wizard/format.ts +275 -0
- package/src/system/setup-wizard/index.ts +45 -0
- package/src/system/setup-wizard/open-settings.ts +26 -0
- package/src/system/setup-wizard/plan.ts +230 -0
- package/src/system/setup-wizard/probe-cache.ts +34 -0
- package/src/system/setup-wizard/probe.ts +486 -0
- package/src/system/setup-wizard/runtime-features.ts +85 -0
- package/src/system/setup-wizard/sudo.ts +178 -0
- package/src/system/setup-wizard/types.ts +133 -0
- package/src/system/virtual-stream-deck.ts +67 -0
- package/src/themes/css.ts +210 -0
- package/src/themes/default/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/default/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Italic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Light.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Thin.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/OFL.txt +93 -0
- package/src/themes/default/assets/OFL.txt +93 -0
- package/src/themes/default/assets/README.txt +106 -0
- package/src/themes/default/assets/static/IBMPlexSans-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ThinItalic.ttf +0 -0
- package/src/themes/default/components.css +81 -0
- package/src/themes/default/sirenodeck.json +170 -0
- package/src/themes/index.ts +14 -0
- package/src/themes/light/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/light/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/OFL.txt +93 -0
- package/src/themes/light/assets/OFL.txt +93 -0
- package/src/themes/light/assets/README.txt +106 -0
- package/src/themes/light/assets/static/IBMPlexSans-Light.ttf +0 -0
- package/src/themes/light/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
- package/src/themes/light/assets/static/IBMPlexSans-Thin.ttf +0 -0
- package/src/themes/light/components.css +70 -0
- package/src/themes/light/sirenodeck.json +170 -0
- package/src/themes/loader.ts +343 -0
- package/src/themes/manifest.ts +164 -0
- package/src/themes/use-resolved-theme.tsx +32 -0
- package/src/ui/ButtonFrame.tsx +230 -0
- package/src/ui/contexts/AssetCacheContext.tsx +43 -0
- package/src/ui/index.ts +20 -0
- package/src/ui/primitives/Chip.tsx +53 -0
- package/src/ui/primitives/Icon.tsx +209 -0
- package/src/ui/primitives/Label.tsx +82 -0
- package/src/ui/primitives/ProgressBar.tsx +38 -0
- package/src/ui/primitives/TapIndicator.tsx +67 -0
- package/src/ui/primitives/Text.tsx +810 -0
- package/src/ui/primitives/index.ts +6 -0
- package/src/ui/surfaces/BarsSurface.tsx +211 -0
- package/src/ui/surfaces/IconLabelProgressSurface.tsx +133 -0
- package/src/ui/surfaces/IconLabelSurface.tsx +59 -0
- package/src/ui/surfaces/LabelValueListSurface.tsx +202 -0
- package/src/ui/surfaces/PaginatedSurface.tsx +164 -0
- package/src/ui/surfaces/SplitActionSurface.tsx +98 -0
- package/src/ui/surfaces/TemporaryErrorSurface.tsx +68 -0
- package/src/ui/surfaces/ValueChart.tsx +218 -0
- package/src/ui/surfaces/index.ts +32 -0
- package/src/ui/surfaces/paginated-schema.ts +22 -0
- package/src/ui/theme-presentation-builder.ts +184 -0
- package/src/ui/theme-presentation.tsx +146 -0
- package/src/ui/utils/cn.ts +12 -0
- package/src/util/cache-paths.ts +38 -0
- package/src/util/config-diff.ts +16 -0
- package/src/util/daemon-control.ts +85 -0
- package/src/util/daemon.ts +575 -0
- package/src/util/device-config.ts +56 -0
- package/src/util/errors.ts +13 -0
- package/src/util/log-reader.ts +114 -0
- package/src/util/log-tail.ts +103 -0
- package/src/util/logger.ts +417 -0
- package/src/util/process-title.ts +79 -0
- package/src/util/single-instance.ts +202 -0
- package/src/version.ts +10 -0
- package/src/vite/index.ts +6 -0
- package/src/vite/virtual-modules.ts +631 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PubSub } from "@/core/pub-sub"
|
|
2
|
+
|
|
3
|
+
import type { Runtime } from "./runtime"
|
|
4
|
+
|
|
5
|
+
export const subscribeNavigateDeck = (
|
|
6
|
+
pubSub: PubSub,
|
|
7
|
+
runtime: Runtime,
|
|
8
|
+
): (() => void) => {
|
|
9
|
+
return pubSub.subscribe("runtime:navigate-deck", (payload: unknown) => {
|
|
10
|
+
if (
|
|
11
|
+
typeof payload !== "object" ||
|
|
12
|
+
payload === null ||
|
|
13
|
+
!("deckId" in payload)
|
|
14
|
+
) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const deckId = String((payload as { deckId: unknown }).deckId)
|
|
18
|
+
const addToHistory =
|
|
19
|
+
"addToHistory" in payload
|
|
20
|
+
? Boolean((payload as { addToHistory: unknown }).addToHistory)
|
|
21
|
+
: true
|
|
22
|
+
// ponytail: position arrives as a real field on the payload — never parse it
|
|
23
|
+
// from buttonId. The id format `[position]-[deck]-[page]` is opaque; pages
|
|
24
|
+
// 2+ collide with the page-nav slot's bare-digit prefix if you try.
|
|
25
|
+
const sourcePosition =
|
|
26
|
+
"position" in payload &&
|
|
27
|
+
(payload as { position: unknown }).position !== undefined &&
|
|
28
|
+
Number.isFinite(Number((payload as { position: unknown }).position))
|
|
29
|
+
? Number((payload as { position: unknown }).position)
|
|
30
|
+
: undefined
|
|
31
|
+
const sourceDeckId = runtime.getActiveDeckId()
|
|
32
|
+
// ponytail: check existence BEFORE calling navigateToDeck so we never mutate
|
|
33
|
+
// nav state for a missing target. deckExists is the runtime's public check.
|
|
34
|
+
if (!runtime.deckExists(deckId) && sourcePosition !== undefined) {
|
|
35
|
+
pubSub.publish("runtime:buttonError", {
|
|
36
|
+
deckId: sourceDeckId,
|
|
37
|
+
position: sourcePosition,
|
|
38
|
+
durationMs: 5000,
|
|
39
|
+
details: `missing-navigation-target: ${deckId}`,
|
|
40
|
+
})
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
runtime.navigateToDeck(deckId, { addToHistory })
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SystemButtonType } from "./system-buttons/types"
|
|
2
|
+
|
|
3
|
+
export interface RuntimeDeck {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
buttons: ReadonlyArray<{
|
|
7
|
+
id: string
|
|
8
|
+
type: string
|
|
9
|
+
position?: number
|
|
10
|
+
config?: unknown
|
|
11
|
+
}>
|
|
12
|
+
isMain?: boolean
|
|
13
|
+
processNames?: ReadonlyArray<string>
|
|
14
|
+
windowNames?: ReadonlyArray<string>
|
|
15
|
+
autoShow?: boolean
|
|
16
|
+
icon?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RuntimeState {
|
|
20
|
+
navStackDepth: number
|
|
21
|
+
hasOverlayDeckAvailable: boolean
|
|
22
|
+
lockActive?: boolean
|
|
23
|
+
inOverlayMode?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const computeSystemButtonForSlotN1 = (
|
|
27
|
+
deck: RuntimeDeck,
|
|
28
|
+
state: RuntimeState,
|
|
29
|
+
): SystemButtonType | null => {
|
|
30
|
+
if (state.lockActive === true) return null
|
|
31
|
+
if (deck.isMain) return "core:settings-entry"
|
|
32
|
+
if (state.inOverlayMode === true) return "core:overlay-toggle"
|
|
33
|
+
return "core:back"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const injectSystemButtons = <T extends RuntimeDeck>(
|
|
37
|
+
decks: ReadonlyArray<T>,
|
|
38
|
+
keyCount: number,
|
|
39
|
+
options?: { lockActive?: boolean },
|
|
40
|
+
): ReadonlyArray<T> => {
|
|
41
|
+
const n1Position = keyCount - 1
|
|
42
|
+
return decks.map((deck) => {
|
|
43
|
+
const systemButtonType = computeSystemButtonForSlotN1(deck, {
|
|
44
|
+
navStackDepth: 1,
|
|
45
|
+
hasOverlayDeckAvailable: false,
|
|
46
|
+
lockActive: options?.lockActive === true,
|
|
47
|
+
})
|
|
48
|
+
if (systemButtonType === null) return deck
|
|
49
|
+
const filtered = deck.buttons.filter((b) => b.position !== n1Position)
|
|
50
|
+
return {
|
|
51
|
+
...deck,
|
|
52
|
+
buttons: [
|
|
53
|
+
...filtered,
|
|
54
|
+
{
|
|
55
|
+
id: `${n1Position}-${deck.id}-0`,
|
|
56
|
+
type: systemButtonType,
|
|
57
|
+
position: n1Position,
|
|
58
|
+
} as T["buttons"][number],
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type ReactElement } from "react"
|
|
2
|
+
|
|
3
|
+
import { Icon } from "@/ui/primitives/Icon"
|
|
4
|
+
import { Label } from "@/ui/primitives/Label"
|
|
5
|
+
import { IconLabelSurface } from "@/ui/surfaces/IconLabelSurface"
|
|
6
|
+
import { TemporaryErrorSurface } from "@/ui/surfaces/TemporaryErrorSurface"
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SYSTEM_BUTTON_TYPES,
|
|
10
|
+
isSystemButtonType,
|
|
11
|
+
type SystemButtonType,
|
|
12
|
+
} from "./types"
|
|
13
|
+
|
|
14
|
+
const SYSTEM_BUTTON_LAYOUT: Record<
|
|
15
|
+
SystemButtonType,
|
|
16
|
+
{ source: string; label: string; details?: string }
|
|
17
|
+
> = {
|
|
18
|
+
"core:back": { source: "icon://arrow-left", label: "Back" },
|
|
19
|
+
"core:settings-entry": { source: "icon://settings", label: "Settings" },
|
|
20
|
+
"core:overlay-toggle": { source: "icon://layers", label: "Overlay" },
|
|
21
|
+
"core:temporary-error": {
|
|
22
|
+
source: "icon://triangle-alert",
|
|
23
|
+
label: "Error",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const SYSTEM_BUTTON_LABELS: ReadonlyArray<string> = SYSTEM_BUTTON_TYPES
|
|
28
|
+
|
|
29
|
+
export const isSystemButton = (type: string): type is SystemButtonType =>
|
|
30
|
+
isSystemButtonType(type)
|
|
31
|
+
|
|
32
|
+
const FALLBACK_ERROR_DETAILS = "check logs"
|
|
33
|
+
|
|
34
|
+
export const renderSystemButton = (
|
|
35
|
+
type: string,
|
|
36
|
+
iconOverride?: string,
|
|
37
|
+
details?: string,
|
|
38
|
+
labelOverride?: string,
|
|
39
|
+
): ReactElement | null => {
|
|
40
|
+
if (!isSystemButtonType(type)) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
const layout = SYSTEM_BUTTON_LAYOUT[type]
|
|
44
|
+
|
|
45
|
+
if (type === "core:temporary-error") {
|
|
46
|
+
return (
|
|
47
|
+
<TemporaryErrorSurface
|
|
48
|
+
source={iconOverride ?? layout.source}
|
|
49
|
+
label={layout.label}
|
|
50
|
+
details={details ?? layout.details ?? FALLBACK_ERROR_DETAILS}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<IconLabelSurface
|
|
57
|
+
source={iconOverride ?? layout.source}
|
|
58
|
+
label={labelOverride ?? layout.label}
|
|
59
|
+
{...(details !== undefined
|
|
60
|
+
? { details }
|
|
61
|
+
: layout.details !== undefined
|
|
62
|
+
? { details: layout.details }
|
|
63
|
+
: {})}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface OverlayToggleSurfaceProps {
|
|
69
|
+
deckIcon: string
|
|
70
|
+
overlayDeckName?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const OverlayToggleSurface = ({
|
|
74
|
+
deckIcon,
|
|
75
|
+
overlayDeckName,
|
|
76
|
+
}: OverlayToggleSurfaceProps): ReactElement => {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className="flex flex-col items-center justify-center gap-1"
|
|
80
|
+
data-sireno-overlay-toggle="true"
|
|
81
|
+
>
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
<Icon source={deckIcon} size={26} />
|
|
84
|
+
<Icon source="icon://slash" size={14} />
|
|
85
|
+
<Icon source="icon://layers" size={26} />
|
|
86
|
+
</div>
|
|
87
|
+
<Label text={overlayDeckName ?? "Overlay"} lines={2} />
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const renderOverlayToggleButton = (
|
|
93
|
+
deckIcon: string,
|
|
94
|
+
overlayDeckName?: string,
|
|
95
|
+
): ReactElement => (
|
|
96
|
+
<OverlayToggleSurface deckIcon={deckIcon} overlayDeckName={overlayDeckName} />
|
|
97
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ponytail: pagination is handled by the `core:page-nav` addon button
|
|
2
|
+
// (tap=next, hold=prev) in builtin-addons/core/buttons/page-nav/, not by a
|
|
3
|
+
// system button. Keeping the system-button surface small and deliberate.
|
|
4
|
+
export const SYSTEM_BUTTON_TYPES = [
|
|
5
|
+
"core:back",
|
|
6
|
+
"core:settings-entry",
|
|
7
|
+
"core:overlay-toggle",
|
|
8
|
+
"core:temporary-error",
|
|
9
|
+
] as const
|
|
10
|
+
|
|
11
|
+
export type SystemButtonType = (typeof SYSTEM_BUTTON_TYPES)[number]
|
|
12
|
+
|
|
13
|
+
export const isSystemButtonType = (value: string): value is SystemButtonType =>
|
|
14
|
+
(SYSTEM_BUTTON_TYPES as ReadonlyArray<string>).includes(value)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { connectStreamDeck, type StreamDeckDevice } from "@/device/stream-deck"
|
|
2
|
+
|
|
3
|
+
import type pino from "pino"
|
|
4
|
+
|
|
5
|
+
// ponytail: extracted from RealOutputClient.pushBlackFrame so any caller
|
|
6
|
+
// (current or future) can push a black frame WITHOUT going through the full
|
|
7
|
+
// daemon init. Today the daemon pushes a black frame on its own teardown in
|
|
8
|
+
// runPipeline's finally block; the helper is preserved for that single use
|
|
9
|
+
// site and stays best-effort because the device may be unplugged at the
|
|
10
|
+
// moment of exit.
|
|
11
|
+
const buildBlackBuffer = (keyCount: number): Buffer => {
|
|
12
|
+
const stride = 3 * 8
|
|
13
|
+
const total = keyCount * stride * 8
|
|
14
|
+
return Buffer.alloc(total)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const pushBlackFrame = async (
|
|
18
|
+
device: StreamDeckDevice,
|
|
19
|
+
logger: pino.Logger,
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
const buf = buildBlackBuffer(device.getKeyCount())
|
|
23
|
+
for (let i = 0; i < device.getKeyCount(); i++) {
|
|
24
|
+
await device.fillKeyBuffer(i, buf.subarray(0, 3 * 8 * 8))
|
|
25
|
+
}
|
|
26
|
+
// ponytail: brightness is a feature report processed synchronously by the
|
|
27
|
+
// device firmware, so dimming to 0 is a guaranteed "off" signal even if the
|
|
28
|
+
// queued image writes above are dropped at USB disconnect.
|
|
29
|
+
await device.setBrightness(0)
|
|
30
|
+
// ponytail: give the queued USB writes a moment to drain before the caller
|
|
31
|
+
// closes the device; otherwise node-hid may flush to a closed handle and
|
|
32
|
+
// the panel keeps the last fully-rendered frame.
|
|
33
|
+
await new Promise<void>((r) => setTimeout(r, 200))
|
|
34
|
+
logger.info("black-frame: pushed")
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.warn(
|
|
37
|
+
{ err: (err as Error).message },
|
|
38
|
+
"black-frame: push failed (non-fatal)",
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BlackFrameConfig {
|
|
44
|
+
readonly serial: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ponytail: parent-process entry point. Opens a fresh device, pushes the
|
|
48
|
+
// black frame, closes. All errors are non-fatal — the device may be gone,
|
|
49
|
+
// the daemon may be mid-shutdown, the operator may have unplugged the cable.
|
|
50
|
+
export const pushBlackFrameToDevice = async (
|
|
51
|
+
config: BlackFrameConfig,
|
|
52
|
+
logger: pino.Logger,
|
|
53
|
+
): Promise<void> => {
|
|
54
|
+
let device: StreamDeckDevice | null = null
|
|
55
|
+
try {
|
|
56
|
+
device = await connectStreamDeck({ serial: config.serial })
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.warn(
|
|
59
|
+
{ err: (err as Error).message, serial: config.serial },
|
|
60
|
+
"black-frame: failed to connect to device (non-fatal)",
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await pushBlackFrame(device, logger)
|
|
66
|
+
} finally {
|
|
67
|
+
try {
|
|
68
|
+
await device.close()
|
|
69
|
+
} catch {
|
|
70
|
+
void 0
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
connectStreamDeck,
|
|
3
|
+
StreamDeckSelectionError,
|
|
4
|
+
type ConnectStreamDeckOptions,
|
|
5
|
+
type StreamDeckDevice,
|
|
6
|
+
} from "./stream-deck"
|
|
7
|
+
export { listDevices, type DeviceDescriptor } from "./registry"
|
|
8
|
+
export {
|
|
9
|
+
UDEV_RULES,
|
|
10
|
+
UdevPermissionError,
|
|
11
|
+
formatInstallInstructions,
|
|
12
|
+
installUdevRules,
|
|
13
|
+
} from "./linux-udev"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const UDEV_RULES = `# sirenodeck — Elgato Stream Deck udev rules
|
|
2
|
+
# Grants current user access to Elgato USB devices without sudo.
|
|
3
|
+
|
|
4
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", TAG+="uaccess"
|
|
5
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", TAG+="uaccess"
|
|
6
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006d", TAG+="uaccess"
|
|
7
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006e", TAG+="uaccess"
|
|
8
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0080", TAG+="uaccess"
|
|
9
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0084", TAG+="uaccess"
|
|
10
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0086", TAG+="uaccess"
|
|
11
|
+
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0090", TAG+="uaccess"
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
export class UdevPermissionError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message)
|
|
17
|
+
this.name = "UdevPermissionError"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const formatInstallInstructions = (): string =>
|
|
22
|
+
`Stream Deck udev rules not installed. To install:
|
|
23
|
+
|
|
24
|
+
sudo tee /etc/udev/rules.d/70-sirenodeck.rules > /dev/null <<'EOF'
|
|
25
|
+
${UDEV_RULES.trimEnd()}
|
|
26
|
+
EOF
|
|
27
|
+
sudo udevadm control --reload-rules && sudo udevadm trigger
|
|
28
|
+
|
|
29
|
+
Then unplug and replug your Stream Deck.
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
export const installUdevRules = async (): Promise<void> => {
|
|
33
|
+
throw new UdevPermissionError(
|
|
34
|
+
"installUdevRules() requires running with elevated privileges; use the manual instructions from formatInstallInstructions()",
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface DeviceModelSpec {
|
|
2
|
+
readonly id: string
|
|
3
|
+
readonly name: string
|
|
4
|
+
readonly keyCount: number
|
|
5
|
+
readonly columns: number
|
|
6
|
+
readonly rows: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DEVICE_MODELS: ReadonlyArray<DeviceModelSpec> = [
|
|
10
|
+
{
|
|
11
|
+
id: "original",
|
|
12
|
+
name: "Stream Deck Original",
|
|
13
|
+
keyCount: 15,
|
|
14
|
+
columns: 5,
|
|
15
|
+
rows: 3,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "originalv2",
|
|
19
|
+
name: "Stream Deck Original V2",
|
|
20
|
+
keyCount: 15,
|
|
21
|
+
columns: 5,
|
|
22
|
+
rows: 3,
|
|
23
|
+
},
|
|
24
|
+
{ id: "mk2", name: "Stream Deck MK.2", keyCount: 15, columns: 5, rows: 3 },
|
|
25
|
+
{ id: "plus", name: "Stream Deck +", keyCount: 32, columns: 8, rows: 4 },
|
|
26
|
+
{ id: "mini", name: "Stream Deck Mini", keyCount: 6, columns: 3, rows: 2 },
|
|
27
|
+
{ id: "xl", name: "Stream Deck XL", keyCount: 32, columns: 8, rows: 4 },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_DEVICE_MODEL_ID = "mk2"
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_KEY_COUNT = 15
|
|
33
|
+
|
|
34
|
+
export const BUTTON_SIZE_PX = 96
|
|
35
|
+
export const DECK_PADDING_PX = 16
|
|
36
|
+
export const DECK_GAP_PX = 8
|
|
37
|
+
|
|
38
|
+
export const resolveDeckGap = (
|
|
39
|
+
value: string | null | undefined,
|
|
40
|
+
compact = false,
|
|
41
|
+
): number => {
|
|
42
|
+
if (value === "false" || value === "0") return 0
|
|
43
|
+
if (value === "true" || value === "8") return DECK_GAP_PX
|
|
44
|
+
return compact ? 0 : DECK_GAP_PX
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const deckDimensions = (
|
|
48
|
+
model: Pick<DeviceModelSpec, "columns" | "rows">,
|
|
49
|
+
gap: number,
|
|
50
|
+
padding = DECK_PADDING_PX,
|
|
51
|
+
) => ({
|
|
52
|
+
width:
|
|
53
|
+
model.columns * BUTTON_SIZE_PX + (model.columns - 1) * gap + padding * 2,
|
|
54
|
+
height: model.rows * BUTTON_SIZE_PX + (model.rows - 1) * gap + padding * 2,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export const isKnownDeviceModel = (id: string): id is DeviceModelSpec["id"] =>
|
|
58
|
+
DEVICE_MODELS.some((m) => m.id === id)
|
|
59
|
+
|
|
60
|
+
export const getDeviceModel = (id: string): DeviceModelSpec => {
|
|
61
|
+
const found = DEVICE_MODELS.find((m) => m.id === id)
|
|
62
|
+
if (found === undefined) {
|
|
63
|
+
throw new Error(`Unknown device model: ${id}`)
|
|
64
|
+
}
|
|
65
|
+
return found
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const resolveKeyCount = (id: string | undefined): number => {
|
|
69
|
+
if (id === undefined) return DEFAULT_KEY_COUNT
|
|
70
|
+
if (!isKnownDeviceModel(id)) {
|
|
71
|
+
throw new Error(`Unknown device model: ${id}`)
|
|
72
|
+
}
|
|
73
|
+
return getDeviceModel(id).keyCount
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const gridForKeyCount = (
|
|
77
|
+
keyCount: number,
|
|
78
|
+
): { columns: number; rows: number } => {
|
|
79
|
+
if (keyCount === 15) return { columns: 5, rows: 3 }
|
|
80
|
+
if (keyCount === 32) return { columns: 8, rows: 4 }
|
|
81
|
+
if (keyCount === 6) return { columns: 3, rows: 2 }
|
|
82
|
+
if (keyCount === 8) return { columns: 4, rows: 2 }
|
|
83
|
+
if (keyCount === 3) return { columns: 3, rows: 1 }
|
|
84
|
+
if (keyCount === 2) return { columns: 2, rows: 1 }
|
|
85
|
+
if (keyCount === 1) return { columns: 1, rows: 1 }
|
|
86
|
+
const columns = Math.ceil(Math.sqrt(keyCount))
|
|
87
|
+
const rows = Math.ceil(keyCount / columns)
|
|
88
|
+
return { columns, rows }
|
|
89
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type pino from "pino"
|
|
2
|
+
|
|
3
|
+
import type { StreamDeckDevice, StreamDeckKeyEvent } from "./stream-deck"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a Stream Deck so losing the hardware is a pause, not a crash.
|
|
7
|
+
*
|
|
8
|
+
* ponytail: a KVM switching to another computer removes the USB device. The
|
|
9
|
+
* SDK then emits `error`, which nothing subscribed to — an unhandled `error`
|
|
10
|
+
* on an EventEmitter throws, the daemon's uncaughtException guard fired, and
|
|
11
|
+
* the service exited with no recovery. Writes could fail the same way.
|
|
12
|
+
*
|
|
13
|
+
* This keeps the daemon alive instead: the surrounding pipeline (vite,
|
|
14
|
+
* Playwright, the bridge, the runtime) is untouched by USB coming and going,
|
|
15
|
+
* so only the device handle is swapped. While the deck is away, writes are
|
|
16
|
+
* dropped silently and a watcher retries for as long as it takes; when it
|
|
17
|
+
* returns, brightness and key listeners are reattached and the caller is asked
|
|
18
|
+
* to repaint.
|
|
19
|
+
*/
|
|
20
|
+
export interface ReconnectingDeviceDeps {
|
|
21
|
+
readonly serial: string
|
|
22
|
+
readonly connect: (opts: {
|
|
23
|
+
readonly serial: string
|
|
24
|
+
}) => Promise<StreamDeckDevice>
|
|
25
|
+
readonly logger: pino.Logger
|
|
26
|
+
/** Gap between reconnect attempts. */
|
|
27
|
+
readonly retryDelayMs?: number
|
|
28
|
+
/** Called after a fresh handle is attached, to restore brightness/repaint. */
|
|
29
|
+
readonly onReconnect?: (device: StreamDeckDevice) => void | Promise<void>
|
|
30
|
+
/** Called when the hardware goes away. */
|
|
31
|
+
readonly onDisconnect?: (err: unknown) => void
|
|
32
|
+
readonly sleep?: (ms: number) => Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ReconnectingDevice extends StreamDeckDevice {
|
|
36
|
+
/** Whether hardware is attached right now. */
|
|
37
|
+
isConnected(): boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_RETRY_DELAY_MS = 2_000
|
|
41
|
+
|
|
42
|
+
export const createReconnectingDevice = (
|
|
43
|
+
initial: StreamDeckDevice,
|
|
44
|
+
deps: ReconnectingDeviceDeps,
|
|
45
|
+
): ReconnectingDevice => {
|
|
46
|
+
const retryDelayMs = deps.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS
|
|
47
|
+
const sleep =
|
|
48
|
+
deps.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms)))
|
|
49
|
+
|
|
50
|
+
let current: StreamDeckDevice | null = initial
|
|
51
|
+
let stopped = false
|
|
52
|
+
let watching = false
|
|
53
|
+
let detachCurrent: (() => void) | null = null
|
|
54
|
+
|
|
55
|
+
const keyHandlers = new Set<(event: StreamDeckKeyEvent) => void>()
|
|
56
|
+
const errorHandlers = new Set<(err: unknown) => void>()
|
|
57
|
+
|
|
58
|
+
const attach = (device: StreamDeckDevice): void => {
|
|
59
|
+
const offKeys = device.onKeyEvent((event) => {
|
|
60
|
+
for (const h of keyHandlers) h(event)
|
|
61
|
+
})
|
|
62
|
+
const offError = device.onError((err) => {
|
|
63
|
+
lose(err)
|
|
64
|
+
})
|
|
65
|
+
detachCurrent = () => {
|
|
66
|
+
offKeys()
|
|
67
|
+
offError()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lose = (err: unknown): void => {
|
|
72
|
+
if (current === null || stopped) return
|
|
73
|
+
const gone = current
|
|
74
|
+
current = null
|
|
75
|
+
detachCurrent?.()
|
|
76
|
+
detachCurrent = null
|
|
77
|
+
deps.logger.warn(
|
|
78
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
79
|
+
"device: Stream Deck disconnected — holding the deck and watching for it to come back",
|
|
80
|
+
)
|
|
81
|
+
for (const h of errorHandlers) h(err)
|
|
82
|
+
deps.onDisconnect?.(err)
|
|
83
|
+
// Best effort: the handle is already dead, and close() on a vanished HID
|
|
84
|
+
// device commonly throws.
|
|
85
|
+
void Promise.resolve()
|
|
86
|
+
.then(() => gone.close())
|
|
87
|
+
.catch(() => undefined)
|
|
88
|
+
void watch()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const watch = async (): Promise<void> => {
|
|
92
|
+
if (watching || stopped) return
|
|
93
|
+
watching = true
|
|
94
|
+
let attempts = 0
|
|
95
|
+
// Deliberately unbounded: the user asked for the deck to be picked up
|
|
96
|
+
// whenever it returns, which on a KVM can be hours later. Nothing here
|
|
97
|
+
// accumulates, and the daemon is otherwise idle without a device.
|
|
98
|
+
while (!stopped && current === null) {
|
|
99
|
+
attempts += 1
|
|
100
|
+
try {
|
|
101
|
+
const device = await deps.connect({ serial: deps.serial })
|
|
102
|
+
if (stopped) {
|
|
103
|
+
void device.close().catch(() => undefined)
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
current = device
|
|
107
|
+
attach(device)
|
|
108
|
+
deps.logger.info(
|
|
109
|
+
{ attempts, serial: deps.serial },
|
|
110
|
+
"device: Stream Deck reconnected",
|
|
111
|
+
)
|
|
112
|
+
try {
|
|
113
|
+
await deps.onReconnect?.(device)
|
|
114
|
+
} catch (err) {
|
|
115
|
+
deps.logger.warn({ err }, "device: post-reconnect restore failed")
|
|
116
|
+
}
|
|
117
|
+
break
|
|
118
|
+
} catch {
|
|
119
|
+
// Not back yet. Logged sparsely so an overnight absence does not
|
|
120
|
+
// produce thousands of identical lines.
|
|
121
|
+
if (attempts === 1 || attempts % 30 === 0) {
|
|
122
|
+
deps.logger.info(
|
|
123
|
+
{ attempts, serial: deps.serial, retryDelayMs },
|
|
124
|
+
"device: still waiting for the Stream Deck",
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
await sleep(retryDelayMs)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
watching = false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Runs a hardware call, treating transport failure as a disconnect. */
|
|
134
|
+
const guard = async (
|
|
135
|
+
action: (device: StreamDeckDevice) => Promise<void>,
|
|
136
|
+
): Promise<void> => {
|
|
137
|
+
const device = current
|
|
138
|
+
// No hardware: drop the write. The renderer keeps ticking against a device
|
|
139
|
+
// that is not there, and its change tracker is reset on reconnect so the
|
|
140
|
+
// dropped frames are repainted then.
|
|
141
|
+
if (device === null) return
|
|
142
|
+
try {
|
|
143
|
+
await action(device)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
lose(err)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Wire the device we were handed, not just the ones we reconnect to —
|
|
150
|
+
// otherwise the first disconnect is never noticed and key events from the
|
|
151
|
+
// initial handle never reach the caller.
|
|
152
|
+
attach(initial)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
// Identity stays that of the device we were asked to track, so callers
|
|
156
|
+
// that captured the descriptor keep working across a swap.
|
|
157
|
+
serial: initial.serial,
|
|
158
|
+
path: initial.path,
|
|
159
|
+
model: initial.model,
|
|
160
|
+
getKeyCount: () => (current ?? initial).getKeyCount(),
|
|
161
|
+
isConnected: () => current !== null,
|
|
162
|
+
async setBrightness(value: number): Promise<void> {
|
|
163
|
+
await guard((d) => d.setBrightness(value))
|
|
164
|
+
},
|
|
165
|
+
async fillKeyBuffer(keyIndex: number, buffer: Buffer): Promise<void> {
|
|
166
|
+
await guard((d) => d.fillKeyBuffer(keyIndex, buffer))
|
|
167
|
+
},
|
|
168
|
+
onKeyEvent(handler: (event: StreamDeckKeyEvent) => void): () => void {
|
|
169
|
+
keyHandlers.add(handler)
|
|
170
|
+
return () => {
|
|
171
|
+
keyHandlers.delete(handler)
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
onError(handler: (err: unknown) => void): () => void {
|
|
175
|
+
errorHandlers.add(handler)
|
|
176
|
+
return () => {
|
|
177
|
+
errorHandlers.delete(handler)
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
async close(): Promise<void> {
|
|
181
|
+
stopped = true
|
|
182
|
+
const device = current
|
|
183
|
+
current = null
|
|
184
|
+
detachCurrent?.()
|
|
185
|
+
detachCurrent = null
|
|
186
|
+
keyHandlers.clear()
|
|
187
|
+
errorHandlers.clear()
|
|
188
|
+
if (device !== null) await device.close()
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { DEFAULT_RETRY_DELAY_MS }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listStreamDecks,
|
|
3
|
+
type StreamDeckDeviceInfo,
|
|
4
|
+
} from "@elgato-stream-deck/node"
|
|
5
|
+
import { resolveKeyCount } from "@/device/models"
|
|
6
|
+
|
|
7
|
+
export type DeviceTransport = "real" | "emulated"
|
|
8
|
+
|
|
9
|
+
export interface DeviceDescriptor {
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly model: string
|
|
12
|
+
readonly keyCount: number
|
|
13
|
+
readonly label: string
|
|
14
|
+
readonly transport: DeviceTransport
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const toDescriptor = (info: StreamDeckDeviceInfo): DeviceDescriptor => {
|
|
18
|
+
const model = info.model
|
|
19
|
+
const id = info.serialNumber ?? info.path
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
model,
|
|
23
|
+
keyCount: resolveKeyCount(model),
|
|
24
|
+
label: `${model.toUpperCase()} (${id})`,
|
|
25
|
+
transport: "real",
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const listDevices = async (): Promise<DeviceDescriptor[]> => {
|
|
30
|
+
try {
|
|
31
|
+
const infos = await listStreamDecks()
|
|
32
|
+
return infos.map(toDescriptor).sort((a, b) => a.id.localeCompare(b.id))
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// ponytail: log so real errors (missing model entry, HID I/O failure) aren't hidden as "no devices"
|
|
35
|
+
console.warn({ err }, "device enumeration failed")
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
}
|