@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,53 @@
|
|
|
1
|
+
import type { AddonManifestV1 } from "@/addon/api"
|
|
2
|
+
|
|
3
|
+
import { globalService } from "./backend"
|
|
4
|
+
import mediaPlayerBackend from "./buttons/media-player/backend"
|
|
5
|
+
import mediaPlayerFrontend from "./buttons/media-player/frontend"
|
|
6
|
+
import mediaMuteBackend from "./buttons/volume/mute/backend"
|
|
7
|
+
import mediaMuteFrontend from "./buttons/volume/mute/frontend"
|
|
8
|
+
import volumeDownBackend from "./buttons/volume/volume-down/backend"
|
|
9
|
+
import volumeDownFrontend from "./buttons/volume/volume-down/frontend"
|
|
10
|
+
import volumeUpBackend from "./buttons/volume/volume-up/backend"
|
|
11
|
+
import volumeUpFrontend from "./buttons/volume/volume-up/frontend"
|
|
12
|
+
|
|
13
|
+
export const manifest: AddonManifestV1 = {
|
|
14
|
+
apiVersion: 1,
|
|
15
|
+
name: "media",
|
|
16
|
+
buttonTypes: {
|
|
17
|
+
"media:player": {
|
|
18
|
+
frontend: mediaPlayerFrontend,
|
|
19
|
+
service: {
|
|
20
|
+
...mediaPlayerBackend,
|
|
21
|
+
gestureHandlers: ["tap", "dbl-tap", "hold"] as const,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
"media:mute": {
|
|
25
|
+
frontend: mediaMuteFrontend,
|
|
26
|
+
service: {
|
|
27
|
+
...mediaMuteBackend,
|
|
28
|
+
gestureHandlers: ["tap"] as const,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
"media:volume:up": {
|
|
32
|
+
frontend: volumeUpFrontend,
|
|
33
|
+
service: {
|
|
34
|
+
...volumeUpBackend,
|
|
35
|
+
gestureHandlers: ["tap"] as const,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
"media:volume:down": {
|
|
39
|
+
frontend: volumeDownFrontend,
|
|
40
|
+
service: {
|
|
41
|
+
...volumeDownBackend,
|
|
42
|
+
gestureHandlers: ["tap"] as const,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
globalService,
|
|
47
|
+
// ponytail: addon-level OS checks (playerctl/wpctl/osascript/powershell)
|
|
48
|
+
// are built at the Node edge by check-runner, not here. This manifest
|
|
49
|
+
// is bundled into the frontend vite graph; importing node:child_process
|
|
50
|
+
// here would externalize it for the browser and break the SPA on load.
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default manifest
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const formatTime = (seconds: number): string => {
|
|
2
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "0:00"
|
|
3
|
+
const total = Math.floor(seconds)
|
|
4
|
+
const mins = Math.floor(total / 60)
|
|
5
|
+
const secs = total % 60
|
|
6
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const computeProgress = (
|
|
10
|
+
currentTime: number,
|
|
11
|
+
totalTime: number,
|
|
12
|
+
): number => {
|
|
13
|
+
if (!Number.isFinite(totalTime) || totalTime <= 0) return 0
|
|
14
|
+
if (!Number.isFinite(currentTime) || currentTime < 0) return 0
|
|
15
|
+
return Math.min(100, Math.max(0, (currentTime / totalTime) * 100))
|
|
16
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MediaStatus,
|
|
3
|
+
MediaStatusProvider,
|
|
4
|
+
ProviderExecutor,
|
|
5
|
+
} from "./types"
|
|
6
|
+
|
|
7
|
+
interface DarwinDeps {
|
|
8
|
+
readonly executor: ProviderExecutor
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ponytail: this provider used to hardcode `tell application "Spotify"` for
|
|
12
|
+
// every call. Three problems, all user-visible:
|
|
13
|
+
//
|
|
14
|
+
// 1. AppleScript's `tell application "X"` LAUNCHES X if it isn't running. The
|
|
15
|
+
// media addon polls getStatus on a timer, so on any Mac with Spotify
|
|
16
|
+
// installed but closed the poller reopened Spotify over and over.
|
|
17
|
+
// 2. The volume verbs drove Spotify's own in-app volume, not the system
|
|
18
|
+
// volume, so `media:volume:up` did nothing audible when another app was
|
|
19
|
+
// playing — and nothing at all without Spotify.
|
|
20
|
+
// 3. getStatus hardcoded `volume: 1, muted: false, totalTime: 0,
|
|
21
|
+
// currentTime: 0`, so the volume readout was pinned to full and the
|
|
22
|
+
// progress bar never moved.
|
|
23
|
+
//
|
|
24
|
+
// Now: the running player is discovered from the System Events process list
|
|
25
|
+
// (which neither launches anything nor needs an Accessibility grant), track
|
|
26
|
+
// state comes from whichever supported player is actually running, and volume
|
|
27
|
+
// is the real system output volume. The Linux provider is player-agnostic via
|
|
28
|
+
// MPRIS; this is the closest macOS equivalent.
|
|
29
|
+
|
|
30
|
+
// ASCII unit separator - cannot occur in track metadata.
|
|
31
|
+
const SEP = "\u001f"
|
|
32
|
+
|
|
33
|
+
interface PlayerSpec {
|
|
34
|
+
/** Process name as it appears in the System Events process list. */
|
|
35
|
+
readonly process: string
|
|
36
|
+
/** Application name to address in AppleScript. */
|
|
37
|
+
readonly app: string
|
|
38
|
+
/** `duration of current track` unit — Spotify reports ms, Music seconds. */
|
|
39
|
+
readonly durationUnit: "ms" | "s"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Order is preference order when several are running.
|
|
43
|
+
const PLAYERS: ReadonlyArray<PlayerSpec> = [
|
|
44
|
+
{ process: "Spotify", app: "Spotify", durationUnit: "ms" },
|
|
45
|
+
{ process: "Music", app: "Music", durationUnit: "s" },
|
|
46
|
+
{ process: "TV", app: "TV", durationUnit: "s" },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
const STATUS_MAP: Readonly<Record<string, MediaStatus["playStatus"]>> = {
|
|
50
|
+
playing: "play",
|
|
51
|
+
paused: "pause",
|
|
52
|
+
stopped: "stop",
|
|
53
|
+
"fast forwarding": "play",
|
|
54
|
+
rewinding: "play",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const UNAVAILABLE: MediaStatus = {
|
|
58
|
+
track: null,
|
|
59
|
+
totalTime: 0,
|
|
60
|
+
currentTime: 0,
|
|
61
|
+
playStatus: "unavailable",
|
|
62
|
+
volume: 1,
|
|
63
|
+
muted: false,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const LIST_PROCESSES = `tell application "System Events" to get name of every process whose background only is false`
|
|
67
|
+
|
|
68
|
+
const statusScript = (app: string): string => `set sep to (ASCII character 31)
|
|
69
|
+
tell application "${app}"
|
|
70
|
+
set st to (player state as string)
|
|
71
|
+
try
|
|
72
|
+
set pos to player position
|
|
73
|
+
on error
|
|
74
|
+
set pos to 0
|
|
75
|
+
end try
|
|
76
|
+
try
|
|
77
|
+
set t to name of current track
|
|
78
|
+
set a to artist of current track
|
|
79
|
+
set al to album of current track
|
|
80
|
+
set d to duration of current track
|
|
81
|
+
on error
|
|
82
|
+
set t to ""
|
|
83
|
+
set a to ""
|
|
84
|
+
set al to ""
|
|
85
|
+
set d to 0
|
|
86
|
+
end try
|
|
87
|
+
set out to st & sep & pos & sep & d & sep & t & sep & a & sep & al
|
|
88
|
+
end tell
|
|
89
|
+
return out`
|
|
90
|
+
|
|
91
|
+
const VOLUME_SCRIPT = `set sep to (ASCII character 31)
|
|
92
|
+
set s to (get volume settings)
|
|
93
|
+
return (output volume of s as string) & sep & (output muted of s as string)`
|
|
94
|
+
|
|
95
|
+
const clamp01 = (n: number): number => Math.max(0, Math.min(1, n))
|
|
96
|
+
|
|
97
|
+
const toNumber = (raw: string | undefined): number => {
|
|
98
|
+
const n = Number.parseFloat((raw ?? "").trim())
|
|
99
|
+
return Number.isFinite(n) ? n : 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const parseVolumeSettings = (
|
|
103
|
+
stdout: string,
|
|
104
|
+
): { volume: number; muted: boolean } => {
|
|
105
|
+
const [vol, muted] = stdout.trim().split(SEP)
|
|
106
|
+
return {
|
|
107
|
+
volume: clamp01(toNumber(vol) / 100),
|
|
108
|
+
muted: (muted ?? "").trim().toLowerCase() === "true",
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const parsePlayerStatus = (
|
|
113
|
+
stdout: string,
|
|
114
|
+
durationUnit: PlayerSpec["durationUnit"],
|
|
115
|
+
): Omit<MediaStatus, "volume" | "muted"> => {
|
|
116
|
+
const parts = stdout.trim().split(SEP)
|
|
117
|
+
const state = (parts[0] ?? "").trim().toLowerCase()
|
|
118
|
+
const currentTime = toNumber(parts[1])
|
|
119
|
+
const rawDuration = toNumber(parts[2])
|
|
120
|
+
const totalTime = durationUnit === "ms" ? rawDuration / 1000 : rawDuration
|
|
121
|
+
const name = (parts[3] ?? "").trim()
|
|
122
|
+
const artist = (parts[4] ?? "").trim()
|
|
123
|
+
const album = (parts[5] ?? "").trim()
|
|
124
|
+
return {
|
|
125
|
+
track:
|
|
126
|
+
name.length > 0 && name !== "missing value"
|
|
127
|
+
? {
|
|
128
|
+
name,
|
|
129
|
+
artist: artist === "missing value" ? "" : artist,
|
|
130
|
+
...(album.length > 0 && album !== "missing value" ? { album } : {}),
|
|
131
|
+
}
|
|
132
|
+
: null,
|
|
133
|
+
totalTime,
|
|
134
|
+
currentTime,
|
|
135
|
+
playStatus: STATUS_MAP[state] ?? "unavailable",
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const runOsascript = async (
|
|
140
|
+
deps: DarwinDeps,
|
|
141
|
+
script: string,
|
|
142
|
+
timeoutMs = 2_000,
|
|
143
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> =>
|
|
144
|
+
deps.executor.run("osascript", ["-e", script], { timeoutMs })
|
|
145
|
+
|
|
146
|
+
const requireOsascript = async (
|
|
147
|
+
deps: DarwinDeps,
|
|
148
|
+
script: string,
|
|
149
|
+
): Promise<void> => {
|
|
150
|
+
const result = await runOsascript(deps, script, 5_000)
|
|
151
|
+
if (result.exitCode !== 0) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`osascript failed: ${result.stderr.trim() || "exit " + result.exitCode}`,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The supported player that is currently running, or null. Reads the System
|
|
160
|
+
* Events process list rather than probing each app, because probing an app is
|
|
161
|
+
* what launches it.
|
|
162
|
+
*/
|
|
163
|
+
const activePlayer = async (deps: DarwinDeps): Promise<PlayerSpec | null> => {
|
|
164
|
+
const result = await runOsascript(deps, LIST_PROCESSES)
|
|
165
|
+
if (result.exitCode !== 0) return null
|
|
166
|
+
const running = new Set(
|
|
167
|
+
result.stdout
|
|
168
|
+
.trim()
|
|
169
|
+
.split(",")
|
|
170
|
+
.map((s) => s.trim()),
|
|
171
|
+
)
|
|
172
|
+
return PLAYERS.find((p) => running.has(p.process)) ?? null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const readVolume = async (
|
|
176
|
+
deps: DarwinDeps,
|
|
177
|
+
): Promise<{ volume: number; muted: boolean }> => {
|
|
178
|
+
const result = await runOsascript(deps, VOLUME_SCRIPT)
|
|
179
|
+
if (result.exitCode !== 0) return { volume: 1, muted: false }
|
|
180
|
+
return parseVolumeSettings(result.stdout)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const setSystemVolume = async (
|
|
184
|
+
deps: DarwinDeps,
|
|
185
|
+
fraction: number,
|
|
186
|
+
): Promise<void> => {
|
|
187
|
+
const pct = Math.round(clamp01(fraction) * 100)
|
|
188
|
+
await requireOsascript(deps, `set volume output volume ${pct}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ponytail: `media-control` (brew install media-control) is the macOS analogue
|
|
192
|
+
// of Linux's playerctl — one system-wide interface over whatever is actually
|
|
193
|
+
// playing, including browsers, rather than a per-app AppleScript dictionary.
|
|
194
|
+
// It wraps MediaRemote, so it sees Chrome, Safari, Spotify, Music, VLC and the
|
|
195
|
+
// rest through the same Now Playing surface the media keys drive.
|
|
196
|
+
//
|
|
197
|
+
// Everything below the helper stays as the fallback: when media-control is not
|
|
198
|
+
// installed we can still talk to the scriptable desktop players directly, so a
|
|
199
|
+
// user who only uses Spotify keeps working without installing anything.
|
|
200
|
+
const MEDIA_CONTROL_BIN = "media-control"
|
|
201
|
+
|
|
202
|
+
export const MEDIA_CONTROL_HINT =
|
|
203
|
+
"install it with `brew install media-control` for media detection across all apps (browsers included)"
|
|
204
|
+
|
|
205
|
+
interface MediaControlPayload {
|
|
206
|
+
readonly title?: string | null
|
|
207
|
+
readonly artist?: string | null
|
|
208
|
+
readonly album?: string | null
|
|
209
|
+
readonly duration?: number | null
|
|
210
|
+
readonly elapsedTime?: number | null
|
|
211
|
+
readonly elapsedTimeNow?: number | null
|
|
212
|
+
readonly playing?: boolean | null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const parseMediaControlPayload = (
|
|
216
|
+
stdout: string,
|
|
217
|
+
): Omit<MediaStatus, "volume" | "muted"> | null => {
|
|
218
|
+
const text = stdout.trim()
|
|
219
|
+
if (text.length === 0) return null
|
|
220
|
+
let raw: MediaControlPayload
|
|
221
|
+
try {
|
|
222
|
+
raw = JSON.parse(text) as MediaControlPayload
|
|
223
|
+
} catch {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
const title = typeof raw.title === "string" ? raw.title.trim() : ""
|
|
227
|
+
// No title means nothing is loaded in the Now Playing surface at all.
|
|
228
|
+
if (title.length === 0) {
|
|
229
|
+
return {
|
|
230
|
+
track: null,
|
|
231
|
+
totalTime: 0,
|
|
232
|
+
currentTime: 0,
|
|
233
|
+
playStatus: "unavailable",
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const artist = typeof raw.artist === "string" ? raw.artist.trim() : ""
|
|
237
|
+
const album = typeof raw.album === "string" ? raw.album.trim() : ""
|
|
238
|
+
const duration = typeof raw.duration === "number" ? raw.duration : 0
|
|
239
|
+
const elapsed =
|
|
240
|
+
typeof raw.elapsedTimeNow === "number"
|
|
241
|
+
? raw.elapsedTimeNow
|
|
242
|
+
: typeof raw.elapsedTime === "number"
|
|
243
|
+
? raw.elapsedTime
|
|
244
|
+
: 0
|
|
245
|
+
return {
|
|
246
|
+
track: { name: title, artist, ...(album.length > 0 ? { album } : {}) },
|
|
247
|
+
totalTime: Number.isFinite(duration) ? Math.max(0, duration) : 0,
|
|
248
|
+
currentTime: Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0,
|
|
249
|
+
// `playing` is the only authoritative flag; a paused track still reports a
|
|
250
|
+
// full payload, so absence of `playing` means paused rather than stopped.
|
|
251
|
+
playStatus: raw.playing === true ? "play" : "pause",
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export const createDarwinProvider = (deps: DarwinDeps): MediaStatusProvider => {
|
|
256
|
+
// null = not probed yet, true/false = cached result of `media-control test`.
|
|
257
|
+
let helperUsable: boolean | null = null
|
|
258
|
+
|
|
259
|
+
const hasMediaControl = async (): Promise<boolean> => {
|
|
260
|
+
if (helperUsable !== null) return helperUsable
|
|
261
|
+
try {
|
|
262
|
+
// `test` reports whether the tool can operate on this macOS version —
|
|
263
|
+
// MediaRemote access has been tightened repeatedly, so a present binary
|
|
264
|
+
// is not proof of a working one.
|
|
265
|
+
const r = await deps.executor.run(MEDIA_CONTROL_BIN, ["test"], {
|
|
266
|
+
timeoutMs: 2_000,
|
|
267
|
+
})
|
|
268
|
+
helperUsable = r.exitCode === 0
|
|
269
|
+
} catch {
|
|
270
|
+
helperUsable = false
|
|
271
|
+
}
|
|
272
|
+
return helperUsable
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const mediaControlStatus = async (): Promise<Omit<
|
|
276
|
+
MediaStatus,
|
|
277
|
+
"volume" | "muted"
|
|
278
|
+
> | null> => {
|
|
279
|
+
try {
|
|
280
|
+
const r = await deps.executor.run(
|
|
281
|
+
MEDIA_CONTROL_BIN,
|
|
282
|
+
["get", "--now", "--no-artwork"],
|
|
283
|
+
{ timeoutMs: 2_000 },
|
|
284
|
+
)
|
|
285
|
+
if (r.exitCode !== 0) return null
|
|
286
|
+
return parseMediaControlPayload(r.stdout)
|
|
287
|
+
} catch {
|
|
288
|
+
return null
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const mediaControlCommand = async (command: string): Promise<boolean> => {
|
|
293
|
+
if (!(await hasMediaControl())) return false
|
|
294
|
+
try {
|
|
295
|
+
const r = await deps.executor.run(MEDIA_CONTROL_BIN, [command], {
|
|
296
|
+
timeoutMs: 2_000,
|
|
297
|
+
})
|
|
298
|
+
return r.exitCode === 0
|
|
299
|
+
} catch {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Control verbs are no-ops when nothing is playing — deliberately NOT a
|
|
305
|
+
// throw, so a deck press on an idle machine doesn't surface an error tile.
|
|
306
|
+
const tellPlayer = async (command: string): Promise<void> => {
|
|
307
|
+
const player = await activePlayer(deps)
|
|
308
|
+
if (player === null) return
|
|
309
|
+
await requireOsascript(
|
|
310
|
+
deps,
|
|
311
|
+
`tell application "${player.app}" to ${command}`,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** media-control first (covers every app), scriptable players as fallback. */
|
|
316
|
+
const control = async (
|
|
317
|
+
helperCommand: string,
|
|
318
|
+
appleScriptCommand: string,
|
|
319
|
+
): Promise<void> => {
|
|
320
|
+
if (await mediaControlCommand(helperCommand)) return
|
|
321
|
+
await tellPlayer(appleScriptCommand)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
async getStatus() {
|
|
326
|
+
if (await hasMediaControl()) {
|
|
327
|
+
const [status, volume] = await Promise.all([
|
|
328
|
+
mediaControlStatus(),
|
|
329
|
+
readVolume(deps),
|
|
330
|
+
])
|
|
331
|
+
if (status !== null) return { ...status, ...volume }
|
|
332
|
+
return { ...UNAVAILABLE, ...volume }
|
|
333
|
+
}
|
|
334
|
+
return legacyGetStatus(deps)
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
async play() {
|
|
338
|
+
await control("play", "play")
|
|
339
|
+
},
|
|
340
|
+
async pause() {
|
|
341
|
+
await control("pause", "pause")
|
|
342
|
+
},
|
|
343
|
+
async toggle() {
|
|
344
|
+
await control("toggle-play-pause", "playpause")
|
|
345
|
+
},
|
|
346
|
+
async next() {
|
|
347
|
+
await control("next-track", "next track")
|
|
348
|
+
},
|
|
349
|
+
async previous() {
|
|
350
|
+
await control("previous-track", "previous track")
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
async setVolume(value) {
|
|
354
|
+
await setSystemVolume(deps, value)
|
|
355
|
+
},
|
|
356
|
+
async volumeUp(step) {
|
|
357
|
+
const { volume } = await readVolume(deps)
|
|
358
|
+
await setSystemVolume(deps, volume + step)
|
|
359
|
+
},
|
|
360
|
+
async volumeDown(step) {
|
|
361
|
+
const { volume } = await readVolume(deps)
|
|
362
|
+
await setSystemVolume(deps, volume - step)
|
|
363
|
+
},
|
|
364
|
+
async toggleMute() {
|
|
365
|
+
const { muted } = await readVolume(deps)
|
|
366
|
+
await requireOsascript(
|
|
367
|
+
deps,
|
|
368
|
+
`set volume ${muted ? "without" : "with"} output muted`,
|
|
369
|
+
)
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* AppleScript-only status path, used when media-control is not installed. It
|
|
376
|
+
* can only see the scriptable desktop players, which is why media-control is
|
|
377
|
+
* preferred — but it keeps Spotify/Music users working with no extra install.
|
|
378
|
+
*/
|
|
379
|
+
const legacyGetStatus = async (deps: DarwinDeps): Promise<MediaStatus> => {
|
|
380
|
+
const [player, volume] = await Promise.all([
|
|
381
|
+
activePlayer(deps),
|
|
382
|
+
readVolume(deps),
|
|
383
|
+
])
|
|
384
|
+
if (player === null) return { ...UNAVAILABLE, ...volume }
|
|
385
|
+
const result = await runOsascript(deps, statusScript(player.app))
|
|
386
|
+
if (result.exitCode !== 0) return { ...UNAVAILABLE, ...volume }
|
|
387
|
+
return {
|
|
388
|
+
...parsePlayerStatus(result.stdout, player.durationUnit),
|
|
389
|
+
...volume,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createDarwinProvider } from "./darwin"
|
|
2
|
+
import { createLinuxProvider } from "./linux"
|
|
3
|
+
import type { MediaStatusProvider, ProviderExecutor } from "./types"
|
|
4
|
+
import { createWindowsProvider } from "./windows"
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
MediaStatus,
|
|
8
|
+
MediaStatusProvider,
|
|
9
|
+
MediaTrack,
|
|
10
|
+
ProviderExecutor,
|
|
11
|
+
} from "./types"
|
|
12
|
+
|
|
13
|
+
export const createMediaProvider = (
|
|
14
|
+
platform: NodeJS.Platform,
|
|
15
|
+
executor: ProviderExecutor,
|
|
16
|
+
): MediaStatusProvider => {
|
|
17
|
+
switch (platform) {
|
|
18
|
+
case "linux":
|
|
19
|
+
return createLinuxProvider({ executor })
|
|
20
|
+
case "darwin":
|
|
21
|
+
return createDarwinProvider({ executor })
|
|
22
|
+
case "win32":
|
|
23
|
+
return createWindowsProvider({ executor })
|
|
24
|
+
default:
|
|
25
|
+
return createLinuxProvider({ executor })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MediaStatus,
|
|
3
|
+
MediaStatusProvider,
|
|
4
|
+
ProviderExecutor,
|
|
5
|
+
} from "./types"
|
|
6
|
+
|
|
7
|
+
const METADATA_TIMEOUT_MS = 2_000
|
|
8
|
+
|
|
9
|
+
interface LinuxDeps {
|
|
10
|
+
readonly executor: ProviderExecutor
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const STATUS_MAP = {
|
|
14
|
+
Playing: "play",
|
|
15
|
+
Paused: "pause",
|
|
16
|
+
Stopped: "stop",
|
|
17
|
+
} as const satisfies Record<string, MediaStatus["playStatus"]>
|
|
18
|
+
|
|
19
|
+
const runWpctl = async (
|
|
20
|
+
deps: LinuxDeps,
|
|
21
|
+
args: ReadonlyArray<string>,
|
|
22
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> => {
|
|
23
|
+
return deps.executor.run("wpctl", args, { timeoutMs: 5_000 })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PLAYER_FORMAT =
|
|
27
|
+
"{{ playerName }}\t{{ status }}\t{{ title }}\t{{ artist }}\t{{ album }}\t{{ mpris:length }}"
|
|
28
|
+
|
|
29
|
+
const selectPlayer = (
|
|
30
|
+
stdout: string,
|
|
31
|
+
): { name: string; status: string } | null => {
|
|
32
|
+
const players = stdout
|
|
33
|
+
.trim()
|
|
34
|
+
.split("\n")
|
|
35
|
+
.map((line) => line.split("\t"))
|
|
36
|
+
.filter(([name, status]) => name && status)
|
|
37
|
+
.map(([name, status]) => ({ name: name!.trim(), status: status!.trim() }))
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
players.find(({ status }) => status === "Playing" || status === "Paused") ??
|
|
41
|
+
players[0] ??
|
|
42
|
+
null
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const readStatus = async (deps: LinuxDeps): Promise<MediaStatus> => {
|
|
47
|
+
const playerResult = await deps.executor.run(
|
|
48
|
+
"playerctl",
|
|
49
|
+
["-a", "metadata", "--format", PLAYER_FORMAT],
|
|
50
|
+
{ timeoutMs: METADATA_TIMEOUT_MS },
|
|
51
|
+
)
|
|
52
|
+
const selectedPlayer = selectPlayer(playerResult.stdout)
|
|
53
|
+
const playerArgs = selectedPlayer ? ["--player", selectedPlayer.name] : []
|
|
54
|
+
|
|
55
|
+
const [metaResult, positionResult] = await Promise.all([
|
|
56
|
+
deps.executor.run(
|
|
57
|
+
"playerctl",
|
|
58
|
+
[
|
|
59
|
+
...playerArgs,
|
|
60
|
+
"metadata",
|
|
61
|
+
"--format",
|
|
62
|
+
"{{ title }}\t{{ artist }}\t{{ album }}\t{{ mpris:length }}",
|
|
63
|
+
],
|
|
64
|
+
{ timeoutMs: METADATA_TIMEOUT_MS },
|
|
65
|
+
),
|
|
66
|
+
deps.executor.run("playerctl", [...playerArgs, "position"], {
|
|
67
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
68
|
+
}),
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
const [playStatusResult, volumeResult] = await Promise.all([
|
|
72
|
+
deps.executor.run("playerctl", [...playerArgs, "status"], {
|
|
73
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
74
|
+
}),
|
|
75
|
+
runWpctl(deps, ["get-volume", "@DEFAULT_AUDIO_SINK@"]),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
const track =
|
|
79
|
+
metaResult.exitCode === 0 && metaResult.stdout.trim().length > 0
|
|
80
|
+
? (() => {
|
|
81
|
+
const parts = metaResult.stdout.trim().split("\t")
|
|
82
|
+
const name = (parts[0] ?? "").trim()
|
|
83
|
+
if (name.length === 0) return null
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
artist: parts[1]?.trim() || "",
|
|
87
|
+
album: parts[2]?.trim() || undefined,
|
|
88
|
+
}
|
|
89
|
+
})()
|
|
90
|
+
: null
|
|
91
|
+
|
|
92
|
+
const totalTime =
|
|
93
|
+
metaResult.exitCode === 0
|
|
94
|
+
? (() => {
|
|
95
|
+
const parts = metaResult.stdout.trim().split("\t")
|
|
96
|
+
const lenStr = parts[3]?.trim()
|
|
97
|
+
if (!lenStr) return 0
|
|
98
|
+
const us = Number.parseInt(lenStr, 10)
|
|
99
|
+
return Math.round(us / 1_000_000)
|
|
100
|
+
})()
|
|
101
|
+
: 0
|
|
102
|
+
|
|
103
|
+
const currentTime =
|
|
104
|
+
positionResult.exitCode === 0
|
|
105
|
+
? Math.round(Number.parseFloat(positionResult.stdout.trim()))
|
|
106
|
+
: 0
|
|
107
|
+
|
|
108
|
+
const wpctlState =
|
|
109
|
+
volumeResult.exitCode === 0
|
|
110
|
+
? parseWpctlVolumeLine(volumeResult.stdout)
|
|
111
|
+
: { volume: 1, muted: false }
|
|
112
|
+
|
|
113
|
+
const statusStr = selectedPlayer?.status ?? playStatusResult.stdout.trim()
|
|
114
|
+
const playStatus: MediaStatus["playStatus"] =
|
|
115
|
+
STATUS_MAP[statusStr as keyof typeof STATUS_MAP] ?? "unavailable"
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
track,
|
|
119
|
+
totalTime,
|
|
120
|
+
currentTime,
|
|
121
|
+
playStatus,
|
|
122
|
+
volume: wpctlState.volume,
|
|
123
|
+
muted: wpctlState.muted,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface WpctlVolumeLine {
|
|
128
|
+
readonly volume: number
|
|
129
|
+
readonly muted: boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const VOLUME_LINE_RE = /^Volume:\s*(\d+(?:\.\d+)?)(?:\s*\[\s*MUTED\s*\])?/im
|
|
133
|
+
|
|
134
|
+
const parseWpctlVolumeLine = (stdout: string): WpctlVolumeLine => {
|
|
135
|
+
const match = stdout.match(VOLUME_LINE_RE)
|
|
136
|
+
if (match === null) return { volume: 1, muted: false }
|
|
137
|
+
const volume = Math.max(0, Math.min(1, parseFloat(match[1] ?? "")))
|
|
138
|
+
return { volume, muted: /\[\s*MUTED\s*\]/i.test(match[0]) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const createLinuxProvider = (deps: LinuxDeps): MediaStatusProvider => ({
|
|
142
|
+
async getStatus() {
|
|
143
|
+
return readStatus(deps)
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async play() {
|
|
147
|
+
await deps.executor.run("playerctl", ["play"], {
|
|
148
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
async pause() {
|
|
152
|
+
await deps.executor.run("playerctl", ["pause"], {
|
|
153
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
async toggle() {
|
|
157
|
+
await deps.executor.run("playerctl", ["play-pause"], {
|
|
158
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
159
|
+
})
|
|
160
|
+
},
|
|
161
|
+
async next() {
|
|
162
|
+
await deps.executor.run("playerctl", ["next"], {
|
|
163
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
async previous() {
|
|
167
|
+
await deps.executor.run("playerctl", ["previous"], {
|
|
168
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
169
|
+
})
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
async setVolume(value: number) {
|
|
173
|
+
await runWpctl(deps, [
|
|
174
|
+
"set-volume",
|
|
175
|
+
"@DEFAULT_AUDIO_SINK@",
|
|
176
|
+
String(Math.round(value * 100)) + "%",
|
|
177
|
+
])
|
|
178
|
+
},
|
|
179
|
+
async volumeUp(step: number) {
|
|
180
|
+
await runWpctl(deps, [
|
|
181
|
+
"set-volume",
|
|
182
|
+
"@DEFAULT_AUDIO_SINK@",
|
|
183
|
+
String(Math.round(step * 100)) + "%+",
|
|
184
|
+
])
|
|
185
|
+
},
|
|
186
|
+
async volumeDown(step: number) {
|
|
187
|
+
await runWpctl(deps, [
|
|
188
|
+
"set-volume",
|
|
189
|
+
"@DEFAULT_AUDIO_SINK@",
|
|
190
|
+
String(Math.round(step * 100)) + "%-",
|
|
191
|
+
])
|
|
192
|
+
},
|
|
193
|
+
async toggleMute() {
|
|
194
|
+
await runWpctl(deps, ["set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"])
|
|
195
|
+
},
|
|
196
|
+
})
|