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,126 @@
1
+ // Serialized into the QML realm. Snapshot ownership stays in ordinary canvases.
2
+ export function createQmlTextures(api) {
3
+ const { elements, state, effect, untrack, signal, cleanup } = api;
4
+ const entries = new WeakMap(), refs = new WeakMap(), pending = new Set();
5
+ const reference = value => {
6
+ let ref = refs.get(value);
7
+ if (!ref) refs.set(value, ref = state({ count: 0, hidden: 0 }));
8
+ return ref;
9
+ };
10
+ function attach(value, delta, hide) {
11
+ if (!value) return;
12
+ const ref = reference(value);
13
+ ref.count += delta;
14
+ if (hide) ref.hidden += delta;
15
+ const element = elements.get(value);
16
+ if (element) {
17
+ if (ref.count) element.setAttribute('data-qml-texture-source', '');
18
+ else element.removeAttribute('data-qml-texture-source');
19
+ if (ref.hidden || (!value.visible && ref.count)) element.setAttribute('data-qml-source-hidden', '');
20
+ else element.removeAttribute('data-qml-source-hidden');
21
+ }
22
+ }
23
+ function make(v, type, element) {
24
+ if (type !== 'ShaderEffectSource') return;
25
+ if (!api.host) throw Error('ShaderEffectSource requires a local QML host renderer');
26
+ Object.assign(v, { sourceItem: null, sourceRect: { x: 0, y: 0, width: 0, height: 0 },
27
+ mipmap:false, smooth:true, textureSize: { width: 0, height: 0 }, hideSource: false, live: true,
28
+ scheduledUpdateCompleted: signal() });
29
+ const canvas = document.createElement('canvas');
30
+ canvas.width = canvas.height = 1;
31
+ canvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none';
32
+ canvas.setAttribute('data-qml-texture', '');
33
+ element.appendChild(canvas);
34
+ const entry = { canvas, revision: state({ value: 0 }), ready: false };
35
+ entries.set(v, entry);
36
+ let source = null, hiding = false, observer, frame, disposed = false, requested = true;
37
+ const queue = () => {
38
+ if (!disposed && frame === undefined) { pending.add(capture); frame = requestAnimationFrame(capture); }
39
+ };
40
+ const capture = () => {
41
+ if (frame !== undefined) cancelAnimationFrame(frame);
42
+ pending.delete(capture);
43
+ frame = undefined;
44
+ if (disposed || !source || !requested) return;
45
+ const input = elements.get(source);
46
+ if (!input?.isConnected || !(source.width > 0 && source.height > 0)) return;
47
+ const rect = v.sourceRect;
48
+ const sw = rect.width > 0 ? rect.width : source.width;
49
+ const sh = rect.height > 0 ? rect.height : source.height;
50
+ const w = Math.ceil(v.textureSize.width > 0 ? v.textureSize.width : sw);
51
+ const h = Math.ceil(v.textureSize.height > 0 ? v.textureSize.height : sh);
52
+ if (!(w > 0 && h > 0) || w > 8192 || h > 8192 || w * h > 16777216)
53
+ throw Error('QML texture exceeds snapshot size limit');
54
+ requested = false;
55
+ if (canvas.width !== w) canvas.width = w;
56
+ if (canvas.height !== h) canvas.height = h;
57
+ const ctx = canvas.getContext('2d');
58
+ ctx.imageSmoothingEnabled=v.smooth!==false;
59
+ ctx.clearRect(0, 0, w, h);
60
+ ctx.drawImage(input, rect.width > 0 ? rect.x : 0, rect.height > 0 ? rect.y : 0, sw, sh, 0, 0, w, h);
61
+ entry.ready = true;
62
+ entry.revision.value++;
63
+ // Only signal once pixels are actually in the destination canvas.
64
+ v.scheduledUpdateCompleted();
65
+ };
66
+ v.scheduleUpdate = () => { requested = true; queue(); };
67
+ effect(() => {
68
+ const next = v.sourceItem, hide = v.hideSource;
69
+ v.smooth;
70
+ if(v.mipmap)throw Error("ShaderEffectSource mipmaps are not implemented");
71
+ const live = v.live;
72
+ const size = v.textureSize, rect = v.sourceRect;
73
+ size.width; size.height; rect.x; rect.y; rect.width; rect.height;
74
+ if (next) { next.width; next.height; next.visible; }
75
+ untrack(() => {
76
+ if (next !== source || hide !== hiding) {
77
+ observer?.disconnect();
78
+ attach(source, -1, hiding);
79
+ source = next; hiding = hide;
80
+ attach(source, 1, hiding);
81
+ if (source) {
82
+ const input = elements.get(source);
83
+ if (!input) throw Error('ShaderEffectSource.sourceItem must be an Item');
84
+ observer = new MutationObserver(() => {
85
+ if (v.live) { requested = true; queue(); }
86
+ });
87
+ observer.observe(input, { subtree: true, attributes: true, childList: true, characterData: true });
88
+ } else {
89
+ entry.ready = false;
90
+ canvas.width = canvas.height = 1;
91
+ entry.revision.value++;
92
+ }
93
+ }
94
+ if (live || !entry.ready || requested) { requested = true; queue(); }
95
+ });
96
+ });
97
+ cleanup(() => {
98
+ disposed = true;
99
+ pending.delete(capture);
100
+ observer?.disconnect();
101
+ if (frame !== undefined) cancelAnimationFrame(frame);
102
+ untrack(() => attach(source, -1, hiding));
103
+ canvas.width = canvas.height = 1;
104
+ canvas.remove();
105
+ entries.delete(v);
106
+ });
107
+ }
108
+ function keepLayout(v) {
109
+ const ref = reference(v);
110
+ const keep = ref.count > 0;
111
+ const element = elements.get(v);
112
+ const hidden = ref.hidden > 0 || (keep && !v.visible);
113
+ if (element && element.hasAttribute('data-qml-source-hidden') !== hidden) {
114
+ if (hidden) element.setAttribute('data-qml-source-hidden', '');
115
+ else element.removeAttribute('data-qml-source-hidden');
116
+ }
117
+ return keep;
118
+ }
119
+ function image(v) {
120
+ const entry = entries.get(v);
121
+ if (!entry) throw Error('ShapePath.fillItem currently requires ShaderEffectSource');
122
+ entry.revision.value;
123
+ return entry.ready ? entry.canvas : null;
124
+ }
125
+ return { make, keepLayout, image, flush: () => { for (const capture of [...pending]) capture(); } };
126
+ }
@@ -0,0 +1,32 @@
1
+ // Optional local-QML bridge to the frontend's standard Wayland clipboard.
2
+ // Ordinary HTML never needs this HTTP endpoint. Create it lazily before the
3
+ // first local QML navigation, including navigation after an about:blank launch.
4
+ export async function attachQmlClipboard(client,browser,done) {
5
+ const clipboard=client.clipboard;
6
+ if(!clipboard)return;
7
+ const navigate=browser.context.navigate;
8
+ let server,setup,closed=false;
9
+ const prepare=()=>setup??=(async()=>{
10
+ const token=crypto.randomUUID();
11
+ server=Bun.serve({hostname:'127.0.0.1',port:0,maxRequestBodySize:1024*1024+1024,
12
+ async fetch(request) {
13
+ if(request.method!=='POST'||new URL(request.url).pathname!=='/'+token||request.headers.has('origin'))return new Response(null,{status:403});
14
+ try {
15
+ const {operation,text}=await request.json();
16
+ if(operation==='read')return Response.json({text:await clipboard.read()});
17
+ if(operation!=='write'||typeof text!=='string')return new Response(null,{status:400});
18
+ await clipboard.write(text);return Response.json({});
19
+ }catch{return new Response(null,{status:503});}
20
+ }});
21
+ await browser.context.addInitScript(`globalThis.__qmlHost?.setClipboardEndpoint(${JSON.stringify(`http://127.0.0.1:${server.port}/${token}`)});`);
22
+ })();
23
+ const wrapped=async function(url,...args) {
24
+ if(!closed&&/^file:.*\.qml(?:[?#]|$)/i.test(String(url)))await prepare();
25
+ return navigate.call(this,url,...args);
26
+ };
27
+ browser.context.navigate=wrapped;
28
+ done.finally(()=>{
29
+ closed=true;server?.stop(true);
30
+ if(browser.context.navigate===wrapped)browser.context.navigate=navigate;
31
+ });
32
+ }
@@ -0,0 +1,16 @@
1
+ // Optional frontend adapter; xdg_toplevel size hints, not an application check.
2
+ export function qmlWindowConstraints(client,window,context) {
3
+ let previous='';
4
+ return async()=>{
5
+ if(!previous&&!/\.qml(?:[?#]|$)/i.test(context.url||''))return;
6
+ const hints=await context.evaluate(`(()=>{const elements=[...document.querySelectorAll('[data-qml-window-min]')];const e=elements.find(e=>e.style.display!=='none');return e?[e.getAttribute('data-qml-window-min'),e.getAttribute('data-qml-window-max')]:null;})()`);
7
+ const key=JSON.stringify(hints);if(previous===key)return;
8
+ const parse=value=>{try{return JSON.parse(value).map(n=>Number.isFinite(n)?Math.max(0,Math.min(8192,Math.ceil(n))):0);}catch{return [0,0];}};
9
+ const min=parse(hints?.[0]),max=parse(hints?.[1]);
10
+ for(let i=0;i<2;i++)if(max[i]&&max[i]<min[i])max[i]=min[i];
11
+ client.request(window.toplevel,'set_min_size',...min);
12
+ client.request(window.toplevel,'set_max_size',...max);
13
+ client.request(window.surface,'commit');
14
+ previous=key;
15
+ };
16
+ }
@@ -0,0 +1,70 @@
1
+ // Local WorkerScript application code executes on its own Bun worker thread.
2
+ // Qt.include uses one script global scope; it is not the QML import resolver.
3
+ import { parentPort, workerData } from 'node:worker_threads';
4
+ import { readFileSync } from 'node:fs';
5
+ import { createContext, Script } from 'node:vm';
6
+ let current = workerData.url,
7
+ bytes = 0,
8
+ depth = 0;
9
+ const context = createContext({
10
+ console,
11
+ WorkerScript: {
12
+ onMessage: null,
13
+ sendMessage: (message) =>
14
+ parentPort.postMessage({ kind: 'message', message }),
15
+ },
16
+ });
17
+ const run = (url) => {
18
+ if (new URL(url).protocol !== 'file:')
19
+ throw Error('WorkerScript imports must be local files');
20
+ if (++depth > 48) throw Error('WorkerScript include depth exceeded');
21
+ const previous = current;
22
+ current = url;
23
+ try {
24
+ const source = readFileSync(new URL(url), 'utf8');
25
+ bytes += Buffer.byteLength(source);
26
+ if (bytes > 4 * 1024 * 1024)
27
+ throw Error('WorkerScript source limit exceeded');
28
+ // Like Qt.include in a WorkerScript, directives supply no import aliases.
29
+ const code = source.replace(
30
+ /^\s*\.(?:pragma\s+library|import\s+[^\n]+)\s*$/gm,
31
+ '',
32
+ );
33
+ new Script(code, { filename: url }).runInContext(context);
34
+ } finally {
35
+ current = previous;
36
+ depth--;
37
+ }
38
+ };
39
+ context.Qt = {
40
+ include: (relative, callback) => {
41
+ let result;
42
+ try {
43
+ run(new URL(relative, current).href);
44
+ result = { status: 0 };
45
+ } catch (exception) {
46
+ result = { status: 3, exception };
47
+ }
48
+ callback?.(result);
49
+ return result;
50
+ },
51
+ };
52
+ try {
53
+ run(current);
54
+ parentPort.postMessage({ kind: 'ready' });
55
+ parentPort.on('message', (message) => {
56
+ try {
57
+ context.WorkerScript.onMessage?.(message);
58
+ } catch (error) {
59
+ parentPort.postMessage({
60
+ kind: 'error',
61
+ error: String(error.stack || error),
62
+ });
63
+ }
64
+ });
65
+ } catch (error) {
66
+ parentPort.postMessage({
67
+ kind: 'error',
68
+ error: String(error.stack || error),
69
+ });
70
+ }
@@ -1,6 +1,8 @@
1
1
  import { extractSvg } from "./svg.js";
2
2
  import { computeElementStyle, controlKind } from "../style/computed-style.js";
3
3
 
4
+ import {textInputSelection} from '../renderer/text-input-paint.js';
5
+
4
6
  const PLACEHOLDER_COLOR = "rgba(117, 117, 117, 1)";
5
7
  const CHECKED_COLOR = "rgba(0, 117, 255, 1)";
6
8
  const DEFAULT_SUMMARY = Object.freeze({ tagName: "SUMMARY", getAttribute: () => null, hasAttribute: () => false });
@@ -99,6 +101,7 @@ export class RenderTreeBuilder {
99
101
  let keys = this.#pseudoKeys.get(element);
100
102
  if (!keys) this.#pseudoKeys.set(element, keys = { before: {}, after: {}, beforeText: {}, afterText: {} });
101
103
  keys.value ??= {};
104
+ const controlType=boxType(style)==="inline"?"inline-block":boxType(style);
102
105
  let boxStyle = style;
103
106
  let text = "";
104
107
  let textStyle = style;
@@ -134,10 +137,22 @@ export class RenderTreeBuilder {
134
137
  textStyle = Object.freeze({ ...style, color: PLACEHOLDER_COLOR });
135
138
  }
136
139
  }
140
+
141
+ if (control === "text" || control === "textarea") {
142
+ const selected = textInputSelection(element, text, textStyle, (part, type, properties, domNode) => {
143
+ keys.inputSelection ??= {};
144
+ keys.inputSelection[part] ??= {};
145
+ return this.#node(domNode, type, nodesById, properties, keys.inputSelection[part]);
146
+ });
147
+ if (selected) return this.#node(element, controlType, nodesById, {
148
+ tagName: element.tagName, style: boxStyle, children: Object.freeze(selected),
149
+ });
150
+ }
151
+
137
152
  const children = text
138
153
  ? [this.#node(element, "text", nodesById, { text, style: textStyle, pseudo: "value", children: Object.freeze([]) }, keys.value)]
139
154
  : [];
140
- return this.#node(element, "inline-block", nodesById, {
155
+ return this.#node(element, controlType, nodesById, {
141
156
  tagName: element.tagName,
142
157
  style: boxStyle,
143
158
  children: Object.freeze(children),
@@ -21,6 +21,7 @@ const isCanvasSizeAttribute = (node, name, namespace = null) => !namespace
21
21
  && node.namespaceURI === "http://www.w3.org/1999/xhtml" && node.tagName === "CANVAS"
22
22
  && (name === "width" || name === "height");
23
23
  const REFLECTED_PROPERTIES = [
24
+ "selectionStart", "selectionEnd", "selectionDirection", "maxLength",
24
25
  "value", "checked", "disabled", "type", "name", "href", "src", "alt", "title",
25
26
  "placeholder", "selected", "readOnly", "required", "min", "max", "step", "rel", "target",
26
27
  "htmlFor", "lang", "dir", "tabIndex", "hidden", "defaultValue", "action", "method",
@@ -204,6 +205,8 @@ export class BindingRealm {
204
205
  }
205
206
 
206
207
  /** The page's Range for an engine Range (one wrapper each). */
208
+ unwrapRange(range) { return RangeBinding.rangeOf(range); }
209
+
207
210
  wrapRange(range) {
208
211
  if (!range) return null;
209
212
  let wrapper = this.#ranges.get(range);
@@ -1097,6 +1100,16 @@ class ElementBinding extends ParentBinding {
1097
1100
 
1098
1101
  get validity() { return this.#validity ??= realmOf(this).wrapValidity(nodeOf(this).validity); }
1099
1102
 
1103
+ setSelectionRange(start, end, direction = 'none') {
1104
+ if(arguments.length<2)throw new TypeError('setSelectionRange requires start and end');
1105
+ nodeOf(this).setSelectionRange(Number(start),Number(end),String(direction));touch(this);
1106
+ }
1107
+ setRangeText(replacement, ...args) {
1108
+ if(!arguments.length)throw new TypeError('setRangeText requires replacement');
1109
+ nodeOf(this).setRangeText(String(replacement),...args.map((v,i)=>i<2?Number(v):String(v)));touch(this);
1110
+ }
1111
+ select() {nodeOf(this).select();touch(this);}
1112
+
1100
1113
  setCustomValidity(message) {
1101
1114
  if (!arguments.length) throw new TypeError("setCustomValidity requires a message");
1102
1115
  nodeOf(this).setCustomValidity(String(message));
@@ -1361,7 +1374,7 @@ function isMedia(wrapper) {
1361
1374
 
1362
1375
  /** on* event handler properties (element.onclick = fn, script.onload = fn). */
1363
1376
  export const EVENT_HANDLER_NAMES = Object.freeze([
1364
- "click", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
1377
+ "click", "auxclick", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
1365
1378
  "keydown", "keyup", "keypress", "beforeinput", "input", "change", "submit", "reset", "focus", "blur", "load", "error",
1366
1379
  "abort", "scroll", "resize", "wheel", "contextmenu", "touchstart", "touchend", "touchmove",
1367
1380
  "pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange", "slotchange", "toggle", "invalid",
@@ -1685,6 +1698,8 @@ class DocumentBinding extends ParentBinding {
1685
1698
  return realmOf(this).wrap(nodeOf(this).createDocumentFragment());
1686
1699
  }
1687
1700
 
1701
+ getSelection() { return realmOf(this).selection?.() ?? null; }
1702
+
1688
1703
  createRange() {
1689
1704
  return realmOf(this).wrapRange(nodeOf(this).createRange());
1690
1705
  }
@@ -14,6 +14,10 @@
14
14
  * whole WASM heap. The copy handed back is only the requested rectangle.
15
15
  */
16
16
 
17
+ // qml handle begin: consume local-QML DOM snapshots without exposing native images
18
+ import { drawQmlDomImage } from "../qml/canvas-dom.js";
19
+ // qml handle end
20
+
17
21
  let CanvasKit = null;
18
22
 
19
23
  /** The CanvasKit `loadGraphics` instantiated. Page canvases share it. */
@@ -225,6 +229,15 @@ function open(node) {
225
229
  if (property === "drawImage" && resolveImage) {
226
230
  source = resolveImage(args[0]);
227
231
  if (!source?.image) return;
232
+
233
+ // qml handle begin: temporary DOM images have synchronous ownership
234
+ if (drawQmlDomImage(source, state.node.deref(), context, args)) {
235
+ if (source.originClean === false) state.originClean = false;
236
+ changed(state);
237
+ return;
238
+ }
239
+ // qml handle end
240
+
228
241
  args[0] = source.image;
229
242
  }
230
243
  const result = context[property](...args);
@@ -333,3 +346,19 @@ function copyImageData(image) {
333
346
  }
334
347
  return { width: image.width, height: image.height, data, colorSpace: "srgb" };
335
348
  }
349
+
350
+ // qml handle begin: software fragment effects draw into the existing canvas surface
351
+ export function paintQmlCanvas(node, draw) {
352
+ if (!canvasContext(node, "2d")) throw Error('Canvas renderer is unavailable');
353
+ const state = surfaces.get(node).state;
354
+ if (!state.raster) throw Error('Canvas surface is unavailable');
355
+ const canvas = state.raster.getCanvas();
356
+ canvas.save();
357
+ try {
358
+ const inverse = CanvasKit.Matrix.invert(canvas.getTotalMatrix());
359
+ if (!inverse) throw Error("Noninvertible QML canvas transform");
360
+ canvas.concat(inverse); draw(canvas);
361
+ }
362
+ finally { canvas.restore(); changed(state); }
363
+ }
364
+ // qml handle end
@@ -0,0 +1,19 @@
1
+ // Keep engine nodes/ranges behind the same DOM wrappers as the rest of the realm.
2
+ export function selectionBinding(window, realm, nodeOf, changed) {
3
+ const native=()=>window.getSelection();
4
+ const api={};
5
+ for(const key of ['anchorOffset','focusOffset','isCollapsed','rangeCount','type'])Object.defineProperty(api,key,{get:()=>native()[key]});
6
+ for(const key of ['anchorNode','focusNode'])Object.defineProperty(api,key,{get:()=>realm.wrap(native()[key])});
7
+ api.toString=()=>native().toString();
8
+ api.getRangeAt=i=>realm.wrapRange(native().getRangeAt(i));
9
+ const mutate=(name,args)=>{const result=native()[name](...args);changed();return result;};
10
+ for(const name of ['removeAllRanges','empty','collapseToStart','collapseToEnd','deleteFromDocument'])api[name]=()=>mutate(name,[]);
11
+ api.collapse=api.setPosition=(node,offset=0)=>mutate('collapse',[nodeOf(node),offset]);
12
+ api.extend=(node,offset=0)=>mutate('extend',[nodeOf(node),offset]);
13
+ api.setBaseAndExtent=(a,ao,f,fo)=>mutate('setBaseAndExtent',[nodeOf(a),ao,nodeOf(f),fo]);
14
+ api.selectAllChildren=node=>mutate('selectAllChildren',[nodeOf(node)]);
15
+ api.containsNode=(node,partial=false)=>native().containsNode(nodeOf(node),partial);
16
+ api.addRange=range=>mutate('addRange',[realm.unwrapRange(range)]);
17
+ api.removeRange=range=>mutate('removeRange',[realm.unwrapRange(range)]);
18
+ return api;
19
+ }
@@ -1,4 +1,5 @@
1
- import {writeClipboard} from '../clipboard.js';
1
+ import { RENDER_INTERVAL_FLAG, renderMinInterval } from "./render-interval.js";
2
+ import {writeClipboard, readClipboard} from '../clipboard.js';
2
3
  /**
3
4
  * Controller-side handle for a renderer process.
4
5
  *
@@ -16,6 +17,7 @@ import {writeClipboard} from '../clipboard.js';
16
17
  import {listenCompositor} from "./compositor-channel.js";
17
18
  import {RawFrames} from "../paint/raw-frames.js";
18
19
  import { PulseClient } from "../audio/pulse-client.js";
20
+ import { SharedAudioConnection } from "../audio/shared-connection.js";
19
21
  import { logEnabled, pipeToLog, RENDERER_LOG_FLAG, writeLog } from "../log.js";
20
22
  import { NetworkService } from "../network/network-service.js";
21
23
  import { COMPOSITOR_FLAG, FULL_STYLE_FLAG, VERIFY_STYLE_FLAG } from "./flags.js";
@@ -33,14 +35,9 @@ const MAX_RESOURCE_BYTES = 32 * 1024 * 1024;
33
35
  // Audio output: one PulseAudio native-protocol connection (jspulse or a
34
36
  // PulseAudio daemon on 127.0.0.1:4713) shared by every renderer, opened on
35
37
  // first use. Renderers cannot open sockets, so their audio comes through here.
36
- let pulse = null;
37
- function pulseClient() {
38
- pulse ??= PulseClient.connect().catch((error) => {
39
- pulse = null;
40
- throw new Error(`no PulseAudio server at 127.0.0.1:4713 (start jspulse): ${error.message}`);
41
- });
42
- return pulse;
43
- }
38
+ const pulse = new SharedAudioConnection(() => PulseClient.connect().catch((error) => {
39
+ throw new Error(`no PulseAudio server at 127.0.0.1:4713 (start jspulse): ${error.message}`);
40
+ }));
44
41
  const MAX_AUDIO_STREAMS = 8;
45
42
  const MAX_AUDIO_CHUNK = 4 * 1024 * 1024;
46
43
 
@@ -55,11 +52,13 @@ const LIFECYCLE_NAMES = new Set(["DOMContentLoaded", "load"]);
55
52
  const DEFAULT_ENTRY = Bun.fileURLToPath(new URL("./main.js", import.meta.url));
56
53
 
57
54
  export class RendererHost {
55
+ #clipboardRead = readClipboard;
58
56
  #clipboardWrite = writeClipboard;
59
57
  #clipboardUntil = 0;
60
58
  #clipboardPending = false;
61
59
  #rawFrames = null;
62
60
  #startEpoch = 0;
61
+ #closed = false;
63
62
  #rejectStartup = null;
64
63
  #entry;
65
64
  #args;
@@ -114,8 +113,10 @@ export class RendererHost {
114
113
  network = new NetworkService({ fetch: fetchImpl }), onFrame = null, onLifecycle = null,
115
114
  // A dedicated loopback socket works independently of the main thread on all platforms.
116
115
  compositor = entry === DEFAULT_ENTRY,
116
+ clipboardRead = readClipboard,
117
117
  clipboardWrite = writeClipboard,
118
118
  } = {}) {
119
+ this.#clipboardRead = clipboardRead;
119
120
  this.#clipboardWrite = clipboardWrite;
120
121
  this.#onFrame = onFrame;
121
122
  this.#onLifecycle = onLifecycle;
@@ -131,6 +132,7 @@ export class RendererHost {
131
132
 
132
133
  /** Starts the renderer if it is not running and waits until it is ready. */
133
134
  start() {
135
+ if (this.#closed) return Promise.reject(Error("Renderer is closed"));
134
136
  if (this.#ready) return this.#ready;
135
137
  let resolveReady;
136
138
  let rejectReady;
@@ -149,13 +151,20 @@ export class RendererHost {
149
151
  ()=>this.#closeChannel(channel));
150
152
  if (epoch !== this.#startEpoch) { channel?.transport.close(); throw Error('Renderer startup cancelled'); }
151
153
  this.#channel = channel;
154
+ // No inherited environment: nothing from the controller leaks in.
155
+ // Windows programs need SystemRoot to load system libraries.
156
+ let env = process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {};
157
+
158
+ // qml handle begin: applications need HOME/PATH/runtime paths, web pages do not
159
+ if (this.#args.includes("--buninu-qml-host")) env = { ...process.env };
160
+ // qml handle end
161
+
152
162
  const child = Bun.spawn({
153
163
  cmd: [process.execPath, this.#entry, ...this.#args,
164
+ RENDER_INTERVAL_FLAG + renderMinInterval(process.env.BUNINU_RENDER_MIN_INTERVAL),
154
165
  ...(logging ? [RENDERER_LOG_FLAG] : []), ...(compositor ? [COMPOSITOR_FLAG, `--compositor-endpoint=${JSON.stringify(channel.transport.endpoint)}`] : []),
155
166
  ...(process.env.BUNINU_VERIFY_STYLE ? [VERIFY_STYLE_FLAG] : []), ...(process.env.BUNINU_FULL_STYLE ? [FULL_STYLE_FLAG] : [])],
156
- // No inherited environment: nothing from the controller leaks in.
157
- // Windows programs need SystemRoot to load system libraries.
158
- env: process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {},
167
+ env,
159
168
  // With BUNINU_LOG, output goes through the controller into the log file.
160
169
  stdio: ["ignore", logging ? "pipe" : "inherit", logging ? "pipe" : "inherit"],
161
170
  serialization: "advanced",
@@ -297,6 +306,7 @@ export class RendererHost {
297
306
  }
298
307
 
299
308
  close() {
309
+ this.#closed = true;
300
310
  this.#startEpoch++;
301
311
  this.#rejectStartup?.(Error("Renderer closed during startup")); this.#rejectStartup=null;
302
312
  this.#rawFrames?.close(); this.#rawFrames=null;
@@ -313,6 +323,7 @@ export class RendererHost {
313
323
  }
314
324
 
315
325
  async #call(method, params, timeout = this.#timeout) {
326
+ if (this.#closed) throw Error("Renderer is closed");
316
327
  if (!this.#child) await this.#restart();
317
328
  await this.start();
318
329
  const child = this.#child;
@@ -365,11 +376,23 @@ export class RendererHost {
365
376
  pending.reject(new Error(`renderer: ${String(message.error).split("\n")[0] || "unknown error"}`));
366
377
  }
367
378
  else pending.resolve(message.result);
368
- } else if (message.type === 'clipboard-write' && Number.isInteger(message.id)) {
379
+ } else if (['clipboard-read','clipboard-read-input'].includes(message.type) && Number.isInteger(message.id)) {
380
+ const reply = (text, error) => {if(this.#child===child) child.send({type:'clipboard-result',id:message.id,text,error});};
381
+ let secure=false;
382
+ try {const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));} catch {}
383
+ if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending) {
384
+ reply(undefined,'Clipboard read requires a recent user action in a secure document'); return;
385
+ }
386
+ this.#clipboardUntil=0; this.#clipboardPending=true;
387
+ Promise.resolve().then(()=>this.#clipboardRead()).then(text=>{
388
+ if(typeof text!=='string'||Buffer.byteLength(text)>1024*1024)throw Error('Clipboard text exceeds limit');
389
+ reply(text);
390
+ }).catch(()=>reply(undefined,'Unable to read system clipboard')).finally(()=>{this.#clipboardPending=false;});
391
+ } else if (['clipboard-write','clipboard-write-input'].includes(message.type) && Number.isInteger(message.id)) {
369
392
  const reply = error => { if(this.#child===child)child.send({type:'clipboard-result',id:message.id,...(error?{error}: {})}); };
370
393
  let secure=false;
371
394
  try { const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname)); } catch {}
372
- if (!secure || Date.now()>this.#clipboardUntil || this.#clipboardPending
395
+ if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending
373
396
  || typeof message.text!=='string' || Buffer.byteLength(message.text)>1024*1024) {
374
397
  reply('Clipboard write requires a recent user action in a secure document'); return;
375
398
  }
@@ -518,20 +541,24 @@ export class RendererHost {
518
541
  return;
519
542
  }
520
543
  const entry = { child, stream: null, early: [] };
521
- entry.opened = pulseClient()
522
- .then((client) => client.openPlayback({
544
+ entry.opened = pulse.acquire(child)
545
+ .then((client) => {
546
+ if (this.#audio.get(id) !== entry) throw Error("Audio owner closed");
547
+ return client.openPlayback({
523
548
  rate, channels, name: String(name ?? "audio").slice(0, 200),
524
549
  leadSeconds: name === "piano" ? 0.12 : undefined,
525
550
  onRequest: (bytes) => send({ event: "request", bytes }),
526
551
  onTiming: name === "piano" ? null : (timing) => send({ event: "timing", ...timing }),
527
552
  onTimingError: (error) => send({ event: "timing-error", message: String(error?.message ?? error) }),
528
- }))
553
+ });
554
+ })
529
555
  .then((stream) => {
530
556
  if (this.#audio.get(id) !== entry) return stream.close();
531
557
  entry.stream = stream;
532
558
  for (const data of entry.early.splice(0)) stream.write(data);
533
559
  send({ event: "ready" });
534
560
  }, (error) => {
561
+ if (this.#audio.get(id) !== entry) return;
535
562
  this.#audio.delete(id);
536
563
  send({ event: "error", message: String(error?.message ?? error) });
537
564
  });
@@ -539,6 +566,7 @@ export class RendererHost {
539
566
  }
540
567
 
541
568
  #closeAudio(child) {
569
+ pulse.release(child);
542
570
  for (const [id, entry] of this.#audio) {
543
571
  if (entry.child !== child) continue;
544
572
  this.#audio.delete(id);
@@ -1,3 +1,4 @@
1
+ import { RENDER_INTERVAL_FLAG, renderMinInterval } from "./render-interval.js";
1
2
  /**
2
3
  * Renderer process entry point.
3
4
  *
@@ -241,22 +242,27 @@ async function startScriptRewriter() {
241
242
 
242
243
  let nextClipboardId=1;
243
244
  const clipboardWrites=new Map();
244
- function clipboardWrite(text) {
245
+ function clipboardRead() { return clipboardWrite(undefined, 'clipboard-read'); }
246
+ function clipboardWrite(text, type = 'clipboard-write') {
245
247
  const id=nextClipboardId++;
246
248
  return new Promise((resolve,reject)=>{
247
249
  const timer=setTimeout(()=>{clipboardWrites.delete(id);reject(Error('Clipboard timed out'));},5000);
248
250
  clipboardWrites.set(id,{resolve,reject,timer});
249
- process.send({type:'clipboard-write',id,text});
251
+ process.send({type,id,text});
250
252
  });
251
253
  }
252
254
 
253
255
  const renderer = await createPageRenderer({
256
+ renderMinInterval: renderMinInterval(process.argv.find(arg => arg.startsWith(RENDER_INTERVAL_FLAG))?.slice(RENDER_INTERVAL_FLAG.length)),
254
257
  fetchResource,
255
258
  rewriteScript,
256
259
  requestNavigation: (url, download = null, fragmentFrom = null, post = null) => process.send({ type: "navigate", url, download, fragmentFrom, post }),
257
260
  requestDownload: ({ url, name, data }) => process.send({ type: "download-data", url, name, data }),
258
261
  pageFetch,
262
+ clipboardRead,
259
263
  clipboardWrite,
264
+ clipboardReadInput:()=>clipboardWrite(undefined,'clipboard-read-input'),
265
+ clipboardWriteInput:text=>clipboardWrite(text,'clipboard-write-input'),
260
266
  webSockets,
261
267
  audio,
262
268
  setCookie: (value) => process.send({ type: "cookie", value }),
@@ -281,11 +287,20 @@ const renderer = await createPageRenderer({
281
287
  videoDecoder: wasmpeg,
282
288
  });
283
289
  await warmUp();
284
- const allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
290
+ let allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
291
+ let hostReason = `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}`;
292
+
293
+ // qml handle begin: controller-selected local QML application process
294
+ if (process.argv.includes("--buninu-qml-host")) {
295
+ allowHostJs = true;
296
+ hostReason = "local QML application renderer";
297
+ }
298
+ // qml handle end
299
+
285
300
  const seccomp = allowHostJs
286
- ? { installed: false, reason: `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}` }
301
+ ? { installed: false, reason: hostReason }
287
302
  : installSeccomp();
288
- const ses = allowHostJs ? { installed: false, reason: `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}` } : lockdownRealm();
303
+ const ses = allowHostJs ? { installed: false, reason: hostReason } : lockdownRealm();
289
304
  renderer.setScripting(allowHostJs ? "host" : ses.installed ? "ses" : "off");
290
305
 
291
306
  /** Hardens the JavaScript realm so page scripts can run in SES compartments. */
@@ -340,7 +355,7 @@ process.on("message", async (message) => {
340
355
  if(message?.type==='clipboard-result') {
341
356
  const pending=clipboardWrites.get(message.id);if(!pending)return;
342
357
  clipboardWrites.delete(message.id);clearTimeout(pending.timer);
343
- if(message.error)pending.reject(Error(String(message.error)));else pending.resolve();
358
+ if(message.error)pending.reject(Error(String(message.error)));else pending.resolve(message.text);
344
359
  return;
345
360
  }
346
361