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