@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,315 @@
1
+ import type { Server as HttpServer } from "node:http"
2
+ import { WebSocketServer, type WebSocket } from "ws"
3
+ import type pino from "pino"
4
+
5
+ import type { DeviceDescriptor } from "@/device/registry"
6
+ import { createLogger } from "@/util/logger"
7
+ import type { DeckTreeMessage } from "@/api/protocol-internal"
8
+ import {
9
+ PROTOCOL_VERSION,
10
+ helloAckMessageSchema,
11
+ helloMessageSchema,
12
+ helloMessageStrictSchema,
13
+ wsMessageSchema,
14
+ type WsMessage,
15
+ } from "./protocol"
16
+ import type { AddonsInventoryMessage } from "@/api/protocol-internal"
17
+
18
+ const HANDSHAKE_TIMEOUT_MS = 5000
19
+ const TOKEN_MISMATCH_CLOSE_CODE = 4001
20
+
21
+ export interface WsBridgeOptions {
22
+ port?: number
23
+ /** Bind address for the WebSocket server. Defaults to "127.0.0.1". */
24
+ host?: string
25
+ /**
26
+ * Advertised host for the URL returned by the bridge. Use when the server
27
+ * binds to 0.0.0.0 but clients must connect via a specific LAN/VPN IP.
28
+ */
29
+ lanHost?: string
30
+ expectedToken: string
31
+ handshakeTimeoutMs?: number
32
+ activeTheme?: { name: string; version?: number }
33
+ logger?: pino.Logger
34
+ // ponytail: when provided, sent as a follow-up to hello-ack so the
35
+ // emulator's Addons tab can render without needing a separate HTTP
36
+ // fetch (which doesn't exist in --emulator mode).
37
+ addonInventory?: AddonsInventoryMessage["addons"]
38
+ }
39
+
40
+ export interface WsBridge {
41
+ readonly port: number
42
+ readonly url: string
43
+ setDevice(device: DeviceDescriptor): void
44
+ // ponytail: late-bound so callers can ship addon inventory after
45
+ // external addons register (e.g. buildExternalScannedAddons runs
46
+ // after the bridge starts in runPipeline). Subsequent connects get
47
+ // the new value; already-connected clients aren't resent (would be
48
+ // redundant — the page is mounting fresh anyway).
49
+ setAddonInventory(inventory: AddonsInventoryMessage["addons"]): void
50
+ setActiveTheme?(theme: { name: string; version?: number }): void
51
+ setDeckTree(tree: Omit<DeckTreeMessage, "type">): void
52
+ broadcast(message: WsMessage): void
53
+ registerCacheablePoller(
54
+ channel: string,
55
+ pollFn: () => unknown | Promise<unknown>,
56
+ ): void
57
+ onMessage(handler: (message: WsMessage, socket: WebSocket) => void): void
58
+ onConnection(handler: (socket: WebSocket) => void): void
59
+ close(): Promise<void>
60
+ }
61
+
62
+ export const startWsBridge = (options: WsBridgeOptions): Promise<WsBridge> => {
63
+ const {
64
+ port = 0,
65
+ host = "127.0.0.1",
66
+ lanHost,
67
+ expectedToken,
68
+ handshakeTimeoutMs = HANDSHAKE_TIMEOUT_MS,
69
+ activeTheme,
70
+ logger = createLogger({ level: "warn", component: "ws-bridge" }),
71
+ } = options
72
+ let { addonInventory } = options
73
+ let currentTheme = activeTheme
74
+ let deckTree: Omit<DeckTreeMessage, "type"> | undefined
75
+
76
+ return new Promise((resolve, reject) => {
77
+ const wss = new WebSocketServer({ port, host })
78
+
79
+ const messageHandlers: Array<
80
+ (message: WsMessage, socket: WebSocket) => void
81
+ > = []
82
+ const connectionHandlers: Array<(socket: WebSocket) => void> = []
83
+ const lastChannels: Record<string, unknown> = {}
84
+ const cacheablePollers = new Map<string, () => unknown | Promise<unknown>>()
85
+ const pendingChannelSubs = new Map<string, Set<WebSocket>>()
86
+ let currentDevice: DeviceDescriptor | null = null
87
+
88
+ const sendToSocket = (socket: WebSocket, message: WsMessage): void => {
89
+ if (socket.readyState === socket.OPEN) {
90
+ socket.send(JSON.stringify(message))
91
+ }
92
+ }
93
+
94
+ wss.on("connection", (socket: WebSocket) => {
95
+ let handshakeDone = false
96
+
97
+ const handshakeTimer = setTimeout(() => {
98
+ if (!handshakeDone) {
99
+ logger.warn(
100
+ {
101
+ peer:
102
+ (
103
+ socket as unknown as {
104
+ _socket: { remoteAddress?: string }
105
+ }
106
+ )._socket.remoteAddress ?? "unknown",
107
+ },
108
+ "ws handshake timed out",
109
+ )
110
+ socket.close(4000, "handshake timeout")
111
+ }
112
+ }, handshakeTimeoutMs)
113
+
114
+ socket.on("message", (raw) => {
115
+ let parsed: unknown
116
+ try {
117
+ parsed = JSON.parse(raw.toString())
118
+ } catch (err) {
119
+ logger.warn(
120
+ { reason: err instanceof Error ? err.message : String(err) },
121
+ "ws message: invalid json",
122
+ )
123
+ clearTimeout(handshakeTimer)
124
+ socket.close(4002, "invalid json")
125
+ return
126
+ }
127
+ const result = wsMessageSchema.safeParse(parsed)
128
+ if (!result.success) {
129
+ logger.warn(
130
+ { issues: result.error.issues },
131
+ "ws message: schema mismatch",
132
+ )
133
+ clearTimeout(handshakeTimer)
134
+ socket.close(4003, "invalid message")
135
+ return
136
+ }
137
+ const message = result.data
138
+ if (!handshakeDone) {
139
+ if (message.type !== "hello") {
140
+ clearTimeout(handshakeTimer)
141
+ socket.close(4004, "expected hello")
142
+ return
143
+ }
144
+ const helloSchema = expectedToken
145
+ ? helloMessageStrictSchema
146
+ : helloMessageSchema
147
+ const helloResult = helloSchema.safeParse(message)
148
+ if (!helloResult.success) {
149
+ clearTimeout(handshakeTimer)
150
+ socket.close(4001, "invalid hello")
151
+ return
152
+ }
153
+ if (expectedToken && helloResult.data.token !== expectedToken) {
154
+ clearTimeout(handshakeTimer)
155
+ socket.close(TOKEN_MISMATCH_CLOSE_CODE, "token mismatch")
156
+ return
157
+ }
158
+ handshakeDone = true
159
+ clearTimeout(handshakeTimer)
160
+ const ack = helloAckMessageSchema.parse({
161
+ type: "hello-ack",
162
+ version: PROTOCOL_VERSION,
163
+ ...(currentDevice !== null ? { device: currentDevice } : {}),
164
+ config:
165
+ currentTheme !== undefined ? { theme: currentTheme.name } : {},
166
+ })
167
+ socket.send(JSON.stringify(ack))
168
+ if (currentDevice !== null) {
169
+ sendToSocket(socket, { type: "device-info", device: currentDevice })
170
+ }
171
+ if (addonInventory !== undefined) {
172
+ sendToSocket(socket, {
173
+ type: "addons-inventory",
174
+ addons: addonInventory,
175
+ })
176
+ }
177
+ if (deckTree !== undefined) {
178
+ sendToSocket(socket, { type: "deck-tree", ...deckTree })
179
+ }
180
+ for (const handler of connectionHandlers) handler(socket)
181
+ return
182
+ }
183
+
184
+ if (message.type === "subscribe-channels") {
185
+ const cached: Record<string, unknown> = {}
186
+ const uncached: string[] = []
187
+ for (const channel of message.channels) {
188
+ if (channel in lastChannels) {
189
+ cached[channel] = lastChannels[channel]
190
+ } else if (cacheablePollers.has(channel)) {
191
+ uncached.push(channel)
192
+ } else {
193
+ let waiting = pendingChannelSubs.get(channel)
194
+ if (waiting === undefined) {
195
+ waiting = new Set()
196
+ pendingChannelSubs.set(channel, waiting)
197
+ }
198
+ waiting.add(socket)
199
+ }
200
+ }
201
+ if (Object.keys(cached).length > 0) {
202
+ sendToSocket(socket, { type: "state", channels: cached })
203
+ }
204
+ for (const channel of uncached) {
205
+ const pollFn = cacheablePollers.get(channel)
206
+ if (pollFn === undefined) continue
207
+ void Promise.resolve(pollFn())
208
+ .then((value) => {
209
+ const msg: WsMessage = {
210
+ type: "state",
211
+ channels: { [channel]: value },
212
+ }
213
+ const payload = JSON.stringify(msg)
214
+ for (const client of wss.clients) {
215
+ if (client.readyState === client.OPEN) client.send(payload)
216
+ }
217
+ })
218
+ .catch(() => {
219
+ // poll on demand failed — ignore, next interval will retry
220
+ })
221
+ }
222
+ return
223
+ }
224
+
225
+ for (const handler of messageHandlers) handler(message, socket)
226
+ })
227
+
228
+ socket.on("error", () => {
229
+ clearTimeout(handshakeTimer)
230
+ })
231
+
232
+ socket.on("close", () => {
233
+ for (const subs of pendingChannelSubs.values()) {
234
+ subs.delete(socket)
235
+ }
236
+ })
237
+ })
238
+
239
+ wss.on("listening", () => {
240
+ const addr = wss.address()
241
+ if (addr === null || typeof addr === "string") {
242
+ reject(new Error("ws bridge: invalid address"))
243
+ return
244
+ }
245
+ const port = addr.port
246
+ const urlHost = lanHost ?? host
247
+ const url = `ws://${urlHost}:${port}`
248
+ const bridge: WsBridge = {
249
+ port,
250
+ url,
251
+ setDevice: (device) => {
252
+ currentDevice = device
253
+ const msg: WsMessage = { type: "device-info", device }
254
+ const payload = JSON.stringify(msg)
255
+ for (const client of wss.clients) {
256
+ if (client.readyState === client.OPEN) client.send(payload)
257
+ }
258
+ },
259
+ setAddonInventory: (inventory) => {
260
+ addonInventory = inventory
261
+ },
262
+ setActiveTheme: (theme) => {
263
+ currentTheme = theme
264
+ },
265
+ setDeckTree: (tree) => {
266
+ deckTree = tree
267
+ },
268
+ broadcast: (message) => {
269
+ if (message.type === "state") {
270
+ Object.assign(lastChannels, message.channels)
271
+ }
272
+ const payload = JSON.stringify(message)
273
+ for (const client of wss.clients) {
274
+ if (client.readyState === client.OPEN) client.send(payload)
275
+ }
276
+ },
277
+ registerCacheablePoller: (channel, pollFn) => {
278
+ cacheablePollers.set(channel, pollFn)
279
+ const waiting = pendingChannelSubs.get(channel)
280
+ if (waiting !== undefined) {
281
+ pendingChannelSubs.delete(channel)
282
+ // ponytail: poll once and broadcast to every waiting socket. The
283
+ // legacy loop fired the probe N times for N sockets — the cache
284
+ // exists to avoid that exact amplification.
285
+ void Promise.resolve(pollFn())
286
+ .then((value) => {
287
+ const msg: WsMessage = {
288
+ type: "state",
289
+ channels: { [channel]: value },
290
+ }
291
+ for (const sock of waiting) sendToSocket(sock, msg)
292
+ lastChannels[channel] = value
293
+ })
294
+ .catch(() => {
295
+ // poll on demand failed — ignore, next interval will retry
296
+ })
297
+ }
298
+ },
299
+ onMessage: (handler) => messageHandlers.push(handler),
300
+ onConnection: (handler) => connectionHandlers.push(handler),
301
+ close: () =>
302
+ new Promise<void>((res) => {
303
+ pendingChannelSubs.clear()
304
+ for (const client of wss.clients) client.close(1000)
305
+ wss.close(() => res())
306
+ }),
307
+ }
308
+ resolve(bridge)
309
+ })
310
+
311
+ wss.on("error", (err) => reject(err))
312
+ })
313
+ }
314
+
315
+ export type WsBridgeHttpServer = HttpServer
@@ -0,0 +1,80 @@
1
+ import { select as clackSelect } from "@/cli/prompt"
2
+ import type pino from "pino"
3
+
4
+ import type { DeviceDescriptor } from "@/device/registry"
5
+ import type { DeviceConfig } from "@/util/device-config"
6
+
7
+ export class NoStreamDeckFoundError extends Error {
8
+ constructor() {
9
+ super("No Stream Deck devices found")
10
+ this.name = "NoStreamDeckFoundError"
11
+ }
12
+ }
13
+
14
+ export interface SelectDeviceResult {
15
+ readonly descriptor: DeviceDescriptor
16
+ readonly savedButStale: boolean
17
+ }
18
+
19
+ export interface SelectDeviceOptions {
20
+ readonly devices: ReadonlyArray<DeviceDescriptor>
21
+ readonly current?: DeviceConfig | null
22
+ readonly logger: pino.Logger
23
+ }
24
+
25
+ const formatChoice = (
26
+ descriptor: DeviceDescriptor,
27
+ hint: string | undefined,
28
+ ): { label: string; value: string; hint?: string } => ({
29
+ label: descriptor.label,
30
+ value: descriptor.id,
31
+ ...(hint !== undefined ? { hint } : {}),
32
+ })
33
+
34
+ export const selectDevice = async (
35
+ options: SelectDeviceOptions,
36
+ ): Promise<SelectDeviceResult> => {
37
+ if (options.devices.length === 0) {
38
+ throw new NoStreamDeckFoundError()
39
+ }
40
+ if (options.devices.length === 1) {
41
+ return { descriptor: options.devices[0]!, savedButStale: false }
42
+ }
43
+
44
+ const currentMatches = options.current
45
+ ? options.devices.find((d) => d.id === options.current!.serial) !==
46
+ undefined
47
+ : false
48
+ if (options.current && currentMatches) {
49
+ const match = options.devices.find((d) => d.id === options.current!.serial)!
50
+ options.logger.debug({ id: match.id }, "using saved device selection")
51
+ return { descriptor: match, savedButStale: false }
52
+ }
53
+
54
+ const savedButStale =
55
+ options.current !== null && options.current !== undefined && !currentMatches
56
+ const choices = options.devices.map((d) =>
57
+ formatChoice(
58
+ d,
59
+ savedButStale && d.id === options.current!.serial
60
+ ? "(saved, disconnected)"
61
+ : undefined,
62
+ ),
63
+ )
64
+ choices.sort((a, b) => {
65
+ const aStale = a.hint === "(saved, disconnected)"
66
+ const bStale = b.hint === "(saved, disconnected)"
67
+ if (aStale && !bStale) return -1
68
+ if (bStale && !aStale) return 1
69
+ return 0
70
+ })
71
+
72
+ const selected = await clackSelect({
73
+ message: "Select a Stream Deck:",
74
+ options: choices,
75
+ })
76
+
77
+ const descriptor = options.devices.find((d) => d.id === selected)
78
+ if (!descriptor) throw new NoStreamDeckFoundError()
79
+ return { descriptor, savedButStale }
80
+ }
@@ -0,0 +1,74 @@
1
+ import type { ActiveAppSnapshot } from "@/system/providers/active-app"
2
+
3
+ const escapeRegex = (raw: string): string =>
4
+ raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
5
+
6
+ const hasGlobMeta = (pattern: string): boolean => /[*?\\|]/.test(pattern)
7
+
8
+ const globToRegex = (pattern: string): RegExp => {
9
+ let source = ""
10
+ for (let i = 0; i < pattern.length; i += 1) {
11
+ const c = pattern[i]
12
+ if (c === "*") source += ".*"
13
+ else if (c === "?") source += "."
14
+ else if (c === "\\" && i + 1 < pattern.length) {
15
+ source += escapeRegex(pattern[i + 1] ?? "")
16
+ i += 1
17
+ } else {
18
+ source += escapeRegex(c ?? "")
19
+ }
20
+ }
21
+ return new RegExp(`^(?:${source})$`, "i")
22
+ }
23
+
24
+ const compileOne = (raw: string): RegExp => {
25
+ const trimmed = raw.trim()
26
+ if (trimmed.length === 0) return /^.*/i
27
+ if (trimmed.includes("|")) {
28
+ const branches = trimmed.split("|").map((s) => {
29
+ const t = s.trim()
30
+ if (t.length === 0) return ".*"
31
+ if (!hasGlobMeta(t)) return escapeRegex(t)
32
+ return globToRegex(t).source
33
+ })
34
+ return new RegExp(`^(?:${branches.join("|")})$`, "i")
35
+ }
36
+ if (!hasGlobMeta(trimmed)) {
37
+ return new RegExp(escapeRegex(trimmed), "i")
38
+ }
39
+ return globToRegex(trimmed)
40
+ }
41
+
42
+ export const matchesPattern = (name: string, pattern: string): boolean => {
43
+ if (name.length === 0) return false
44
+ return compileOne(pattern).test(name)
45
+ }
46
+
47
+ export interface DeckMatcherFields {
48
+ processNames?: ReadonlyArray<string>
49
+ windowNames?: ReadonlyArray<string>
50
+ }
51
+
52
+ const matchGroup = (
53
+ compiled: ReadonlyArray<RegExp>,
54
+ value: string,
55
+ ): boolean => {
56
+ if (compiled.length === 0) return true
57
+ for (const re of compiled) {
58
+ if (re.test(value)) return true
59
+ }
60
+ return false
61
+ }
62
+
63
+ export const compileDeckMatcher = (
64
+ fields: DeckMatcherFields,
65
+ ): ((snapshot: ActiveAppSnapshot) => boolean) => {
66
+ const processCompiled = (fields.processNames ?? []).map(compileOne)
67
+ const windowCompiled = (fields.windowNames ?? []).map(compileOne)
68
+ if (processCompiled.length === 0 && windowCompiled.length === 0) {
69
+ return () => false
70
+ }
71
+ return (snapshot: ActiveAppSnapshot): boolean =>
72
+ matchGroup(processCompiled, snapshot.name) &&
73
+ matchGroup(windowCompiled, snapshot.windowTitle ?? "")
74
+ }
@@ -0,0 +1,117 @@
1
+ import type pino from "pino"
2
+
3
+ import type { ActiveAppProvider, ActiveAppSnapshot } from "../active-app"
4
+ import type { CommandExecutor } from "../shared"
5
+
6
+ export interface DarwinActiveAppDeps {
7
+ readonly executor: CommandExecutor
8
+ readonly logger: pino.Logger
9
+ readonly pollIntervalMs?: number
10
+ }
11
+
12
+ const DEFAULT_POLL_MS = 1_000
13
+
14
+ // ponytail: the old one-liner asked for `{name, name of window 1 of ..., unix
15
+ // id of ...}` and had three separate faults. A bare `name` inside `tell
16
+ // application "System Events"` resolves to System Events ITSELF, so the app
17
+ // name was always "System Events". The result was then split on ",", so any
18
+ // window title containing a comma shifted every field. And reading `window 1`
19
+ // needs an Accessibility (TCC) grant — without it the whole script errors out,
20
+ // losing the app name and pid too, even though neither needs that permission.
21
+ //
22
+ // Now: one script, newline-delimited, with the window title in its own `try`
23
+ // so a missing Accessibility grant (or a window-less app) costs only the title.
24
+ // Name and pid are lines 1 and 2; everything after is the title, so a title
25
+ // containing the delimiter can't corrupt the fields that matter.
26
+ const APPLE_SCRIPT_GET_ACTIVE = `tell application "System Events"
27
+ set p to first process whose frontmost is true
28
+ set n to name of p
29
+ set u to unix id of p
30
+ try
31
+ set w to name of window 1 of p
32
+ on error
33
+ set w to ""
34
+ end try
35
+ return n & linefeed & u & linefeed & w
36
+ end tell`
37
+
38
+ const parseOutput = (
39
+ raw: string,
40
+ ): { name: string; title: string; pid: number | null } | null => {
41
+ if (raw.trim().length === 0) return null
42
+ const lines = raw.replace(/\r\n?/g, "\n").split("\n")
43
+ const name = (lines[0] ?? "").trim()
44
+ if (name.length === 0) return null
45
+ const pid = Number.parseInt((lines[1] ?? "").trim(), 10)
46
+ const title = lines.slice(2).join("\n").trim()
47
+ return { name, title, pid: Number.isFinite(pid) ? pid : null }
48
+ }
49
+
50
+ export const createDarwinActiveAppProvider = async (
51
+ deps: DarwinActiveAppDeps,
52
+ ): Promise<ActiveAppProvider> => {
53
+ const pollMs = deps.pollIntervalMs ?? DEFAULT_POLL_MS
54
+ const subscribers = new Set<(s: ActiveAppSnapshot | null) => void>()
55
+ let last: ActiveAppSnapshot | null = null
56
+ let stopped = false
57
+
58
+ const snapshot = async (): Promise<ActiveAppSnapshot | null> => {
59
+ try {
60
+ const result = await deps.executor.run(
61
+ "osascript",
62
+ ["-e", APPLE_SCRIPT_GET_ACTIVE],
63
+ {
64
+ timeoutMs: 3_000,
65
+ },
66
+ )
67
+ if (result.exitCode !== 0) return last
68
+ const parsed = parseOutput(result.stdout)
69
+ if (parsed === null) return last
70
+ return {
71
+ name: parsed.name,
72
+ windowTitle: parsed.title || null,
73
+ processId: parsed.pid,
74
+ }
75
+ } catch (err) {
76
+ deps.logger.warn({ err }, "active-app: osascript failed")
77
+ return last
78
+ }
79
+ }
80
+
81
+ let interval: ReturnType<typeof setInterval> | null = null
82
+ interval = setInterval(() => {
83
+ if (stopped) return
84
+ void snapshot().then((s) => {
85
+ if (stopped) return
86
+ const same =
87
+ last !== null &&
88
+ s !== null &&
89
+ last.name === s.name &&
90
+ last.windowTitle === s.windowTitle &&
91
+ last.processId === s.processId
92
+ if (!same) {
93
+ last = s
94
+ for (const h of subscribers) h(s)
95
+ }
96
+ })
97
+ }, pollMs)
98
+
99
+ return {
100
+ async getActive() {
101
+ return snapshot()
102
+ },
103
+ subscribe(handler) {
104
+ subscribers.add(handler)
105
+ return () => {
106
+ subscribers.delete(handler)
107
+ }
108
+ },
109
+ async stop() {
110
+ stopped = true
111
+ if (interval !== null) {
112
+ clearInterval(interval)
113
+ interval = null
114
+ }
115
+ },
116
+ }
117
+ }