@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.
Files changed (485) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/bin/_wrapper-shared.js +42 -0
  4. package/bin/dev.js +95 -0
  5. package/bin/sirenodeck.js +42 -0
  6. package/config/default-config.yml +117 -0
  7. package/config/starter-config.yml +54 -0
  8. package/dist/main.mjs +64702 -0
  9. package/package.json +140 -0
  10. package/src/action/executor.ts +156 -0
  11. package/src/action/index.ts +7 -0
  12. package/src/addon/api-types.ts +15 -0
  13. package/src/addon/api.ts +461 -0
  14. package/src/addon/check-runner.ts +84 -0
  15. package/src/addon/index.ts +5 -0
  16. package/src/addon/loader.ts +441 -0
  17. package/src/addon/registry.ts +230 -0
  18. package/src/addon/spec.ts +69 -0
  19. package/src/api/addon.ts +33 -0
  20. package/src/api/asset-url.ts +24 -0
  21. package/src/api/index.ts +50 -0
  22. package/src/api/protocol-internal.ts +656 -0
  23. package/src/api/protocol.ts +2 -0
  24. package/src/api/react/index.ts +11 -0
  25. package/src/api/react/registry.ts +91 -0
  26. package/src/api/react/use-addon-channel.ts +26 -0
  27. package/src/api/react/use-button-state.tsx +23 -0
  28. package/src/api/react/use-deck.ts +24 -0
  29. package/src/assets/logo.png +0 -0
  30. package/src/assets/logo72x72.png +0 -0
  31. package/src/assets/logoFull.png +0 -0
  32. package/src/builtin-addons/brightness/README.md +51 -0
  33. package/src/builtin-addons/brightness/buttons/brightness/backend.ts +13 -0
  34. package/src/builtin-addons/brightness/buttons/brightness/config.ts +10 -0
  35. package/src/builtin-addons/brightness/buttons/brightness/frontend.tsx +50 -0
  36. package/src/builtin-addons/brightness/domain/macos.ts +46 -0
  37. package/src/builtin-addons/brightness/index.d.ts +5 -0
  38. package/src/builtin-addons/brightness/index.ts +20 -0
  39. package/src/builtin-addons/brightness/poller.ts +27 -0
  40. package/src/builtin-addons/brightness/providers/darwin.ts +107 -0
  41. package/src/builtin-addons/brightness/providers/index.ts +81 -0
  42. package/src/builtin-addons/brightness/providers/linux.ts +108 -0
  43. package/src/builtin-addons/brightness/providers/windows.ts +77 -0
  44. package/src/builtin-addons/brightness/sirenodeck.json +6 -0
  45. package/src/builtin-addons/coding-agents/assets/claude-code.svg +1 -0
  46. package/src/builtin-addons/coding-agents/assets/opencode-dark-square.svg +1 -0
  47. package/src/builtin-addons/coding-agents/buttons/agent/backend.ts +82 -0
  48. package/src/builtin-addons/coding-agents/buttons/agent/config.ts +13 -0
  49. package/src/builtin-addons/coding-agents/buttons/agent/frontend.tsx +130 -0
  50. package/src/builtin-addons/coding-agents/buttons/agent/pages.tsx +150 -0
  51. package/src/builtin-addons/coding-agents/buttons/summary/backend.ts +48 -0
  52. package/src/builtin-addons/coding-agents/buttons/summary/config.ts +11 -0
  53. package/src/builtin-addons/coding-agents/buttons/summary/frontend.tsx +254 -0
  54. package/src/builtin-addons/coding-agents/decks/agents.ts +69 -0
  55. package/src/builtin-addons/coding-agents/global/backend.ts +402 -0
  56. package/src/builtin-addons/coding-agents/global-entry.ts +7 -0
  57. package/src/builtin-addons/coding-agents/hook/script.ts +123 -0
  58. package/src/builtin-addons/coding-agents/index.ts +15 -0
  59. package/src/builtin-addons/coding-agents/manifest.ts +115 -0
  60. package/src/builtin-addons/coding-agents/providers/claude-code.ts +470 -0
  61. package/src/builtin-addons/coding-agents/providers/claude-instances.ts +184 -0
  62. package/src/builtin-addons/coding-agents/providers/opencode-instances.ts +108 -0
  63. package/src/builtin-addons/coding-agents/providers/opencode.ts +636 -0
  64. package/src/builtin-addons/coding-agents/providers/registry.ts +120 -0
  65. package/src/builtin-addons/coding-agents/providers/spawn.ts +112 -0
  66. package/src/builtin-addons/coding-agents/shared/claude-hook-events.ts +55 -0
  67. package/src/builtin-addons/coding-agents/shared/claude-status.ts +236 -0
  68. package/src/builtin-addons/coding-agents/shared/live-count.ts +39 -0
  69. package/src/builtin-addons/coding-agents/shared/notifier.ts +58 -0
  70. package/src/builtin-addons/coding-agents/shared/opencode-status.ts +82 -0
  71. package/src/builtin-addons/coding-agents/shared/snapshot.ts +66 -0
  72. package/src/builtin-addons/coding-agents/shared/state.ts +72 -0
  73. package/src/builtin-addons/coding-agents/sirenodeck.json +6 -0
  74. package/src/builtin-addons/coding-agents/types/types.ts +33 -0
  75. package/src/builtin-addons/core/README.md +89 -0
  76. package/src/builtin-addons/core/buttons/action/backend.ts +7 -0
  77. package/src/builtin-addons/core/buttons/action/config.ts +13 -0
  78. package/src/builtin-addons/core/buttons/action/frontend.tsx +16 -0
  79. package/src/builtin-addons/core/buttons/change-deck/backend.ts +18 -0
  80. package/src/builtin-addons/core/buttons/change-deck/config.ts +15 -0
  81. package/src/builtin-addons/core/buttons/change-deck/frontend.tsx +18 -0
  82. package/src/builtin-addons/core/buttons/media-sample/backend.ts +8 -0
  83. package/src/builtin-addons/core/buttons/media-sample/config.ts +7 -0
  84. package/src/builtin-addons/core/buttons/media-sample/frontend.tsx +2 -0
  85. package/src/builtin-addons/core/buttons/page-nav/backend.ts +28 -0
  86. package/src/builtin-addons/core/buttons/page-nav/config.ts +9 -0
  87. package/src/builtin-addons/core/buttons/page-nav/frontend.tsx +68 -0
  88. package/src/builtin-addons/core/buttons/toggle/backend.ts +119 -0
  89. package/src/builtin-addons/core/buttons/toggle/config.ts +57 -0
  90. package/src/builtin-addons/core/buttons/toggle/frontend.tsx +57 -0
  91. package/src/builtin-addons/core/buttons/toggle/global-service.ts +174 -0
  92. package/src/builtin-addons/core/index.ts +120 -0
  93. package/src/builtin-addons/core/sirenodeck.json +6 -0
  94. package/src/builtin-addons/date-time/README.md +73 -0
  95. package/src/builtin-addons/date-time/buttons/analog-clock/backend.ts +10 -0
  96. package/src/builtin-addons/date-time/buttons/analog-clock/config.ts +4 -0
  97. package/src/builtin-addons/date-time/buttons/analog-clock/frontend.tsx +88 -0
  98. package/src/builtin-addons/date-time/buttons/custom/backend.ts +10 -0
  99. package/src/builtin-addons/date-time/buttons/custom/config.ts +9 -0
  100. package/src/builtin-addons/date-time/buttons/custom/frontend.tsx +26 -0
  101. package/src/builtin-addons/date-time/buttons/date/backend.ts +10 -0
  102. package/src/builtin-addons/date-time/buttons/date/config.ts +10 -0
  103. package/src/builtin-addons/date-time/buttons/date/frontend.tsx +42 -0
  104. package/src/builtin-addons/date-time/buttons/time/backend.ts +10 -0
  105. package/src/builtin-addons/date-time/buttons/time/config.ts +9 -0
  106. package/src/builtin-addons/date-time/buttons/time/frontend.tsx +29 -0
  107. package/src/builtin-addons/date-time/index.d.ts +5 -0
  108. package/src/builtin-addons/date-time/index.ts +30 -0
  109. package/src/builtin-addons/date-time/poller.ts +11 -0
  110. package/src/builtin-addons/date-time/shared/format-date.ts +36 -0
  111. package/src/builtin-addons/date-time/shared/format.ts +105 -0
  112. package/src/builtin-addons/date-time/shared/use-now.ts +10 -0
  113. package/src/builtin-addons/date-time/sirenodeck.json +6 -0
  114. package/src/builtin-addons/emoji-selector/README.md +45 -0
  115. package/src/builtin-addons/emoji-selector/assets/launcher.svg +10 -0
  116. package/src/builtin-addons/emoji-selector/assets/smileys.svg +8 -0
  117. package/src/builtin-addons/emoji-selector/buttons/category/backend.ts +14 -0
  118. package/src/builtin-addons/emoji-selector/buttons/category/config.ts +8 -0
  119. package/src/builtin-addons/emoji-selector/buttons/category/frontend.tsx +19 -0
  120. package/src/builtin-addons/emoji-selector/buttons/emoji/backend.ts +7 -0
  121. package/src/builtin-addons/emoji-selector/buttons/emoji/config.ts +9 -0
  122. package/src/builtin-addons/emoji-selector/buttons/emoji/frontend.tsx +11 -0
  123. package/src/builtin-addons/emoji-selector/buttons/launcher/backend.ts +13 -0
  124. package/src/builtin-addons/emoji-selector/buttons/launcher/config.ts +6 -0
  125. package/src/builtin-addons/emoji-selector/buttons/launcher/frontend.tsx +31 -0
  126. package/src/builtin-addons/emoji-selector/data/categories.ts +1084 -0
  127. package/src/builtin-addons/emoji-selector/decks/index.ts +137 -0
  128. package/src/builtin-addons/emoji-selector/index.d.ts +5 -0
  129. package/src/builtin-addons/emoji-selector/index.ts +29 -0
  130. package/src/builtin-addons/emoji-selector/sirenodeck.json +6 -0
  131. package/src/builtin-addons/emoji-selector/support.ts +49 -0
  132. package/src/builtin-addons/index.ts +13 -0
  133. package/src/builtin-addons/internal-settings/README.md +26 -0
  134. package/src/builtin-addons/internal-settings/backend.ts +58 -0
  135. package/src/builtin-addons/internal-settings/buttons/app-info/backend.ts +7 -0
  136. package/src/builtin-addons/internal-settings/buttons/app-info/config.ts +13 -0
  137. package/src/builtin-addons/internal-settings/buttons/app-info/frontend.tsx +18 -0
  138. package/src/builtin-addons/internal-settings/buttons/brightness/common/frontend.tsx +50 -0
  139. package/src/builtin-addons/internal-settings/buttons/brightness-down/backend.ts +16 -0
  140. package/src/builtin-addons/internal-settings/buttons/brightness-down/config.ts +6 -0
  141. package/src/builtin-addons/internal-settings/buttons/brightness-down/frontend.tsx +8 -0
  142. package/src/builtin-addons/internal-settings/buttons/brightness-up/backend.ts +16 -0
  143. package/src/builtin-addons/internal-settings/buttons/brightness-up/config.ts +6 -0
  144. package/src/builtin-addons/internal-settings/buttons/brightness-up/frontend.tsx +8 -0
  145. package/src/builtin-addons/internal-settings/decks/settings.ts +25 -0
  146. package/src/builtin-addons/internal-settings/index.d.ts +4 -0
  147. package/src/builtin-addons/internal-settings/index.ts +53 -0
  148. package/src/builtin-addons/internal-settings/logo72x72.png +0 -0
  149. package/src/builtin-addons/internal-settings/sirenodeck.json +6 -0
  150. package/src/builtin-addons/media/README.md +45 -0
  151. package/src/builtin-addons/media/backend.ts +143 -0
  152. package/src/builtin-addons/media/buttons/media-player/backend.ts +19 -0
  153. package/src/builtin-addons/media/buttons/media-player/components/MediaStatus.tsx +25 -0
  154. package/src/builtin-addons/media/buttons/media-player/components/MediaSurface.tsx +52 -0
  155. package/src/builtin-addons/media/buttons/media-player/components/ProgressBar.tsx +21 -0
  156. package/src/builtin-addons/media/buttons/media-player/components/status-meta.ts +36 -0
  157. package/src/builtin-addons/media/buttons/media-player/config.ts +4 -0
  158. package/src/builtin-addons/media/buttons/media-player/frontend.tsx +29 -0
  159. package/src/builtin-addons/media/buttons/volume/common/frontend.tsx +52 -0
  160. package/src/builtin-addons/media/buttons/volume/mute/backend.ts +13 -0
  161. package/src/builtin-addons/media/buttons/volume/mute/config.ts +4 -0
  162. package/src/builtin-addons/media/buttons/volume/mute/frontend.tsx +26 -0
  163. package/src/builtin-addons/media/buttons/volume/volume-down/backend.ts +14 -0
  164. package/src/builtin-addons/media/buttons/volume/volume-down/config.ts +6 -0
  165. package/src/builtin-addons/media/buttons/volume/volume-down/frontend.tsx +10 -0
  166. package/src/builtin-addons/media/buttons/volume/volume-up/backend.ts +14 -0
  167. package/src/builtin-addons/media/buttons/volume/volume-up/config.ts +6 -0
  168. package/src/builtin-addons/media/buttons/volume/volume-up/frontend.tsx +8 -0
  169. package/src/builtin-addons/media/checks.ts +104 -0
  170. package/src/builtin-addons/media/executor.ts +36 -0
  171. package/src/builtin-addons/media/index.d.ts +5 -0
  172. package/src/builtin-addons/media/index.ts +53 -0
  173. package/src/builtin-addons/media/progress.ts +16 -0
  174. package/src/builtin-addons/media/providers/darwin.ts +391 -0
  175. package/src/builtin-addons/media/providers/index.ts +27 -0
  176. package/src/builtin-addons/media/providers/linux.ts +196 -0
  177. package/src/builtin-addons/media/providers/types.ts +48 -0
  178. package/src/builtin-addons/media/providers/windows.ts +121 -0
  179. package/src/builtin-addons/media/sirenodeck.json +6 -0
  180. package/src/builtin-addons/media/state.ts +19 -0
  181. package/src/builtin-addons/register-builtins.ts +65 -0
  182. package/src/builtin-addons/session/README.md +14 -0
  183. package/src/builtin-addons/session/buttons/session-info/backend.ts +8 -0
  184. package/src/builtin-addons/session/buttons/session-info/config.ts +4 -0
  185. package/src/builtin-addons/session/buttons/session-info/frontend.tsx +2 -0
  186. package/src/builtin-addons/session/buttons/time/backend.ts +7 -0
  187. package/src/builtin-addons/session/buttons/time/config.ts +6 -0
  188. package/src/builtin-addons/session/buttons/time/frontend.tsx +2 -0
  189. package/src/builtin-addons/session/index.d.ts +5 -0
  190. package/src/builtin-addons/session/index.ts +18 -0
  191. package/src/builtin-addons/session/sirenodeck.json +6 -0
  192. package/src/builtin-addons/system-status/README.md +169 -0
  193. package/src/builtin-addons/system-status/buttons/_shared.ts +131 -0
  194. package/src/builtin-addons/system-status/buttons/system-status/frontend.tsx +224 -0
  195. package/src/builtin-addons/system-status/buttons/system-status/schemas.ts +67 -0
  196. package/src/builtin-addons/system-status/domain/catalog.ts +53 -0
  197. package/src/builtin-addons/system-status/domain/chart-sampler.ts +58 -0
  198. package/src/builtin-addons/system-status/domain/display-metrics.ts +128 -0
  199. package/src/builtin-addons/system-status/domain/index.ts +10 -0
  200. package/src/builtin-addons/system-status/domain/live-metrics.ts +1021 -0
  201. package/src/builtin-addons/system-status/domain/metric-ids.ts +0 -0
  202. package/src/builtin-addons/system-status/index.ts +16 -0
  203. package/src/builtin-addons/system-status/manifest.ts +105 -0
  204. package/src/builtin-addons/system-status/shared/metrics-catalog.ts +316 -0
  205. package/src/builtin-addons/system-status/sirenodeck.json +6 -0
  206. package/src/builtin-addons/test-buildin/decks/action-buttons-test.ts +56 -0
  207. package/src/builtin-addons/test-buildin/index.d.ts +5 -0
  208. package/src/builtin-addons/test-buildin/index.ts +16 -0
  209. package/src/builtin-addons/value-display/README.md +59 -0
  210. package/src/builtin-addons/value-display/backend.ts +86 -0
  211. package/src/builtin-addons/value-display/buttons/value-display/backend.ts +17 -0
  212. package/src/builtin-addons/value-display/buttons/value-display/config.ts +36 -0
  213. package/src/builtin-addons/value-display/buttons/value-display/frontend.tsx +54 -0
  214. package/src/builtin-addons/value-display/index.d.ts +5 -0
  215. package/src/builtin-addons/value-display/index.ts +23 -0
  216. package/src/builtin-addons/value-display/sirenodeck.json +6 -0
  217. package/src/builtin-addons/weather/README.md +46 -0
  218. package/src/builtin-addons/weather/backend.ts +121 -0
  219. package/src/builtin-addons/weather/buttons/weather/backend.ts +27 -0
  220. package/src/builtin-addons/weather/buttons/weather/config.ts +29 -0
  221. package/src/builtin-addons/weather/buttons/weather/frontend.tsx +63 -0
  222. package/src/builtin-addons/weather/buttons/weather/icons.tsx +88 -0
  223. package/src/builtin-addons/weather/buttons/weather/pages.tsx +109 -0
  224. package/src/builtin-addons/weather/domain/codes.ts +35 -0
  225. package/src/builtin-addons/weather/domain/fetch.ts +47 -0
  226. package/src/builtin-addons/weather/index.d.ts +5 -0
  227. package/src/builtin-addons/weather/index.ts +23 -0
  228. package/src/builtin-addons/weather/poller.ts +70 -0
  229. package/src/builtin-addons/weather/provider/city-key.ts +9 -0
  230. package/src/builtin-addons/weather/provider/codes.ts +35 -0
  231. package/src/builtin-addons/weather/provider/fetch.ts +97 -0
  232. package/src/builtin-addons/weather/provider/geocode.ts +67 -0
  233. package/src/builtin-addons/weather/provider/types.ts +42 -0
  234. package/src/builtin-addons/weather/sirenodeck.json +6 -0
  235. package/src/cli/coding-agents-onboarding.ts +434 -0
  236. package/src/cli/commands/addon-decks.ts +441 -0
  237. package/src/cli/commands/addon-registry.ts +639 -0
  238. package/src/cli/commands/emulator-mode.ts +289 -0
  239. package/src/cli/commands/install.ts +128 -0
  240. package/src/cli/commands/logs.ts +74 -0
  241. package/src/cli/commands/network-bind.ts +111 -0
  242. package/src/cli/commands/pipeline/helpers.ts +75 -0
  243. package/src/cli/commands/pipeline/preflight.ts +20 -0
  244. package/src/cli/commands/port-identity.ts +152 -0
  245. package/src/cli/commands/reload.ts +37 -0
  246. package/src/cli/commands/restart.ts +139 -0
  247. package/src/cli/commands/run.ts +2270 -0
  248. package/src/cli/commands/service/install.ts +121 -0
  249. package/src/cli/commands/service/render-template.ts +158 -0
  250. package/src/cli/commands/service-manager.ts +184 -0
  251. package/src/cli/commands/spawn-daemon.ts +251 -0
  252. package/src/cli/commands/start.ts +1061 -0
  253. package/src/cli/commands/status.ts +110 -0
  254. package/src/cli/commands/stop.ts +121 -0
  255. package/src/cli/commands/subprocess-supervisor.ts +203 -0
  256. package/src/cli/commands/system-requirements.ts +699 -0
  257. package/src/cli/cwd.ts +28 -0
  258. package/src/cli/first-run.ts +149 -0
  259. package/src/cli/http-server.ts +244 -0
  260. package/src/cli/index.ts +355 -0
  261. package/src/cli/install-root.ts +26 -0
  262. package/src/cli/main.ts +194 -0
  263. package/src/cli/package-manager.ts +175 -0
  264. package/src/cli/probe-deps.ts +45 -0
  265. package/src/cli/prompt.ts +22 -0
  266. package/src/cli/startup-display.ts +452 -0
  267. package/src/config/builtin-icons.ts +57 -0
  268. package/src/config/config-diff.ts +54 -0
  269. package/src/config/discovery.ts +64 -0
  270. package/src/config/icon-resolver.ts +90 -0
  271. package/src/config/include-resolver.ts +202 -0
  272. package/src/config/index.ts +9 -0
  273. package/src/config/loader.ts +187 -0
  274. package/src/config/mutation.ts +514 -0
  275. package/src/config/reference-expander.ts +84 -0
  276. package/src/config/schemas.ts +223 -0
  277. package/src/config/validation.ts +254 -0
  278. package/src/core/asset-id.ts +21 -0
  279. package/src/core/gesture-state.ts +344 -0
  280. package/src/core/icon-asset-registry.ts +127 -0
  281. package/src/core/icon-source.ts +86 -0
  282. package/src/core/index.ts +30 -0
  283. package/src/core/mime.ts +16 -0
  284. package/src/core/pagination.ts +90 -0
  285. package/src/core/pub-sub.ts +126 -0
  286. package/src/core/settings.ts +153 -0
  287. package/src/core/store.ts +88 -0
  288. package/src/core/watcher.ts +73 -0
  289. package/src/deck/addon-handler-bridge.ts +516 -0
  290. package/src/deck/deck-config.ts +271 -0
  291. package/src/deck/editor-surfaces.ts +87 -0
  292. package/src/deck/host-context.ts +18 -0
  293. package/src/deck/index.ts +102 -0
  294. package/src/deck/macro-parse.ts +182 -0
  295. package/src/deck/methods.ts +348 -0
  296. package/src/deck/paginate-deck.ts +98 -0
  297. package/src/deck/position-buttons.ts +57 -0
  298. package/src/deck/runtime/index.ts +5 -0
  299. package/src/deck/runtime/runtime.ts +1060 -0
  300. package/src/deck/runtime-subscriptions.ts +45 -0
  301. package/src/deck/system-back-injection.ts +62 -0
  302. package/src/deck/system-buttons/registry.tsx +97 -0
  303. package/src/deck/system-buttons/types.ts +14 -0
  304. package/src/device/black-frame.ts +73 -0
  305. package/src/device/index.ts +13 -0
  306. package/src/device/linux-udev.ts +36 -0
  307. package/src/device/models.ts +89 -0
  308. package/src/device/reconnecting.ts +193 -0
  309. package/src/device/registry.ts +38 -0
  310. package/src/device/stream-deck.ts +155 -0
  311. package/src/index.ts +74 -0
  312. package/src/outputClient/emulator.ts +357 -0
  313. package/src/outputClient/index.ts +26 -0
  314. package/src/outputClient/open-browser.ts +26 -0
  315. package/src/outputClient/real.ts +393 -0
  316. package/src/outputClient/types.ts +83 -0
  317. package/src/render/browser-renderer.ts +240 -0
  318. package/src/render/buffer-hash.ts +20 -0
  319. package/src/render/editor-handler.ts +197 -0
  320. package/src/render/emulator-server.ts +128 -0
  321. package/src/render/icon-source-resolver.ts +69 -0
  322. package/src/render/index.ts +16 -0
  323. package/src/render/protocol.ts +61 -0
  324. package/src/render/push-raw-image.ts +100 -0
  325. package/src/render/screenshot-cadence.ts +87 -0
  326. package/src/render/state-publisher.ts +110 -0
  327. package/src/render/vite-server.ts +138 -0
  328. package/src/render/ws-bridge.ts +315 -0
  329. package/src/system/device-selection.ts +80 -0
  330. package/src/system/glob-match.ts +74 -0
  331. package/src/system/providers/active-app/darwin.ts +117 -0
  332. package/src/system/providers/active-app/linux.ts +245 -0
  333. package/src/system/providers/active-app/wayland-gnome.ts +303 -0
  334. package/src/system/providers/active-app/windows.ts +94 -0
  335. package/src/system/providers/active-app.ts +97 -0
  336. package/src/system/providers/clipboard/darwin.ts +65 -0
  337. package/src/system/providers/clipboard/linux.ts +163 -0
  338. package/src/system/providers/clipboard/windows.ts +94 -0
  339. package/src/system/providers/clipboard.ts +84 -0
  340. package/src/system/providers/error.ts +16 -0
  341. package/src/system/providers/key-macro/darwin.ts +221 -0
  342. package/src/system/providers/key-macro/linux.ts +651 -0
  343. package/src/system/providers/key-macro/parser.ts +215 -0
  344. package/src/system/providers/key-macro/windows.ts +476 -0
  345. package/src/system/providers/key-macro.ts +79 -0
  346. package/src/system/providers/notification/darwin.ts +36 -0
  347. package/src/system/providers/notification/linux.ts +115 -0
  348. package/src/system/providers/notification/windows.ts +72 -0
  349. package/src/system/providers/notification.ts +75 -0
  350. package/src/system/providers/session/darwin.ts +82 -0
  351. package/src/system/providers/session/linux.ts +175 -0
  352. package/src/system/providers/session/windows.ts +73 -0
  353. package/src/system/providers/session.ts +89 -0
  354. package/src/system/providers/shared.ts +65 -0
  355. package/src/system/requirements.ts +235 -0
  356. package/src/system/setup-wizard/config-seed.ts +30 -0
  357. package/src/system/setup-wizard/format.ts +275 -0
  358. package/src/system/setup-wizard/index.ts +45 -0
  359. package/src/system/setup-wizard/open-settings.ts +26 -0
  360. package/src/system/setup-wizard/plan.ts +230 -0
  361. package/src/system/setup-wizard/probe-cache.ts +34 -0
  362. package/src/system/setup-wizard/probe.ts +486 -0
  363. package/src/system/setup-wizard/runtime-features.ts +85 -0
  364. package/src/system/setup-wizard/sudo.ts +178 -0
  365. package/src/system/setup-wizard/types.ts +133 -0
  366. package/src/system/virtual-stream-deck.ts +67 -0
  367. package/src/themes/css.ts +210 -0
  368. package/src/themes/default/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  369. package/src/themes/default/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  370. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  371. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf +0 -0
  372. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf +0 -0
  373. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf +0 -0
  374. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Italic.ttf +0 -0
  375. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Light.ttf +0 -0
  376. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf +0 -0
  377. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  378. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf +0 -0
  379. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  380. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf +0 -0
  381. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf +0 -0
  382. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Thin.ttf +0 -0
  383. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf +0 -0
  384. package/src/themes/default/assets/IBM_Plex_Mono/OFL.txt +93 -0
  385. package/src/themes/default/assets/OFL.txt +93 -0
  386. package/src/themes/default/assets/README.txt +106 -0
  387. package/src/themes/default/assets/static/IBMPlexSans-Bold.ttf +0 -0
  388. package/src/themes/default/assets/static/IBMPlexSans-BoldItalic.ttf +0 -0
  389. package/src/themes/default/assets/static/IBMPlexSans-ExtraLight.ttf +0 -0
  390. package/src/themes/default/assets/static/IBMPlexSans-ExtraLightItalic.ttf +0 -0
  391. package/src/themes/default/assets/static/IBMPlexSans-Italic.ttf +0 -0
  392. package/src/themes/default/assets/static/IBMPlexSans-Light.ttf +0 -0
  393. package/src/themes/default/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  394. package/src/themes/default/assets/static/IBMPlexSans-Medium.ttf +0 -0
  395. package/src/themes/default/assets/static/IBMPlexSans-MediumItalic.ttf +0 -0
  396. package/src/themes/default/assets/static/IBMPlexSans-Regular.ttf +0 -0
  397. package/src/themes/default/assets/static/IBMPlexSans-SemiBold.ttf +0 -0
  398. package/src/themes/default/assets/static/IBMPlexSans-SemiBoldItalic.ttf +0 -0
  399. package/src/themes/default/assets/static/IBMPlexSans-Thin.ttf +0 -0
  400. package/src/themes/default/assets/static/IBMPlexSans-ThinItalic.ttf +0 -0
  401. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Bold.ttf +0 -0
  402. package/src/themes/default/assets/static/IBMPlexSans_Condensed-BoldItalic.ttf +0 -0
  403. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLight.ttf +0 -0
  404. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLightItalic.ttf +0 -0
  405. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Italic.ttf +0 -0
  406. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Light.ttf +0 -0
  407. package/src/themes/default/assets/static/IBMPlexSans_Condensed-LightItalic.ttf +0 -0
  408. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Medium.ttf +0 -0
  409. package/src/themes/default/assets/static/IBMPlexSans_Condensed-MediumItalic.ttf +0 -0
  410. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Regular.ttf +0 -0
  411. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBold.ttf +0 -0
  412. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBoldItalic.ttf +0 -0
  413. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Thin.ttf +0 -0
  414. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ThinItalic.ttf +0 -0
  415. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Bold.ttf +0 -0
  416. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-BoldItalic.ttf +0 -0
  417. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLight.ttf +0 -0
  418. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLightItalic.ttf +0 -0
  419. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Italic.ttf +0 -0
  420. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Light.ttf +0 -0
  421. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-LightItalic.ttf +0 -0
  422. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Medium.ttf +0 -0
  423. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-MediumItalic.ttf +0 -0
  424. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Regular.ttf +0 -0
  425. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBold.ttf +0 -0
  426. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  427. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Thin.ttf +0 -0
  428. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ThinItalic.ttf +0 -0
  429. package/src/themes/default/components.css +81 -0
  430. package/src/themes/default/sirenodeck.json +170 -0
  431. package/src/themes/index.ts +14 -0
  432. package/src/themes/light/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  433. package/src/themes/light/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  434. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  435. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  436. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  437. package/src/themes/light/assets/IBM_Plex_Mono/OFL.txt +93 -0
  438. package/src/themes/light/assets/OFL.txt +93 -0
  439. package/src/themes/light/assets/README.txt +106 -0
  440. package/src/themes/light/assets/static/IBMPlexSans-Light.ttf +0 -0
  441. package/src/themes/light/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  442. package/src/themes/light/assets/static/IBMPlexSans-Thin.ttf +0 -0
  443. package/src/themes/light/components.css +70 -0
  444. package/src/themes/light/sirenodeck.json +170 -0
  445. package/src/themes/loader.ts +343 -0
  446. package/src/themes/manifest.ts +164 -0
  447. package/src/themes/use-resolved-theme.tsx +32 -0
  448. package/src/ui/ButtonFrame.tsx +230 -0
  449. package/src/ui/contexts/AssetCacheContext.tsx +43 -0
  450. package/src/ui/index.ts +20 -0
  451. package/src/ui/primitives/Chip.tsx +53 -0
  452. package/src/ui/primitives/Icon.tsx +209 -0
  453. package/src/ui/primitives/Label.tsx +82 -0
  454. package/src/ui/primitives/ProgressBar.tsx +38 -0
  455. package/src/ui/primitives/TapIndicator.tsx +67 -0
  456. package/src/ui/primitives/Text.tsx +810 -0
  457. package/src/ui/primitives/index.ts +6 -0
  458. package/src/ui/surfaces/BarsSurface.tsx +211 -0
  459. package/src/ui/surfaces/IconLabelProgressSurface.tsx +133 -0
  460. package/src/ui/surfaces/IconLabelSurface.tsx +59 -0
  461. package/src/ui/surfaces/LabelValueListSurface.tsx +202 -0
  462. package/src/ui/surfaces/PaginatedSurface.tsx +164 -0
  463. package/src/ui/surfaces/SplitActionSurface.tsx +98 -0
  464. package/src/ui/surfaces/TemporaryErrorSurface.tsx +68 -0
  465. package/src/ui/surfaces/ValueChart.tsx +218 -0
  466. package/src/ui/surfaces/index.ts +32 -0
  467. package/src/ui/surfaces/paginated-schema.ts +22 -0
  468. package/src/ui/theme-presentation-builder.ts +184 -0
  469. package/src/ui/theme-presentation.tsx +146 -0
  470. package/src/ui/utils/cn.ts +12 -0
  471. package/src/util/cache-paths.ts +38 -0
  472. package/src/util/config-diff.ts +16 -0
  473. package/src/util/daemon-control.ts +85 -0
  474. package/src/util/daemon.ts +575 -0
  475. package/src/util/device-config.ts +56 -0
  476. package/src/util/errors.ts +13 -0
  477. package/src/util/log-reader.ts +114 -0
  478. package/src/util/log-tail.ts +103 -0
  479. package/src/util/logger.ts +417 -0
  480. package/src/util/process-title.ts +79 -0
  481. package/src/util/single-instance.ts +202 -0
  482. package/src/version.ts +10 -0
  483. package/src/vite/index.ts +6 -0
  484. package/src/vite/virtual-modules.ts +631 -0
  485. 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
+ }