buninu-browser 0.3.0 → 0.5.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 (296) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +40 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +152 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +130 -0
  20. package/frontends/wayland/input.js +75 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +162 -0
  26. package/index.js +28 -1
  27. package/lib/audio/shared-connection.js +31 -0
  28. package/lib/cdp-server.js +15 -2
  29. package/lib/clipboard-command.js +10 -0
  30. package/lib/clipboard-powershell.js +22 -0
  31. package/lib/clipboard.js +37 -2
  32. package/lib/document-types.js +11 -0
  33. package/lib/headless-shell.js +54 -12
  34. package/lib/layout/text-layout.js +17 -9
  35. package/lib/paint/display-list.js +6 -1
  36. package/lib/qml/README.md +1427 -0
  37. package/lib/qml/builtins.js +771 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/canvas.js +76 -0
  40. package/lib/qml/document.js +1199 -0
  41. package/lib/qml/fonts.js +69 -0
  42. package/lib/qml/format.js +9 -0
  43. package/lib/qml/host.js +352 -0
  44. package/lib/qml/images.js +199 -0
  45. package/lib/qml/launcher.js +132 -0
  46. package/lib/qml/loader.js +521 -0
  47. package/lib/qml/media-renderer.js +49 -0
  48. package/lib/qml/media.js +86 -0
  49. package/lib/qml/models.js +622 -0
  50. package/lib/qml/parser.js +381 -0
  51. package/lib/qml/qs +5 -0
  52. package/lib/qml/qscasty +3 -0
  53. package/lib/qml/qsheadless +3 -0
  54. package/lib/qml/qswayland +3 -0
  55. package/lib/qml/reactive.js +182 -0
  56. package/lib/qml/runtime.js +3603 -0
  57. package/lib/qml/shader/fragment.js +83 -0
  58. package/lib/qml/shader/qsb.js +27 -0
  59. package/lib/qml/shader/renderer.js +39 -0
  60. package/lib/qml/shader/runtime.js +61 -0
  61. package/lib/qml/shapes.js +251 -0
  62. package/lib/qml/shortcuts.js +65 -0
  63. package/lib/qml/text.js +109 -0
  64. package/lib/qml/textures.js +126 -0
  65. package/lib/qml/wayland-clipboard.js +32 -0
  66. package/lib/qml/window-constraints.js +16 -0
  67. package/lib/qml/worker-thread.js +70 -0
  68. package/lib/render-tree/index.js +16 -1
  69. package/lib/renderer/bindings.js +16 -1
  70. package/lib/renderer/canvas-2d.js +29 -0
  71. package/lib/renderer/document-selection.js +19 -0
  72. package/lib/renderer/host.js +45 -17
  73. package/lib/renderer/main.js +21 -6
  74. package/lib/renderer/page-realm.js +20 -10
  75. package/lib/renderer/page-renderer.js +497 -42
  76. package/lib/renderer/pointer-move.js +5 -3
  77. package/lib/renderer/render-interval.js +9 -0
  78. package/lib/renderer/text-input-paint.js +54 -0
  79. package/lib/renderer/text-input.js +57 -0
  80. package/lib/style/computed-style.js +10 -4
  81. package/package.json +1 -1
  82. package/test/audio-connection-lifetime.test.js +46 -0
  83. package/test/canvas-paint.test.js +8 -0
  84. package/test/clipboard.test.js +19 -0
  85. package/test/computed-style.test.js +12 -0
  86. package/test/document-selection.test.js +27 -0
  87. package/test/double-click.test.js +85 -0
  88. package/test/fixtures/canvas-damage.js +40 -0
  89. package/test/fixtures/fake-renderer.js +1 -0
  90. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  91. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  92. package/test/fixtures/flea-helpers/bin/bat +3 -0
  93. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  94. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  95. package/test/fixtures/flea-helpers/bin/glow +3 -0
  96. package/test/fixtures/flea-helpers/bin/qs +3 -0
  97. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  98. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  99. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  100. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  101. package/test/fixtures/flea-helpers/check.mjs +36 -0
  102. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  103. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  104. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  105. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  106. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  107. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  108. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  109. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  110. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  111. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  112. package/test/fixtures/pointer-paint-batching.js +23 -0
  113. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  114. package/test/fixtures/qml/flea/LICENSE +21 -0
  115. package/test/fixtures/qml/flea/README.md +25 -0
  116. package/test/fixtures/qml/flea/Theme.qml +8 -0
  117. package/test/fixtures/qml/flea/demo.qml +24 -0
  118. package/test/fixtures/qml/flea/qmldir +3 -0
  119. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  120. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  121. package/test/fixtures/qml/flea-theme/README.md +70 -0
  122. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  123. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  124. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  125. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  126. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  127. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  128. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  129. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  130. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  131. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  132. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  133. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  134. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  135. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  136. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  137. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  138. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  139. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  140. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  141. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  142. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  143. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  144. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  145. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  146. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  147. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  148. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  173. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  174. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  175. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  176. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  177. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  178. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  179. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  180. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  181. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  182. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  183. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  184. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  185. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  186. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  187. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  188. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  189. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  190. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  191. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  192. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  193. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  194. package/test/fixtures/qml/hello.qml +26 -0
  195. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  196. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  197. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  198. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  199. package/test/fixtures/qml/imports/logic/base.js +2 -0
  200. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  201. package/test/fixtures/qml/imports/logic/math.js +3 -0
  202. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  203. package/test/fixtures/qml/imports/main.qml +21 -0
  204. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  205. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  206. package/test/fixtures/qml/multiply.qml +61 -0
  207. package/test/fixtures/qml-canvas-dom.js +90 -0
  208. package/test/fixtures/qml-fonts/main.qml +10 -0
  209. package/test/fixtures/qml-locale/check.mjs +6 -0
  210. package/test/fixtures/qml-locale/main.qml +8 -0
  211. package/test/fixtures/qml-media-audio.js +39 -0
  212. package/test/fixtures/qml-shader/grab.qml +14 -0
  213. package/test/fixtures/qml-shader/gradient.frag +5 -0
  214. package/test/fixtures/qml-shader/main.qml +14 -0
  215. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  216. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  217. package/test/fixtures/qml-shader-check.mjs +24 -0
  218. package/test/fixtures/title-no-paint.js +20 -0
  219. package/test/helpers/flea-sandbox.js +48 -0
  220. package/test/helpers/quickshell-fixture.js +125 -0
  221. package/test/html-clipboard-wayland.test.js +47 -0
  222. package/test/index.test.js +13 -0
  223. package/test/manual-casty-without-qml.js +87 -0
  224. package/test/manual-double-click.html +21 -0
  225. package/test/manual-flea-application.js +49 -0
  226. package/test/manual-flea-binary.js +62 -0
  227. package/test/manual-flea-qml.js +9 -0
  228. package/test/manual-flea-settings.js +40 -0
  229. package/test/manual-flea-theme.js +40 -0
  230. package/test/manual-mouse-buttons.html +18 -0
  231. package/test/manual-omastorm-engine.mjs +19 -0
  232. package/test/manual-omastorm-shaders.mjs +28 -0
  233. package/test/manual-omastorm-software.mjs +25 -0
  234. package/test/manual-pointer-scroll.html +43 -0
  235. package/test/manual-qml-removal.js +46 -0
  236. package/test/manual-without-qml.js +45 -0
  237. package/test/pointer-drag.test.js +104 -0
  238. package/test/qml-canvas-dom.test.js +37 -0
  239. package/test/qml-canvas.test.js +21 -0
  240. package/test/qml-clipboard-bridge.test.js +25 -0
  241. package/test/qml-clipboard-wayland.test.js +40 -0
  242. package/test/qml-connections-timing.test.js +95 -0
  243. package/test/qml-controls-window.test.js +182 -0
  244. package/test/qml-dynamic.test.js +401 -0
  245. package/test/qml-flea-app.test.js +146 -0
  246. package/test/qml-flea-backend.test.js +78 -0
  247. package/test/qml-flea-columns-backend.test.js +38 -0
  248. package/test/qml-flea-compile.test.js +78 -0
  249. package/test/qml-flea-launcher.test.js +88 -0
  250. package/test/qml-flea-markdown-backend.test.js +31 -0
  251. package/test/qml-flea-media.test.js +28 -0
  252. package/test/qml-flea-navigation-backend.test.js +35 -0
  253. package/test/qml-flea-navigation-timing.test.js +147 -0
  254. package/test/qml-flea-preview-backend.test.js +53 -0
  255. package/test/qml-flea-previews.test.js +163 -0
  256. package/test/qml-flea-rows.test.js +101 -0
  257. package/test/qml-flea-textures.test.js +59 -0
  258. package/test/qml-flea-view-switch.test.js +33 -0
  259. package/test/qml-flea-wayland.test.js +68 -0
  260. package/test/qml-flea.test.js +34 -0
  261. package/test/qml-fonts.test.js +31 -0
  262. package/test/qml-grid-multiline.test.js +48 -0
  263. package/test/qml-host.test.js +238 -0
  264. package/test/qml-imports.test.js +102 -0
  265. package/test/qml-inherited-handlers.test.js +51 -0
  266. package/test/qml-input-selection.test.js +66 -0
  267. package/test/qml-interactions.test.js +309 -0
  268. package/test/qml-language.test.js +167 -0
  269. package/test/qml-launcher.test.js +54 -0
  270. package/test/qml-layout-text.test.js +205 -0
  271. package/test/qml-locale.test.js +45 -0
  272. package/test/qml-media.test.js +63 -0
  273. package/test/qml-nonvisual-visible.test.js +19 -0
  274. package/test/qml-persisted-delegates.test.js +45 -0
  275. package/test/qml-quickshell-controls.test.js +65 -0
  276. package/test/qml-quickshell-settings.test.js +62 -0
  277. package/test/qml-repeater.test.js +72 -0
  278. package/test/qml-shader-runtime.test.js +6 -0
  279. package/test/qml-shortcuts.test.js +39 -0
  280. package/test/qml-socket.test.js +60 -0
  281. package/test/qml-software-shader.test.js +55 -0
  282. package/test/qml-style-updates.test.js +124 -0
  283. package/test/qml-textures.test.js +66 -0
  284. package/test/qml-views.test.js +332 -0
  285. package/test/qml-window-update.test.js +36 -0
  286. package/test/qml-worker.test.js +69 -0
  287. package/test/qml.test.js +83 -0
  288. package/test/render-interval.test.js +33 -0
  289. package/test/renderer-host.test.js +11 -0
  290. package/test/text-input-pointer.test.js +31 -0
  291. package/test/text-input-scroll.test.js +29 -0
  292. package/test/text-input.test.js +47 -0
  293. package/test/title-no-paint.test.js +6 -0
  294. package/test/wayland-frontend.test.js +83 -0
  295. package/test/wayland-scroll.test.js +40 -0
  296. package/test/wayland-urlbar.test.js +61 -0
@@ -0,0 +1,120 @@
1
+ // Adapted from buninu-land/lib/transport.js (MIT; LICENSE-land).
2
+ // Linux 64-bit ABI. Only libc is loaded. recvmsg is required for SCM_RIGHTS;
3
+ // Bun's stream sockets do not expose ancillary file descriptors.
4
+ import { dlopen, ptr } from 'bun:ffi';
5
+
6
+ if (!['arm64', 'x64'].includes(process.arch) || !['linux', 'android'].includes(process.platform)) throw Error('Requires 64-bit Linux or Android');
7
+ const signature = (args, returns = 'i32') => ({ args, returns });
8
+ let library;
9
+ let errnoSymbol;
10
+ // Same architecture-specific libc names as buninu-linux's /lib/dlopen.js.
11
+ const muslLibc = `libc.musl-${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}.so.1`;
12
+ const candidates = process.platform === 'android'
13
+ ? [['libc.so', '__errno']]
14
+ : [
15
+ ['libc.so.6', '__errno_location'],
16
+ [muslLibc, '__errno_location'],
17
+ [`/lib/${muslLibc}`, '__errno_location'],
18
+ ['libc.so', '__errno_location'],
19
+ ['libc.so', '__errno'],
20
+ ];
21
+ const failures = [];
22
+ for (const [name, symbol] of candidates) {
23
+ try { library = dlopen(name, {
24
+ connect: signature(['i32','ptr','u32']), socket: signature(['i32','i32','i32']),
25
+ recvmsg: signature(['i32','ptr','i32'], 'i64'), sendmsg: signature(['i32','ptr','i32'], 'i64'),
26
+ dup: signature(['i32']), send: signature(['i32','ptr','u64','i32'], 'i64'), close: signature(['i32']),
27
+
28
+ // Standard Wayland clipboard FD transfer.
29
+ pipe2: signature(['ptr','i32']), fcntl: signature(['i32','i32','i32']),
30
+
31
+ [symbol]: signature([], 'ptr'),
32
+ }); errnoSymbol = symbol; break; }
33
+ catch (error) { failures.push(`${name} (${symbol}): ${error.message}`); }
34
+ }
35
+ if (!library) throw Error(`Cannot load libc on ${process.platform}:\n${failures.join('\n')}`);
36
+ export const libc = library.symbols;
37
+ import { toArrayBuffer } from 'bun:ffi';
38
+ export const errno = () => new Int32Array(toArrayBuffer(libc[errnoSymbol](), 0, 4))[0];
39
+ const retry = () => [4,11].includes(errno());
40
+ export class Connection {
41
+ constructor(fd, removed) {
42
+ this.fd = fd; this.removed = removed; this.fds = []; this.input = Buffer.alloc(0); this.output = []; this.queuedBytes = 0;
43
+ this.data = Buffer.alloc(65536); this.control = Buffer.alloc(4096); this.iov = Buffer.alloc(16); this.msg = Buffer.alloc(56);
44
+ this.iov.writeBigUInt64LE(BigInt(ptr(this.data))); this.iov.writeBigUInt64LE(BigInt(this.data.length), 8);
45
+ this.msg.writeBigUInt64LE(BigInt(ptr(this.iov)), 16); this.msg.writeBigUInt64LE(1n, 24);
46
+ this.msg.writeBigUInt64LE(BigInt(ptr(this.control)), 32);
47
+ }
48
+ send(bytes, fds = []) {
49
+ if (this.closed) return;
50
+ const owned = [];
51
+ try { for (const fd of fds) { const copy = libc.dup(fd); if (copy < 0) throw Error('Cannot duplicate outgoing FD'); owned.push(copy); } }
52
+ catch (e) { for (const fd of owned) libc.close(fd); throw e; }
53
+ this.output.push({ bytes, fds: owned }); this.queuedBytes += bytes.length;
54
+ if (this.queuedBytes > 4*1024*1024) this.close();
55
+ }
56
+ flush() {
57
+ for (let round = 0; round < 64 && this.output.length; round++) {
58
+ const item = this.output[0];
59
+ let n;
60
+ if (item.fds.length) {
61
+ const iov = Buffer.alloc(16), msg = Buffer.alloc(56);
62
+ const size = 16 + item.fds.length * 4, control = Buffer.alloc(Math.ceil(size / 8) * 8);
63
+ iov.writeBigUInt64LE(BigInt(ptr(item.bytes))); iov.writeBigUInt64LE(BigInt(item.bytes.length), 8);
64
+ control.writeBigUInt64LE(BigInt(size)); control.writeInt32LE(1,8); control.writeInt32LE(1,12);
65
+ item.fds.forEach((fd,i) => control.writeInt32LE(fd,16+i*4));
66
+ msg.writeBigUInt64LE(BigInt(ptr(iov)),16); msg.writeBigUInt64LE(1n,24);
67
+ msg.writeBigUInt64LE(BigInt(ptr(control)),32); msg.writeBigUInt64LE(BigInt(control.length),40);
68
+ n = Number(libc.sendmsg(this.fd,msg,0x4000));
69
+ } else n = Number(libc.send(this.fd,item.bytes,item.bytes.length,0x4000));
70
+ if (n < 0) { if (!retry()) this.close(); return; }
71
+ if (!n) return this.close();
72
+ // SCM_RIGHTS is delivered with the first successfully sent byte, even on partial writes.
73
+ for (const fd of item.fds) libc.close(fd); item.fds = [];
74
+ this.queuedBytes -= n;
75
+ if (n === item.bytes.length) this.output.shift(); else item.bytes = item.bytes.subarray(n);
76
+ }
77
+ }
78
+ poll() {
79
+ if (this.closed) return;
80
+ try {
81
+ for (let round = 0; round < 16; round++) {
82
+ this.msg.writeBigUInt64LE(BigInt(this.control.length), 40); this.msg.writeUInt32LE(0, 48);
83
+ const n = Number(libc.recvmsg(this.fd, this.msg, 0x40000000));
84
+ if (n < 0) { if (retry()) break; return this.close(); }
85
+ if (!n) return this.close();
86
+ const end = Number(this.msg.readBigUInt64LE(40));
87
+ for (let p = 0; p + 16 <= end;) {
88
+ const size = Number(this.control.readBigUInt64LE(p)); if (size < 16 || p + size > end) throw Error('Invalid ancillary data');
89
+ if (this.control.readInt32LE(p+8) === 1 && this.control.readInt32LE(p+12) === 1)
90
+ for (let q = p + 16; q + 4 <= p + size; q += 4) this.fds.push(this.control.readInt32LE(q));
91
+ p += Math.ceil(size / 8) * 8;
92
+ }
93
+ if (this.msg.readUInt32LE(48) & 8) throw Error('Truncated ancillary data');
94
+ this.input = Buffer.concat([this.input, this.data.subarray(0,n)]);
95
+ if (this.input.length > 1024*1024 || this.fds.length > 256) throw Error('Client queue limit');
96
+ this.onData();
97
+ if (this.closed) return;
98
+ }
99
+ this.flush();
100
+ } catch (e) { console.error(`Wayland client: ${e.message}`);
101
+ // Flush queued requests before disconnecting.
102
+ this.flush();
103
+ this.close(); }
104
+ }
105
+ close() {
106
+ if (this.closed) return; this.closed = true; libc.close(this.fd);
107
+ for (const item of this.output) for (const fd of item.fds) libc.close(fd);
108
+ this.output = []; this.queuedBytes = 0;
109
+ for (const fd of this.fds) libc.close(fd); this.fds = []; this.onClose?.(); this.removed();
110
+ }
111
+ }
112
+
113
+ export function connectUnix(path) {
114
+ if (Buffer.byteLength(path)>107 || path.includes('\0')) throw Error('Invalid Wayland socket path');
115
+ const fd=libc.socket(1,1|0x800|0x80000,0);
116
+ if(fd<0)throw Error(`socket: errno ${errno()}`);
117
+ const address=Buffer.alloc(110); address.writeUInt16LE(1);address.write(path,2);
118
+ if(libc.connect(fd,address,Buffer.byteLength(path)+3)<0){const code=errno();libc.close(fd);throw Error(`connect ${path}: errno ${code}`);}
119
+ return new Connection(fd,()=>{});
120
+ }
@@ -0,0 +1,162 @@
1
+ // Casty's editor is vendored beside this adapter. Only rendering and browser
2
+ // actions live here; selection, history, dragging and double clicks stay there.
3
+ import {UrlBar,PromptHistory,toURL} from './casty/urlbar.js';
4
+ import {COMMANDS,parseCommand,parseZoom} from './casty/commands.js';
5
+ import {loadKeyBindings,toKeyName} from './casty/keys.js';
6
+ import {loadConfig} from './casty/config.js';
7
+ import {searchBookmarks} from './casty/bookmarks.js';
8
+ import {displayWidth,fitWidth} from './casty/prompt-width.js';
9
+ import {downloadPage} from './casty/download.js';
10
+ import {HintMode} from './casty/hints.js';
11
+ import {findInPage,toggleEruda} from './casty/page-commands.js';
12
+ import {readClipboard,writeClipboard} from '../../lib/clipboard.js';
13
+
14
+ export const BAR_HEIGHT=32;
15
+ export const WAYLAND_COMMANDS=COMMANDS.filter(c=>!['cdp','kitty','pixelmouse'].includes(c.name));
16
+ export function editorKey(key,modifiers=0) {
17
+ const letters={ArrowUp:'A',ArrowDown:'B',ArrowRight:'C',ArrowLeft:'D',Home:'H',End:'F'};
18
+ if(letters[key])return `\x1b[${modifiers?`1;${1+(modifiers&8?1:0)+(modifiers&1?2:0)+(modifiers&2?4:0)}`:''}${letters[key]}`;
19
+ if(key==='Delete')return '\x1b[3~';
20
+ if(key==='Backspace')return '\x7f';
21
+ if(key==='Enter')return '\r';if(key==='Escape')return '\x1b';if(key==='Tab')return '\t';
22
+ if(modifiers&2&&/^[a-z]$/i.test(key))return String.fromCharCode(key.toLowerCase().charCodeAt(0)-96);
23
+ return key.length===1||[...key].length===1?key:'';
24
+ }
25
+ class PixelEditor extends UrlBar {
26
+ constructor(onChange,history) {super({history,commandEntry:true});this.changed=onChange;this.columns=80;}
27
+ render(){this._dirty=false;this.changed?.();}
28
+ }
29
+ export class WaylandUrlBar {
30
+ constructor(context,{changed=()=>{},close=()=>{},clipboard=null,history=new PromptHistory()}={}) {
31
+ this.context=context;this.changed=changed;this.quit=close;this.height=BAR_HEIGHT;
32
+ this.clipboard=clipboard||{read:readClipboard,write:writeClipboard};
33
+ this.editor=new PixelEditor(()=>{this.dirty=true;this.caret=true;changed();},history);
34
+ this.bindings=loadKeyBindings();this.zoom=1;this.width=0;this.cellWidth=10;this.cache=new Map();
35
+ this.client={send:(method,params)=>this.send(method,params)};
36
+ this.hints=new HintMode(changed);
37
+ this.timer=setInterval(()=>{if(this.editor.editing){this.caret=!this.caret;this.dirty=true;changed();}},500);
38
+ this.timer.unref?.();this.dirty=true;
39
+ }
40
+ async initialize() {
41
+ const {loadGraphics}=await import('../../lib/renderer/page-renderer.js');
42
+ this.graphics=await loadGraphics();
43
+ const paragraph=this.paragraph('M','#ffffff');this.cellWidth=paragraph.getMaxIntrinsicWidth();
44
+ return this;
45
+ }
46
+ updateURL(url){if(url!==this.editor.currentUrl)this.editor.setUrl(url||'about:blank');}
47
+ flash(text){clearTimeout(this.statusTimer);this.editor.setStatus(String(text));this.editor.render();this.statusTimer=setTimeout(()=>{this.editor.clearStatus();this.editor.render();},5000);}
48
+ open(command=false,text=null){if(this.editor.editing)this.editor.cancelEditing();this.editor.startEditing({command,text}).then(result=>{
49
+ if(result?.mode==='cmd')return this.command(result.line);
50
+ if(result?.url)return this.navigate(result.url);
51
+ }).catch(error=>this.flash(error.message));}
52
+ async navigate(url){this.editor.loading=true;this.editor.render();try{await this.context.navigate(url);this.updateURL(this.context.url);}finally{this.editor.loading=false;this.editor.render();this.changed();}}
53
+ async press(key,options={}) {
54
+ const m=options.modifiers||0,name=toKeyName({key,modifiers:m});
55
+ if(this.editor.editing) {
56
+ if(name==='ctrl+c'||name==='alt+c')await this.clipboard.write(this.editor.selectedText||this.editor.text);
57
+ else if(name==='ctrl+x') {if(this.editor.selectedText){await this.clipboard.write(this.editor.selectedText);this.editor._clearIfSelected();this.editor.render();}}
58
+ else if(name==='ctrl+v')this.editor.insertText((await this.clipboard.read()).replace(/[\r\n]+/g,' '));
59
+ else this.editor.handleInput(editorKey(key,m));
60
+ return;
61
+ }
62
+ if(this.hints.active){await this.hints.handleInput(editorKey(key,m));return;}
63
+ const action=this.bindings[name];
64
+ // Native page editing must receive Ctrl-V itself (including preventDefault,
65
+ // selection replacement and beforeinput), using the frontend clipboard.
66
+ if(action==='paste')return this.context.press(key,options);
67
+ if(action){await this.action(action);return;}
68
+ return this.context.press(key,options);
69
+ }
70
+ async pointerButton(x,y,button,state,options={}) {
71
+ if(button<272||button>274)return;
72
+ if((y<this.height&&!this.pageButtons)||this.editor.dragging) {
73
+ if(button!==272)return;
74
+ const col=Math.max(1,Math.floor(x/this.cellWidth)+1);
75
+ if(state) {
76
+ if(!this.editor.editing){this.open(col<=3);if(col>3)this.editor.armDrag(col);}
77
+ else this.editor.pointerDown(col);
78
+ }else this.editor.pointerUp();
79
+ return;
80
+ }
81
+ if(state&&this.editor.editing)this.editor.cancelEditing();
82
+ const mask=button===272?1:button===273?2:4;
83
+ this.pageButtons=state?(this.pageButtons||0)|mask:(this.pageButtons||0)&~mask;
84
+ if(this.context.pointerButton)return this.context.pointerButton(x/this.zoom,(y-this.height)/this.zoom,button,state,options);
85
+ if(!state)return this.context.click(x/this.zoom,(y-this.height)/this.zoom,button===272?options:{...options,button:button===273?"right":"middle"});
86
+ }
87
+ move(x,y,options={}){if(this.editor.dragging){this.editor.pointerMove(Math.max(1,Math.floor(x/this.cellWidth)+1));return;}if(y>=this.height||this.pageButtons)return this.context.move(x/this.zoom,(y-this.height)/this.zoom,options);}
88
+ scroll(dx,dy,at){if(at?.y<this.height){if(this.editor.editing){if(dy<0)this.editor.historyUp();else this.editor.historyDown();}return;}return this.context.scroll(dx/this.zoom,dy/this.zoom,at?{x:at.x/this.zoom,y:(at.y-this.height)/this.zoom}:null);}
89
+ async command(line) {
90
+ const {name,arg,error}=parseCommand(line);if(error){this.flash(error);return;}
91
+ if(!WAYLAND_COMMANDS.some(c=>c.name===name)){this.flash(`${name} is not used by the Wayland frontend`);return;}
92
+ if(name==='open'){const url=toURL(arg);if(url)await this.navigate(url);else this.flash('Bookmark not found');}
93
+ else if(name==='bookmark'){const match=searchBookmarks(arg)[0];if(match)await this.navigate(match.url);else this.flash('Bookmark not found');}
94
+ else if(name==='help')this.flash(WAYLAND_COMMANDS.map(c=>c.usage).join(' · '));
95
+ else if(name==='find'){const result=await findInPage(this.client,arg);this.flash(result.error||`${result.text}: ${result.total?result.index+1:0}/${result.total}`);}
96
+ else if(name==='zoom'){if(!arg)this.flash(`Zoom ${Math.round(this.zoom*100)}%`);else{const zoom=parseZoom(arg,this.zoom);if(zoom===null)this.flash('Invalid zoom');else{this.zoom=zoom;this.changed();}}}
97
+ else await this.action({links:'hints',copyurl:'copy_url',pasteurl:'paste_url'}[name]||name);
98
+ }
99
+ async action(action) {
100
+ if(action==='quit'){this.quit();return;}
101
+ if(action==='url_bar'){this.open();return;}
102
+ if(action==='command'){this.open(true);return;}
103
+ if(action==='paste_url'){this.open(false,(await this.clipboard.read()).replace(/[\r\n]+/g,' '));return;}
104
+ if(action==='back')await this.context.goBack();
105
+ else if(action==='forward')await this.context.goForward();
106
+ else if(action==='reload')await this.context.reload();
107
+ else if(action==='home')await this.navigate(toURL(loadConfig().homeUrl));
108
+ else if(action==='copy_url'){await this.clipboard.write(this.context.url);this.flash('Copied URL');}
109
+ else if(action==='copy'){
110
+ const text=await this.context.evaluate(`(()=>{const e=document.activeElement;return e&&typeof e.selectionStart==='number'?e.value.slice(e.selectionStart,e.selectionEnd):String(getSelection())})()`);
111
+ if(text)await this.clipboard.write(text);
112
+ }else if(action==='paste')await this.context.type(await this.clipboard.read());
113
+ else if(action==='download')this.flash('Saved: '+await downloadPage(this.client));
114
+ else if(action==='hints'){void this.hints.start(this.client).catch(e=>this.flash(e.message));}
115
+ else if(action==='eruda')this.flash(await toggleEruda(this.client));
116
+ else if(['zoom_in','zoom_out','zoom_reset'].includes(action))this.zoom=parseZoom({zoom_in:'+',zoom_out:'-',zoom_reset:'reset'}[action],this.zoom);
117
+ this.updateURL(this.context.url);this.changed();
118
+ }
119
+ // Existing Casty helpers expect send(); route their small subset directly to
120
+ // the engine. There is no CDP socket or extra remote debugging server here.
121
+ async send(method,p={}) {
122
+ if(method==='Runtime.evaluate')return {result:{value:await this.context.evaluate(p.expression)}};
123
+ if(method==='Input.dispatchMouseEvent'){if(p.type==='mouseReleased')await this.context.click(p.x,p.y);else if(p.type==='mouseMoved')await this.context.move(p.x,p.y);return {};}
124
+ if(method==='Input.insertText'){await this.context.type(p.text);return {};}
125
+ if(method==='Page.getFrameTree'){const r=this.context.resourceContent(this.context.url);return {frameTree:{frame:{id:'main',url:this.context.url,mimeType:r?.contentType||''}}};}
126
+ if(method==='Page.getResourceContent'){const r=this.context.resourceContent(p.url);if(!r)throw Error('Page source unavailable');return {content:Buffer.from(r.body).toString('base64'),base64Encoded:true};}
127
+ throw Error(`Unsupported frontend helper method: ${method}`);
128
+ }
129
+ paragraph(text,color) {
130
+ const key=color+'\n'+text;let p=this.cache.get(key);if(p)return p;
131
+ const {CanvasKit:k,fontCollection,monospaceFamily,fontFamilies}=this.graphics;
132
+ const builder=k.ParagraphBuilder.MakeFromFontCollection(new k.ParagraphStyle({textStyle:{color:k.parseColorString(color),fontSize:16,fontFamilies:[monospaceFamily,...fontFamilies]},maxLines:1}),fontCollection);
133
+ builder.addText(text);p=builder.build();builder.delete();p.layout(100000);
134
+ if(this.cache.size>=256){const first=this.cache.keys().next().value;this.cache.get(first).delete();this.cache.delete(first);}
135
+ this.cache.set(key,p);return p;
136
+ }
137
+ pixels(width) {
138
+ if(width!==this.width){this.width=width;this.dirty=true;this.editor.columns=Math.max(1,Math.floor(width/this.cellWidth));}
139
+ if(!this.dirty&&this.bytes)return this.bytes;
140
+ const {CanvasKit:k}=this.graphics;if(!this.surface||this.surface.width()!==width){this.surface?.delete();this.surface=k.MakeSurface(width,this.height);}
141
+ const canvas=this.surface.getCanvas(),paint=new k.Paint();
142
+ canvas.clear(k.parseColorString(this.editor.editing?'#005f87':'#303030'));
143
+ const cols=this.editor.columns;
144
+ const ansi=this.editor.editing?this.editor._editLine(cols):this.editor._displayLine(cols);
145
+ let inverse=false,x=0;
146
+ for(const part of ansi.split(/(\x1b\[[\d;]*m)/)) {
147
+ if(part.startsWith('\x1b')){if(part==='\x1b[7m')inverse=true;else if(part==='\x1b[27m')inverse=false;continue;}
148
+ for(const char of part){const w=displayWidth(char)*this.cellWidth;
149
+ if(inverse){paint.setColor(k.parseColorString('#f4f8ff'));canvas.drawRect(k.XYWHRect(x,0,w,this.height),paint);}
150
+ if(char.trim()){const p=this.paragraph(char,inverse?'#005f87':'#f4f8ff');canvas.drawParagraph(p,x,(this.height-p.getHeight())/2);}
151
+ x+=w;if(x>=width)break;
152
+ }
153
+ }
154
+ if(this.editor.editing&&!this.editor.selectionRange&&this.caret){const view=this.editor._editView(cols);paint.setColor(k.parseColorString('#72e0e8'));canvas.drawRect(k.XYWHRect(view.cursorCol*this.cellWidth,6,1,20),paint);}
155
+ paint.delete();this.surface.flush();const image=this.surface.makeImageSnapshot();
156
+ try{this.bytes=Buffer.from(image.readPixels(0,0,{width,height:this.height,colorType:k.ColorType.RGBA_8888,alphaType:k.AlphaType.Unpremul,colorSpace:k.ColorSpace.SRGB}));}finally{image.delete();}
157
+ // Only the small toolbar strip is converted; page BGRA bytes remain untouched.
158
+ for(let i=0;i<this.bytes.length;i+=4){const red=this.bytes[i];this.bytes[i]=this.bytes[i+2];this.bytes[i+2]=red;}
159
+ this.dirty=false;return this.bytes;
160
+ }
161
+ dispose(){clearInterval(this.timer);clearTimeout(this.statusTimer);clearInterval(this.editor._spinTimer);this.editor.cancelEditing();for(const p of this.cache.values())p.delete();this.cache.clear();this.surface?.delete();}
162
+ }
package/index.js CHANGED
@@ -43,6 +43,14 @@ const INFORMATION = new Map([
43
43
  /** Splits the command line into a frontend and the arguments it receives. */
44
44
  export function chooseFrontend(argv) {
45
45
  const [first, ...rest] = argv;
46
+
47
+ // wayland handle begin: independent native frontend
48
+ if (first === "--wayland") return { frontend: "wayland", args: rest };
49
+ // wayland handle end
50
+
51
+ if (["--quickshell-wayland", "--quickshell-headless", "--quickshell-casty"].includes(first))
52
+ return { frontend: first.slice(2), args: rest };
53
+
46
54
  if (first === "--casty") return { frontend: "casty", args: rest };
47
55
  if (first === "--headless") return { frontend: "headless", args: rest };
48
56
  if (RESERVED_FRONTENDS.has(first)) return { frontend: first.slice(2), args: rest, reserved: true };
@@ -66,13 +74,17 @@ export function castyCommand(args, which = Bun.which) {
66
74
  }
67
75
 
68
76
  export function usage(name = packageInfo.name) {
77
+ // wayland handle begin: frontend help
78
+ const waylandHelp = ' --wayland\n Experimental standard Wayland SHM window; --wayland --help for options\n';
79
+ // wayland handle end
80
+
69
81
  return `Usage:
70
82
  ${name} [URL] [CASTY_OPTIONS...] [-- BROWSER_FLAGS...]
71
83
  ${name} --casty [CASTY_ARGS...]
72
84
  ${name} --headless [ENGINE_OPTIONS...]
73
85
 
74
86
  Frontends (first argument only):
75
- --casty
87
+ ${typeof waylandHelp === "undefined" ? "" : waylandHelp} --casty
76
88
  Browse in the terminal with casty (the default)
77
89
  Needs a terminal with the Kitty graphics protocol
78
90
  Any first argument not listed here also starts casty,
@@ -81,6 +93,9 @@ Frontends (first argument only):
81
93
  --headless
82
94
  Start the engine only, as a CDP endpoint
83
95
  The engine's options: ${name} --headless --help
96
+ --quickshell-wayland, --quickshell-headless, --quickshell-casty
97
+ Experimental Quickshell CLI (-p FILE_OR_DIRECTORY / -c CONFIG)
98
+ Use -- before backend arguments; each entry supports --help
84
99
  --win32, --gtk, --appkit, --qt
85
100
  Native windowed frontends (reserved, not implemented yet)
86
101
 
@@ -147,6 +162,18 @@ if (import.meta.main) {
147
162
  await printReadme();
148
163
  process.exit(0);
149
164
  }
165
+
166
+ if (frontend.startsWith("quickshell-")) {
167
+ const entry = join(import.meta.dir, "lib/qml/qs" + frontend.slice("quickshell-".length));
168
+ process.exit(await run([process.execPath, entry, ...args]) ?? 1);
169
+ }
170
+
171
+ // wayland handle begin: launch the isolated frontend
172
+ if (frontend === "wayland") {
173
+ process.exit(await run([process.execPath, join(import.meta.dir, "frontends/wayland/index.js"), ...args]) ?? 1);
174
+ }
175
+ // wayland handle end
176
+
150
177
  const code = frontend === "headless"
151
178
  ? await run([process.execPath, ENGINE, ...args])
152
179
  : await run(castyCommand(args), { ...process.env, CASTY_BROWSER: ENGINE });
@@ -0,0 +1,31 @@
1
+ // Keep one connection while audio-owning renderer processes are alive. A
2
+ // completed stream does not release its owner: the page may play again.
3
+ export class SharedAudioConnection {
4
+ #connect;
5
+ #pending = null;
6
+ #owners = new Set();
7
+
8
+ constructor(connect) { this.#connect = connect; }
9
+
10
+ acquire(owner) {
11
+ this.#owners.add(owner);
12
+ if (!this.#pending) {
13
+ const pending = Promise.resolve().then(this.#connect).catch(error => {
14
+ if (this.#pending === pending) this.#pending = null;
15
+ throw error;
16
+ });
17
+ this.#pending = pending;
18
+ }
19
+ return this.#pending;
20
+ }
21
+
22
+ release(owner) {
23
+ this.#owners.delete(owner);
24
+ if (this.#owners.size || !this.#pending) return;
25
+ const pending = this.#pending;
26
+ this.#pending = null;
27
+ // Also close connections whose authentication is still in flight. A new
28
+ // owner gets a separate connection and cannot inherit this closing one.
29
+ void pending.then(client => client.close()).catch(() => {});
30
+ }
31
+ }
package/lib/cdp-server.js CHANGED
@@ -701,7 +701,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
701
701
  case "Input.dispatchMouseEvent": {
702
702
  const target = getSessionTarget(state, sessionId);
703
703
  if (params.type === "mouseMoved") {
704
- await ctx.move?.(params.x, params.y);
704
+ await ctx.move?.(params.x, params.y, {modifiers:params.modifiers ?? 0});
705
705
  return {};
706
706
  }
707
707
  if (params.type === "mouseWheel") {
@@ -709,9 +709,22 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
709
709
  return {};
710
710
  }
711
711
 
712
+ if (ctx.pointerButton && (params.type === 'mousePressed' || params.type === 'mouseReleased')) {
713
+ const navigation = await ctx.pointerButton(params.x,params.y,
714
+ params.button==='right'?273:params.button==='middle'?274:272,
715
+ params.type==='mousePressed'?1:0,
716
+ {clickCount:params.clickCount,modifiers:params.modifiers});
717
+ if (navigation?.url) {
718
+ navigateTarget(state,target,navigation.url,emit,{addHistory:true});
719
+ target.title=navigation.title ?? await getContextTitle(ctx,target);
720
+ target.history[target.historyIndex].title=target.title;
721
+ if(target.screencast)await pushScreencastFrame(ctx,state,target);
722
+ }
723
+ return {};
724
+ }
712
725
  if (params.type === "mouseReleased") {
713
726
  target.focusedSelector = target.pendingSelector;
714
- applyClick(target, target.pendingSelector);
727
+ if (!params.button || params.button === "left") applyClick(target, target.pendingSelector);
715
728
  const navigation = await ctx.click?.(params.x, params.y, {
716
729
  button: params.button,
717
730
  clickCount: params.clickCount,
@@ -0,0 +1,10 @@
1
+ // Adapter for jsmdcui's unchanged PowerShell clipboard functions.
2
+ export function runSync(command, options = {}) {
3
+ try {
4
+ const p = Bun.spawnSync(command, {
5
+ stdin: options.stdin == null ? 'ignore' : Buffer.from(options.stdin),
6
+ stdout: options.stdout ?? 'pipe', stderr: 'pipe', timeout: options.timeout ?? 2000,
7
+ });
8
+ return {ok:p.success, stdout:p.stdout?.toString() ?? '', stderr:p.stderr?.toString() ?? ''};
9
+ } catch (error) { return {ok:false, stdout:'', stderr:String(error)}; }
10
+ }
@@ -0,0 +1,22 @@
1
+ // Functions copied verbatim from jsmdcui/src/platform/clipboard.js (MIT).
2
+ // Keep command spelling, quoting and trailing-newline handling unchanged.
3
+ import { runSync } from "./clipboard-command.js";
4
+
5
+ function powershellClipboard(shell) {
6
+ return {
7
+ name: "powershell",
8
+ // Get-Clipboard -Raw appends \r\n to stdout; strip exactly one trailing line ending.
9
+ read: () => outputOrThrow(runSync([shell, "-NoProfile", "-Command", "Get-Clipboard -Raw"], {})).replace(/\r?\n$/, ""),
10
+ write: (text) => {
11
+ text=(text+'').replaceAll("'","''") ;
12
+ return runSync([shell, "-NoProfile", "-Command", `Set-Clipboard '${text}'`], { stdout: "ignore" }).ok ;
13
+ },
14
+ };
15
+ }
16
+
17
+ function outputOrThrow(result) {
18
+ if (!result.ok) throw new Error(result.stderr || result.stdout || "clipboard command failed");
19
+ return result.stdout;
20
+ }
21
+
22
+ export { powershellClipboard };
package/lib/clipboard.js CHANGED
@@ -1,9 +1,9 @@
1
+ import {powershellClipboard} from './clipboard-powershell.js';
1
2
  // Host-side text clipboard. No shell interpolation; page bytes go only to stdin.
2
3
  export function clipboardCommands(platform = process.platform) {
3
4
  return [
4
5
  ['xclip','-selection','clipboard'],
5
- ...(platform === 'win32' ? [['powershell.exe','-NoProfile','-NonInteractive','-Command',
6
- '[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())']]
6
+ ...(platform === 'win32' ? [['powershell.exe']]
7
7
  : platform === 'darwin' ? [['pbcopy']] : [['wl-copy'],['termux-clipboard-set']]),
8
8
  ];
9
9
  }
@@ -13,6 +13,10 @@ export async function writeClipboard(text, {which = Bun.which, spawn = Bun.spawn
13
13
  const executable=which(command[0]); if(!executable)continue;
14
14
  let child,timer;
15
15
  try {
16
+ if(command[0]==='powershell.exe') {
17
+ if(powershellClipboard(executable).write(text))return;
18
+ continue;
19
+ }
16
20
  child=spawn([executable,...command.slice(1)],{stdin:Buffer.from(text),stdout:'ignore',stderr:'ignore'});
17
21
  timer=setTimeout(()=>child.kill(9),3000);
18
22
  if(await child.exited === 0)return;
@@ -20,3 +24,34 @@ export async function writeClipboard(text, {which = Bun.which, spawn = Bun.spawn
20
24
  }
21
25
  throw Error('No working clipboard command (xclip preferred)');
22
26
  }
27
+
28
+ // Read only on an explicit paste request; never poll the system clipboard.
29
+ export async function readClipboard({which = Bun.which, spawn = Bun.spawn, platform = process.platform} = {}) {
30
+ const commands = [['xclip','-selection','clipboard','-o'],
31
+ ...(platform === 'win32' ? [['powershell.exe','-NoProfile','-Command','Get-Clipboard -Raw']]
32
+ : platform === 'darwin' ? [['pbpaste']] : [['wl-paste','--no-newline'],['termux-clipboard-get']])];
33
+ for (const command of commands) {
34
+ const executable = which(command[0]); if (!executable) continue;
35
+ let child, timer;
36
+ try {
37
+ if(command[0]==='powershell.exe') {
38
+ const text=powershellClipboard(executable).read();
39
+ if(Buffer.byteLength(text)>1024*1024)throw Error('Clipboard text exceeds limit');
40
+ return text;
41
+ }
42
+ child = spawn([executable,...command.slice(1)], {stdin:'ignore',stdout:'pipe',stderr:'ignore'});
43
+ timer = setTimeout(()=>child.kill(9),3000);
44
+ const chunks=[]; let size=0;
45
+ for await (const chunk of child.stdout) {
46
+ size += chunk.length;
47
+ if (size > 1024*1024) { child.kill(9); throw Error('Clipboard text exceeds limit'); }
48
+ chunks.push(Buffer.from(chunk));
49
+ }
50
+ if (await child.exited === 0) {
51
+ const text=Buffer.concat(chunks).toString();
52
+ return command[0]==='powershell.exe' ? text.replace(/\r?\n$/, '') : text;
53
+ }
54
+ } catch {} finally {clearTimeout(timer);}
55
+ }
56
+ throw Error('No working clipboard read command (xclip preferred)');
57
+ }
@@ -1,3 +1,7 @@
1
+ // qml handle begin: response classification
2
+ import {isQmlDocument, QML_MIME} from "./qml/format.js";
3
+ // qml handle end
4
+
1
5
  /**
2
6
  * What a top-level response looks like in the viewport. HTML is shown as
3
7
  * itself; anything else gets a small generated HTML document, as browsers do
@@ -73,6 +77,13 @@ function previewKind(name, type) {
73
77
  export function displayDocument({ url, contentType = "", body }) {
74
78
  const { type, charset } = parseContentType(contentType);
75
79
  const name = fileName(url);
80
+
81
+ // qml handle begin: preserve QML source for the renderer
82
+ if (body.byteLength <= DISPLAY_LIMIT && isQmlDocument(url, contentType)) {
83
+ return {source: decode(body, charset), contentType: QML_MIME};
84
+ }
85
+ // qml handle end
86
+
76
87
  if (HTML_TYPES.has(type) || (type === "" && looksLikeHtml(body))) {
77
88
  return { source: decode(body, charset), contentType: contentType || "text/html" };
78
89
  }
@@ -1,3 +1,8 @@
1
+ // qml handle begin: select a dedicated local application renderer
2
+ import {isQmlDocument} from './qml/format.js';
3
+ // Local QML uses a dedicated host renderer.
4
+ // qml handle end
5
+
1
6
  /**
2
7
  * Headless shell controller.
3
8
  *
@@ -7,8 +12,8 @@
7
12
  * runs page content itself.
8
13
  *
9
14
  * Sandbox flags: `--no-sandbox` is accepted for Chromium compatibility (casty
10
- * always passes it) but ignored. Only `--dangerously-allow-host-js` relaxes
11
- * the renderer sandbox, for every page.
15
+ * always passes it) but ignored. `--dangerously-allow-host-js` relaxes
16
+ * the renderer sandbox for every page.
12
17
  */
13
18
  import { CdpServer } from "./cdp-server.js";
14
19
  import { DEFAULT_DOWNLOAD_DIR, isAttachment, saveDownload, suggestedFilename } from "./download.js";
@@ -80,12 +85,14 @@ export async function runHeadlessShell(args = Bun.argv.slice(2)) {
80
85
  * anything that needs a renderer waits for it.
81
86
  *
82
87
  * @param {{ allowHostJs?: boolean, spareRenderer?: boolean, network?: NetworkService, rendererTimeout?: number,
83
- * waitForRenderer?: boolean, mobile?: boolean, webkit?: boolean }} options
88
+ * waitForRenderer?: boolean, mobile?: boolean, webkit?: boolean,
89
+ * clipboard?: {read: Function, write: Function} }} options
84
90
  * WebKit identity defaults on; webkit: false selects Chromium. Mobile takes priority.
85
91
  */
86
92
  export async function createBrowser({
87
93
  allowHostJs = false, spareRenderer = true, network = new NetworkService(), rendererTimeout = 30_000, mobile = false,
88
94
  waitForRenderer = true, webkit = true,
95
+ clipboard = null,
89
96
  } = {}) {
90
97
  // Explicit presentation profiles fix the identity for requests and scripts.
91
98
  const initialUserAgent = mobile ? MOBILE_USER_AGENT : webkit ? WEBKIT_USER_AGENT : DESKTOP_USER_AGENT;
@@ -158,10 +165,18 @@ export async function createBrowser({
158
165
  }
159
166
 
160
167
  function createHost() {
168
+ let args = [...(allowHostJs ? [DANGEROUSLY_ALLOW_HOST_JS] : []), ...(mobile ? [MOBILE_FLAG] : [])];
169
+
170
+ // qml handle begin: internal options for a dedicated application renderer
171
+ if (arguments[0]?.qmlHost) args = ["--buninu-qml-host", ...args];
172
+ // qml handle end
173
+
161
174
  const host = new RendererHost({
175
+ // Native frontends supply one clipboard for page APIs and input defaults.
176
+ ...(clipboard?{clipboardRead:()=>clipboard.read(),clipboardWrite:text=>clipboard.write(text)}:{}),
162
177
  network,
163
178
  timeout: rendererTimeout,
164
- args: [...(allowHostJs ? [DANGEROUSLY_ALLOW_HOST_JS] : []), ...(mobile ? [MOBILE_FLAG] : [])],
179
+ args,
165
180
  // A replacement renderer starts blank; bring the current page back.
166
181
  onRestart: async (invoke) => {
167
182
  await replayState(invoke);
@@ -225,10 +240,22 @@ export async function createBrowser({
225
240
 
226
241
  /** The renderer for a URL's origin, swapping processes on an origin change. */
227
242
  async function rendererFor(url) {
228
- const key = originKey(url);
243
+ let key = originKey(url);
244
+ let acquireHost = () => {
245
+ const next = spare ?? createHost();
246
+ spare = null;
247
+ return next;
248
+ };
249
+
250
+ // qml handle begin: never reuse application realms or take an ordinary spare
251
+ if (arguments[1]?.qmlHost) {
252
+ key = Symbol("qml document");
253
+ acquireHost = () => createHost({ qmlHost: true });
254
+ }
255
+ // qml handle end
256
+
229
257
  if (current && key === currentKey) return current;
230
- const next = spare ?? createHost();
231
- spare = null;
258
+ const next = acquireHost();
232
259
  await next.start();
233
260
  await replayState((method, ...params) => next.call(method, ...params));
234
261
  const previous = current;
@@ -307,7 +334,18 @@ export async function createBrowser({
307
334
  writeLog("navigation", `${url}: ${document.error ?? `HTTP ${document.status ?? 200}`}`
308
335
  + `${document.url === url ? "" : ` -> ${document.url}`} in ${Math.round(fetched - started)}ms`);
309
336
  if (!invoke) await rendererReady;
310
- const host = invoke ? current : await rendererFor(document.url);
337
+ let selectRenderer = rendererFor;
338
+
339
+ // qml handle begin: host privileges require a local top-level QML document
340
+ const qmlHost = !document.error && new URL(document.url).protocol === "file:"
341
+ && isQmlDocument(document.url, document.contentType);
342
+ // A restarted renderer may restore a response of a different type.
343
+ const restoreChangedType = invoke && (typeof currentKey === "symbol") !== qmlHost;
344
+ if (restoreChangedType) invoke = null;
345
+ selectRenderer = url => rendererFor(url, { qmlHost });
346
+ // qml handle end
347
+
348
+ const host = invoke ? current : await selectRenderer(document.url);
311
349
  let generation;
312
350
  try {
313
351
  generation = await (invoke ?? ((method, ...params) => host.call(method, ...params)))("loadDocument", document);
@@ -389,6 +427,7 @@ export async function createBrowser({
389
427
  }
390
428
 
391
429
  const context = {
430
+ get url() { return currentUrl; },
392
431
  navigate,
393
432
  get userAgent() { return userAgent ?? initialUserAgent; },
394
433
  setDownloadBehavior({ behavior = "allow", downloadPath = DEFAULT_DOWNLOAD_DIR, eventsEnabled = false } = {}) {
@@ -477,11 +516,14 @@ export async function createBrowser({
477
516
  lifecycleState() {
478
517
  return [...reachedLifecycle(committed.host, committed.generation)];
479
518
  },
480
- move(x, y) {
481
- return call("move", x / pageScale(), y / pageScale());
519
+ move(x, y, options = {}) {
520
+ return call("move", x / pageScale(), y / pageScale(), options);
521
+ },
522
+ pointerButton(x, y, button, state, options = {}) {
523
+ return this.click(x, y, {...options, button:button===273?'right':button===274?'middle':'left', phase:state?'down':'up'});
482
524
  },
483
- async click(x, y) {
484
- const clicked = await call("click", x / pageScale(), y / pageScale());
525
+ async click(x, y, options = {}) {
526
+ const clicked = await call("click", x / pageScale(), y / pageScale(), options);
485
527
  const href = typeof clicked === "string" ? clicked : clicked?.url;
486
528
  if (typeof href !== "string") return null;
487
529
  if (clicked.fragmentFrom) {