@hienlh/ppm 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/CLAUDE.md +29 -4
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/bun.lock +16 -1
- package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +1 -0
- package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-CdDpOwjh.js} +1 -1
- package/dist/web/assets/ai-settings-section-B1hQyjAd.js +1 -0
- package/dist/web/assets/ai-settings-section-X2UOfSaS.js +1 -0
- package/dist/web/assets/{angular-html-DkR4iCth.js → angular-html-G66FNlyn.js} +1 -1
- package/dist/web/assets/{angular-ts-DcyL4Chx.js → angular-ts-B-PnUMyt.js} +1 -1
- package/dist/web/assets/{api-client-bfB7cXRM.js → api-client-C1ZwRL4Y.js} +1 -1
- package/dist/web/assets/api-client-pQL53cDQ.js +1 -0
- package/dist/web/assets/{api-settings-BcNjZIvG.js → api-settings-_7Q-ULxp.js} +1 -1
- package/dist/web/assets/{api-themes-DBfyVUtX.js → api-themes-DRAERo4Q.js} +1 -1
- package/dist/web/assets/{apl-BVlrVtjt.js → apl-BmDqqwE8.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-whgnq3ss.js → appearance-settings-section-DLoPK0LD.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-Dx6nBEKT.js +1 -0
- package/dist/web/assets/{astro-MjJlozLj.js → astro-BfwMNfxr.js} +1 -1
- package/dist/web/assets/{audio-preview-Bc54GCAV.js → audio-preview-DsYvgKqj.js} +1 -1
- package/dist/web/assets/{blade-DnJWi1fz.js → blade-BsFV4pnN.js} +1 -1
- package/dist/web/assets/branch-review-tab-G84Ojzae.js +1 -0
- package/dist/web/assets/{bsl-DCmJxSIz.js → bsl-Cw9Outng.js} +1 -1
- package/dist/web/assets/{button-VW4QGKCn.js → button-B9CTrsEf.js} +1 -1
- package/dist/web/assets/{cairo-C8dDenub.js → cairo-DOK5Wqnd.js} +1 -1
- package/dist/web/assets/chat-tab-DQJfjm8E.js +15 -0
- package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
- package/dist/web/assets/code-editor-D04iUPsZ.js +7 -0
- package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-BSccLRGT.js} +1 -1
- package/dist/web/assets/{coffee-DvFzqhgN.js → coffee-DvXuBmEJ.js} +1 -1
- package/dist/web/assets/{conflict-editor-DenQUOT2.js → conflict-editor-C4ay676e.js} +2 -2
- package/dist/web/assets/context-menu-C6pIsf83.js +1 -0
- package/dist/web/assets/{cpp-BLJN0umi.js → cpp-zCmm8rKA.js} +1 -1
- package/dist/web/assets/{crystal-BXR-9fGw.js → crystal-CdQx6lB6.js} +1 -1
- package/dist/web/assets/{csv-preview-CClrJOuR.js → csv-preview-DbwEmG7o.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-D4fhcMfT.js → data-grid-overlay-editor-6xUCeLHh.js} +1 -1
- package/dist/web/assets/{database-viewer-DItjm8wB.js → database-viewer-S6LsALjX.js} +1 -1
- package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
- package/dist/web/assets/diff-viewer-3oBA7vgQ.js +4 -0
- package/dist/web/assets/{docx-preview-DYBkiUaE.js → docx-preview-CXuk-emm.js} +1 -1
- package/dist/web/assets/{edge-rWOlCmXT.js → edge-E8QLt7oi.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-COtmo54G.js → edit-diff-preview-BGGGecVw.js} +1 -1
- package/dist/web/assets/{editor-language-service-DxkZ51E5.js → editor-language-service-DJmaKVY0.js} +1 -1
- package/dist/web/assets/{elixir-E7Y82okW.js → elixir-DPb_-72P.js} +1 -1
- package/dist/web/assets/{elm-DTPGbc-w.js → elm-BMzJY5b0.js} +1 -1
- package/dist/web/assets/{erb-DHqLHrE6.js → erb-AABQNtMK.js} +1 -1
- package/dist/web/assets/{erlang-Byg0nNiV.js → erlang-CEIKOkfP.js} +1 -1
- package/dist/web/assets/{esm-D_DmOM7n.js → esm-B9RVMpwV.js} +1 -1
- package/dist/web/assets/explorer-body-BQ0jyAGv.js +2 -0
- package/dist/web/assets/explorer-window-content-jdkWt34J.js +1 -0
- package/dist/web/assets/{extension-manager-section-BhwB6nqr.js → extension-manager-section-DG1guILi.js} +1 -1
- package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-BGSDUbdK.js} +1 -1
- package/dist/web/assets/file-icons-BXdvggK2.js +1 -0
- package/dist/web/assets/file-icons.generated-BtBErEwb.css +1 -0
- package/dist/web/assets/file-store-ojgMXgAw.js +1 -0
- package/dist/web/assets/files-settings-section-Gnyd7han.js +1 -0
- package/dist/web/assets/{format-file-meta-DOpzJJhS.js → format-file-meta-C77h04cZ.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-CY1n0Xf6.js → fortran-fixed-form-CrXvLHyi.js} +1 -1
- package/dist/web/assets/{fsharp-_vEDSCJz.js → fsharp-DCI4qAz_.js} +1 -1
- package/dist/web/assets/{gdresource-B6XT0wAC.js → gdresource-Vb2kO1MG.js} +1 -1
- package/dist/web/assets/{general-settings-section-BcxwiZ0X.js → general-settings-section-V6Khh15V.js} +1 -1
- package/dist/web/assets/{git-commit-Wk9ovq59.js → git-commit-BMI_OWn-.js} +1 -1
- package/dist/web/assets/git-log-panel-Bi513qjT.js +1 -0
- package/dist/web/assets/{git-rebase-DSarGPvK.js → git-rebase-D-d_2iqZ.js} +1 -1
- package/dist/web/assets/{git-repo-store-Dy69YCHN.js → git-repo-store-88Z8v-pF.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-C6qRruHI.js +1 -0
- package/dist/web/assets/{glide-data-grid-BvgAqSUr.js → glide-data-grid-DnQwYSmQ.js} +7 -7
- package/dist/web/assets/{glimmer-js-CkYI35_P.js → glimmer-js-BmNNiR0Z.js} +1 -1
- package/dist/web/assets/{glimmer-ts-BqmIZcds.js → glimmer-ts-DQE2Ufha.js} +1 -1
- package/dist/web/assets/{glsl-Be-yBCF5.js → glsl-BcIDsxRJ.js} +1 -1
- package/dist/web/assets/{graphql-a5DrENHY.js → graphql-DGKSLmGH.js} +1 -1
- package/dist/web/assets/group-chat-tab-DpczdwyV.js +1 -0
- package/dist/web/assets/{hack-BLAg20aD.js → hack-YiC8bEIb.js} +1 -1
- package/dist/web/assets/{haml-Dj6bGOQH.js → haml-DjblxAsz.js} +1 -1
- package/dist/web/assets/{handlebars-dmEa36Oh.js → handlebars-BQepOXaU.js} +1 -1
- package/dist/web/assets/{html-yacZYzSE.js → html-Cayj_2GP.js} +1 -1
- package/dist/web/assets/{html-derivative-lLi3-RNT.js → html-derivative-Weq79bL6.js} +1 -1
- package/dist/web/assets/{http-1KaxcE4e.js → http-BeLfdGdz.js} +1 -1
- package/dist/web/assets/{hurl-ST-dEEZR.js → hurl-D9rypCLK.js} +1 -1
- package/dist/web/assets/{hxml-CpDtuq__.js → hxml-clG8mmSH.js} +1 -1
- package/dist/web/assets/{image-preview-CJVxL0uH.js → image-preview-CzCc4_Do.js} +1 -1
- package/dist/web/assets/image-preview-geometry-9lgeLYB3.js +1 -0
- package/dist/web/assets/index-BH99TIE0.css +1 -0
- package/dist/web/assets/index-DU68xk06.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-BOSpdwlz.js +1 -0
- package/dist/web/assets/{jinja-Cjw-evAn.js → jinja-BShBWfs5.js} +1 -1
- package/dist/web/assets/{jira-watcher-section-DIC1yIV3.js → jira-watcher-section-f2HUbGDY.js} +1 -1
- package/dist/web/assets/{jison-DfATMYoY.js → jison-dN2Nf_Dr.js} +1 -1
- package/dist/web/assets/{julia-DDCaxiqT.js → julia-5U0vR-uJ.js} +1 -1
- package/dist/web/assets/{just-CJL529u6.js → just-BMppVRy6.js} +1 -1
- package/dist/web/assets/keybindings-store-DCRsnKg5.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-BpvVEw_Y.js} +1 -1
- package/dist/web/assets/{latex-Zc4EpLpW.js → latex-DSSOKjis.js} +1 -1
- package/dist/web/assets/{liquid-B_6KMNYS.js → liquid-C24dKoyI.js} +1 -1
- package/dist/web/assets/{lua-BtS2idK7.js → lua-DNOsnyhn.js} +1 -1
- package/dist/web/assets/markdown-renderer-Bcaf9_bA.js +1 -0
- package/dist/web/assets/markdown-renderer-DGJp8HC3.js +3 -0
- package/dist/web/assets/{marko-Wk6LBnck.js → marko-26MHO_Q9.js} +1 -1
- package/dist/web/assets/{mdc-cRaXBrzL.js → mdc-Dhw9mGrC.js} +1 -1
- package/dist/web/assets/{media-url-DL8OCGGg.js → media-url-yfqHpPz0.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-CoUauvLF.js → mobile-bottom-sheet-C3TniEP-.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-Bol3XAO1.js +1 -0
- package/dist/web/assets/{nextflow-Bos2wvDV.js → nextflow-xH4oJTex.js} +1 -1
- package/dist/web/assets/{nginx-DMlOwzdr.js → nginx-CMz4w0M8.js} +1 -1
- package/dist/web/assets/{nim-D2dZQA88.js → nim-D8FnIYsx.js} +1 -1
- package/dist/web/assets/notification-store-BXt54VIn.js +1 -0
- package/dist/web/assets/{notifications-settings-section-DPUROMw-.js → notifications-settings-section-DqlPDfNX.js} +1 -1
- package/dist/web/assets/{number-overlay-editor-DZ2i_Bnb.js → number-overlay-editor-CRGwULhR.js} +1 -1
- package/dist/web/assets/{overview-panel-R146OIom.js → overview-panel-Di-3aV9p.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BGIE1QdU.js +1 -0
- package/dist/web/assets/panel-store-CboGJAwh.js +1 -0
- package/dist/web/assets/{pdf-preview-BWUPGLp6.js → pdf-preview-CmBI6E7d.js} +1 -1
- package/dist/web/assets/{perl-C5-Z6fPn.js → perl-D9iNWYwb.js} +1 -1
- package/dist/web/assets/{php-BXNNhkMH.js → php-Bw3Qo0Zv.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-Dg-QTbXX.js +1 -0
- package/dist/web/assets/{postgres-viewer-CjOvaruz.js → postgres-viewer-8ZAqEALv.js} +3 -3
- package/dist/web/assets/{ppmbot-settings-section-DrvWh8l8.js → ppmbot-settings-section-DKJUA3Kq.js} +1 -1
- package/dist/web/assets/problems-panel-CFcoUx8h.js +1 -0
- package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-EKr2dqG6.js} +1 -1
- package/dist/web/assets/project-framework-store-B70Q3dNM.js +1 -0
- package/dist/web/assets/project-store-B87hLRSk.js +1 -0
- package/dist/web/assets/{proxy-settings-section-Bv0Ny-Yy.js → proxy-settings-section-B7qRVl8w.js} +1 -1
- package/dist/web/assets/{pug-C1zf1yWy.js → pug-fT5JC6Vm.js} +1 -1
- package/dist/web/assets/{qml-OW58jGpD.js → qml-P_wEDB1w.js} +1 -1
- package/dist/web/assets/{query-audit-section-BcHJZcVC.js → query-audit-section-By6qyqdB.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DZ6IVu2s.js +1 -0
- package/dist/web/assets/{razor-DzjSaobM.js → razor-Drrt2_Ph.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-0WDYsyCA.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-COYDg16H.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-2DqSuS2U.js +1 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-Dm_qMJMH.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-UodhsF7W.js +1 -0
- package/dist/web/assets/{rst-RfuihOUW.js → rst-Dr2mW4-h.js} +1 -1
- package/dist/web/assets/{ruby-CNO3pli1.js → ruby-C6EpUsTY.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Cb_hVU8x.js → run-in-terminal-CdefEe_R.js} +1 -1
- package/dist/web/assets/{sas-DZY5BtXx.js → sas-D8ZEeP6v.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-DO8xAOVj.js → schedules-settings-section-bG2lYfGv.js} +1 -1
- package/dist/web/assets/{scroll-area-LtNV8g5o.js → scroll-area-BzeiOd-B.js} +1 -1
- package/dist/web/assets/{scss--4g-REcN.js → scss-ITL7jQ1f.js} +1 -1
- package/dist/web/assets/{send-to-chat-Cnpru1M0.js → send-to-chat-F207yH3u.js} +1 -1
- package/dist/web/assets/{separator-Cds1yxy8.js → separator-TRMsJUYe.js} +1 -1
- package/dist/web/assets/settings-body-C06bFarv.js +2 -0
- package/dist/web/assets/settings-store-krC6bc79.js +2 -0
- package/dist/web/assets/settings-tab-34RSaASd.js +1 -0
- package/dist/web/assets/settings-window-content-Dr_2lmHH.js +1 -0
- package/dist/web/assets/{shaderlab-Cu4sBiVn.js → shaderlab-mW-IIlhk.js} +1 -1
- package/dist/web/assets/{shellsession-DWzJfEv7.js → shellsession-DhNl5STt.js} +1 -1
- package/dist/web/assets/{soy-BnqE-5Ff.js → soy-SdY0Qjtr.js} +1 -1
- package/dist/web/assets/{sparql-Ctg__NhZ.js → sparql-ftqZo2i-.js} +1 -1
- package/dist/web/assets/{sql-query-editor-D9k19T_M.js → sql-query-editor-BgRH0cHD.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-DUxGslpa.js} +1 -1
- package/dist/web/assets/{stata-Qk2h66XW.js → stata-Bj7Yjb4b.js} +1 -1
- package/dist/web/assets/{surrealql-8QEX7EfH.js → surrealql-CLhYGAfB.js} +1 -1
- package/dist/web/assets/{svelte-D_mubC5-.js → svelte-DTAlRHhK.js} +1 -1
- package/dist/web/assets/system-monitor-body-IZuLn7Qh.js +2 -0
- package/dist/web/assets/system-monitor-tab-DiSA490Z.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-CEaFISgj.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-D0TN8Wrq.js} +1 -1
- package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
- package/dist/web/assets/team-member-sheet-DDPNcCjd.js +2 -0
- package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-DCiW_1-E.js} +1 -1
- package/dist/web/assets/{templ-C1jV0fsO.js → templ-BMfhACxb.js} +1 -1
- package/dist/web/assets/{terminal-tab-Cs0sGljx.js → terminal-tab-4ypP0NDY.js} +3 -3
- package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
- package/dist/web/assets/tool-cards-Dd6E7Q3k.js +9 -0
- package/dist/web/assets/treemap-KZPCXAKY-CE30wCXa.js +1 -0
- package/dist/web/assets/{ts-tags-B5m47rEm.js → ts-tags-CrR5IYI8.js} +1 -1
- package/dist/web/assets/{twig-C_br6GIl.js → twig-DchfUmyI.js} +1 -1
- package/dist/web/assets/{use-blob-url-C-SGi_Hb.js → use-blob-url-CPtCcY3i.js} +1 -1
- package/dist/web/assets/{use-inline-blame-B4trxGcR.js → use-inline-blame-vrfFHuaR.js} +1 -1
- package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CsuZu49X.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-BIVYJBIn.js} +1 -1
- package/dist/web/assets/{use-open-team-member-DENsjp0s.js → use-open-team-member-C3mrUzIC.js} +1 -1
- package/dist/web/assets/{use-websocket-LgXiX1qN.js → use-websocket-BMbufhX7.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-Cbig5zA6.js → vendor-mermaid-BitiXJlL.js} +1 -1
- package/dist/web/assets/{vendor-ui-BBdkypKW.js → vendor-ui-DmZ-Mb-H.js} +1 -1
- package/dist/web/assets/video-preview-Byj8P_jA.js +1 -0
- package/dist/web/assets/{vue-DQwCnLdr.js → vue-1EL6Pw1h.js} +1 -1
- package/dist/web/assets/{vue-html-T_EoGpPJ.js → vue-html-BPQNwPC6.js} +1 -1
- package/dist/web/assets/{vue-vine-DmxLqX50.js → vue-vine-Bb_dsWLt.js} +1 -1
- package/dist/web/assets/{window-pip-registry-3l4hoxuD.js → window-pip-registry-yD6esSju.js} +1 -1
- package/dist/web/assets/ws-auth-CEYVQ8NH.js +1 -0
- package/dist/web/assets/{xml-Bs6Bw8is.js → xml-dxkLNA0K.js} +1 -1
- package/dist/web/assets/{xsl-f5ZNhRCc.js → xsl-Bw2adwrl.js} +1 -1
- package/dist/web/index.html +27 -26
- package/dist/web/sw.js +1 -1
- package/docs/architecture/plugins-and-tracing.md +285 -0
- package/docs/lessons-learned.md +31 -0
- package/docs/system-architecture.md +1 -0
- package/package.json +2 -1
- package/packages/ext-git-graph/src/compare-diff-base.test.ts +173 -0
- package/packages/ext-git-graph/src/compare-view.ts +22 -3
- package/packages/ext-git-graph/src/extension.ts +6 -0
- package/packages/ext-git-graph/src/types.ts +19 -0
- package/packages/ext-git-graph/src/webview-html.test.ts +134 -1
- package/packages/ext-git-graph/src/webview-html.ts +277 -27
- package/scripts/gen-file-icons.ts +493 -242
- package/src/providers/claude-agent-sdk.ts +42 -1
- package/src/providers/codex-app-server/codex-history.ts +77 -23
- package/src/server/index.ts +42 -7
- package/src/server/routes/chat.ts +15 -11
- package/src/server/routes/git.ts +83 -3
- package/src/server/routes/remote-desktop.ts +7 -2
- package/src/server/ws/global.ts +1 -1
- package/src/server/ws/remote-desktop.ts +12 -5
- package/src/services/binary-content.ts +25 -0
- package/src/services/chat-search.service.ts +1 -1
- package/src/services/chat.service.ts +9 -0
- package/src/services/event-loop-lag.ts +208 -0
- package/src/services/file-lines.ts +114 -0
- package/src/services/file-watcher/recreated-dir-poller.ts +108 -6
- package/src/services/file-watcher/watch-tree.ts +131 -10
- package/src/services/file-watcher.service.ts +73 -23
- package/src/services/git-branch-diff/branch-diff-parse.ts +151 -0
- package/src/services/git-branch-diff/branch-diff.service.ts +79 -0
- package/src/services/git-hunks/git-hunks.service.ts +26 -11
- package/src/services/git-hunks/unified-diff.ts +15 -1
- package/src/services/git.service.ts +74 -24
- package/src/services/jsonl-transcript-parser.ts +138 -6
- package/src/services/log-rotate.ts +142 -0
- package/src/services/media-transcode/ffmpeg-capabilities.ts +72 -2
- package/src/services/remote-desktop/remote-desktop-audio.ts +193 -0
- package/src/services/remote-desktop/remote-desktop-capture-input.ts +87 -14
- package/src/services/remote-desktop/remote-desktop-capture.ts +40 -7
- package/src/services/remote-desktop/remote-desktop-clipboard.ts +166 -0
- package/src/services/remote-desktop/remote-desktop-displays.ts +77 -1
- package/src/services/remote-desktop/remote-desktop-encoder-args.ts +25 -6
- package/src/services/remote-desktop/remote-desktop-evdev-key-map.ts +69 -0
- package/src/services/remote-desktop/remote-desktop-input-linux.ts +253 -0
- package/src/services/remote-desktop/remote-desktop-input-uinput.ts +242 -0
- package/src/services/remote-desktop/remote-desktop-input.ts +40 -8
- package/src/services/remote-desktop/remote-desktop-linux-session.ts +146 -0
- package/src/services/remote-desktop/remote-desktop-ogg-opus.ts +144 -0
- package/src/services/remote-desktop/remote-desktop-privacy.ts +163 -0
- package/src/services/remote-desktop/remote-desktop-quality.ts +138 -0
- package/src/services/remote-desktop/remote-desktop-requirements.ts +158 -5
- package/src/services/remote-desktop/remote-desktop-resolution.ts +302 -0
- package/src/services/remote-desktop/remote-desktop-session.ts +523 -21
- package/src/services/remote-desktop/remote-desktop-x11.ts +250 -0
- package/src/services/supervisor.ts +47 -4
- package/src/shared/remote-desktop-custom-quality.ts +117 -0
- package/src/shared/remote-desktop-quality.ts +127 -0
- package/src/types/chat.ts +4 -0
- package/src/types/git.ts +52 -0
- package/src/web/components/branch-review/branch-review-tab.tsx +360 -0
- package/src/web/components/branch-review/branch-review-tree-row.tsx +138 -0
- package/src/web/components/chat/attachment-chips.tsx +30 -6
- package/src/web/components/chat/chat-tab.tsx +12 -1
- package/src/web/components/chat/message-events.tsx +273 -0
- package/src/web/components/chat/message-list.tsx +107 -861
- package/src/web/components/chat/message-markdown.tsx +34 -0
- package/src/web/components/chat/message-media.tsx +174 -0
- package/src/web/components/chat/message-user-bubble.tsx +399 -0
- package/src/web/components/chat/provider-selector.tsx +30 -22
- package/src/web/components/chat/session-history-list.tsx +2 -2
- package/src/web/components/chat/session-list-panel.tsx +3 -2
- package/src/web/components/editor/binary-diff-view.tsx +212 -0
- package/src/web/components/editor/diff-viewer.tsx +76 -3
- package/src/web/components/explorer/file-tree.tsx +235 -134
- package/src/web/components/explorer/flatten-visible-tree.ts +35 -40
- package/src/web/components/explorer/tree-node.tsx +69 -69
- package/src/web/components/explorer/use-tree-row-dnd.ts +3 -4
- package/src/web/components/git/git-status-panel.tsx +25 -0
- package/src/web/components/layout/draggable-tab.tsx +4 -2
- package/src/web/components/layout/nav-section-rail.tsx +12 -3
- package/src/web/components/layout/status-bar.tsx +10 -6
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/tab-pool.tsx +1 -0
- package/src/web/components/layout/upgrade-button.tsx +5 -1
- package/src/web/components/remote-desktop/remote-desktop-clipboard-client.ts +67 -0
- package/src/web/components/remote-desktop/remote-desktop-clipboard-notice.tsx +127 -0
- package/src/web/components/remote-desktop/remote-desktop-codec-labels.ts +28 -0
- package/src/web/components/remote-desktop/remote-desktop-custom-quality-dialog.tsx +144 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +13 -2
- package/src/web/components/remote-desktop/remote-desktop-mobile-settings.tsx +232 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +21 -8
- package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +82 -3
- package/src/web/components/remote-desktop/remote-desktop-recording.ts +76 -0
- package/src/web/components/remote-desktop/remote-desktop-resolution-list.ts +100 -0
- package/src/web/components/remote-desktop/remote-desktop-toolbar.tsx +497 -0
- package/src/web/components/remote-desktop/remote-desktop-view-style.ts +145 -0
- package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +126 -35
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +11 -0
- package/src/web/components/remote-desktop/use-opus-audio-player.ts +148 -0
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +284 -1
- package/src/web/components/remote-desktop/use-remote-desktop-readiness.ts +75 -1
- package/src/web/components/remote-desktop/use-remote-desktop-recorder.ts +103 -0
- package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +19 -2
- package/src/web/components/remote-desktop/use-remote-input-capture.ts +39 -3
- package/src/web/components/settings/theme-picker.tsx +2 -1
- package/src/web/components/ui/tooltip.tsx +15 -2
- package/src/web/hooks/use-delayed-flag.ts +35 -0
- package/src/web/lib/branch-review-state.ts +175 -0
- package/src/web/lib/file-icon-name.ts +62 -0
- package/src/web/lib/file-icons.generated.ts +3287 -25
- package/src/web/lib/file-icons.tsx +71 -73
- package/src/web/lib/flatten-expansions.ts +18 -5
- package/src/web/lib/message-ordinals.ts +23 -0
- package/src/web/lib/provider-logos.tsx +143 -0
- package/src/web/lib/tab-type-icons.ts +11 -0
- package/src/web/stores/file-store.ts +73 -35
- package/src/web/stores/panel-utils.ts +5 -0
- package/src/web/stores/project-framework-store.ts +82 -0
- package/src/web/stores/project-store.ts +7 -1
- package/src/web/stores/settings-store.ts +128 -0
- package/src/web/stores/tab-store.ts +1 -0
- package/src/web/stores/window-panel-persistence.ts +1 -0
- package/src/web/styles/file-icons.generated.css +3282 -234
- package/src/web/styles/globals.css +26 -0
- package/dist/web/assets/accounts-settings-section-BuxD9zH3.js +0 -1
- package/dist/web/assets/ai-settings-section-Dd9tOUp0.js +0 -1
- package/dist/web/assets/ai-settings-section-FrU2X8Fu.js +0 -1
- package/dist/web/assets/api-client-BUCo1lms.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-5K0wEkAn.js +0 -1
- package/dist/web/assets/chat-tab-Cr6_KSGO.js +0 -15
- package/dist/web/assets/code-editor-DfmlGamv.js +0 -7
- package/dist/web/assets/context-menu-DOODKZdF.js +0 -1
- package/dist/web/assets/diff-viewer-BpNj7pN9.js +0 -4
- package/dist/web/assets/explorer-body-D-5z464e.js +0 -2
- package/dist/web/assets/explorer-window-content-7-1wuRul.js +0 -1
- package/dist/web/assets/file-icons-EzPL0q-k.js +0 -1
- package/dist/web/assets/file-icons.generated-CfaJuvZu.css +0 -1
- package/dist/web/assets/file-store-DMABxOkV.js +0 -1
- package/dist/web/assets/files-settings-section-CZzABuKz.js +0 -1
- package/dist/web/assets/git-log-panel-CRN4JVOx.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DkVT2MAM.js +0 -1
- package/dist/web/assets/group-chat-tab-DFa5mAx5.js +0 -1
- package/dist/web/assets/index-D7DbXD3G.css +0 -1
- package/dist/web/assets/index-ZtfxZOyb.js +0 -52
- package/dist/web/assets/info-3K5VOQVL-BBsmKGzA.js +0 -1
- package/dist/web/assets/keybindings-store-tVfhPANS.js +0 -1
- package/dist/web/assets/markdown-renderer-CvL1pWeK.js +0 -3
- package/dist/web/assets/markdown-renderer-sJNNVhlS.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-CZ0HZ2E_.js +0 -1
- package/dist/web/assets/notification-store-BZbsPnW0.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-DwmYPOlx.js +0 -1
- package/dist/web/assets/panel-store-BVLBRn6M.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D0oogIFi.js +0 -1
- package/dist/web/assets/problems-panel-B7BWVhdy.js +0 -1
- package/dist/web/assets/project-store-BhMbVln3.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-CQIJOQGa.js +0 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-ByjaeZKX.js +0 -2
- package/dist/web/assets/remote-desktop-mobile-view-D13mtr9F.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-CbqTCaC6.js +0 -1
- package/dist/web/assets/settings-body-Rnu-FcX-.js +0 -2
- package/dist/web/assets/settings-store-DZPgPtEA.js +0 -2
- package/dist/web/assets/settings-tab-C6PHxs0Z.js +0 -1
- package/dist/web/assets/settings-window-content-CQ4gT2qj.js +0 -1
- package/dist/web/assets/system-monitor-body-2vQ1kMxE.js +0 -2
- package/dist/web/assets/system-monitor-tab-krAcvwF7.js +0 -1
- package/dist/web/assets/team-member-sheet-2FnRmk1a.js +0 -2
- package/dist/web/assets/tool-cards-DU_S2o51.js +0 -9
- package/dist/web/assets/treemap-KZPCXAKY-BhEtjljr.js +0 -1
- package/dist/web/assets/use-remote-desktop-display-choice-BK_f787P.js +0 -1
- package/dist/web/assets/video-preview-BDNwumbW.js +0 -1
- package/dist/web/assets/ws-auth-DEAJiGCR.js +0 -1
- /package/dist/web/assets/{abap-tkptwpsK.js → abap-IN2yINdN.js} +0 -0
- /package/dist/web/assets/{actionscript-3-Di9TMGwg.js → actionscript-3-C_t772eR.js} +0 -0
- /package/dist/web/assets/{ada-CB9F079D.js → ada-DPme3emm.js} +0 -0
- /package/dist/web/assets/{andromeeda-Dcl8jpb7.js → andromeeda-DSV4i4mZ.js} +0 -0
- /package/dist/web/assets/{apache-D0Nk0qsH.js → apache-CWr8s8I8.js} +0 -0
- /package/dist/web/assets/{apex-mQIoWncD.js → apex-Z20JnDF5.js} +0 -0
- /package/dist/web/assets/{applescript-C6ljbcsI.js → applescript-BKNPu3ga.js} +0 -0
- /package/dist/web/assets/{apply-theme-BwIMNXyX.js → apply-theme-CbR31K6I.js} +0 -0
- /package/dist/web/assets/{ara-WFqRPGPe.js → ara-hWdY3O96.js} +0 -0
- /package/dist/web/assets/{asciidoc-bNd8w4nf.js → asciidoc-eynhq496.js} +0 -0
- /package/dist/web/assets/{asm-BQ1_F580.js → asm-Bu02Kq4P.js} +0 -0
- /package/dist/web/assets/{aurora-x-Ciqs_Dbu.js → aurora-x-BHBGXbIZ.js} +0 -0
- /package/dist/web/assets/{awk-GCR-6KZV.js → awk-B4ojezyv.js} +0 -0
- /package/dist/web/assets/{ayu-dark-DxJXuj99.js → ayu-dark-D0GQzFi9.js} +0 -0
- /package/dist/web/assets/{ayu-light-jvSyJWDV.js → ayu-light-BVYb5Yn8.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-Cg3BPO6H.js → ayu-mirage-BGFW6oNb.js} +0 -0
- /package/dist/web/assets/{ballerina-BPz4CrX6.js → ballerina-B6Og3IKT.js} +0 -0
- /package/dist/web/assets/{bat-CSTs2hIG.js → bat-D8YFILfx.js} +0 -0
- /package/dist/web/assets/{beancount-DMateAZX.js → beancount-BjlbBBuX.js} +0 -0
- /package/dist/web/assets/{berry-9lXg0GRM.js → berry-Cz1ygeoj.js} +0 -0
- /package/dist/web/assets/{bibtex-DIbqmvFg.js → bibtex-CVkJceJh.js} +0 -0
- /package/dist/web/assets/{bicep-02WGsoJ8.js → bicep-DrhYI5GX.js} +0 -0
- /package/dist/web/assets/{bird2-BY0tbu3k.js → bird2-BV-KJtmc.js} +0 -0
- /package/dist/web/assets/{c-jMBX_9Je.js → c-CuLlvxbG.js} +0 -0
- /package/dist/web/assets/{c3-B8JVbNjW.js → c3-CI7-1CJ0.js} +0 -0
- /package/dist/web/assets/{cadence-DUIf1tvL.js → cadence-DjxhNP3Q.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-DGEc0s4P.js → can-open-in-ppm-cv5--yHX.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-FhUCVxex.js → catppuccin-frappe-CEozpJRh.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-CzfrtmgL.js → catppuccin-latte-C4sXQFMc.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-6xL0lOtj.js → catppuccin-macchiato-Ci-fDhXy.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-D39-xS3P.js → catppuccin-mocha-CH7YXw8d.js} +0 -0
- /package/dist/web/assets/{clarity-D8TeGvoT.js → clarity-BWrJ7UmT.js} +0 -0
- /package/dist/web/assets/{clojure-Dx_acSX7.js → clojure-C94fh2hb.js} +0 -0
- /package/dist/web/assets/{cmake-CyeYPq8o.js → cmake-CtvCkMsG.js} +0 -0
- /package/dist/web/assets/{codeowners-DrTI0lmI.js → codeowners-CJnubqzH.js} +0 -0
- /package/dist/web/assets/{codeql-CPkFxVW-.js → codeql-BOoP5HY2.js} +0 -0
- /package/dist/web/assets/{common-lisp-B0k-3LTj.js → common-lisp-B9nlMFJd.js} +0 -0
- /package/dist/web/assets/{coq-uygpo7to.js → coq-CgzIQiSH.js} +0 -0
- /package/dist/web/assets/{csharp-DKUYGfSp.js → csharp-4xAIx3jw.js} +0 -0
- /package/dist/web/assets/{css-DoOUp7v4.js → css-D4YoLYGn.js} +0 -0
- /package/dist/web/assets/{csv-B3YE13gi.js → csv-C2v-ZakW.js} +0 -0
- /package/dist/web/assets/{csv-parser-DXePqmXe.js → csv-parser-_oDBUkwQ.js} +0 -0
- /package/dist/web/assets/{cue-DViO2wvn.js → cue-Bbznh_an.js} +0 -0
- /package/dist/web/assets/{cypher-4BMCIW5N.js → cypher-BKhVEARF.js} +0 -0
- /package/dist/web/assets/{d-BT_sTHas.js → d-D6IhWopl.js} +0 -0
- /package/dist/web/assets/{dark-plus-DVVXu-1N.js → dark-plus-xAWu790O.js} +0 -0
- /package/dist/web/assets/{dart-DHz6f6_6.js → dart-Bj6S9O1j.js} +0 -0
- /package/dist/web/assets/{data-grid-types-OgUxMLdQ.js → data-grid-types-C5bthITf.js} +0 -0
- /package/dist/web/assets/{dax-ChYCjweK.js → dax-CF2zvaqq.js} +0 -0
- /package/dist/web/assets/{desktop-D6Lt4p04.js → desktop-CnmyxMKE.js} +0 -0
- /package/dist/web/assets/{diff-D8TDe4hP.js → diff-Qi8WuJsI.js} +0 -0
- /package/dist/web/assets/{dist-DOTlFQJA.js → dist-Bjseolz4.js} +0 -0
- /package/dist/web/assets/{dist-CrBOCtCW.js → dist-Czq7BUMy.js} +0 -0
- /package/dist/web/assets/{docker-Bsp0YbPx.js → docker-vLbbNP5d.js} +0 -0
- /package/dist/web/assets/{dotenv-B_7giSY_.js → dotenv-DJMvvNzn.js} +0 -0
- /package/dist/web/assets/{dracula-TrQL2iB5.js → dracula-CtAh2rhB.js} +0 -0
- /package/dist/web/assets/{dracula-soft-n2XOlgh-.js → dracula-soft-CXtuAAf6.js} +0 -0
- /package/dist/web/assets/{dream-maker-CdIEdhpT.js → dream-maker-D5YQyK49.js} +0 -0
- /package/dist/web/assets/{editor-font-BDrdiGf1.js → editor-font-ClTC9gaD.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CSbixs6u.js → emacs-lisp-CGTjbiqF.js} +0 -0
- /package/dist/web/assets/{everforest-dark-BuZahZYp.js → everforest-dark-D59p7vAQ.js} +0 -0
- /package/dist/web/assets/{everforest-light-DPH53z5H.js → everforest-light-B3Qtud3V.js} +0 -0
- /package/dist/web/assets/{fennel-B1-K_hlJ.js → fennel-BCyHNUwf.js} +0 -0
- /package/dist/web/assets/{fish-Cy12Dbua.js → fish-DDDlLV2G.js} +0 -0
- /package/dist/web/assets/{fluent-B_82ys8E.js → fluent-By54R9tl.js} +0 -0
- /package/dist/web/assets/{format-bytes-D5sBgIqU.js → format-bytes-D1AQyUCt.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BRQdNzZF.js → fortran-free-form-tP8mRym_.js} +0 -0
- /package/dist/web/assets/{gdscript-XBDmLUm9.js → gdscript-nHC2z6Te.js} +0 -0
- /package/dist/web/assets/{gdshader-DhVfB3u4.js → gdshader-K-cSmPlW.js} +0 -0
- /package/dist/web/assets/{genie-X3K29Y49.js → genie-BAcRhvEe.js} +0 -0
- /package/dist/web/assets/{gherkin-DKIx-6cX.js → gherkin-CcifxMLb.js} +0 -0
- /package/dist/web/assets/{github-dark-Bf7zdp5s.js → github-dark-67KKlMdB.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DqsBBV11.js → github-dark-default-tja8pidR.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-X2gqRz6w.js → github-dark-dimmed-CtjhYQtx.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-Z7tvbGX4.js → github-dark-high-contrast-Bf-LPFgn.js} +0 -0
- /package/dist/web/assets/{github-light-BKxt-7zZ.js → github-light-CbJxE7n3.js} +0 -0
- /package/dist/web/assets/{github-light-default-B0liF4d4.js → github-light-default-DRQmdHxt.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-F4encJwr.js → github-light-high-contrast-CJkDgTeM.js} +0 -0
- /package/dist/web/assets/{gleam-Pz2bHgBB.js → gleam-D4LCl2ri.js} +0 -0
- /package/dist/web/assets/{gn-CgmdNsPU.js → gn-COY8zZmC.js} +0 -0
- /package/dist/web/assets/{gnuplot-qLNF5yZ9.js → gnuplot-ROk1VitR.js} +0 -0
- /package/dist/web/assets/{go-CSDUNhE7.js → go-H30c3p0C.js} +0 -0
- /package/dist/web/assets/{groovy-B3NEnOHF.js → groovy-DBLCaelV.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-DcoftiUX.js → gruvbox-dark-hard-DJy4irps.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-D1wVSuD8.js → gruvbox-dark-medium-Dhfw1QTO.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-DWpunymH.js → gruvbox-dark-soft-BncJU-8w.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-rHMvyUdS.js → gruvbox-light-hard-Cq12_YvM.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-3O2KiXO7.js → gruvbox-light-medium-DqVja5_v.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-lNlXJPwH.js → gruvbox-light-soft-IvwhveGF.js} +0 -0
- /package/dist/web/assets/{haskell-aaG3aC0h.js → haskell-DA1JV-MC.js} +0 -0
- /package/dist/web/assets/{haxe-CPcHhUDd.js → haxe-V9RTRc5E.js} +0 -0
- /package/dist/web/assets/{hcl-CMQNlR_4.js → hcl-hr1lyZ10.js} +0 -0
- /package/dist/web/assets/{hjson-DeuklPFj.js → hjson-D3vW3i7X.js} +0 -0
- /package/dist/web/assets/{hlsl-BI3hlsaM.js → hlsl-BB85gfJT.js} +0 -0
- /package/dist/web/assets/{horizon-C7-gOcjs.js → horizon-DU8OOnj8.js} +0 -0
- /package/dist/web/assets/{horizon-bright-Cxt9bRW7.js → horizon-bright-DNMUclW6.js} +0 -0
- /package/dist/web/assets/{houston-DbF2G7iD.js → houston-DAiZKf6T.js} +0 -0
- /package/dist/web/assets/{hy-DRG6Hs2I.js → hy-C6Zv0LMe.js} +0 -0
- /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-CGvlv8zR.js} +0 -0
- /package/dist/web/assets/{imba-CxKv_2TW.js → imba-Didnp6Y-.js} +0 -0
- /package/dist/web/assets/{ini-BiHsyCdO.js → ini-m0T7q4gQ.js} +0 -0
- /package/dist/web/assets/{input-D-dR8RI6.js → input-C7Q8FxeL.js} +0 -0
- /package/dist/web/assets/{java-D3RZzgDB.js → java-Buy07jGn.js} +0 -0
- /package/dist/web/assets/{javascript-4FmSqG-T.js → javascript-Cjq3CbRL.js} +0 -0
- /package/dist/web/assets/{json-CjB236_E.js → json-BSVu8IqD.js} +0 -0
- /package/dist/web/assets/{json5-bkfO-Rco.js → json5-NN_Vb7e6.js} +0 -0
- /package/dist/web/assets/{jsonc-BwaTeLyv.js → jsonc-BdyZhBUw.js} +0 -0
- /package/dist/web/assets/{jsonl-CfxNSwGd.js → jsonl-BsDSNvwi.js} +0 -0
- /package/dist/web/assets/{jsonnet-CQETCYrp.js → jsonnet-Gm963F-U.js} +0 -0
- /package/dist/web/assets/{jssm-D5o3Zzdz.js → jssm-Ds3bKWgJ.js} +0 -0
- /package/dist/web/assets/{jsx-BAMILPLo.js → jsx-BT5mW58X.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-BhGVxu3R.js → kanagawa-dragon-BvNvf7xd.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-FH4Y59ro.js → kanagawa-lotus-CuSiliSn.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-eilvP9MB.js → kanagawa-wave-C6g-d6jT.js} +0 -0
- /package/dist/web/assets/{kdl-BQ-yvXUK.js → kdl-Bx4kZef-.js} +0 -0
- /package/dist/web/assets/{kotlin-BuCx0-VT.js → kotlin-qvaVvCcm.js} +0 -0
- /package/dist/web/assets/{kusto-DAN_n_Cy.js → kusto-BPLyNCN6.js} +0 -0
- /package/dist/web/assets/{laserwave-Cq2BlxrT.js → laserwave-Sf8ZEhWP.js} +0 -0
- /package/dist/web/assets/{lean-Dhq7uXKw.js → lean-DGe6uqgk.js} +0 -0
- /package/dist/web/assets/{less-CxaMQb8j.js → less-CqM85x-9.js} +0 -0
- /package/dist/web/assets/{lib-BVwmtv2i.js → lib-CD6pOS4u.js} +0 -0
- /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-CPh1Av0r.js} +0 -0
- /package/dist/web/assets/{light-plus-B4sMAend.js → light-plus-DBtKwhFA.js} +0 -0
- /package/dist/web/assets/{llvm-DQ6-B_6Y.js → llvm-Dpbq3gky.js} +0 -0
- /package/dist/web/assets/{log-Bb_vqvu7.js → log-qKMcPm9S.js} +0 -0
- /package/dist/web/assets/{logo-BzfYv3A_.js → logo-DFo7i5KH.js} +0 -0
- /package/dist/web/assets/{luau-BnAeNR77.js → luau-DGlqWFyL.js} +0 -0
- /package/dist/web/assets/{make-B6IFuxpC.js → make-CixdvUQe.js} +0 -0
- /package/dist/web/assets/{markdown-Z0xi77Fn.js → markdown-CmRnbngE.js} +0 -0
- /package/dist/web/assets/{material-theme-1scy1SYn.js → material-theme-WDUbafB-.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-G-JykXQP.js → material-theme-darker-u8y2xpOk.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-BtKkCMVq.js → material-theme-lighter-cyQZK3Tg.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-irrDC89L.js → material-theme-ocean-LBAVvw7k.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-Eq5_K4LM.js → material-theme-palenight-BJqhBxL0.js} +0 -0
- /package/dist/web/assets/{matlab-BE3oCXZC.js → matlab-B38sOJee.js} +0 -0
- /package/dist/web/assets/{mdx-DYiuFTGw.js → mdx-C8QrE_IW.js} +0 -0
- /package/dist/web/assets/{mermaid-DVlaEvtw.js → mermaid-BQ-eeU8A.js} +0 -0
- /package/dist/web/assets/{min-dark-DSkeTpQa.js → min-dark-D4gNa5tv.js} +0 -0
- /package/dist/web/assets/{min-light-CFv2Q0uF.js → min-light-Dfbhn_PL.js} +0 -0
- /package/dist/web/assets/{mipsasm-CiAFuZQ2.js → mipsasm-bLOhJkdm.js} +0 -0
- /package/dist/web/assets/{mojo-ARshcaSQ.js → mojo-B0BCOMQW.js} +0 -0
- /package/dist/web/assets/{monaco-dotenv-language-BDYnKcnr.js → monaco-dotenv-language-uB-wok6C.js} +0 -0
- /package/dist/web/assets/{monokai-CPTV-SOg.js → monokai-Bdw_07OM.js} +0 -0
- /package/dist/web/assets/{moonbit-D57B0dZr.js → moonbit-BFdGaeuk.js} +0 -0
- /package/dist/web/assets/{move-qgy3G_o9.js → move-CyXQFGPM.js} +0 -0
- /package/dist/web/assets/{narrat-CZ3rC-oc.js → narrat-9DvTD3_3.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-BxxIOcI4.js → nextflow-groovy-C4VY3kNY.js} +0 -0
- /package/dist/web/assets/{night-owl-B1ryGCda.js → night-owl-feYm66V0.js} +0 -0
- /package/dist/web/assets/{night-owl-light-BZvNnvMF.js → night-owl-light-DALqFjvx.js} +0 -0
- /package/dist/web/assets/{nix-BVBQi8yv.js → nix-C1kPA9Sw.js} +0 -0
- /package/dist/web/assets/{nord-3NXw613h.js → nord-1dvrj1Al.js} +0 -0
- /package/dist/web/assets/{nushell-BMOLRalb.js → nushell-DSQ_9Uqj.js} +0 -0
- /package/dist/web/assets/{objective-c-AxruEUgh.js → objective-c-Dhcn5ZK8.js} +0 -0
- /package/dist/web/assets/{objective-cpp-cFT0qRsD.js → objective-cpp-CGVYCSrp.js} +0 -0
- /package/dist/web/assets/{ocaml-_8JH6O8J.js → ocaml-rxXW21An.js} +0 -0
- /package/dist/web/assets/{odin-nwYvR0O6.js → odin-ClSJX2DE.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-C1LaYJaH.js → one-dark-pro-3gbWDfFW.js} +0 -0
- /package/dist/web/assets/{one-light-B7rMc__c.js → one-light-C2q5ZZAw.js} +0 -0
- /package/dist/web/assets/{openscad-Cwes8bKO.js → openscad-eHXeafBl.js} +0 -0
- /package/dist/web/assets/{pascal-DMt-6tdZ.js → pascal-kKha3Txs.js} +0 -0
- /package/dist/web/assets/{pkl-QBTyYQmB.js → pkl-CTiPytg-.js} +0 -0
- /package/dist/web/assets/{plastic-BTSUp8Fk.js → plastic-D1Ygezi4.js} +0 -0
- /package/dist/web/assets/{plsql-DQv3IFXP.js → plsql-D_gbgZIr.js} +0 -0
- /package/dist/web/assets/{po-CNwZx4tW.js → po-BkhbDUnq.js} +0 -0
- /package/dist/web/assets/{poimandres-8Os7Qequ.js → poimandres-Do6NywrA.js} +0 -0
- /package/dist/web/assets/{polar-b4yK8oD8.js → polar-B3B23wca.js} +0 -0
- /package/dist/web/assets/{postcss-sFctwpxx.js → postcss-Dv_YBvWd.js} +0 -0
- /package/dist/web/assets/{powerquery-Cg_WrtuN.js → powerquery-fKJWI-Sh.js} +0 -0
- /package/dist/web/assets/{powershell-C7Ifz95h.js → powershell-B4Y7KIgx.js} +0 -0
- /package/dist/web/assets/{prisma-Z52HUJzQ.js → prisma-J--XdTXX.js} +0 -0
- /package/dist/web/assets/{prolog-DLqW-zR1.js → prolog-BBTg4Vb3.js} +0 -0
- /package/dist/web/assets/{proto-BOIzxCCJ.js → proto-DWlum3T8.js} +0 -0
- /package/dist/web/assets/{puppet-De7TjZIf.js → puppet-ZiUfc9JV.js} +0 -0
- /package/dist/web/assets/{purescript-R76ou1WT.js → purescript-CmT8SCHB.js} +0 -0
- /package/dist/web/assets/{python-Dprm0tNP.js → python-BURpj-fq.js} +0 -0
- /package/dist/web/assets/{qmldir-Clp_jpNw.js → qmldir-CpaiJywQ.js} +0 -0
- /package/dist/web/assets/{qss-eQPYYzSo.js → qss-C-zdz0i5.js} +0 -0
- /package/dist/web/assets/{r-CCcRHIf5.js → r-K56BeS-s.js} +0 -0
- /package/dist/web/assets/{racket-CzE472yp.js → racket-gkn6Z_Jw.js} +0 -0
- /package/dist/web/assets/{raku-DQwXrTSi.js → raku-C0ZBuKlx.js} +0 -0
- /package/dist/web/assets/{red-KCS6wyop.js → red-BWZ6x-3p.js} +0 -0
- /package/dist/web/assets/{reg-CcrAddUh.js → reg-Dt_EsyGR.js} +0 -0
- /package/dist/web/assets/{regexp-DDbud-Ri.js → regexp-CT6BLG2C.js} +0 -0
- /package/dist/web/assets/{rel-99GDgaHp.js → rel-MJ2LcyzJ.js} +0 -0
- /package/dist/web/assets/{resolve-theme-CW2niGV2.js → resolve-theme-B_ukrg_8.js} +0 -0
- /package/dist/web/assets/{riscv-Dm2eNv0R.js → riscv-BZYWzMAA.js} +0 -0
- /package/dist/web/assets/{ron-CzFt0ygH.js → ron-CLbwHWMn.js} +0 -0
- /package/dist/web/assets/{rose-pine-Ck6lCrCP.js → rose-pine-BPiI_daR.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-vtALRJNj.js → rose-pine-dawn-BjkJc7dW.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-DPto8qXw.js → rose-pine-moon-BQSxkt7g.js} +0 -0
- /package/dist/web/assets/{rosmsg-NtS6429H.js → rosmsg-yt4151pJ.js} +0 -0
- /package/dist/web/assets/{rust-BjIXvF9h.js → rust-1dAcengL.js} +0 -0
- /package/dist/web/assets/{sass-CQ23wf5E.js → sass-DUgtQJsg.js} +0 -0
- /package/dist/web/assets/{scala-LfKLM143.js → scala-CxoGYpS0.js} +0 -0
- /package/dist/web/assets/{scheme-D3SLBXlF.js → scheme-B2K3u9e3.js} +0 -0
- /package/dist/web/assets/{sdbl-ODRd2rGN.js → sdbl-C0Ygk0n6.js} +0 -0
- /package/dist/web/assets/{shellscript-3KB1G_oA.js → shellscript-C2CvhqMk.js} +0 -0
- /package/dist/web/assets/{slack-dark-DTtyWmIy.js → slack-dark-DYdgzRGj.js} +0 -0
- /package/dist/web/assets/{slack-ochin-Bu0PApk3.js → slack-ochin-X-WD0WCg.js} +0 -0
- /package/dist/web/assets/{smalltalk-haMT-Sbe.js → smalltalk-Dp7g_zb9.js} +0 -0
- /package/dist/web/assets/{snazzy-light-DXJzefE9.js → snazzy-light-DWHWr-a6.js} +0 -0
- /package/dist/web/assets/{solarized-dark-B6LyYa18.js → solarized-dark-CxdLsjvb.js} +0 -0
- /package/dist/web/assets/{solarized-light-QLADebkv.js → solarized-light-Cz9rAvG5.js} +0 -0
- /package/dist/web/assets/{solidity-mzfq1aDc.js → solidity-D33gHHdn.js} +0 -0
- /package/dist/web/assets/{splunk-CyH4RWBp.js → splunk-BFbOroYw.js} +0 -0
- /package/dist/web/assets/{sql-CWVlT5D3.js → sql-ChpxvmdY.js} +0 -0
- /package/dist/web/assets/{ssh-config-DNvQJ1Qx.js → ssh-config-CUuSQ6JG.js} +0 -0
- /package/dist/web/assets/{streaming-store-uDu3PoN1.js → streaming-store-BYukdB56.js} +0 -0
- /package/dist/web/assets/{stylus-ByQQdtmf.js → stylus-DDihP0_W.js} +0 -0
- /package/dist/web/assets/{swift-D1cpY4ow.js → swift-XoalUn4x.js} +0 -0
- /package/dist/web/assets/{synthwave-84-BDF_-QiW.js → synthwave-84-DjkG0Bff.js} +0 -0
- /package/dist/web/assets/{system-verilog-BYrkMydC.js → system-verilog-f_pLKMYd.js} +0 -0
- /package/dist/web/assets/{systemd-CQm3zyK0.js → systemd-DjDcLfxp.js} +0 -0
- /package/dist/web/assets/{talonscript-DMW5CzE1.js → talonscript-BpUzYcDl.js} +0 -0
- /package/dist/web/assets/{tasl-BhFzMCr2.js → tasl-DP-cUbg8.js} +0 -0
- /package/dist/web/assets/{tcl-D7VEAF-9.js → tcl-BHoy-dfE.js} +0 -0
- /package/dist/web/assets/{terraform-DdT5A0Vy.js → terraform-vCVxN25B.js} +0 -0
- /package/dist/web/assets/{tokyo-night-WGLMM6Q9.js → tokyo-night-D0u4xZba.js} +0 -0
- /package/dist/web/assets/{toml-CZ7ipXHZ.js → toml-BS8D0__A.js} +0 -0
- /package/dist/web/assets/{tsv-B1zVvoTr.js → tsv-DCb-NQh3.js} +0 -0
- /package/dist/web/assets/{tsx-D4HwRUBq.js → tsx-DF8YeRgG.js} +0 -0
- /package/dist/web/assets/{turtle-B4r8KEJH.js → turtle-Bhx1GlIy.js} +0 -0
- /package/dist/web/assets/{typescript-BnHhxMtp.js → typescript-CAVD2Wb6.js} +0 -0
- /package/dist/web/assets/{typespec-DP2q46GT.js → typespec-D7mchdY_.js} +0 -0
- /package/dist/web/assets/{typst-DFsD_yaD.js → typst-CeXzq6Yw.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-BQ9Bv5Ga.js → use-is-mobile-TkcwP1Gx.js} +0 -0
- /package/dist/web/assets/{use-visual-viewport-CbwnktSG.js → use-visual-viewport-Lp878uzs.js} +0 -0
- /package/dist/web/assets/{v-Bji8hSVT.js → v-DhzO54je.js} +0 -0
- /package/dist/web/assets/{vala-D3TIgGmU.js → vala-Dw_RAk3E.js} +0 -0
- /package/dist/web/assets/{vb-DvcxFS2q.js → vb-CiXdNh2Y.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-BqPDsE9b.js → vendor-xterm-BHEY6Ctu.js} +0 -0
- /package/dist/web/assets/{verilog-Dy7cF35d.js → verilog-fITdxqwV.js} +0 -0
- /package/dist/web/assets/{vesper-BzjwUv9J.js → vesper-B1wm_At7.js} +0 -0
- /package/dist/web/assets/{vhdl-DuFnFNmW.js → vhdl-ebHjqKp9.js} +0 -0
- /package/dist/web/assets/{viml-yAPgKlbz.js → viml-Dgmnoxoo.js} +0 -0
- /package/dist/web/assets/{vitesse-black-oSSQdyIZ.js → vitesse-black-BZ5gMb7l.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BZxwPihB.js → vitesse-dark-Lij7kkOt.js} +0 -0
- /package/dist/web/assets/{vitesse-light-uIigycvt.js → vitesse-light-DW6Wq7VC.js} +0 -0
- /package/dist/web/assets/{vyper-CX3NAxm5.js → vyper-6FxDnVNg.js} +0 -0
- /package/dist/web/assets/{wasm-6mEOOvVa.js → wasm-BDxaNPCj.js} +0 -0
- /package/dist/web/assets/{wasm-D9td37qO.js → wasm-DKhAtzfe.js} +0 -0
- /package/dist/web/assets/{wenyan-lwNIHgk6.js → wenyan-C0k0DYG_.js} +0 -0
- /package/dist/web/assets/{wgsl-DQAzCfog.js → wgsl-Y2zagbdn.js} +0 -0
- /package/dist/web/assets/{wikitext-DTXmt5NE.js → wikitext-C4QGWzkY.js} +0 -0
- /package/dist/web/assets/{wit-Bdw_7WDH.js → wit-CQESzjHy.js} +0 -0
- /package/dist/web/assets/{wolfram-DqyHDZ07.js → wolfram-B-ia69Ia.js} +0 -0
- /package/dist/web/assets/{yaml-CPXhdByd.js → yaml-CEYYTQPm.js} +0 -0
- /package/dist/web/assets/{zenscript-6s_i5zek.js → zenscript-BggWXIeJ.js} +0 -0
- /package/dist/web/assets/{zig-B2htezJ_.js → zig-CbaUfBQe.js} +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X11 input backend: `bun:ffi` into libXtst's XTEST extension — no helper process, matching
|
|
3
|
+
* `remote-desktop-input-win32.ts` (user32/SendInput) and `-darwin.ts` (CoreGraphics).
|
|
4
|
+
*
|
|
5
|
+
* Selected by `remote-desktop-input.ts` on `linux`, and it answers only for an X11 session:
|
|
6
|
+
* XTEST is an X server extension, so a Wayland compositor has nothing to inject into and this
|
|
7
|
+
* backend reports unavailable there (`remote-desktop-input-uinput.ts` covers that case).
|
|
8
|
+
*
|
|
9
|
+
* Keys go through `remote-desktop-evdev-key-map.ts` + `X11_KEYCODE_OFFSET`, never through
|
|
10
|
+
* `XKeysymToKeycode` — see that file for why a keysym lookup transposes a non-US keyboard.
|
|
11
|
+
*
|
|
12
|
+
* Text is harder than on the other two platforms: X11 has no "type this Unicode string" call
|
|
13
|
+
* (`KEYEVENTF_UNICODE` / `CGEventKeyboardSetUnicodeString` have no counterpart). So `text()`
|
|
14
|
+
* has two paths:
|
|
15
|
+
* - **Fast path** — the character already exists in the active keymap, which covers all ASCII
|
|
16
|
+
* on any ordinary layout. Press its real keycode, with Shift when it sits in the shifted
|
|
17
|
+
* slot. No mapping change, no round trip per character.
|
|
18
|
+
* - **Slow path** — anything else (`ệ`, `日`, an emoji). A scratch keycode with no keysyms of
|
|
19
|
+
* its own is temporarily bound to the character's Unicode keysym, pressed, and unbound
|
|
20
|
+
* again. This needs a real delay after the remap, not just `XSync`: the server applies the
|
|
21
|
+
* change immediately but the focused *client* only learns about it when it processes the
|
|
22
|
+
* `MappingNotify`, and a key pressed before then is interpreted with the old mapping.
|
|
23
|
+
*
|
|
24
|
+
* Caveat shared with macOS/Windows: injected keys enter the host's input stack *above* its
|
|
25
|
+
* IME, so a Telex/ibus input method running on the host will still rewrite what arrives
|
|
26
|
+
* (typing "World" through a Telex IME lands as "ửold"). That is the host's IME doing its job,
|
|
27
|
+
* not a bug here.
|
|
28
|
+
*/
|
|
29
|
+
import { asPointer, getX11, type X11Connection } from "./remote-desktop-x11.ts";
|
|
30
|
+
import { detectLinuxSession, type LinuxSession } from "./remote-desktop-linux-session.ts";
|
|
31
|
+
import { codeToEvdev, MODIFIER_EVDEV_CODES, X11_KEYCODE_OFFSET } from "./remote-desktop-evdev-key-map.ts";
|
|
32
|
+
import { RemoteInputUnavailableError, type InputTargetRect, type RemoteInputBackend } from "./remote-desktop-input-backend.ts";
|
|
33
|
+
|
|
34
|
+
/** X core protocol button numbers. 4/5 are the wheel's two directions — X11 has no scroll
|
|
35
|
+
* axis, a notch *is* a button click. */
|
|
36
|
+
const BUTTON_LEFT = 1;
|
|
37
|
+
const BUTTON_RIGHT = 3;
|
|
38
|
+
const BUTTON_WHEEL_UP = 4;
|
|
39
|
+
const BUTTON_WHEEL_DOWN = 5;
|
|
40
|
+
/** One notch, matching `WHEEL_DELTA` on Windows — the client sends in these units. */
|
|
41
|
+
const WHEEL_DELTA = 120;
|
|
42
|
+
/** Above this a runaway client value is clamped rather than turned into thousands of clicks. */
|
|
43
|
+
const MAX_WHEEL_NOTCHES = 64;
|
|
44
|
+
/** ShiftLeft, for reaching the shifted half of the keymap in `text()`. */
|
|
45
|
+
const SHIFT_KEYCODE = 42 + X11_KEYCODE_OFFSET;
|
|
46
|
+
/** How long a client needs to process the `MappingNotify` from a scratch-keycode remap before
|
|
47
|
+
* a key pressed on it is read with the new mapping. `xdotool type` uses 12ms for the same
|
|
48
|
+
* reason; below ~8ms characters start arriving as the previous mapping on a busy host. */
|
|
49
|
+
const REMAP_SETTLE_MS = 12;
|
|
50
|
+
|
|
51
|
+
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
52
|
+
|
|
53
|
+
/** The X11 keysym for a Unicode code point. Latin-1 is its own keysym range; everything else
|
|
54
|
+
* uses the `0x01000000 | codepoint` convention every X server understands. */
|
|
55
|
+
function unicodeKeysym(cp: number): number {
|
|
56
|
+
return (cp >= 0x20 && cp <= 0x7e) || (cp >= 0xa0 && cp <= 0xff) ? cp : 0x01000000 | cp;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function connect(): Promise<X11Connection> {
|
|
60
|
+
const session: LinuxSession | null = detectLinuxSession();
|
|
61
|
+
if (session?.kind !== "x11") {
|
|
62
|
+
throw new RemoteInputUnavailableError("remote input needs an X11 session (Wayland uses uinput)");
|
|
63
|
+
}
|
|
64
|
+
const conn = await getX11(session);
|
|
65
|
+
if (!conn?.xtst || !conn.hasXTest) {
|
|
66
|
+
throw new RemoteInputUnavailableError("the X server has no XTEST extension (install libXtst)");
|
|
67
|
+
}
|
|
68
|
+
return conn;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Where a 0..1 fraction lands in root-window coordinates. X11 puts every monitor in one root
|
|
72
|
+
* coordinate space, so a per-monitor target is just an offset rect inside it. */
|
|
73
|
+
function toRootCoords(
|
|
74
|
+
conn: X11Connection, xFrac: number, yFrac: number, target: InputTargetRect | null,
|
|
75
|
+
): { x: number; y: number } {
|
|
76
|
+
const rect = target ?? {
|
|
77
|
+
x: 0, y: 0,
|
|
78
|
+
width: conn.x11.XDisplayWidth(conn.dpy, conn.screen),
|
|
79
|
+
height: conn.x11.XDisplayHeight(conn.dpy, conn.screen),
|
|
80
|
+
};
|
|
81
|
+
const clamp = (v: number) => (Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0);
|
|
82
|
+
return {
|
|
83
|
+
x: Math.round(rect.x + clamp(xFrac) * Math.max(0, rect.width - 1)),
|
|
84
|
+
y: Math.round(rect.y + clamp(yFrac) * Math.max(0, rect.height - 1)),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function pointer(
|
|
89
|
+
xFrac: number, yFrac: number, button: "left" | "right" | null, down: boolean | null,
|
|
90
|
+
target: InputTargetRect | null,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
const conn = await connect();
|
|
93
|
+
const { x, y } = toRootCoords(conn, xFrac, yFrac, target);
|
|
94
|
+
conn.xtst!.XTestFakeMotionEvent(conn.dpy, conn.screen, x, y, 0);
|
|
95
|
+
if (button && down !== null) {
|
|
96
|
+
conn.xtst!.XTestFakeButtonEvent(conn.dpy, button === "left" ? BUTTON_LEFT : BUTTON_RIGHT, down ? 1 : 0, 0);
|
|
97
|
+
}
|
|
98
|
+
conn.x11.XFlush(conn.dpy);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** `deltaY` in 120-unit notches, positive = away from the user (scroll up), matching the
|
|
102
|
+
* Windows/macOS backends. X11 turns each notch into a press+release of button 4 or 5. */
|
|
103
|
+
async function wheel(deltaY: number): Promise<void> {
|
|
104
|
+
const conn = await connect();
|
|
105
|
+
const notches = Math.min(MAX_WHEEL_NOTCHES, Math.round(Math.abs(deltaY) / WHEEL_DELTA));
|
|
106
|
+
if (notches === 0) return;
|
|
107
|
+
const button = deltaY > 0 ? BUTTON_WHEEL_UP : BUTTON_WHEEL_DOWN;
|
|
108
|
+
for (let i = 0; i < notches; i++) {
|
|
109
|
+
conn.xtst!.XTestFakeButtonEvent(conn.dpy, button, 1, 0);
|
|
110
|
+
conn.xtst!.XTestFakeButtonEvent(conn.dpy, button, 0, 0);
|
|
111
|
+
}
|
|
112
|
+
conn.x11.XFlush(conn.dpy);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function key(code: string, down: boolean): Promise<boolean> {
|
|
116
|
+
const evdev = codeToEvdev(code);
|
|
117
|
+
if (evdev === null) return false;
|
|
118
|
+
const conn = await connect();
|
|
119
|
+
conn.xtst!.XTestFakeKeyEvent(conn.dpy, evdev + X11_KEYCODE_OFFSET, down ? 1 : 0, 0);
|
|
120
|
+
conn.x11.XFlush(conn.dpy);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** keysym → the keycode that produces it and whether Shift is needed. Rebuilt per `text()`
|
|
125
|
+
* call rather than cached: a layout switch would otherwise leave a stale index pressing
|
|
126
|
+
* keycodes that now produce different characters, which is worse than one round trip. */
|
|
127
|
+
function buildKeysymIndex(conn: X11Connection): Map<number, { keycode: number; shift: boolean }> {
|
|
128
|
+
const { ffi, x11, dpy } = conn;
|
|
129
|
+
const index = new Map<number, { keycode: number; shift: boolean }>();
|
|
130
|
+
const min = new Int32Array(1), max = new Int32Array(1);
|
|
131
|
+
x11.XDisplayKeycodes(dpy, ffi.ptr(min), ffi.ptr(max));
|
|
132
|
+
const count = max[0]! - min[0]! + 1;
|
|
133
|
+
if (count <= 0) return index;
|
|
134
|
+
|
|
135
|
+
const perCode = new Int32Array(1);
|
|
136
|
+
const syms = x11.XGetKeyboardMapping(dpy, min[0]!, count, ffi.ptr(perCode));
|
|
137
|
+
if (!syms) return index;
|
|
138
|
+
const base = asPointer(Number(syms));
|
|
139
|
+
// Only the first two slots: they are the plain and Shift levels. Level 3/4 need AltGr, so a
|
|
140
|
+
// character living only there is left to the scratch-keycode path instead of guessing.
|
|
141
|
+
const slots = Math.min(2, perCode[0]!);
|
|
142
|
+
for (let i = 0; i < count; i++) {
|
|
143
|
+
for (let slot = 0; slot < slots; slot++) {
|
|
144
|
+
const keysym = Number(ffi.read.u64(base, (i * perCode[0]! + slot) * 8));
|
|
145
|
+
if (keysym !== 0 && !index.has(keysym)) {
|
|
146
|
+
index.set(keysym, { keycode: min[0]! + i, shift: slot === 1 });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
x11.XFree(syms);
|
|
151
|
+
return index;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A keycode with no keysyms of its own, safe to borrow for one character. null when the
|
|
155
|
+
* keymap is full (then non-ASCII text is dropped rather than clobbering a real key). */
|
|
156
|
+
function findScratchKeycode(conn: X11Connection): number | null {
|
|
157
|
+
const { ffi, x11, dpy } = conn;
|
|
158
|
+
const min = new Int32Array(1), max = new Int32Array(1);
|
|
159
|
+
x11.XDisplayKeycodes(dpy, ffi.ptr(min), ffi.ptr(max));
|
|
160
|
+
const count = max[0]! - min[0]! + 1;
|
|
161
|
+
if (count <= 0) return null;
|
|
162
|
+
const perCode = new Int32Array(1);
|
|
163
|
+
const syms = x11.XGetKeyboardMapping(dpy, min[0]!, count, ffi.ptr(perCode));
|
|
164
|
+
if (!syms) return null;
|
|
165
|
+
const base = asPointer(Number(syms));
|
|
166
|
+
let found: number | null = null;
|
|
167
|
+
// Search from the top: high keycodes are where a layout is least likely to grow into.
|
|
168
|
+
for (let i = count - 1; i >= 0 && found === null; i--) {
|
|
169
|
+
let empty = true;
|
|
170
|
+
for (let slot = 0; slot < perCode[0]!; slot++) {
|
|
171
|
+
if (Number(ffi.read.u64(base, (i * perCode[0]! + slot) * 8)) !== 0) { empty = false; break; }
|
|
172
|
+
}
|
|
173
|
+
if (empty) found = min[0]! + i;
|
|
174
|
+
}
|
|
175
|
+
x11.XFree(syms);
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One string at a time.
|
|
181
|
+
*
|
|
182
|
+
* The slow path borrows a spare keycode, and `findScratchKeycode` is deterministic — two
|
|
183
|
+
* overlapping calls pick the *same* one. They then interleave at the two settle sleeps, and
|
|
184
|
+
* whichever finishes first unbinds the keycode the other is still using, so the loser taps a
|
|
185
|
+
* key bound to nothing and types silence, or types the winner's character.
|
|
186
|
+
*
|
|
187
|
+
* The client makes this ordinary rather than exotic: the virtual keyboard sends one `text`
|
|
188
|
+
* message per keystroke and another at `compositionend`, and the slow path is exactly the
|
|
189
|
+
* non-ASCII input — Vietnamese, CJK — where those arrive fastest.
|
|
190
|
+
*/
|
|
191
|
+
let textQueue: Promise<void> = Promise.resolve();
|
|
192
|
+
|
|
193
|
+
function text(str: string): Promise<void> {
|
|
194
|
+
const next = textQueue.then(() => injectText(str));
|
|
195
|
+
// The chain must survive a failed string, or one error would wedge every later one.
|
|
196
|
+
textQueue = next.catch(() => undefined);
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function injectText(str: string): Promise<void> {
|
|
201
|
+
const conn = await connect();
|
|
202
|
+
const { ffi, x11, xtst, dpy } = conn;
|
|
203
|
+
const index = buildKeysymIndex(conn);
|
|
204
|
+
let scratch: number | null | undefined;
|
|
205
|
+
const scratchSyms = new BigUint64Array(1);
|
|
206
|
+
|
|
207
|
+
const tap = (keycode: number, shift: boolean) => {
|
|
208
|
+
if (shift) xtst!.XTestFakeKeyEvent(dpy, SHIFT_KEYCODE, 1, 0);
|
|
209
|
+
xtst!.XTestFakeKeyEvent(dpy, keycode, 1, 0);
|
|
210
|
+
xtst!.XTestFakeKeyEvent(dpy, keycode, 0, 0);
|
|
211
|
+
if (shift) xtst!.XTestFakeKeyEvent(dpy, SHIFT_KEYCODE, 0, 0);
|
|
212
|
+
x11.XFlush(dpy);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
for (const ch of str) {
|
|
217
|
+
const cp = ch.codePointAt(0);
|
|
218
|
+
if (cp === undefined) continue;
|
|
219
|
+
const hit = index.get(unicodeKeysym(cp));
|
|
220
|
+
if (hit) { tap(hit.keycode, hit.shift); continue; }
|
|
221
|
+
|
|
222
|
+
if (scratch === undefined) scratch = findScratchKeycode(conn);
|
|
223
|
+
if (scratch === null) continue; // no borrowable keycode — drop rather than clobber a real key
|
|
224
|
+
scratchSyms[0] = BigInt(unicodeKeysym(cp));
|
|
225
|
+
x11.XChangeKeyboardMapping(dpy, scratch, 1, ffi.ptr(scratchSyms), 1);
|
|
226
|
+
x11.XSync(dpy, 0);
|
|
227
|
+
await sleep(REMAP_SETTLE_MS);
|
|
228
|
+
tap(scratch, false);
|
|
229
|
+
await sleep(REMAP_SETTLE_MS);
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
// Always hand the scratch keycode back, including after a throw mid-string: leaving a
|
|
233
|
+
// Unicode keysym bound to it would make that key type a stray character forever.
|
|
234
|
+
if (typeof scratch === "number") {
|
|
235
|
+
scratchSyms[0] = 0n;
|
|
236
|
+
x11.XChangeKeyboardMapping(dpy, scratch, 1, ffi.ptr(scratchSyms), 1);
|
|
237
|
+
x11.XSync(dpy, 0);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function releaseAllModifiers(): Promise<void> {
|
|
243
|
+
const conn = await connect();
|
|
244
|
+
for (const evdev of MODIFIER_EVDEV_CODES) {
|
|
245
|
+
conn.xtst!.XTestFakeKeyEvent(conn.dpy, evdev + X11_KEYCODE_OFFSET, 0, 0);
|
|
246
|
+
}
|
|
247
|
+
conn.x11.XFlush(conn.dpy);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const x11InputBackend: RemoteInputBackend = {
|
|
251
|
+
id: "linux-xtest",
|
|
252
|
+
pointer, wheel, key, text, releaseAllModifiers,
|
|
253
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wayland input backend: two virtual devices on `/dev/uinput`, driven straight from the
|
|
3
|
+
* kernel's evdev interface.
|
|
4
|
+
*
|
|
5
|
+
* Why not XTEST: XTEST is an X server extension. A Wayland compositor is not an X server, and
|
|
6
|
+
* the protocol has no input-injection request at all by design — so the only way in is to be
|
|
7
|
+
* an input *device*. uinput events enter below the compositor, which means they also work on
|
|
8
|
+
* X11 (useful: it is how this backend is testable on an X11 host) and they are indistinguishable
|
|
9
|
+
* from a real keyboard/mouse to everything above.
|
|
10
|
+
*
|
|
11
|
+
* **Two devices, not one.** libinput classifies a device from the capabilities it declares, and
|
|
12
|
+
* a single device advertising both a full keyboard and absolute axes gets misread — typically as
|
|
13
|
+
* a touchpad, which then applies pointer acceleration and treats absolute coordinates as
|
|
14
|
+
* relative motion, so the pointer drifts instead of landing where it was told. A pointer device
|
|
15
|
+
* (absolute axes + buttons + wheel, `INPUT_PROP_DIRECT`) and a keyboard device (keys only) are
|
|
16
|
+
* each unambiguous.
|
|
17
|
+
*
|
|
18
|
+
* **No `text()`.** uinput carries key codes, not characters: there is no counterpart to
|
|
19
|
+
* `KEYEVENTF_UNICODE` (Windows), `CGEventKeyboardSetUnicodeString` (macOS) or even the
|
|
20
|
+
* scratch-keysym remap the X11 backend uses, because the layout that turns a key code into a
|
|
21
|
+
* character lives in the compositor. `text` is therefore absent, the facade's `injectText`
|
|
22
|
+
* returns false, and the client falls back to per-key events. Consequence: a phone soft keyboard
|
|
23
|
+
* that reports no key codes (most Android IMEs) cannot type on a Wayland host.
|
|
24
|
+
*
|
|
25
|
+
* **Absolute coordinates cover the whole desktop.** The kernel knows nothing about monitor
|
|
26
|
+
* layout, so 0..65535 maps to the compositor's full pointer area. Per-monitor targeting is not
|
|
27
|
+
* possible here the way it is under X11's single root-window coordinate space.
|
|
28
|
+
*/
|
|
29
|
+
import { closeSync, openSync, writeSync } from "node:fs";
|
|
30
|
+
import { ALL_EVDEV_CODES, codeToEvdev, MODIFIER_EVDEV_CODES } from "./remote-desktop-evdev-key-map.ts";
|
|
31
|
+
import { RemoteInputUnavailableError, type InputTargetRect, type RemoteInputBackend } from "./remote-desktop-input-backend.ts";
|
|
32
|
+
|
|
33
|
+
/** ioctl request numbers, verified against `linux/uinput.h` on x86-64 rather than derived by
|
|
34
|
+
* hand: `_IOW` encodes the struct size, so a wrong `sizeof` yields a silently different
|
|
35
|
+
* request number and `ioctl` fails with EINVAL for no visible reason. */
|
|
36
|
+
const UI_DEV_CREATE = 0x5501;
|
|
37
|
+
const UI_DEV_DESTROY = 0x5502;
|
|
38
|
+
const UI_DEV_SETUP = 0x405c5503; // _IOW('U', 3, struct uinput_setup) sizeof 92
|
|
39
|
+
const UI_ABS_SETUP = 0x401c5504; // _IOW('U', 4, struct uinput_abs_setup) sizeof 28
|
|
40
|
+
const UI_SET_EVBIT = 0x40045564;
|
|
41
|
+
const UI_SET_KEYBIT = 0x40045565;
|
|
42
|
+
const UI_SET_RELBIT = 0x40045566;
|
|
43
|
+
const UI_SET_ABSBIT = 0x40045567;
|
|
44
|
+
const UI_SET_PROPBIT = 0x4004556e;
|
|
45
|
+
|
|
46
|
+
const EV_SYN = 0, EV_KEY = 1, EV_REL = 2, EV_ABS = 3;
|
|
47
|
+
const SYN_REPORT = 0;
|
|
48
|
+
const ABS_X = 0, ABS_Y = 1;
|
|
49
|
+
const REL_WHEEL = 8;
|
|
50
|
+
const BTN_LEFT = 0x110, BTN_RIGHT = 0x111, BTN_MIDDLE = 0x112;
|
|
51
|
+
const INPUT_PROP_DIRECT = 0x01;
|
|
52
|
+
const BUS_USB = 0x03;
|
|
53
|
+
|
|
54
|
+
/** Absolute axis range. Matches Windows `SendInput`'s 0..65535 normalisation, so both
|
|
55
|
+
* platforms carry the same "fraction of the surface" semantics. */
|
|
56
|
+
const ABS_MAX = 65535;
|
|
57
|
+
/** `struct input_event` on x86-64: 16-byte timeval (kernel fills it when zero) + u16 type +
|
|
58
|
+
* u16 code + s32 value. */
|
|
59
|
+
const EVENT_SIZE = 24;
|
|
60
|
+
/** One notch, matching `WHEEL_DELTA` on the other backends. */
|
|
61
|
+
const WHEEL_DELTA = 120;
|
|
62
|
+
const MAX_WHEEL_NOTCHES = 64;
|
|
63
|
+
|
|
64
|
+
type Ffi = typeof import("bun:ffi");
|
|
65
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- dlopen symbol types are per-call */
|
|
66
|
+
type Libc = { ioctl: any };
|
|
67
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
68
|
+
|
|
69
|
+
interface Devices { pointer: number; keyboard: number }
|
|
70
|
+
|
|
71
|
+
let ffiModule: Ffi | null = null;
|
|
72
|
+
let libc: Libc | null = null;
|
|
73
|
+
let devices: Devices | null = null;
|
|
74
|
+
let exitHookInstalled = false;
|
|
75
|
+
|
|
76
|
+
function eventBuf(entries: Array<[type: number, code: number, value: number]>): Uint8Array {
|
|
77
|
+
const buf = new Uint8Array(EVENT_SIZE * entries.length);
|
|
78
|
+
const dv = new DataView(buf.buffer);
|
|
79
|
+
entries.forEach(([type, code, value], i) => {
|
|
80
|
+
const at = i * EVENT_SIZE;
|
|
81
|
+
dv.setUint16(at + 16, type, true);
|
|
82
|
+
dv.setUint16(at + 18, code, true);
|
|
83
|
+
dv.setInt32(at + 20, value, true);
|
|
84
|
+
});
|
|
85
|
+
return buf;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `struct uinput_setup`: input_id (bustype/vendor/product/version) + char name[80] + u32. */
|
|
89
|
+
function setupBuf(name: string, product: number): Uint8Array {
|
|
90
|
+
const buf = new Uint8Array(92);
|
|
91
|
+
const dv = new DataView(buf.buffer);
|
|
92
|
+
dv.setUint16(0, BUS_USB, true);
|
|
93
|
+
dv.setUint16(2, 0x1d6b, true); // Linux Foundation — a virtual device, not a spoofed vendor
|
|
94
|
+
dv.setUint16(4, product, true);
|
|
95
|
+
dv.setUint16(6, 1, true);
|
|
96
|
+
buf.set(new TextEncoder().encode(name).subarray(0, 79), 8);
|
|
97
|
+
return buf;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** `struct uinput_abs_setup`: u16 code + 2 pad + input_absinfo (value/min/max/fuzz/flat/res). */
|
|
101
|
+
function absSetupBuf(code: number, max: number): Uint8Array {
|
|
102
|
+
const buf = new Uint8Array(28);
|
|
103
|
+
const dv = new DataView(buf.buffer);
|
|
104
|
+
dv.setUint16(0, code, true);
|
|
105
|
+
dv.setInt32(8, 0, true); // minimum
|
|
106
|
+
dv.setInt32(12, max, true); // maximum
|
|
107
|
+
return buf;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function loadLibc(): Promise<Libc> {
|
|
111
|
+
if (libc) return libc;
|
|
112
|
+
if (!ffiModule) ffiModule = await import("bun:ffi");
|
|
113
|
+
const { dlopen, FFIType } = ffiModule;
|
|
114
|
+
// `ioctl` is variadic in C, but every request here passes one integer/pointer argument, which
|
|
115
|
+
// the x86-64 ABI puts in the same register a fixed third parameter would use.
|
|
116
|
+
libc = dlopen("libc.so.6", {
|
|
117
|
+
ioctl: { args: [FFIType.i32, FFIType.u64, FFIType.u64], returns: FFIType.i32 },
|
|
118
|
+
}).symbols as Libc;
|
|
119
|
+
return libc;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ioctlOrThrow(lib: Libc, fd: number, request: number, arg: number | bigint, what: string): void {
|
|
123
|
+
if (lib.ioctl(fd, BigInt(request), typeof arg === "bigint" ? arg : BigInt(arg)) < 0) {
|
|
124
|
+
throw new RemoteInputUnavailableError(`uinput ${what} failed (is /dev/uinput writable?)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createPointerDevice(lib: Libc, ffi: Ffi): number {
|
|
129
|
+
const fd = openSync("/dev/uinput", "w");
|
|
130
|
+
ioctlOrThrow(lib, fd, UI_SET_EVBIT, EV_KEY, "SET_EVBIT EV_KEY");
|
|
131
|
+
for (const btn of [BTN_LEFT, BTN_RIGHT, BTN_MIDDLE]) ioctlOrThrow(lib, fd, UI_SET_KEYBIT, btn, "SET_KEYBIT");
|
|
132
|
+
ioctlOrThrow(lib, fd, UI_SET_EVBIT, EV_REL, "SET_EVBIT EV_REL");
|
|
133
|
+
ioctlOrThrow(lib, fd, UI_SET_RELBIT, REL_WHEEL, "SET_RELBIT");
|
|
134
|
+
ioctlOrThrow(lib, fd, UI_SET_EVBIT, EV_ABS, "SET_EVBIT EV_ABS");
|
|
135
|
+
// `INPUT_PROP_DIRECT` is what stops libinput reading absolute axes as touchpad motion.
|
|
136
|
+
ioctlOrThrow(lib, fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT, "SET_PROPBIT");
|
|
137
|
+
for (const axis of [ABS_X, ABS_Y]) {
|
|
138
|
+
ioctlOrThrow(lib, fd, UI_SET_ABSBIT, axis, "SET_ABSBIT");
|
|
139
|
+
const abs = absSetupBuf(axis, ABS_MAX);
|
|
140
|
+
ioctlOrThrow(lib, fd, UI_ABS_SETUP, BigInt(ffi.ptr(abs)), "ABS_SETUP");
|
|
141
|
+
}
|
|
142
|
+
const setup = setupBuf("PPM Remote Pointer", 0x0001);
|
|
143
|
+
ioctlOrThrow(lib, fd, UI_DEV_SETUP, BigInt(ffi.ptr(setup)), "DEV_SETUP");
|
|
144
|
+
ioctlOrThrow(lib, fd, UI_DEV_CREATE, 0, "DEV_CREATE");
|
|
145
|
+
return fd;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createKeyboardDevice(lib: Libc, ffi: Ffi): number {
|
|
149
|
+
const fd = openSync("/dev/uinput", "w");
|
|
150
|
+
ioctlOrThrow(lib, fd, UI_SET_EVBIT, EV_KEY, "SET_EVBIT EV_KEY");
|
|
151
|
+
for (const code of ALL_EVDEV_CODES) ioctlOrThrow(lib, fd, UI_SET_KEYBIT, code, "SET_KEYBIT");
|
|
152
|
+
const setup = setupBuf("PPM Remote Keyboard", 0x0002);
|
|
153
|
+
ioctlOrThrow(lib, fd, UI_DEV_SETUP, BigInt(ffi.ptr(setup)), "DEV_SETUP");
|
|
154
|
+
ioctlOrThrow(lib, fd, UI_DEV_CREATE, 0, "DEV_CREATE");
|
|
155
|
+
return fd;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Create both devices once. The compositor needs a moment to notice a new device before the
|
|
159
|
+
* first event will be routed, so creation is awaited rather than raced with the first click. */
|
|
160
|
+
async function ensureDevices(): Promise<Devices> {
|
|
161
|
+
if (devices) return devices;
|
|
162
|
+
if (!ffiModule) ffiModule = await import("bun:ffi");
|
|
163
|
+
const lib = await loadLibc();
|
|
164
|
+
const created = { pointer: createPointerDevice(lib, ffiModule), keyboard: createKeyboardDevice(lib, ffiModule) };
|
|
165
|
+
devices = created;
|
|
166
|
+
if (!exitHookInstalled) {
|
|
167
|
+
exitHookInstalled = true;
|
|
168
|
+
// Without DEV_DESTROY the virtual devices outlive the process and pile up in the
|
|
169
|
+
// compositor's device list on every restart.
|
|
170
|
+
process.on("exit", destroyDevices);
|
|
171
|
+
}
|
|
172
|
+
await new Promise<void>((r) => setTimeout(r, 120));
|
|
173
|
+
return created;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Destroy both virtual devices now, rather than at process exit.
|
|
177
|
+
*
|
|
178
|
+
* They are process-wide, not per session: one Wayland session leaves a "PPM Remote Keyboard"
|
|
179
|
+
* and a "PPM Remote Pointer" in the compositor's device list for the life of the server, where
|
|
180
|
+
* they show up in system settings and in any tool that enumerates input devices. The next
|
|
181
|
+
* session recreates them — including the ~120 ms `ensureDevices` waits for the compositor to
|
|
182
|
+
* notice them — so the only cost is on the first event after a reconnect. */
|
|
183
|
+
export function releaseUinputDevices(): void {
|
|
184
|
+
destroyDevices();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function destroyDevices(): void {
|
|
188
|
+
if (!devices || !libc) return;
|
|
189
|
+
for (const fd of [devices.pointer, devices.keyboard]) {
|
|
190
|
+
try { libc.ioctl(fd, BigInt(UI_DEV_DESTROY), 0n); } catch { /* going away anyway */ }
|
|
191
|
+
try { closeSync(fd); } catch { /* ditto */ }
|
|
192
|
+
}
|
|
193
|
+
devices = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function emit(fd: number, entries: Array<[number, number, number]>): void {
|
|
197
|
+
const buf = eventBuf([...entries, [EV_SYN, SYN_REPORT, 0]]);
|
|
198
|
+
writeSync(fd, buf, 0, buf.byteLength);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** `target` is accepted for interface parity but cannot be honoured: the kernel has no monitor
|
|
202
|
+
* layout, so absolute axes always span the compositor's whole pointer area. */
|
|
203
|
+
async function pointer(
|
|
204
|
+
xFrac: number, yFrac: number, button: "left" | "right" | null, down: boolean | null,
|
|
205
|
+
_target: InputTargetRect | null,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
const dev = await ensureDevices();
|
|
208
|
+
const clamp = (v: number) => (Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0);
|
|
209
|
+
const events: Array<[number, number, number]> = [
|
|
210
|
+
[EV_ABS, ABS_X, Math.round(clamp(xFrac) * ABS_MAX)],
|
|
211
|
+
[EV_ABS, ABS_Y, Math.round(clamp(yFrac) * ABS_MAX)],
|
|
212
|
+
];
|
|
213
|
+
if (button && down !== null) events.push([EV_KEY, button === "left" ? BTN_LEFT : BTN_RIGHT, down ? 1 : 0]);
|
|
214
|
+
emit(dev.pointer, events);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function wheel(deltaY: number): Promise<void> {
|
|
218
|
+
const dev = await ensureDevices();
|
|
219
|
+
const notches = Math.round(deltaY / WHEEL_DELTA);
|
|
220
|
+
const clamped = Math.max(-MAX_WHEEL_NOTCHES, Math.min(MAX_WHEEL_NOTCHES, notches));
|
|
221
|
+
if (clamped === 0) return;
|
|
222
|
+
// REL_WHEEL is positive away from the user, the same sign the facade already carries.
|
|
223
|
+
emit(dev.pointer, [[EV_REL, REL_WHEEL, clamped]]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function key(code: string, down: boolean): Promise<boolean> {
|
|
227
|
+
const evdev = codeToEvdev(code);
|
|
228
|
+
if (evdev === null) return false;
|
|
229
|
+
const dev = await ensureDevices();
|
|
230
|
+
emit(dev.keyboard, [[EV_KEY, evdev, down ? 1 : 0]]);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function releaseAllModifiers(): Promise<void> {
|
|
235
|
+
const dev = await ensureDevices();
|
|
236
|
+
emit(dev.keyboard, MODIFIER_EVDEV_CODES.map((c) => [EV_KEY, c, 0] as [number, number, number]));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const uinputInputBackend: RemoteInputBackend = {
|
|
240
|
+
id: "linux-uinput",
|
|
241
|
+
pointer, wheel, key, releaseAllModifiers,
|
|
242
|
+
};
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
* from the registry below; the session and routes never import a platform module directly.
|
|
4
4
|
*
|
|
5
5
|
* Every backend module is safe to *import* anywhere (FFI is loaded lazily inside each), so the
|
|
6
|
-
* registry can hold them all statically — only the selected one ever `dlopen`s.
|
|
7
|
-
*
|
|
6
|
+
* registry can hold them all statically — only the selected one ever `dlopen`s.
|
|
7
|
+
*
|
|
8
|
+
* Linux is the one platform whose backend cannot be decided by `process.platform` alone: XTEST
|
|
9
|
+
* is an X server extension and a Wayland compositor has no equivalent, so the choice is made
|
|
10
|
+
* per *session* inside `linuxInputBackend`.
|
|
8
11
|
*/
|
|
9
12
|
import { win32InputBackend } from "./remote-desktop-input-win32.ts";
|
|
10
13
|
import { darwinInputBackend } from "./remote-desktop-input-darwin.ts";
|
|
14
|
+
import { x11InputBackend } from "./remote-desktop-input-linux.ts";
|
|
15
|
+
import { uinputInputBackend, releaseUinputDevices } from "./remote-desktop-input-uinput.ts";
|
|
16
|
+
import { detectLinuxSession } from "./remote-desktop-linux-session.ts";
|
|
11
17
|
import { RemoteInputUnavailableError, type InputTargetRect, type RemoteInputBackend } from "./remote-desktop-input-backend.ts";
|
|
12
18
|
|
|
13
19
|
export { RemoteInputUnavailableError, type InputTargetRect, type RemoteInputBackend };
|
|
@@ -17,11 +23,34 @@ const BACKENDS: Partial<Record<NodeJS.Platform, RemoteInputBackend>> = {
|
|
|
17
23
|
darwin: darwinInputBackend,
|
|
18
24
|
};
|
|
19
25
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
26
|
+
/** Linux picks by session, not by platform: XTEST is an X server extension, so a Wayland
|
|
27
|
+
* compositor is driven as a virtual input device through uinput instead. Returns null on a
|
|
28
|
+
* host with no graphical session at all (a headless server, a CI container) so
|
|
29
|
+
* `isInputAvailable()` stays honest there. */
|
|
30
|
+
function linuxInputBackend(session = detectLinuxSession()): RemoteInputBackend | null {
|
|
31
|
+
if (!session) return null;
|
|
32
|
+
return session.kind === "x11" ? x11InputBackend : uinputInputBackend;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The backend for this host, or null when the platform has none. `session` is only read on
|
|
36
|
+
* Linux and is passed explicitly by tests — it otherwise probes the host, which a headless
|
|
37
|
+
* runner answers differently from a desktop. */
|
|
38
|
+
export function getInputBackend(
|
|
39
|
+
platform: NodeJS.Platform = process.platform,
|
|
40
|
+
session = platform === "linux" ? detectLinuxSession() : null,
|
|
41
|
+
): RemoteInputBackend | null {
|
|
42
|
+
if (platform === "linux") return linuxInputBackend(session);
|
|
22
43
|
return BACKENDS[platform] ?? null;
|
|
23
44
|
}
|
|
24
45
|
|
|
46
|
+
/** Give back anything the injector holds between calls, for when no session needs it.
|
|
47
|
+
*
|
|
48
|
+
* Only uinput holds something: XTEST, `SendInput` and CoreGraphics own nothing between
|
|
49
|
+
* events, so this is a no-op everywhere else and is safe to call unconditionally. */
|
|
50
|
+
export function releaseRemoteInput(): void {
|
|
51
|
+
releaseUinputDevices();
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
/** Sync, cheap: does this platform have an injector at all? Used on the per-event hot path to
|
|
26
55
|
* drop input messages early. "Would it actually work" (OS permissions) is answered by
|
|
27
56
|
* `remote-desktop-requirements.ts`. */
|
|
@@ -29,25 +58,28 @@ export function isInputAvailable(): boolean {
|
|
|
29
58
|
return getInputBackend() !== null;
|
|
30
59
|
}
|
|
31
60
|
|
|
61
|
+
/** Throws `RemoteInputUnavailableError`. Every caller below is `async` on purpose, so an
|
|
62
|
+
* unsupported platform surfaces as a *rejected promise* rather than a synchronous throw —
|
|
63
|
+
* the session forwards input from a WS message handler and only ever `await`s these. */
|
|
32
64
|
function required(): RemoteInputBackend {
|
|
33
65
|
const backend = getInputBackend();
|
|
34
66
|
if (!backend) throw new RemoteInputUnavailableError();
|
|
35
67
|
return backend;
|
|
36
68
|
}
|
|
37
69
|
|
|
38
|
-
export function injectPointer(
|
|
70
|
+
export async function injectPointer(
|
|
39
71
|
xFrac: number, yFrac: number, button: "left" | "right" | null, down: boolean | null, target: InputTargetRect | null = null,
|
|
40
72
|
): Promise<void> {
|
|
41
73
|
return required().pointer(xFrac, yFrac, button, down, target);
|
|
42
74
|
}
|
|
43
75
|
|
|
44
|
-
export function injectWheel(deltaY: number): Promise<void> {
|
|
76
|
+
export async function injectWheel(deltaY: number): Promise<void> {
|
|
45
77
|
// A zero delta is a no-op on every OS — settle it here so no backend is even consulted.
|
|
46
|
-
if (Math.round(deltaY) === 0) return
|
|
78
|
+
if (Math.round(deltaY) === 0) return;
|
|
47
79
|
return required().wheel(deltaY);
|
|
48
80
|
}
|
|
49
81
|
|
|
50
|
-
export function injectKey(code: string, down: boolean): Promise<boolean> {
|
|
82
|
+
export async function injectKey(code: string, down: boolean): Promise<boolean> {
|
|
51
83
|
return required().key(code, down);
|
|
52
84
|
}
|
|
53
85
|
|