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
@@ -1,7 +1,7 @@
1
1
  const previousTargets = new WeakMap();
2
2
 
3
3
  /** Deliver pointer/mouse boundary events before movement, as a real pointer does. */
4
- export function dispatchPointerMove(window, realm, target, x, y) {
4
+ export function dispatchPointerMove(window, realm, target, x, y, options = {}) {
5
5
  const document = window.document;
6
6
  const previous = previousTargets.get(document) ?? null;
7
7
  previousTargets.set(document, target);
@@ -12,7 +12,9 @@ export function dispatchPointerMove(window, realm, target, x, y) {
12
12
  const pointer = type.startsWith('pointer');
13
13
  const Constructor = pointer ? window.PointerEvent : window.MouseEvent;
14
14
  const event = new Constructor(type, { clientX: x, clientY: y, relatedTarget, bubbles,
15
- composed: bubbles, cancelable: bubbles, button: pointer ? -1 : 0, buttons: 0,
15
+ composed: bubbles, cancelable: bubbles, button: pointer ? -1 : 0, buttons: options.buttons ?? 0,
16
+ altKey:!!(options.modifiers&1),ctrlKey:!!(options.modifiers&2),
17
+ metaKey:!!(options.modifiers&4),shiftKey:!!(options.modifiers&8),
16
18
  pointerId: 1, pointerType: 'mouse', isPrimary: true });
17
19
  if (realm) realm.dispatch(node, event); else node.dispatchEvent(event);
18
20
  };
@@ -25,5 +27,5 @@ export function dispatchPointerMove(window, realm, target, x, y) {
25
27
  }
26
28
  }
27
29
  send(target, 'pointermove', null);
28
- send(target, 'mousemove', null);
30
+ if (!options.suppressMouse) send(target, 'mousemove', null);
29
31
  }
@@ -0,0 +1,9 @@
1
+ export const RENDER_INTERVAL_FLAG = "--buninu-render-min-interval=";
2
+
3
+ // Validate in the controller, then pass only this number to the sandboxed child.
4
+ export function renderMinInterval(value, warn = console.warn) {
5
+ if (value === undefined) return 16;
6
+ if (/^\d+$/.test(value) && Number(value) <= 60000) return Number(value);
7
+ warn("buninu-browser: BUNINU_RENDER_MIN_INTERVAL must be an integer from 0 to 60000 ms; using 16 ms");
8
+ return 16;
9
+ }
@@ -0,0 +1,54 @@
1
+ import { computeElementStyle } from '../style/computed-style.js';
2
+ export const inputPresentation = new WeakMap();
3
+ export const inputOwner = new WeakMap();
4
+
5
+ const spans = new WeakMap();
6
+ const carets = new WeakMap();
7
+ export const inputCaretNode = element => carets.get(element);
8
+
9
+ // Form controls share native selection and caret presentation across frontends.
10
+ export function textInputSelection(element, text, style, make) {
11
+ const options=inputPresentation.get(element)||{};
12
+ const focused=element.ownerDocument.activeElement===element;
13
+ const value=String(element.value??'');
14
+ const position=at=>element.type==='password'?[...value.slice(0,at)].length:at;
15
+ const start=position(element.selectionStart??0),end=position(element.selectionEnd??0);
16
+ const raw=(focused||options.persistent)&&end>start ? {start,end,background:options.background||'#3874f2',color:options.selectedColor||'white'}:null;
17
+ const caretAt = (at, selected=false) => {
18
+ let span=carets.get(element);
19
+ if(!span){span=element.ownerDocument.createElement('span');carets.set(element,span);inputOwner.set(span,element);}
20
+ const height=options.height||parseFloat(style.fontSize)||16;
21
+ const color=options.color||style.color;
22
+ const on=!selected&&options.on!==false&&options.visible!==false;
23
+ span.style.cssText=`display:inline-block;width:0;height:${height}px;border-left:1px solid ${on?color:'transparent'};margin-right:-1px;vertical-align:text-bottom;pointer-events:none`;
24
+ return make('caret','inline-block',{tagName:'SPAN',style:computeElementStyle(span,style),children:Object.freeze([])},span);
25
+ };
26
+ if (!raw) {
27
+ if(!focused||element.disabled||options.visible===false)return null;
28
+ const node=(part,text)=>make(part,'text',{text,style,valueStart:part==='after'?start:0,pseudo:'value',children:Object.freeze([])},element);
29
+ return [node('before',text.slice(0,start)),caretAt(start),node('after',text.slice(start))];
30
+ }
31
+ const { background, color } = raw;
32
+ const from = Math.max(0, Math.min(text.length, start)),
33
+ to = Math.max(from, Math.min(text.length, end));
34
+ if (from === to) return null;
35
+ const textNode = (part, text, textStyle = style, domNode = element) => make(part, 'text', {
36
+ text, style: textStyle, valueStart:part==='selected'?from:part==='after'?to:0, pseudo: 'value', children: Object.freeze([]),
37
+ }, domNode);
38
+ let span = spans.get(element);
39
+ if (!span) { span = element.ownerDocument.createElement('span'); spans.set(element, span); inputOwner.set(span,element); }
40
+ span.style.pointerEvents = 'none';
41
+ span.style.backgroundColor = background;
42
+ span.style.color = color;
43
+ const selectedStyle = computeElementStyle(span, style);
44
+ const children = [];
45
+ if (from) children.push(textNode('before', text.slice(0, from)));
46
+ if(focused&&element.selectionDirection==='backward')children.push(caretAt(from,true));
47
+ children.push(make('selection', 'inline', {
48
+ tagName: 'SPAN', style: selectedStyle,
49
+ children: Object.freeze([textNode('selected', text.slice(from, to), selectedStyle, span)]),
50
+ }, span));
51
+ if(focused&&element.selectionDirection!=='backward')children.push(caretAt(to,true));
52
+ if (to < text.length) children.push(textNode('after', text.slice(to)));
53
+ return children;
54
+ }
@@ -0,0 +1,57 @@
1
+ // Shared editing defaults for HTML and QML's real input/textarea elements.
2
+ export function isTextInput(element) {
3
+ return !!element && !element.disabled && (element.tagName==='TEXTAREA' ||
4
+ element.tagName==='INPUT' && ['text','search','url','email','tel','password'].includes(element.type));
5
+ }
6
+ export const inputRange = element => ({text:String(element.value),start:element.selectionStart??0,end:element.selectionEnd??0});
7
+ export function selectInput(element,start,end=start) {
8
+ element.setSelectionRange(Math.min(start,end),Math.max(start,end),end<start?'backward':'forward');
9
+ }
10
+ const columns=new WeakMap();
11
+ export function moveInputSelection(element,event) {
12
+ if(!isTextInput(element)||event.altKey)return false;
13
+ const {text,start,end}=inputRange(element),ctrl=event.ctrlKey||event.metaKey,key=event.key;
14
+ if(ctrl&&key.toLowerCase()==='a'){selectInput(element,0,text.length);return true;}
15
+ if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(key))return false;
16
+ let at=element.selectionDirection==='backward'?start:end;
17
+ const anchor=at===start?end:start,multi=element.tagName==='TEXTAREA';
18
+ const lineStart=text.lastIndexOf('\n',at-1)+1;
19
+ if(key==='ArrowUp'||key==='ArrowDown') {
20
+ if(!multi)return false;
21
+ const previous=columns.get(element),column=previous?.at===at?previous.column:at-lineStart;
22
+ if(key==='ArrowUp') {
23
+ const stop=lineStart-1;at=stop<0?0:Math.min(text.lastIndexOf('\n',stop-1)+1+column,stop);
24
+ } else {
25
+ const next=text.indexOf('\n',at),stop=next<0?-1:text.indexOf('\n',next+1);
26
+ at=next<0?text.length:Math.min(next+1+column,stop<0?text.length:stop);
27
+ }
28
+ columns.set(element,{at,column});
29
+ } else {
30
+ columns.delete(element);
31
+ if(key==='Home')at=multi&&!ctrl?lineStart:0;
32
+ else if(key==='End'){const stop=text.indexOf('\n',at);at=multi&&!ctrl&&stop>=0?stop:text.length;}
33
+ else if(!event.shiftKey&&start!==end)at=key==='ArrowLeft'?start:end;
34
+ else if(key==='ArrowLeft') {
35
+ at-=[...text.slice(0,at)].pop()?.length??0;
36
+ if(ctrl){while(at>0&&/\s/u.test(text[at]))at--;while(at>0&&/[\p{L}\p{N}_]/u.test(text[at-1]))at--;}
37
+ } else {
38
+ at+=text.codePointAt(at)>0xffff?2:at<text.length?1:0;
39
+ if(ctrl){while(at<text.length&&/[\p{L}\p{N}_]/u.test(text[at]))at++;while(at<text.length&&/\s/u.test(text[at]))at++;}
40
+ }
41
+ }
42
+ selectInput(element,event.shiftKey?anchor:at,at);return true;
43
+ }
44
+ export async function inputClipboard(element,operation,{read,write,replace}) {
45
+ if(!isTextInput(element)||operation!=='copy'&&element.readOnly)return;
46
+ const range=inputRange(element);
47
+ if(operation==='copy'||operation==='cut') {
48
+ if(element.type==='password'||range.start===range.end)return;
49
+ await write(range.text.slice(range.start,range.end));
50
+ if(operation==='copy')return;
51
+ }
52
+ let text=operation==='paste'?await read():'';
53
+ if(String(element.value)!==range.text||element.selectionStart!==range.start||element.selectionEnd!==range.end||element.readOnly||!element.isConnected)return;
54
+ if(element.tagName!=='TEXTAREA')text=text.replace(/[\r\n]+/g,'');
55
+ if(element.maxLength>=0)text=text.slice(0,Math.max(0,element.maxLength-(range.text.length-range.end+range.start)));
56
+ replace(text,operation==='paste'?'insertFromPaste':'deleteByCut');
57
+ }
@@ -211,6 +211,7 @@ const INITIAL = Object.freeze({
211
211
  visibility: "visible",
212
212
  // Hit testing only (inherited): "none" lets clicks pass through.
213
213
  pointerEvents: "auto",
214
+ userSelect: "auto",
214
215
  // Breaking inside words (CSS Text 3 §5): only with these set.
215
216
  overflowWrap: "normal",
216
217
  wordBreak: "normal",
@@ -281,11 +282,11 @@ const UA_DECLARATIONS = new Map(Object.entries({
281
282
  * fills them in when checked.
282
283
  */
283
284
  const CONTROL_FONT = "font-size: 13.333px; font-family: sans-serif";
284
- const FIELD = `${CONTROL_FONT}; border: 1px solid #767676; background-color: white`;
285
- const BUTTON_LIKE = `${CONTROL_FONT}; border: 1px solid #767676; border-radius: 2px; padding: 1px 6px; background-color: #efefef; text-align: center`;
285
+ const FIELD = `${CONTROL_FONT}; color: black; border: 1px solid #767676; background-color: white`;
286
+ const BUTTON_LIKE = `${CONTROL_FONT}; color: black; border: 1px solid #767676; border-radius: 2px; padding: 1px 6px; background-color: #efefef; text-align: center`;
286
287
  const UA_CONTROL_DECLARATIONS = Object.freeze({
287
- textarea: parseDeclarations(`${FIELD}; font-family: monospace; padding: 2px; white-space: pre-wrap; overflow-wrap: break-word`),
288
- text: parseDeclarations(`${FIELD}; padding: 1px 2px; white-space: pre`),
288
+ textarea: parseDeclarations(`${FIELD}; font-family: monospace; padding: 2px; white-space: pre-wrap; overflow-wrap: break-word; overflow: auto`),
289
+ text: parseDeclarations(`${FIELD}; padding: 1px 2px; white-space: pre; overflow: hidden`),
289
290
  checkbox: parseDeclarations("width: 13px; height: 13px; margin: 3px 3px 3px 4px; border: 1px solid #767676; border-radius: 2px; background-color: white; box-sizing: border-box"),
290
291
  radio: parseDeclarations("width: 13px; height: 13px; margin: 3px 3px 0 5px; border: 1px solid #767676; border-radius: 50%; background-color: white; box-sizing: border-box"),
291
292
  button: parseDeclarations(`${BUTTON_LIKE}; white-space: pre`),
@@ -1413,6 +1414,10 @@ function setProperty(style, name, value, parentStyle = null) {
1413
1414
  if (["visible", "hidden", "collapse"].includes(value)) style.visibility = value === "collapse" ? "hidden" : value;
1414
1415
  break;
1415
1416
  // SVG-only values (visiblePainted, fill, ...) hit like auto for HTML boxes.
1417
+ case "user-select":
1418
+ case "-webkit-user-select":
1419
+ if (["auto", "none", "text", "all"].includes(value)) style.userSelect = value;
1420
+ break;
1416
1421
  case "pointer-events":
1417
1422
  if (/^[a-z]+$/i.test(value)) style.pointerEvents = value === "none" ? "none" : "auto";
1418
1423
  break;
@@ -2517,6 +2522,7 @@ function cssValue(style, name) {
2517
2522
  : style.lineHeight == null ? "normal" : `${style.lineHeight}px`,
2518
2523
  "list-style-type": style.listStyleType,
2519
2524
  "pointer-events": style.pointerEvents,
2525
+ "user-select": style.userSelect,
2520
2526
  "text-align": style.textAlign,
2521
2527
  "white-space": style.whiteSpace,
2522
2528
  })[name] ?? style[name];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-browser",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Buninu Browser is a browser engine written in JavaScript and WebAssembly for Bun.",
5
5
  "keywords": [
6
6
  "buninu",
@@ -0,0 +1,46 @@
1
+ import {test, expect} from 'bun:test';
2
+ import {SharedAudioConnection} from '../lib/audio/shared-connection.js';
3
+
4
+ test('audio connection survives one renderer closing, ends with the last owner, and reconnects', async () => {
5
+ let opened = 0, closed = 0;
6
+ const pool = new SharedAudioConnection(async () => ({id:++opened, close(){closed++;}}));
7
+ const a={}, b={};
8
+ expect(await pool.acquire(a)).toBe(await pool.acquire(b));
9
+ pool.release(a); await Promise.resolve();
10
+ expect(closed).toBe(0);
11
+ pool.release(b); await Promise.resolve();
12
+ expect(closed).toBe(1);
13
+ expect((await pool.acquire(a)).id).toBe(2);
14
+ pool.release(a); await Promise.resolve();
15
+ expect(closed).toBe(2);
16
+ });
17
+
18
+ test('closing during authentication does not close or clear a newer connection', async () => {
19
+ let resolveOld, closed = 0, calls = 0;
20
+ const pool = new SharedAudioConnection(() => ++calls===1
21
+ ? new Promise(resolve=>{resolveOld=resolve;})
22
+ : {close(){closed++;}});
23
+ const a={},b={};
24
+ const old=pool.acquire(a); await Promise.resolve();
25
+ pool.release(a);
26
+ const current=await pool.acquire(b);
27
+ resolveOld({close(){closed++;}}); await old; await Promise.resolve();
28
+ expect(closed).toBe(1);
29
+ expect(await pool.acquire(b)).toBe(current);
30
+ pool.release(b); await Promise.resolve();
31
+ expect(closed).toBe(2);
32
+ });
33
+
34
+ test('late authentication failure does not invalidate a newer connection', async () => {
35
+ let rejectOld, calls=0;
36
+ const pool=new SharedAudioConnection(()=>++calls===1
37
+ ? new Promise((_,reject)=>{rejectOld=reject;}) : {close(){}});
38
+ const a={},b={};
39
+ const old=pool.acquire(a); await Promise.resolve();
40
+ pool.release(a);
41
+ const current=await pool.acquire(b);
42
+ rejectOld(Error('old auth failed'));
43
+ await expect(old).rejects.toThrow('old auth failed');
44
+ expect(await pool.acquire(b)).toBe(current);
45
+ pool.release(b);
46
+ });
@@ -7,3 +7,11 @@ test("canvas bitmaps paint, update without DOM mutations, scale, reset and survi
7
7
  expect(stderr).toBe("");
8
8
  expect(exit).toBe(0);
9
9
  }, 15_000);
10
+
11
+ test("canvas damage matches full repaint with transparency, clipping and overlapping content", async () => {
12
+ const child = Bun.spawn([process.execPath, new URL("./fixtures/canvas-damage.js", import.meta.url).pathname],
13
+ { stdout: "pipe", stderr: "pipe" });
14
+ const [exit, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
15
+ expect(stderr).toBe("");
16
+ expect(exit).toBe(0);
17
+ }, 15_000);
@@ -33,3 +33,22 @@ test('a real button click writes through controller; page-only calls cannot over
33
33
  expect(await host.call('evaluate','window.copyStatus')).toBe('copied');
34
34
  }finally{host.close();}
35
35
  },20000);
36
+
37
+ test('Windows also uses Bun.which-resolved xclip before its platform fallback',async()=>{
38
+ const seen=[];
39
+ await writeClipboard('Windows 中文',{platform:'win32',which:name=>{seen.push(name);return name==='xclip'?'C:/bin/xclip.exe':null;},spawn:(args,opts)=>{expect(args).toEqual(['C:/bin/xclip.exe','-selection','clipboard']);expect(opts.stdin.toString()).toBe('Windows 中文');return {exited:Promise.resolve(0),kill(){}};}});
40
+ expect(seen).toEqual(['xclip']);
41
+ });
42
+
43
+ test('clipboard read is one-shot, preserves empty/text values, and prefers xclip on Windows too',async()=>{
44
+ const {readClipboard}=await import('../lib/clipboard.js');
45
+ const calls=[];let value='中文\n';
46
+ const options={platform:'win32',which:name=>name==='xclip'?'/bin/xclip':null,spawn:(args)=>{
47
+ calls.push(args);return {stdout:new Response(value).body,exited:Promise.resolve(0),kill(){}};
48
+ }};
49
+ expect(await readClipboard(options)).toBe('中文\n');
50
+ expect(calls).toEqual([['/bin/xclip','-selection','clipboard','-o']]);
51
+ value='';expect(await readClipboard(options)).toBe('');
52
+ await Bun.sleep(30);expect(calls).toHaveLength(2);
53
+ await expect(readClipboard({...options,which:()=>null})).rejects.toThrow('No working');
54
+ });
@@ -859,3 +859,15 @@ test("a linked style sheet's url()s resolve against the sheet", () => {
859
859
  )).toBe(`a { background: url(https://site.test/css/img/a.png) } b { background: url("https://site.test/b.png") } c { background: url(data:image/png;base64,AA) }
860
860
  d { background: url(https://cdn.test/d.png) } e { mask: url(#m) }`);
861
861
  });
862
+
863
+ test("form controls default to readable dark text but author colors still win", () => {
864
+ const {document,window}=parseHTMLDocument(`<body style="color:lightblue"><textarea></textarea><input><button>Button</button><select><option>Option</option></select><textarea id="inherit" style="color:inherit"></textarea><input id="custom" style="color:red"></body>`);
865
+ try {
866
+ const styles=new StyleEngine().compute(document);
867
+ for(const e of document.querySelectorAll('textarea,input,button,select')) {
868
+ if(!e.id)expect(styles.get(e).color).toBe('rgba(0, 0, 0, 1)');
869
+ }
870
+ expect(styles.get(document.querySelector('#inherit')).color).toBe(styles.get(document.body).color);
871
+ expect(styles.get(document.querySelector('#custom')).color).toBe('rgba(255, 0, 0, 1)');
872
+ } finally {window.happyDOM.abort()}
873
+ });
@@ -0,0 +1,27 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {createBrowser} from '../lib/headless-shell.js';
3
+ test('drag selects document text and controls without stealing canceled mouse gestures',async()=>{
4
+ let copied='';const b=await createBrowser({spareRenderer:false,clipboard:{read:async()=>copied,write:async text=>{copied=text}}});
5
+ const c=b.context,run=s=>c.evaluate(s);
6
+ const down=(x,y)=>c.pointerButton(x,y,272,1),up=(x,y)=>c.pointerButton(x,y,272,0);
7
+ try{
8
+ await c.resize(500,300,1);await c.navigate('data:text/html,'+encodeURIComponent('<style>body{margin:0;font:20px monospace}p{margin:0;height:30px}input{font:20px monospace;padding:0;border:0}</style><p id=a>abcdefghij</p><p id=b>klmnopqrst</p><input value="abcdefghij">'));
9
+ await down(12,12);await c.move(60,12);await up(60,12);
10
+ expect(await run('getSelection().toString()')).toBe('bcde');
11
+ await c.press('c',{modifiers:2});for(let i=0;i<50&&!copied;i++)await Bun.sleep(10);expect(copied).toBe('bcde');
12
+ await down(60,12);await c.move(12,12);await up(12,12);expect(await run('getSelection().toString()')).toBe('bcde');
13
+ await down(12,12);await c.move(36,42);await up(36,42);expect(await run('getSelection().toString()')).toContain('klm');
14
+ await down(12,72);await c.move(60,72);await up(60,72);
15
+ expect(await run('(()=>{const e=document.querySelector("input");return [e.selectionStart,e.selectionEnd]})()')).toEqual([1,5]);
16
+ await run('document.getElementById("a").onmousedown=e=>e.preventDefault();getSelection().removeAllRanges()');
17
+ await down(12,12);await c.move(60,12);await up(60,12);expect(await run('getSelection().toString()')).toBe('');
18
+ await run('getSelection().selectAllChildren(document.getElementById("a"))');expect(await run('getSelection().toString()')).toBe('abcdefghij');
19
+ expect(await run('document.getSelection()===window.getSelection()')).toBe(true);
20
+ await run('document.getElementById("a").onmousedown=null;document.getElementById("a").style.userSelect="none";getSelection().removeAllRanges()');
21
+ await down(12,12);await c.move(60,12);await up(60,12);expect(await run('getSelection().toString()')).toBe('');
22
+ await run('document.getElementById("a").style.userSelect="text";getSelection().selectAllChildren(document.getElementById("a"))');
23
+ await run('globalThis.selectedRange=getSelection().getRangeAt(0);getSelection().removeAllRanges();getSelection().addRange(selectedRange)');
24
+ expect(await run('getSelection().toString()')).toBe('abcdefghij');
25
+ await Bun.write('/tmp/document-selection.png' ,Buffer.from(await c.screenshot({format:'png'}),'base64'));
26
+ }finally{b.close()}
27
+ },30000);
@@ -0,0 +1,85 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {createBrowser} from '../lib/headless-shell.js';
3
+ import {CdpServer} from '../lib/cdp-server.js';
4
+ import {CDP} from '../tools/browser-lab.js';
5
+ import {attachInput} from '../frontends/wayland/input.js';
6
+ import {mkdtempSync,rmSync} from 'node:fs';
7
+ import {join} from 'node:path';
8
+ const html=`<body style="margin:0"><div id="box" style="width:200px;height:100px">Double click</div><script>globalThis.events=[];for(const type of ['mousedown','mouseup','click','dblclick'])document.getElementById('box').addEventListener(type,e=>{globalThis.events.push(type+':'+e.detail);e.preventDefault()})</script>`;
9
+ function pointer(context){
10
+ let seat,ptr;const errors=[];
11
+ const client={globals:new Map([['wl_seat',1]]),connection:{closed:false},bind:(n,v,cb)=>{seat=cb;return 1},object:(n,cb)=>{ptr=cb;return 2},request(){}};
12
+ attachInput(client,{surface:3,closed:false},context,e=>errors.push(e));seat('capabilities',[1]);ptr('enter',[1,3,20,20]);
13
+ return {event:ptr,errors,tap(time,x=20,y=20){ptr('motion',[time,x,y]);ptr('button',[1,time,272,1]);ptr('button',[2,time+30,272,0]);}};
14
+ }
15
+ const until=async(fn)=>{for(let i=0;i<100;i++){if(await fn())return;await Bun.sleep(20)}throw Error('input did not settle')};
16
+
17
+ test('CDP clickCount reaches DOM clicks and dblclick after cancelled click',async()=>{
18
+ const b=await createBrowser({spareRenderer:false});const server=CdpServer.create(b.context).listen(0);
19
+ const cdp=new CDP(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
20
+ try{
21
+ await cdp.ready;
22
+ const {targetId}=await cdp.send('Target.createTarget',{url:'data:text/html,'+encodeURIComponent(html)});
23
+ const {sessionId}=await cdp.send('Target.attachToTarget',{targetId,flatten:true});cdp.sessionId=sessionId;
24
+ await b.context.navigate('data:text/html,'+encodeURIComponent(html));
25
+ for(const clickCount of [1,2,3])for(const type of ['mousePressed','mouseReleased'])await cdp.send('Input.dispatchMouseEvent',{type,x:20,y:20,button:'left',clickCount});
26
+ expect(await b.context.evaluate('events')).toEqual(['mousedown:1','mouseup:1','click:1','mousedown:2','mouseup:2','click:2','dblclick:2','mousedown:3','mouseup:3','click:3']);
27
+ }finally{cdp.close();server.stop(true);b.close()}
28
+ },30000);
29
+
30
+ test('Wayland clicks reach HTML and QML double-tap handlers',async()=>{
31
+ const b=await createBrowser({spareRenderer:false});const dir=mkdtempSync('/tmp/double-click-');
32
+ try{
33
+ await b.context.navigate('data:text/html,'+encodeURIComponent(html));
34
+ const p=pointer(b.context);p.tap(1000);p.tap(1180,28,24);
35
+ await until(async()=>(await b.context.evaluate('events.length'))===7);
36
+ expect(await b.context.evaluate('events.at(-1)')).toBe('dblclick:2');expect(p.errors).toEqual([]);
37
+ await Bun.write(join(dir,'main.qml'),`import QtQuick
38
+ Rectangle {width:200;height:100;color:"white";property int doubles:0
39
+ Text {id:status;text:"doubles="+parent.doubles}
40
+ TapHandler {onDoubleTapped:parent.doubles++}
41
+ }`);
42
+ await b.context.navigate(Bun.pathToFileURL(join(dir,'main.qml')).href);
43
+ const q=pointer(b.context);q.tap(2000,60,60);q.tap(2180,65,63);
44
+ await until(async()=>(await b.context.evaluate('document.getElementById("status").textContent'))==='doubles=1');
45
+ expect(q.errors).toEqual([]);
46
+ }finally{b.close();rmSync(dir,{recursive:true,force:true})}
47
+ },30000);
48
+
49
+ test('Wayland count resets on timeout, distance and leave and handles timestamp wrap',async()=>{
50
+ const counts=[];const p=pointer({click:(x,y,o)=>counts.push(o.clickCount),move(){}});
51
+ p.tap(0xffffff00);p.tap(0x20);p.tap(1000);p.tap(1100,100,100);
52
+ p.event('leave',[]);p.event('enter',[1,3,100,100]);p.tap(1150,100,100);
53
+ await Bun.sleep(0);expect(counts).toEqual([1,2,1,1,1]);
54
+ });
55
+
56
+ test('CDP and Wayland preserve non-primary buttons without primary activation',async()=>{
57
+ const b=await createBrowser({spareRenderer:false});const server=CdpServer.create(b.context).listen(0);
58
+ const cdp=new CDP(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
59
+ const dir=mkdtempSync('/tmp/nonprimary-click-');
60
+ try {
61
+ await cdp.ready;
62
+ const url='data:text/html,'+encodeURIComponent(`<body style="margin:0"><a id="box" href="https://example.invalid/" style="display:block;width:200px;height:100px">link</a><script>globalThis.events=[];const box=document.getElementById("box");for(const t of ['mousedown','mouseup','click','auxclick','contextmenu'])box.addEventListener(t,e=>events.push([t,e.button,e.buttons]));box.onauxclick=e=>events.push(['property',e.button]);</script>`);
63
+ const {targetId}=await cdp.send('Target.createTarget',{url});
64
+ const {sessionId}=await cdp.send('Target.attachToTarget',{targetId,flatten:true});cdp.sessionId=sessionId;
65
+ await b.context.navigate(url);
66
+ for(const type of ['mousePressed','mouseReleased'])await cdp.send('Input.dispatchMouseEvent',{type,x:30,y:40,button:'right',clickCount:1});
67
+ expect(await b.context.evaluate('events')).toEqual([['mousedown',2,2],['contextmenu',2,2],['mouseup',2,0],['auxclick',2,0],['property',2]]);
68
+ expect(b.context.url).toBe(url);
69
+ await b.context.evaluate('events=[]');
70
+ const p=pointer(b.context);p.event('button',[1,1000,274,1]);p.event('button',[2,1030,274,0]);
71
+ await until(async()=>(await b.context.evaluate('events.length'))===4);
72
+ expect(await b.context.evaluate('events')).toEqual([['mousedown',1,4],['mouseup',1,0],['auxclick',1,0],['property',1]]);
73
+ await Bun.write(join(dir,'main.qml'),`import QtQuick
74
+ Rectangle {width:200;height:100;color:"white"
75
+ Text {id:status;text:"none"}
76
+ TapHandler {acceptedButtons:Qt.LeftButton | Qt.RightButton | Qt.MiddleButton;onTapped:(point,button)=>{status.text="button="+button}}
77
+ }`);
78
+ await b.context.navigate(Bun.pathToFileURL(join(dir,'main.qml')).href);
79
+ const q=pointer(b.context);
80
+ for(const [code,expected] of [[273,2],[274,4]]){
81
+ q.event('button',[1,2000,code,1]);q.event('button',[2,2030,code,0]);
82
+ await until(async()=>(await b.context.evaluate('document.getElementById("status").textContent'))==='button='+expected);
83
+ }
84
+ }finally{cdp.close();server.stop(true);b.close();rmSync(dir,{recursive:true,force:true})}
85
+ },30000);
@@ -0,0 +1,40 @@
1
+ import {strict as assert} from 'node:assert';
2
+ import {createPageRenderer,loadGraphics} from '../../lib/renderer/page-renderer.js';
3
+ const {CanvasKit:kit}=await loadGraphics();
4
+ const traces=[];
5
+ const r=await createPageRenderer({onFrame:()=>{},trace:s=>traces.push(s)});
6
+ async function pixels(){
7
+ const image=kit.MakeImageFromEncoded(Buffer.from(await r.screenshot(),'base64'));
8
+ try{return image.readPixels(0,0,{width:image.width(),height:image.height(),colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB})}finally{image.delete()}
9
+ }
10
+ try{
11
+ r.resize(400,300,1);r.setScripting('host');
12
+ await r.loadDocument({url:'https://damage.test/',contentType:'text/html',source:`
13
+ <!doctype html><html><head><style>body{margin:0;background:#abc}.clip{position:absolute;left:30px;top:20px;width:90px;height:80px;overflow:hidden;border-radius:15px;background:yellow}canvas{width:120px;height:100px;opacity:.6}.over{position:absolute;left:50px;top:40px;width:30px;height:30px;background:green}</style></head><body>
14
+ <div class=clip><canvas width=120 height=100></canvas></div><div class=over></div><p style="position:absolute;top:160px">Preserve text outside canvas</p>
15
+ <script>globalThis.ctx=document.querySelector('canvas').getContext('2d');</script></body></html>`});
16
+ await r.whenLoaded();
17
+ await r.evaluate("ctx.fillRect(0,0,1,1)");
18
+ await pixels();await Bun.sleep(100);
19
+ for(const command of ["ctx.fillStyle='red';ctx.fillRect(0,0,120,100)","ctx.clearRect(0,0,120,100);ctx.fillStyle='blue';ctx.fillRect(15,15,20,20)","ctx.clearRect(0,0,120,100)"]){
20
+ traces.length=0;
21
+ await r.evaluate(command);
22
+ const partial=await pixels();
23
+ assert.ok(traces.some(s=>s.includes('canvas damage')),'exercise the partial paint path');
24
+ r.resize(401,300,1);r.resize(400,300,1);
25
+ assert.deepEqual(await pixels(),partial,'partial pixels must match a full repaint, including transparency, clipping and occlusion');
26
+ }
27
+ // Geometry/transform changes must fall back, then remain pixel-correct.
28
+ traces.length=0;
29
+ await r.evaluate("document.querySelector('.clip').style.transform='scale(1.2)';ctx.fillRect(0,0,80,80)");
30
+ const transformed=await pixels();
31
+ assert.ok(!traces.some(s=>s.includes('canvas damage')));
32
+ r.resize(401,300,1);r.resize(400,300,1);
33
+ assert.deepEqual(await pixels(),transformed);
34
+ traces.length=0;
35
+ await r.evaluate("ctx.clearRect(0,0,120,100)");
36
+ const cleared=await pixels();
37
+ assert.ok(!traces.some(s=>s.includes('canvas damage')),'transformed canvas stays on full paint');
38
+ r.resize(401,300,1);r.resize(400,300,1);
39
+ assert.deepEqual(await pixels(),cleared);
40
+ }finally{r.dispose()}
@@ -7,6 +7,7 @@ process.on("message", async (message) => {
7
7
  if (message?.type !== "call") return;
8
8
  const [argument] = message.params;
9
9
  switch (message.method) {
10
+ case "configuration": return process.send({ type: "result", id: message.id, result: { args: process.argv, secret: process.env.BUNINU_TEST_PRIVATE_ENV } });
10
11
  case "echo": return process.send({ type: "result", id: message.id, result: { pid: process.pid, argument } });
11
12
  case "hang": for (;;) {}
12
13
  case "fail": return process.send({ type: "result", id: message.id, error: "Error: page crashed\n at somewhere" });
@@ -0,0 +1,36 @@
1
+ # Build context: buninu-browser repository root. Compatible with js-udocker.
2
+ FROM oven/bun:1.4.4
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+ ENV LC_ALL=C.UTF-8
5
+ RUN set -eu; if [ -f /usr/bin/perl ] && [ ! -e /usr/bin/perl5.40.1 ]; then cp /usr/bin/perl /usr/bin/perl5.40.1; fi
6
+ RUN set -eu; apt-get update; apt-get install -y --no-install-recommends ca-certificates curl zstd shared-mime-info util-linux libgcc-s1; apt-get clean
7
+ RUN set -eu; case "$(uname -m)" in aarch64|arm64) arch=aarch64 ;; x86_64) arch=x86_64 ;; *) exit 1 ;; esac; curl -fL --retry 3 "https://pkgs.omarchy.org/stable/$arch/flea-0.3.12-1-$arch.pkg.tar.zst" -o /tmp/flea.pkg.tar.zst; tar --zstd -xf /tmp/flea.pkg.tar.zst -C / usr; /usr/bin/flea --version
8
+ RUN set -eu; mkdir -p /usr/share/omarchy; curl -fL --retry 3 https://codeload.github.com/basecamp/omarchy/tar.gz/3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d -o /tmp/omarchy.tar.gz; tar -xzf /tmp/omarchy.tar.gz --strip-components=1 -C /usr/share/omarchy omarchy-3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d/shell/Commons omarchy-3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d/shell/Ui omarchy-3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d/LICENSE; ln -sfn shell /usr/share/omarchy/qs
9
+ COPY buninu-browser.js /opt/buninu-browser/buninu-browser.js
10
+ COPY frontends /opt/buninu-browser/frontends
11
+ COPY usr /opt/buninu-browser/usr
12
+ COPY lib /opt/buninu-browser/lib
13
+ COPY vendor /opt/buninu-browser/vendor
14
+ COPY LICENSES /opt/buninu-browser/LICENSES
15
+ COPY LICENSE /opt/buninu-browser/LICENSE
16
+ COPY THIRD_PARTY_NOTICES.md /opt/buninu-browser/THIRD_PARTY_NOTICES.md
17
+ COPY package.json /opt/buninu-browser/package.json
18
+ COPY test/fixtures/flea-helpers /usr/local/lib/flea-helpers
19
+ COPY test/fixtures/flea-thumbnail-stub /opt/flea-thumbnail-stub
20
+ COPY test/fixtures/tone.mp3 /root/flea-demo/tone.mp3
21
+ COPY test/fixtures/long-video.mp4 /root/flea-demo/long-video.mp4
22
+ WORKDIR /usr/local/lib/flea-helpers
23
+ RUN set -eu; bun i --backend=copyfile jsmdcui@0.19.2; chmod +x /usr/local/lib/flea-helpers/bin/*; for tool in qs bat glow bwrap; do ln -sfn /usr/local/lib/flea-helpers/bin/$tool /usr/local/bin/$tool; done; install -Dm755 /opt/flea-thumbnail-stub/flea-thumbnail-stub /usr/local/bin/flea-thumbnail-stub; install -Dm644 /opt/flea-thumbnail-stub/flea-placeholder.thumbnailer /usr/share/thumbnailers/flea-placeholder.thumbnailer
24
+ RUN set -eu; install -Dm644 source-mime.xml /usr/share/mime/packages/buninu-source.xml; update-mime-database /usr/share/mime
25
+ RUN set -eu; printf 'const answer = 42;\n' > /root/flea-demo/hello.js; bun /usr/local/lib/flea-helpers/check.mjs
26
+ ENV HOME=/root
27
+ ENV XDG_RUNTIME_DIR=/tmp
28
+ ENV WAYLAND_DISPLAY=wayland-buninu
29
+ ENV XDG_CONFIG_HOME=/root/.config
30
+ ENV XDG_DATA_HOME=/root/.local/share
31
+ ENV XDG_STATE_HOME=/root/.local/state
32
+ ENV XDG_CACHE_HOME=/root/.cache
33
+ ENV QSG_RHI_BACKEND=opengl
34
+ WORKDIR /root
35
+ ENTRYPOINT []
36
+ CMD ["/usr/bin/flea", "--gui", "/root/flea-demo"]
@@ -0,0 +1,43 @@
1
+ #!/usr/local/bin/bun
2
+ // Experimental Flea bat argv adapter, using the unchanged bundled glow highlighter.
3
+ import {RuntimeRegistry} from './node_modules/jsmdcui/src/runtime/registry.js';
4
+ import {Colorscheme} from './node_modules/jsmdcui/src/config/colorscheme.js';
5
+ import {SyntaxDefinition,detectSyntax,loadSyntaxDefinitions} from './node_modules/jsmdcui/src/highlight/parser.js';
6
+ import {Highlighter} from './node_modules/jsmdcui/src/highlight/highlighter.js';
7
+ import {styleToAnsi} from './node_modules/jsmdcui/src/display/ansi-style.js';
8
+ import {fileURLToPath} from 'node:url';
9
+ import {adaptSyntaxRules} from './syntax-compat.mjs';
10
+ let name='',lang='',file=null,first=1,last=Infinity;
11
+ for(const arg of Bun.argv.slice(2)){
12
+ if(['--no-config','--no-custom-assets','--theme=ansi','--color=always','--style=plain','--paging=never','--wrap=never'].includes(arg))continue;
13
+ if(arg.startsWith('--file-name='))name=arg.slice(12);
14
+ else if(arg.startsWith('--language='))lang=arg.slice(11);
15
+ else if(arg.startsWith('--line-range=')){const m=/^--line-range=(\d+):(\d+)$/.exec(arg);if(!m)throw Error('Unsupported line range');first=+m[1];last=+m[2];}
16
+ else if(arg.startsWith('-')&&arg!=='-')throw Error('Unsupported bat option: '+arg);
17
+ else if(file!==null)throw Error('Only one input is supported');else file=arg;
18
+ }
19
+ const text=await (file&&file!=='-'?Bun.file(file):Bun.stdin).text();
20
+ if(Buffer.byteLength(text)>1048576)throw Error('Preview exceeds 1 MiB');
21
+ const runtime=new RuntimeRegistry({repoRoot:fileURLToPath(new URL('./node_modules/jsmdcui',import.meta.url)),configDir:'/tmp'});
22
+ await runtime.init({user:false});
23
+ const scheme=await new Colorscheme(runtime).load('simple'),defs=await loadSyntaxDefinitions(runtime);
24
+ adaptSyntaxRules(defs,SyntaxDefinition);
25
+ const lines=text.replace(/\r\n?/g,'\n').split('\n');if(lines.at(-1)==='')lines.pop();
26
+ const aliases={js:'javascript',jsx:'javascript',mjs:'javascript',cjs:'javascript',node:'javascript',nodejs:'javascript',ts:'typescript',tsx:'typescript',mts:'typescript',cts:'typescript',rs:'rust',py:'python',python3:'python',sh:'shell',bash:'shell',shellscript:'shell',md:'markdown',yml:'yaml',jsonc:'json',json5:'json',jsonl:'json',ndjson:'json',cpp:'c++',cc:'c++',cxx:'c++',hpp:'c++',cs:'csharp','c#':'csharp',rb:'ruby',golang:'go',kt:'kotlin',kts:'kotlin',ps1:'powershell',pwsh:'powershell',bat:'batch',cmd:'batch',docker:'dockerfile',make:'makefile',cmakelists:'cmake',tf:'terraform',proto:'proto',protobuf:'proto',tex:'tex',latex:'tex',diff:'patch',dotenv:'ini'};
27
+ const byType=new Map(defs.map(d=>[d.filetype.toLowerCase(),d]));
28
+ const lookup=key=>byType.get(aliases[key.toLowerCase()]||key.toLowerCase());
29
+ const path=name||file||'',ext=path.split('.').at(-1).toLowerCase();
30
+ const def=lang?lookup(lang):lookup(({jsx:'jsx',mts:'ts',cts:'ts',jsonc:'json',json5:'json',jsonl:'json',ndjson:'json',env:'ini'})[ext]||'')||detectSyntax(defs,{path,firstLine:lines[0]||'',lines:lines.slice(0,50)});
31
+ if(lang&&!def)process.exit(1);
32
+ const highlighter=def?new Highlighter(def,defs):null;let state=null;
33
+ for(let i=0;i<Math.min(lines.length,last);i++){
34
+ const line=lines[i],result=highlighter?.highlightLine(line,state);if(result)state=result.state;
35
+ if(i+1<first)continue;
36
+ if(!result){process.stdout.write(line+'\n');continue;}
37
+ const changes=[...result.changes.entries()];let output='';
38
+ for(let j=0;j<changes.length-1;j++){
39
+ const [from,group]=changes[j],to=changes[j+1][0];
40
+ output+=styleToAnsi(scheme.get(group)).replace(/38;5;(\d+)/g,(_,n)=>String(+n<8?30+(+n):90+(+n)-8))+line.slice(from,to);
41
+ }
42
+ process.stdout.write(output+'\x1b[0m\n');
43
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ sd=$(dirname "$(realpath "$0")")
3
+ exec bun "$sd/../bat.mjs" "$@"
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ # Experiment only: no namespace/filesystem isolation; outer prlimit remains.
3
+ envargs=()
4
+ while (($#)); do
5
+ case "$1" in
6
+ --unshare-*|--die-with-parent|--new-session|--clearenv) shift ;; --setenv) envargs+=("$2=$3"); shift 3 ;; --ro-bind|--bind|--symlink) shift 3 ;; --proc|--dev|--tmpfs|--dir) shift 2 ;;
7
+ --json-status-fd) fd=$2; [[ $fd =~ ^[0-9]+$ ]] || exit 2; eval "exec ${fd}>&-"; shift 2 ;; --) shift; break ;; --*) echo "Unsupported experimental bwrap option: $1" >&2; exit 2 ;; *) break ;;
8
+ esac
9
+ done
10
+ exec env "${envargs[@]}" "$@"
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # Diagnostic only: retains real bwrap; currently fails under bunproot.
3
+ args=()
4
+ for arg in "$@"; do
5
+ case "$arg" in
6
+ --unshare-all) args+=(--unshare-user --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup-try) ;; --unshare-net) ;; *) args+=("$arg") ;;
7
+ esac
8
+ done
9
+ exec /usr/bin/bwrap-real "${args[@]}"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ sd=$(dirname "$(realpath "$0")")
3
+ exec bun "$sd/../node_modules/jsmdcui/src/index.js" --cat "$@"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ export QML_IMPORT_PATH="/usr/share/omarchy${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}"
3
+ exec bun /opt/buninu-browser/lib/qml/qswayland "$@"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ echo "${0##*/}: unavailable in this experiment" >&2
3
+ exit 1