@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,1060 @@
1
+ import type pino from "pino"
2
+
3
+ import type { PubSub } from "@/core/pub-sub"
4
+ import type { Store } from "@/core/store"
5
+ import type { GestureKind } from "@/core/gesture-state"
6
+ import type { ActiveAppProvider } from "@/system/providers/active-app"
7
+ import type { SessionProvider, SessionState } from "@/system/providers/session"
8
+ import { getRequiredCapability } from "@/system/requirements"
9
+ import { compileDeckMatcher } from "@/system/glob-match"
10
+ import type { Methods } from "../methods"
11
+ import { describeEditorSurfaces } from "../editor-surfaces"
12
+ import type {
13
+ EditorAddonOwner,
14
+ EditorSourceTarget,
15
+ EditorSurfaceDescriptor,
16
+ } from "../editor-surfaces"
17
+
18
+ type ActiveAppProviderLike = Pick<ActiveAppProvider, "getActive" | "stop">
19
+
20
+ export interface RuntimeButton {
21
+ id: string
22
+ type: string
23
+ position?: number
24
+ config?: unknown
25
+ actions?: {
26
+ tap?: string
27
+ dbltap?: string
28
+ hold?: string
29
+ }
30
+ full?: boolean
31
+ variant?: string
32
+ buttonColor?:
33
+ | "blue"
34
+ | "green"
35
+ | "purple"
36
+ | "cyan"
37
+ | "magenta"
38
+ | "amber"
39
+ | "lime"
40
+ sourceTarget?: EditorSourceTarget
41
+ }
42
+
43
+ export interface RuntimeDeck {
44
+ id: string
45
+ name: string
46
+ buttons: ReadonlyArray<RuntimeButton>
47
+ isMain?: boolean
48
+ processNames?: ReadonlyArray<string>
49
+ windowNames?: ReadonlyArray<string>
50
+ autoShow?: boolean
51
+ isOverlayDeck?: boolean
52
+ icon?: string
53
+ background?: string
54
+ paginated?: boolean
55
+ // ponytail: addon-generated decks inherit the parent's window/process trigger
56
+ // so the runtime can switch to them automatically when the watched window
57
+ // comes to the foreground. Preserved through materialization.
58
+ trigger?: unknown
59
+ isOverlay?: boolean
60
+ config?: Record<string, unknown>
61
+ buttonColor?:
62
+ | "blue"
63
+ | "green"
64
+ | "purple"
65
+ | "cyan"
66
+ | "magenta"
67
+ | "amber"
68
+ | "lime"
69
+ /**
70
+ * Variant token name resolved from the active theme. Falls back to
71
+ * "default" when the theme doesn't declare the requested variant.
72
+ */
73
+ variant?: string
74
+ buttonErrors?: ReadonlyArray<{
75
+ position: number
76
+ buttonId?: string
77
+ details: string
78
+ }>
79
+ sourceDeckId?: string
80
+ projectionId?: string
81
+ pageIndex?: number
82
+ editable?: boolean
83
+ addonOwner?: EditorAddonOwner
84
+ }
85
+
86
+ export interface RuntimeButtonHandler {
87
+ onTap?: (ctx: ButtonActionContext) => void | Promise<void>
88
+ onDblTap?: (ctx: ButtonActionContext) => void | Promise<void>
89
+ onHold?: (ctx: ButtonActionContext) => void | Promise<void>
90
+ dispose?: () => void | Promise<void>
91
+ }
92
+
93
+ export interface ButtonActionContext {
94
+ buttonId: string
95
+ deckId: string
96
+ config: unknown
97
+ gesture: GestureKind
98
+ position: number | undefined
99
+ }
100
+
101
+ export interface GestureEvent {
102
+ readonly gesture: GestureKind
103
+ readonly at: number
104
+ readonly position: number | undefined
105
+ }
106
+
107
+ export type GestureListener = (buttonId: string, event: GestureEvent) => void
108
+
109
+ export interface MountedButton {
110
+ addonName: string
111
+ buttonId: string
112
+ type: string
113
+ config: unknown
114
+ }
115
+
116
+ export interface CreateRuntimeOptions {
117
+ decks: ReadonlyArray<RuntimeDeck>
118
+ pubSub: PubSub
119
+ store: Store
120
+ logger: pino.Logger
121
+ getMethods: () => Methods
122
+ }
123
+
124
+ export interface Runtime {
125
+ getActiveDeck(): RuntimeDeck
126
+ getActiveDeckId(): string
127
+ deckExists(id: string): boolean
128
+ setDecks(decks: ReadonlyArray<RuntimeDeck>): void
129
+ navigateToDeck(id: string, options?: { addToHistory?: boolean }): void
130
+ goBack(): void
131
+ setOverlay(
132
+ deckId: string | null,
133
+ opts?: { source?: "autoShow" | "manual" },
134
+ ): void
135
+ getOverlay(): RuntimeDeck | null
136
+ registerButtonHandler(buttonId: string, handler: RuntimeButtonHandler): void
137
+ unregisterButtonHandler(buttonId: string): void
138
+ mountAddonButtons(
139
+ addonName: string,
140
+ buttons: ReadonlyArray<MountedButton>,
141
+ ): void
142
+ dispatchGesture(buttonId: string, gesture: GestureKind): Promise<void>
143
+ invokeAction(buttonId: string, gesture: GestureKind): Promise<void>
144
+ setGestureListener(listener: GestureListener | null): void
145
+ invalidate(): void
146
+ setActiveAppProvider(provider: ActiveAppProviderLike): void
147
+ setSessionProvider(provider: SessionProvider): void
148
+ isLockActive(): boolean
149
+ stopActiveAppPolling(): Promise<void>
150
+ navStackDepth(): number
151
+ hasOverlayDeckAvailable(): boolean
152
+ getAvailableOverlayDeckIcon(): string | null
153
+ getAvailableOverlayDeckName(): string | null
154
+ getBrightness(): number
155
+ setBrightness(value: number): void
156
+ getEditorSurfaces(options?: {
157
+ keyCount?: number
158
+ revision?: number
159
+ }): EditorSurfaceDescriptor[]
160
+ }
161
+
162
+ export const createRuntime = (options: CreateRuntimeOptions): Runtime => {
163
+ const { pubSub, store, getMethods } = options
164
+ const logger = options.logger.child({ component: "runtime" })
165
+ let { decks } = options
166
+ let gestureListener: GestureListener | null = null
167
+ const mainDeck = decks.find((d) => d.isMain) ?? decks[0]
168
+ if (mainDeck === undefined) {
169
+ throw new Error("createRuntime: at least one deck is required")
170
+ }
171
+
172
+ const handlers = new Map<string, RuntimeButtonHandler>()
173
+ const navStack: string[] = [mainDeck.id]
174
+ let transientDeckId: string | null = null
175
+ let overlayDeckId: string | null = null
176
+ let availableOverlayDeckId: string | null = null
177
+ let overlayPreviousActiveId: string | null = null
178
+ const overlayNavStacks = new Map<string, string[]>()
179
+ const buttonIndex = new Map<
180
+ string,
181
+ { deckId: string; button: RuntimeDeck["buttons"][number] }
182
+ >()
183
+ for (const deck of decks) {
184
+ for (const button of deck.buttons) {
185
+ if (!buttonIndex.has(button.id)) {
186
+ buttonIndex.set(button.id, { deckId: deck.id, button })
187
+ }
188
+ // ponytail: also index the composite "{deckId}:{button.id}" so callers
189
+ // can dispatch by either form. findButton consults both maps.
190
+ const composite = `${deck.id}:${button.id}`
191
+ if (!buttonIndex.has(composite)) {
192
+ buttonIndex.set(composite, { deckId: deck.id, button })
193
+ }
194
+ }
195
+ }
196
+ let brightness = 50
197
+ let lockActive = false
198
+ let preLockActiveDeckId: string | null = null
199
+ let preLockOverlayDeckId: string | null = null
200
+ let sessionUnsubscribe: (() => void) | null = null
201
+
202
+ const deckById = (id: string): RuntimeDeck | undefined =>
203
+ decks.find((d) => d.id === id)
204
+
205
+ const setDecks = (next: ReadonlyArray<RuntimeDeck>): void => {
206
+ decks = next
207
+ // ponytail: rebuild a global button-id → ref map so bare-id lookups in
208
+ // findButton are O(1) instead of scanning all decks in deck order. Set
209
+ // first-wins (not last-wins) to preserve the legacy scan behavior.
210
+ buttonIndex.clear()
211
+ for (const deck of decks) {
212
+ if (!Array.isArray(deck.buttons)) continue
213
+ for (const button of deck.buttons) {
214
+ if (!buttonIndex.has(button.id)) {
215
+ buttonIndex.set(button.id, { deckId: deck.id, button })
216
+ }
217
+ const composite = `${deck.id}:${button.id}`
218
+ if (!buttonIndex.has(composite)) {
219
+ buttonIndex.set(composite, { deckId: deck.id, button })
220
+ }
221
+ }
222
+ }
223
+ // Clear any navigation/overlay state that pointed at decks we no longer have.
224
+ const currentActive = transientDeckId ?? navStack[navStack.length - 1]
225
+ if (currentActive !== undefined && deckById(currentActive) === undefined) {
226
+ navStack.length = 0
227
+ const fallback = decks.find((d) => d.isMain) ?? decks[0]
228
+ if (fallback !== undefined) navStack.push(fallback.id)
229
+ transientDeckId = null
230
+ }
231
+ if (overlayDeckId !== null && deckById(overlayDeckId) === undefined) {
232
+ // ponytail: clear both bits so a later setOverlay(null) doesn't restore
233
+ // a deck id that no longer exists in the registry.
234
+ overlayDeckId = null
235
+ overlayPreviousActiveId = null
236
+ }
237
+ pubSub.publish("runtime:activeDeck", { deckId: getActiveDeckId() })
238
+ }
239
+
240
+ const findButton = (
241
+ id: string,
242
+ ): { deckId: string; button: RuntimeDeck["buttons"][number] } | null => {
243
+ // ponytail: build a global button-id index in setDecks so bare-id
244
+ // lookups don't depend on deck order.
245
+ return buttonIndex.get(id) ?? null
246
+ }
247
+
248
+ // ponytail: lock-mode escape hatch — only these button types can navigate out of lock mode
249
+ const LOCK_FOLDER_NAV_TYPES: ReadonlySet<string> = new Set([
250
+ "core:change-deck",
251
+ "core:page-nav",
252
+ ])
253
+
254
+ const getActiveDeck = (): RuntimeDeck => {
255
+ const id = getActiveDeckId()
256
+ const deck = deckById(id)
257
+ if (deck === undefined) throw new Error(`Active deck '${id}' not found`)
258
+ return deck
259
+ }
260
+
261
+ const getActiveDeckId = (): string => {
262
+ if (lockActive) return "core:lock"
263
+ // ponytail: while overlay mode is on, transientDeckId (used by page-nav)
264
+ // must take precedence — otherwise paginated overlay pages are invisible
265
+ // to the runtime, and dismissing overlay after paginating restores the
266
+ // overlay root instead of the page the user was on.
267
+ if (overlayDeckId !== null) {
268
+ if (transientDeckId !== null) return transientDeckId
269
+ const stack = overlayNavStacks.get(overlayDeckId)
270
+ if (stack !== undefined && stack.length > 0) {
271
+ return stack[stack.length - 1] ?? overlayDeckId
272
+ }
273
+ return overlayDeckId
274
+ }
275
+ return transientDeckId ?? navStack[navStack.length - 1] ?? mainDeck.id
276
+ }
277
+
278
+ const snapshotRegularActiveDeckId = (): string =>
279
+ overlayPreviousActiveId ??
280
+ transientDeckId ??
281
+ navStack[navStack.length - 1] ??
282
+ mainDeck.id
283
+
284
+ const enterLockMode = (): void => {
285
+ if (lockActive) return
286
+ preLockActiveDeckId = snapshotRegularActiveDeckId()
287
+ preLockOverlayDeckId = overlayDeckId
288
+ lockActive = true
289
+ logger.info(
290
+ { preLockActiveDeckId, preLockOverlayDeckId },
291
+ "runtime: lock active",
292
+ )
293
+ pubSub.publish("runtime:lock-mode", {
294
+ active: true,
295
+ reason: "session-locked",
296
+ })
297
+ pubSub.publish("runtime:activeDeck", { deckId: "core:lock" })
298
+ pubSub.publish("runtime:invalidate", undefined)
299
+ }
300
+
301
+ const navigateToDeck = (
302
+ id: string,
303
+ navOptions?: { addToHistory?: boolean },
304
+ ): void => {
305
+ if (id === getActiveDeckId()) return
306
+ let resolvedId = id
307
+ let target = deckById(id)
308
+ // ponytail: paginated decks drop the base id (e.g. `demo-media` becomes
309
+ // `demo-media-p1`/`demo-media-p2`). Fall back to the first paginated page
310
+ // when the base id no longer exists, so `core:change-deck` targets keep
311
+ // working without callers knowing about pagination.
312
+ if (target === undefined) {
313
+ const firstPage = deckById(`${id}-p1`)
314
+ if (firstPage !== undefined) {
315
+ target = firstPage
316
+ resolvedId = firstPage.id
317
+ }
318
+ }
319
+ if (target === undefined) {
320
+ logger.warn({ deckId: id }, "navigateToDeck: deck not found")
321
+ return
322
+ }
323
+ const previousActiveId = getActiveDeckId()
324
+ if (overlayDeckId !== null) {
325
+ // ponytail: while overlay mode is on, both change-deck (addToHistory:true)
326
+ // and page-nav (addToHistory:false) push onto the overlay's nav stack so
327
+ // re-toggling the overlay restores the user's last position. Page-nav
328
+ // used to set transientDeckId which the runtime dropped on every
329
+ // getActiveDeckId query — see https://example.invalid/quick-009.
330
+ const stack = overlayNavStacks.get(overlayDeckId)
331
+ if (stack === undefined) {
332
+ overlayNavStacks.set(overlayDeckId, [overlayDeckId])
333
+ }
334
+ const current = overlayNavStacks.get(overlayDeckId)!
335
+ if (current[current.length - 1] !== resolvedId) {
336
+ current.push(resolvedId)
337
+ }
338
+ transientDeckId = null
339
+ } else if (navOptions?.addToHistory === false) {
340
+ transientDeckId = resolvedId
341
+ } else {
342
+ navStack.push(resolvedId)
343
+ transientDeckId = null
344
+ }
345
+ pubSub.publish("runtime:deck-inactive", { deckId: previousActiveId })
346
+ pubSub.publish("runtime:activeDeck", { deckId: resolvedId })
347
+ }
348
+
349
+ const goBack = (): void => {
350
+ if (overlayDeckId !== null) {
351
+ const stack = overlayNavStacks.get(overlayDeckId)
352
+ if (stack !== undefined && stack.length > 1) {
353
+ stack.pop()
354
+ const prev = stack[stack.length - 1]!
355
+ pubSub.publish("runtime:deck-inactive", { deckId: prev })
356
+ pubSub.publish("runtime:activeDeck", { deckId: prev })
357
+ } else {
358
+ setOverlay(null)
359
+ }
360
+ return
361
+ }
362
+ if (transientDeckId !== null) {
363
+ pubSub.publish("runtime:deck-inactive", { deckId: transientDeckId })
364
+ const pageMatch = /^(.*)-p\d+$/.exec(transientDeckId)
365
+ if (pageMatch !== null) {
366
+ const basePageId = `${pageMatch[1]}-p1`
367
+ if (navStack[navStack.length - 1] === basePageId) {
368
+ navStack.pop()
369
+ pubSub.publish("runtime:deck-inactive", { deckId: basePageId })
370
+ }
371
+ }
372
+ transientDeckId = null
373
+ const prev = navStack[navStack.length - 1] ?? mainDeck.id
374
+ pubSub.publish("runtime:activeDeck", { deckId: prev })
375
+ return
376
+ }
377
+ if (navStack.length <= 1) {
378
+ logger.warn("goBack: at root deck; nothing to pop")
379
+ return
380
+ }
381
+ const popped = navStack[navStack.length - 1]
382
+ navStack.pop()
383
+ const prev = navStack[navStack.length - 1]
384
+ if (prev === undefined) return
385
+ if (popped !== undefined) {
386
+ pubSub.publish("runtime:deck-inactive", { deckId: popped })
387
+ }
388
+ pubSub.publish("runtime:activeDeck", { deckId: prev })
389
+ }
390
+
391
+ const setOverlay = (
392
+ deckId: string | null,
393
+ opts?: { source?: "autoShow" | "manual" },
394
+ ): void => {
395
+ if (deckId !== null && deckById(deckId) === undefined) {
396
+ logger.warn({ deckId }, "setOverlay: deck not found")
397
+ return
398
+ }
399
+ if (deckId !== null && !overlayNavStacks.has(deckId)) {
400
+ overlayNavStacks.set(deckId, [deckId])
401
+ }
402
+ const previousActiveId = getActiveDeckId()
403
+ const previousOverlayId = overlayDeckId
404
+ if (previousOverlayId === null && deckId !== null) {
405
+ overlayPreviousActiveId = previousActiveId
406
+ }
407
+ overlayDeckId = deckId
408
+ pubSub.publish(
409
+ "runtime:overlay",
410
+ opts?.source !== undefined ? { deckId, source: opts.source } : { deckId },
411
+ )
412
+ if (deckId !== null) {
413
+ if (previousOverlayId !== null) {
414
+ pubSub.publish("runtime:deck-inactive", { deckId: previousOverlayId })
415
+ } else {
416
+ pubSub.publish("runtime:deck-inactive", { deckId: previousActiveId })
417
+ }
418
+ // ponytail: if the overlay has nav history (stack top != deckId), the
419
+ // user was already navigating within this overlay. Publish the stack
420
+ // top so the broadcast matches what getActiveDeckId will report, and
421
+ // re-toggling restores the page the user was on.
422
+ const stack = overlayNavStacks.get(deckId)
423
+ const targetDeck =
424
+ stack !== undefined && stack.length > 0
425
+ ? (stack[stack.length - 1] ?? deckId)
426
+ : deckId
427
+ pubSub.publish("runtime:activeDeck", { deckId: targetDeck })
428
+ } else {
429
+ // ponytail: previousActiveId (e.g. a paginated page like -p2) is the
430
+ // deck actually being deactivated; previousOverlayId is the overlay
431
+ // root. Publish the page so subscribers see the right deck going dark.
432
+ if (previousActiveId !== previousOverlayId) {
433
+ pubSub.publish("runtime:deck-inactive", { deckId: previousActiveId })
434
+ }
435
+ // ponytail: clear transientDeckId so the dismissed overlay's paginated
436
+ // page doesn't bleed into the regular nav state. Without this,
437
+ // getActiveDeckId keeps returning the page even after overlayDeckId
438
+ // is null.
439
+ transientDeckId = null
440
+ const restoreId = overlayPreviousActiveId ?? mainDeck.id
441
+ overlayPreviousActiveId = null
442
+ pubSub.publish("runtime:activeDeck", { deckId: restoreId })
443
+ }
444
+ }
445
+
446
+ const getOverlay = (): RuntimeDeck | null => {
447
+ if (overlayDeckId === null) return null
448
+ return deckById(overlayDeckId) ?? null
449
+ }
450
+
451
+ const registerButtonHandler = (
452
+ buttonId: string,
453
+ handler: RuntimeButtonHandler,
454
+ ): void => {
455
+ handlers.set(buttonId, handler)
456
+ }
457
+
458
+ const unregisterButtonHandler = (buttonId: string): void => {
459
+ handlers.delete(buttonId)
460
+ }
461
+
462
+ const mountAddonButtons = (
463
+ addonName: string,
464
+ buttons: ReadonlyArray<MountedButton>,
465
+ ): void => {
466
+ for (const btn of buttons) {
467
+ logger.debug(
468
+ { addonName, buttonId: btn.buttonId, type: btn.type },
469
+ "mounted addon button",
470
+ )
471
+ }
472
+ }
473
+
474
+ const handleSystemButton = (type: string, gesture: GestureKind): boolean => {
475
+ if (type === "core:back") {
476
+ if (gesture === "tap") {
477
+ goBack()
478
+ return true
479
+ }
480
+ if (gesture === "hold") {
481
+ // ponytail: hold always goes to main, never toggles overlay
482
+ if (overlayDeckId !== null) setOverlay(null)
483
+ navStack.length = 0
484
+ navStack.push(mainDeck.id)
485
+ const prevTransient = transientDeckId
486
+ transientDeckId = null
487
+ if (prevTransient !== null) {
488
+ pubSub.publish("runtime:deck-inactive", { deckId: prevTransient })
489
+ }
490
+ pubSub.publish("runtime:activeDeck", { deckId: mainDeck.id })
491
+ return true
492
+ }
493
+ if (gesture === "dbl-tap") {
494
+ if (overlayDeckId !== null) {
495
+ setOverlay(null)
496
+ } else if (availableOverlayDeckId !== null) {
497
+ setOverlay(availableOverlayDeckId)
498
+ }
499
+ return true
500
+ }
501
+ return true
502
+ }
503
+ // ponytail: gesture table — see ARCHITECTURE §5.
504
+ // core:settings-entry tap = open settings overlay; dbl-tap = toggle overlay.
505
+ // core:overlay-toggle tap/dbl-tap = toggle overlay (legacy fallback).
506
+ if (type === "core:settings-entry") {
507
+ if (gesture === "tap") {
508
+ navigateToDeck("internal-settings:settings", { addToHistory: true })
509
+ return true
510
+ }
511
+ if (gesture === "dbl-tap") {
512
+ if (overlayDeckId !== null) {
513
+ setOverlay(null)
514
+ } else if (availableOverlayDeckId !== null) {
515
+ setOverlay(availableOverlayDeckId)
516
+ }
517
+ return true
518
+ }
519
+ return false
520
+ }
521
+ if (type === "core:overlay-toggle") {
522
+ // ponytail: tap = step back within overlay path; dbl-tap = toggle
523
+ // overlay on/off; hold is left unhandled so it can fall through if the
524
+ // user assigns a hold action elsewhere.
525
+ if (gesture === "tap") {
526
+ goBack()
527
+ return true
528
+ }
529
+ if (gesture === "dbl-tap") {
530
+ if (overlayDeckId !== null) {
531
+ setOverlay(null)
532
+ } else if (availableOverlayDeckId !== null) {
533
+ setOverlay(availableOverlayDeckId)
534
+ }
535
+ return true
536
+ }
537
+ return false
538
+ }
539
+ return false
540
+ }
541
+
542
+ const invokeAction = async (
543
+ buttonId: string,
544
+ gesture: GestureKind,
545
+ ): Promise<void> => {
546
+ let found: { deckId: string; button: RuntimeDeck["buttons"][number] } | null
547
+ try {
548
+ found = findButton(buttonId)
549
+ } catch (err) {
550
+ // ponytail: findButton indexes into a Map — should never throw, but
551
+ // a corrupted deck entry would otherwise crash the daemon on every
552
+ // gesture. Log + bail; let the next gesture try again.
553
+ logger.error({ err, buttonId }, "runtime: findButton threw")
554
+ return
555
+ }
556
+ if (found === null) {
557
+ logger.warn({ buttonId }, "invokeAction: button not found")
558
+ return
559
+ }
560
+
561
+ try {
562
+ if (handleSystemButton(found.button.type, gesture)) {
563
+ return
564
+ }
565
+
566
+ if (lockActive && found.deckId === "core:lock") {
567
+ if (LOCK_FOLDER_NAV_TYPES.has(found.button.type)) {
568
+ lockActive = false
569
+ preLockActiveDeckId = null
570
+ preLockOverlayDeckId = null
571
+ if (overlayDeckId !== null) setOverlay(null)
572
+ pubSub.publish("runtime:lock-mode", {
573
+ active: false,
574
+ reason: "escape",
575
+ })
576
+ logger.info(
577
+ { buttonId, gesture, buttonType: found.button.type },
578
+ "runtime: lock escaped via folder-nav button",
579
+ )
580
+ } else {
581
+ logger.debug(
582
+ { buttonId, gesture, buttonType: found.button.type },
583
+ "runtime: lock-mode gesture suppressed",
584
+ )
585
+ return
586
+ }
587
+ }
588
+
589
+ if (found.deckId !== getActiveDeckId() && found.deckId !== "core:lock") {
590
+ logger.debug(
591
+ { buttonId, deckId: found.deckId, activeDeckId: getActiveDeckId() },
592
+ "invokeAction: gesture on inactive deck, dropping",
593
+ )
594
+ return
595
+ }
596
+
597
+ const userAction =
598
+ gesture === "tap"
599
+ ? found.button.actions?.tap
600
+ : gesture === "dbl-tap"
601
+ ? found.button.actions?.dbltap
602
+ : found.button.actions?.hold
603
+
604
+ const deck = deckById(found.deckId)
605
+ // ponytail: Array.isArray is tighter than — rejects
606
+ // , , and non-array values uniformly. Defends
607
+ // against any RuntimeDeck that ends up with missing
608
+ // (corrupted state, future refactor, runtime injection).
609
+ const position =
610
+ found.button.position ??
611
+ (Array.isArray(deck?.buttons)
612
+ ? deck.buttons.findIndex((b) => b.id === found.button.id)
613
+ : -1)
614
+
615
+ logger.info(
616
+ {
617
+ buttonId,
618
+ deckId: found.deckId,
619
+ position,
620
+ gesture,
621
+ buttonType: found.button.type,
622
+ buttonActions: found.button.actions,
623
+ userAction,
624
+ },
625
+ "[runtime] invokeAction resolved",
626
+ )
627
+
628
+ if (userAction !== undefined) {
629
+ logger.info(
630
+ { buttonId, gesture, action: userAction },
631
+ "[addon:sirenodeck] user action",
632
+ )
633
+ const capability = getRequiredCapability(userAction)
634
+ if (capability !== null && !getMethods().checkRequirement(capability)) {
635
+ logger.warn(
636
+ { buttonId, gesture, action: userAction, capability },
637
+ "[runtime] action skipped: missing system requirement",
638
+ )
639
+ if (position >= 0) {
640
+ getMethods().showTemporaryError(
641
+ found.deckId,
642
+ position,
643
+ undefined,
644
+ found.button.id,
645
+ `missing-requirement: ${capability}`,
646
+ )
647
+ }
648
+ return
649
+ }
650
+ try {
651
+ await getMethods().dispatch(userAction)
652
+ } catch (err) {
653
+ logger.error(
654
+ { buttonId, gesture, err },
655
+ "[addon:sirenodeck] user action failed",
656
+ )
657
+ if (position >= 0) {
658
+ getMethods().showTemporaryError(
659
+ found.deckId,
660
+ position,
661
+ undefined,
662
+ found.button.id,
663
+ `action-failed: ${userAction}`,
664
+ )
665
+ }
666
+ }
667
+ return
668
+ }
669
+
670
+ const handlerKey = `${found.deckId}:${found.button.id}`
671
+ const handler = handlers.get(handlerKey)
672
+ if (handler === undefined) {
673
+ // Dynamic decks can be rebuilt after addon services mount. Pagination
674
+ // is declarative, so keep its navigation working for those new buttons.
675
+ if (found.button.type === "core:page-nav") {
676
+ const config = found.button.config
677
+ if (typeof config === "object" && config !== null) {
678
+ const target =
679
+ gesture === "tap"
680
+ ? (config as { nextDeckId?: unknown }).nextDeckId
681
+ : gesture === "hold"
682
+ ? (config as { prevDeckId?: unknown }).prevDeckId
683
+ : undefined
684
+ if (typeof target === "string") {
685
+ navigateToDeck(target, { addToHistory: false })
686
+ }
687
+ }
688
+ }
689
+ return
690
+ }
691
+ const ctx: ButtonActionContext = {
692
+ buttonId,
693
+ deckId: found.deckId,
694
+ config: found.button.config,
695
+ gesture,
696
+ position: found.button.position,
697
+ }
698
+ const fn =
699
+ gesture === "tap"
700
+ ? handler.onTap
701
+ : gesture === "dbl-tap"
702
+ ? handler.onDblTap
703
+ : handler.onHold
704
+ if (fn === undefined) {
705
+ return
706
+ }
707
+ await fn(ctx)
708
+ } catch (err) {
709
+ // ponytail: a single bad button-press must not crash the entire
710
+ // daemon. Log the failure with full context so operators see it in
711
+ // service.log and can file a bug — but keep serving subsequent
712
+ // gestures instead of exiting via killChildrenAndExit.
713
+ logger.error(
714
+ {
715
+ err,
716
+ buttonId,
717
+ gesture,
718
+ deckId: found.deckId,
719
+ buttonType: found.button.type,
720
+ },
721
+ "invokeAction: uncaught error in handler",
722
+ )
723
+ }
724
+ }
725
+
726
+ const dispatchGesture = async (
727
+ buttonId: string,
728
+ gesture: GestureKind,
729
+ ): Promise<void> => {
730
+ const found = findButton(buttonId)
731
+ if (found === null) {
732
+ logger.warn({ buttonId }, "dispatchGesture: button not found")
733
+ return
734
+ }
735
+ gestureListener?.(found.button.id, {
736
+ gesture,
737
+ at: Date.now(),
738
+ position: found.button.position,
739
+ })
740
+ await invokeAction(buttonId, gesture)
741
+ }
742
+
743
+ const setGestureListener = (listener: GestureListener | null): void => {
744
+ gestureListener = listener
745
+ }
746
+
747
+ const invalidate = (): void => {
748
+ pubSub.publish("runtime:invalidate", { activeDeckId: getActiveDeckId() })
749
+ }
750
+
751
+ void store
752
+
753
+ const overlayDecks = (): Array<{
754
+ deck: RuntimeDeck
755
+ matcher: ReturnType<typeof compileDeckMatcher>
756
+ specificity: number
757
+ }> => {
758
+ const result: Array<{
759
+ deck: RuntimeDeck
760
+ matcher: ReturnType<typeof compileDeckMatcher>
761
+ specificity: number
762
+ }> = []
763
+ for (const deck of decks) {
764
+ const hasProcess =
765
+ deck.processNames !== undefined && deck.processNames.length > 0
766
+ const hasWindow =
767
+ deck.windowNames !== undefined && deck.windowNames.length > 0
768
+ if (!hasProcess && !hasWindow) continue
769
+ result.push({
770
+ deck,
771
+ matcher: compileDeckMatcher({
772
+ processNames: deck.processNames,
773
+ windowNames: deck.windowNames,
774
+ }),
775
+ specificity: (hasProcess ? 1 : 0) + (hasWindow ? 1 : 0),
776
+ })
777
+ }
778
+ return result
779
+ }
780
+
781
+ let activeAppPoll: ReturnType<typeof setInterval> | null = null
782
+ let activeAppProvider: ActiveAppProviderLike | null = null
783
+ let lastOverlayDeckId: string | null = overlayDeckId
784
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
785
+ let pendingOverlayDeckId: string | null = null
786
+ let latestActiveAppSnapshot: {
787
+ name: string
788
+ windowTitle: string | null
789
+ processId: number | null
790
+ } | null = null
791
+
792
+ const applyOverlay = (deckId: string | null): void => {
793
+ if (deckId !== null && deckById(deckId) === undefined) {
794
+ logger.warn({ deckId }, "active-app: overlay deck not found")
795
+ return
796
+ }
797
+ if (deckId === null) {
798
+ // ponytail: no overlay matches anymore. If we had one active, dismiss it.
799
+ if (overlayDeckId !== null) {
800
+ logger.info(
801
+ { prevOverlayId: overlayDeckId },
802
+ "active-app: dismissing previous overlay (no match)",
803
+ )
804
+ setOverlay(null, { source: "autoShow" })
805
+ }
806
+ lastOverlayDeckId = null
807
+ return
808
+ }
809
+ if (overlayDeckId !== null && deckId !== overlayDeckId) {
810
+ // ponytail: overlay mode is a routing branch. If the new match is
811
+ // auto-show it replaces the current overlay; if it's not auto-show the
812
+ // current overlay dismisses and the new match stays as available-only
813
+ // (manual toggle required). In both cases the previous overlay's
814
+ // overlayNavStacks entry is preserved for re-activation.
815
+ const newDeck = deckById(deckId)
816
+ if (newDeck === undefined) return
817
+ if (newDeck.autoShow === true) {
818
+ logger.info(
819
+ { prevOverlayId: overlayDeckId, newMatch: deckId },
820
+ "active-app: switching overlay (match moved, autoShow)",
821
+ )
822
+ setOverlay(deckId, { source: "autoShow" })
823
+ } else {
824
+ logger.info(
825
+ { prevOverlayId: overlayDeckId, newMatch: deckId },
826
+ "active-app: dismissing current overlay (new match is not autoShow)",
827
+ )
828
+ setOverlay(null, { source: "autoShow" })
829
+ }
830
+ lastOverlayDeckId = deckId
831
+ return
832
+ }
833
+ if (deckId === lastOverlayDeckId) return
834
+ const deck = deckById(deckId)
835
+ if (deck === undefined) return
836
+ logger.info(
837
+ { deckId, autoShow: deck.autoShow === true },
838
+ "active-app: applying overlay",
839
+ )
840
+ if (deck.autoShow !== true) {
841
+ lastOverlayDeckId = deckId
842
+ return
843
+ }
844
+ lastOverlayDeckId = deckId
845
+ setOverlay(deckId, { source: "autoShow" })
846
+ }
847
+
848
+ const computeOverlayFor = (snapshot: {
849
+ name: string
850
+ windowTitle: string | null
851
+ processId: number | null
852
+ }): string | null => {
853
+ let bestId: string | null = null
854
+ let bestSpecificity = -1
855
+ for (const { deck, matcher, specificity } of overlayDecks()) {
856
+ if (!matcher(snapshot)) continue
857
+ if (specificity > bestSpecificity) {
858
+ bestId = deck.id
859
+ bestSpecificity = specificity
860
+ }
861
+ }
862
+ if (availableOverlayDeckId !== bestId) {
863
+ const prev = availableOverlayDeckId
864
+ availableOverlayDeckId = bestId
865
+ if (bestId !== null) {
866
+ logger.info(
867
+ {
868
+ from: prev,
869
+ to: bestId,
870
+ snapshot: {
871
+ name: snapshot.name,
872
+ windowTitle: snapshot.windowTitle,
873
+ },
874
+ },
875
+ "active-app: overlay deck available",
876
+ )
877
+ } else if (prev !== null) {
878
+ logger.info({ from: prev }, "active-app: no overlay deck matches")
879
+ }
880
+ pubSub.publish("runtime:overlay-available", { deckId: bestId })
881
+ }
882
+ return bestId
883
+ }
884
+
885
+ const scheduleOverlay = (deckId: string | null): void => {
886
+ pendingOverlayDeckId = deckId
887
+ if (debounceTimer !== null) clearTimeout(debounceTimer)
888
+ debounceTimer = setTimeout(() => {
889
+ debounceTimer = null
890
+ const next = pendingOverlayDeckId
891
+ pendingOverlayDeckId = null
892
+ applyOverlay(next)
893
+ }, 200)
894
+ }
895
+
896
+ const startActiveAppLoop = (provider: ActiveAppProviderLike): void => {
897
+ if (activeAppPoll !== null) return
898
+ logger.info("active-app: poll loop started")
899
+ activeAppPoll = setInterval(() => {
900
+ void provider.getActive().then((snapshot) => {
901
+ if (snapshot === null) {
902
+ latestActiveAppSnapshot = null
903
+ scheduleOverlay(null)
904
+ return
905
+ }
906
+ latestActiveAppSnapshot = snapshot
907
+ logger.debug(
908
+ {
909
+ snapshot: {
910
+ name: snapshot.name,
911
+ windowTitle: snapshot.windowTitle,
912
+ },
913
+ },
914
+ "active-app: snapshot",
915
+ )
916
+ scheduleOverlay(computeOverlayFor(snapshot))
917
+ })
918
+ }, 1000)
919
+ }
920
+
921
+ const stopActiveAppLoop = (): void => {
922
+ if (debounceTimer !== null) {
923
+ clearTimeout(debounceTimer)
924
+ debounceTimer = null
925
+ }
926
+ pendingOverlayDeckId = null
927
+ if (activeAppPoll !== null) {
928
+ clearInterval(activeAppPoll)
929
+ activeAppPoll = null
930
+ }
931
+ }
932
+
933
+ const setActiveAppProvider = (provider: ActiveAppProviderLike): void => {
934
+ activeAppProvider = provider
935
+ startActiveAppLoop(provider)
936
+ }
937
+
938
+ const setSessionProvider = (provider: SessionProvider): void => {
939
+ if (sessionUnsubscribe !== null) {
940
+ sessionUnsubscribe()
941
+ sessionUnsubscribe = null
942
+ }
943
+ const handle = (state: SessionState): void => {
944
+ if (state === "locked" && !lockActive) {
945
+ enterLockMode()
946
+ return
947
+ }
948
+ if (state === "locked" && lockActive) {
949
+ preLockActiveDeckId = snapshotRegularActiveDeckId()
950
+ preLockOverlayDeckId = overlayDeckId
951
+ logger.info("runtime: lock snapshot refreshed on re-lock")
952
+ return
953
+ }
954
+ if (state === "unlocked" && lockActive) {
955
+ lockActive = false
956
+ const overlaySnapshot = preLockOverlayDeckId
957
+ const activeSnapshot = preLockActiveDeckId
958
+ preLockActiveDeckId = null
959
+ preLockOverlayDeckId = null
960
+ if (
961
+ overlaySnapshot !== null &&
962
+ latestActiveAppSnapshot !== null &&
963
+ computeOverlayFor(latestActiveAppSnapshot) === overlaySnapshot
964
+ ) {
965
+ logger.info(
966
+ { overlayId: overlaySnapshot },
967
+ "runtime: overlay auto-resumed on unlock (trigger still matches)",
968
+ )
969
+ setOverlay(overlaySnapshot, { source: "autoShow" })
970
+ } else {
971
+ if (overlayDeckId !== null) setOverlay(null)
972
+ const restoreId = activeSnapshot ?? mainDeck.id
973
+ logger.info(
974
+ { restoreId, overlayWas: overlaySnapshot, triggerMatches: false },
975
+ "runtime: lock cleared, restoring previous deck",
976
+ )
977
+ navigateToDeck(restoreId, { addToHistory: false })
978
+ }
979
+ pubSub.publish("runtime:lock-mode", {
980
+ active: false,
981
+ reason: "session-unlocked",
982
+ })
983
+ pubSub.publish("runtime:invalidate", undefined)
984
+ }
985
+ }
986
+ sessionUnsubscribe = provider.subscribe(handle)
987
+ }
988
+
989
+ const stopActiveAppPolling = async (): Promise<void> => {
990
+ stopActiveAppLoop()
991
+ if (activeAppProvider !== null) {
992
+ try {
993
+ await activeAppProvider.stop()
994
+ } catch (err) {
995
+ logger.warn({ err }, "active-app: provider stop() failed")
996
+ }
997
+ activeAppProvider = null
998
+ }
999
+ }
1000
+
1001
+ const runtime: Runtime = {
1002
+ getActiveDeck,
1003
+ getActiveDeckId,
1004
+ deckExists: (id: string) =>
1005
+ deckById(id) !== undefined || deckById(`${id}-p1`) !== undefined,
1006
+ setDecks,
1007
+ navigateToDeck,
1008
+ goBack,
1009
+ setOverlay,
1010
+ getOverlay,
1011
+ registerButtonHandler,
1012
+ unregisterButtonHandler,
1013
+ mountAddonButtons,
1014
+ dispatchGesture,
1015
+ invokeAction,
1016
+ setGestureListener,
1017
+ invalidate,
1018
+ setActiveAppProvider,
1019
+ setSessionProvider,
1020
+ isLockActive: () => lockActive,
1021
+ stopActiveAppPolling,
1022
+ navStackDepth: () => navStack.length,
1023
+ hasOverlayDeckAvailable: () =>
1024
+ availableOverlayDeckId !== null || pendingOverlayDeckId !== null,
1025
+ getAvailableOverlayDeckIcon: (): string | null => {
1026
+ const id = availableOverlayDeckId ?? pendingOverlayDeckId
1027
+ if (id === null) return null
1028
+ const deck = deckById(id)
1029
+ return deck?.icon ?? null
1030
+ },
1031
+ getAvailableOverlayDeckName: (): string | null => {
1032
+ const id = availableOverlayDeckId ?? pendingOverlayDeckId
1033
+ if (id === null) return null
1034
+ const deck = deckById(id)
1035
+ return deck?.name ?? null
1036
+ },
1037
+ getBrightness: () => brightness,
1038
+ setBrightness: (value: number) => {
1039
+ const clamped = Math.max(10, Math.min(100, Math.round(value)))
1040
+ if (clamped === brightness) return
1041
+ brightness = clamped
1042
+ pubSub.publish("sireno:settings:brightness", { value: clamped })
1043
+ },
1044
+ getEditorSurfaces: ({ keyCount = 15, revision = 0 } = {}) => {
1045
+ return describeEditorSurfaces({
1046
+ decks,
1047
+ keyCount,
1048
+ revision,
1049
+ systemState: {
1050
+ navStackDepth: navStack.length,
1051
+ hasOverlayDeckAvailable: availableOverlayDeckId !== null,
1052
+ lockActive,
1053
+ inOverlayMode: overlayDeckId !== null,
1054
+ },
1055
+ })
1056
+ },
1057
+ }
1058
+
1059
+ return runtime
1060
+ }