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,80 @@
1
+ // A standard wl_data_device text client. Only explicit copy/paste transfers bytes.
2
+ import {readSync, writeSync} from 'node:fs';
3
+ import {libc} from './transport.js';
4
+ const mimeTypes=['text/plain;charset=utf-8','text/plain','UTF8_STRING','STRING'];
5
+ const limit=1024*1024;
6
+ export class WaylandClipboard {
7
+ constructor(client) {
8
+ this.client=client;this.serial=0;this.offers=new Map();this.sources=new Set();this.closed=false;
9
+ const c=client;
10
+ this.manager=c.bind('wl_data_device_manager',1);
11
+ const seat=c.bind('wl_seat',1);
12
+ this.device=c.object('wl_data_device',(event,args)=>{
13
+ if(event==='data_offer') {
14
+ const id=args[0],types=new Set();this.offers.set(id,types);
15
+ c.objects.set(id,{iface:'wl_data_offer',event:(name,[mime])=>{if(name==='offer')types.add(mime);}});
16
+ }
17
+ if(event==='selection') {
18
+ this.selection=args[0];
19
+ for(const id of this.offers.keys())if(id!==this.selection){c.request(id,'destroy');this.offers.delete(id);c.objects.delete(id);}
20
+ }
21
+ });c.request(this.manager,'get_data_device',this.device,seat);
22
+ }
23
+ async transfer(fd,bytes) {
24
+ const chunks=[];let length=0,offset=0;const deadline=Date.now()+2500;
25
+ try {
26
+ libc.fcntl(fd,4,libc.fcntl(fd,3,0)|2048);
27
+ while(!this.closed&&Date.now()<deadline) {
28
+ try {
29
+ if(bytes) {
30
+ if(offset===bytes.length)return;
31
+ offset+=writeSync(fd,bytes,offset,bytes.length-offset);
32
+ } else {
33
+ const chunk=Buffer.alloc(65536),n=readSync(fd,chunk,0,chunk.length,null);
34
+ if(!n)return Buffer.concat(chunks).toString();
35
+ length+=n;if(length>limit)throw Error('Clipboard exceeds limit');chunks.push(chunk.subarray(0,n));
36
+ }
37
+ } catch(error) {if(!['EAGAIN','EINTR','EWOULDBLOCK'].includes(error.code))throw error;}
38
+ await Bun.sleep(4);
39
+ }
40
+ throw Error('Clipboard transfer interrupted or timed out');
41
+ } finally {libc.close(fd);}
42
+ }
43
+ async write(text) {
44
+ if(this.closed||!this.serial)throw Error('Clipboard requires focused Wayland input');
45
+ const bytes=Buffer.from(text);if(bytes.length>limit)throw Error('Clipboard exceeds limit');
46
+ const c=this.client;
47
+ const source=c.object('wl_data_source',(name,args)=>{
48
+ if(name==='send') {
49
+ if(!mimeTypes.includes(args[0]))libc.close(args[1]);
50
+ else this.transfer(args[1],bytes).catch(()=>{});
51
+ }
52
+ if(name==='cancelled') {c.request(source,'destroy');this.sources.delete(source);}
53
+ });
54
+ this.sources.add(source);
55
+ c.request(this.manager,'create_data_source',source);
56
+ for(const mime of mimeTypes)c.request(source,'offer',mime);
57
+ c.request(this.device,'set_selection',source,this.serial);
58
+ await c.sync();
59
+ }
60
+ async read() {
61
+ if(this.closed)throw Error('Clipboard closed');
62
+ const types=this.offers.get(this.selection),mime=mimeTypes.find(type=>types?.has(type));
63
+ if(!mime)return '';
64
+ const fds=new Int32Array(2);if(libc.pipe2(fds,2048|0x80000)<0)throw Error('Cannot create clipboard pipe');
65
+ try{this.client.request(this.selection,'receive',mime,fds[1]);}
66
+ catch(error){libc.close(fds[0]);throw error;}
67
+ finally{libc.close(fds[1]);}
68
+ return this.transfer(fds[0]);
69
+ }
70
+ close(){this.closed=true;}
71
+ }
72
+
73
+ // The standard data device belongs to every Wayland frontend, not QML.
74
+ export function attachClipboard(client,done) {
75
+ if(!client.globals.has('wl_data_device_manager'))return;
76
+ const clipboard=new WaylandClipboard(client);
77
+ client.clipboard=clipboard;
78
+ done.finally(()=>clipboard.close());
79
+ return clipboard;
80
+ }
@@ -0,0 +1,130 @@
1
+ import {readFileSync,unlinkSync} from 'node:fs';
2
+ import {resolve} from 'node:path';
3
+ import {pathToFileURL} from 'node:url';
4
+ import {createBrowser} from '../../lib/headless-shell.js';
5
+ import {WaylandClient,WaylandWindow} from './client.js';
6
+ import {attachInput} from './input.js';
7
+ import {attachClipboard} from './clipboard.js';
8
+ import {WaylandUrlBar,BAR_HEIGHT} from './urlbar.js';
9
+ import {readClipboard,writeClipboard} from '../../lib/clipboard.js';
10
+
11
+ // qml handle begin: local QML clipboard uses this frontend's Wayland seat
12
+ import {qmlWindowConstraints} from '../../lib/qml/window-constraints.js';
13
+ import {attachQmlClipboard} from '../../lib/qml/wayland-clipboard.js';
14
+ // qml handle end
15
+
16
+ export async function launch({url='about:blank',width=800,height=600,socket,chrome}={}) {
17
+ // qml handle begin: local application windows default to no browser chrome
18
+ chrome ??= !/\.qml(?:[?#]|$)/i.test(url);
19
+ // qml handle end
20
+
21
+ let browser,client,window,off,titleTimer,toolbar,closed=false,busy=false,dirty=true,ready=false;
22
+ let pageDirty=true,pageBytes,pageSize='',navigation=0,navigating=0;
23
+ let resolveDone;const done=new Promise(resolve=>resolveDone=resolve);
24
+ const close=()=>{if(closed)return;closed=true;clearInterval(titleTimer);off?.();toolbar?.dispose();try{window?.close();}finally{client?.close();browser?.close();resolveDone();}};
25
+ const fail=e=>{console.error(`Wayland frontend: ${e.message}`);close();};
26
+ try{
27
+ client=new WaylandClient(socket);await client.initialize();
28
+ attachClipboard(client,done);
29
+ const clipboard={
30
+ read:async()=>{
31
+ if(client.clipboard?.selection)return client.clipboard.read();
32
+ const text=await readClipboard();
33
+ // A successful host paste seeds the compositor's text cache too.
34
+ if(text&&client.clipboard?.serial)await client.clipboard.write(text);
35
+ return text;
36
+ },
37
+ write:text=>client.clipboard?client.clipboard.write(text):writeClipboard(text),
38
+ };
39
+ browser=await createBrowser({spareRenderer:false,clipboard});
40
+
41
+ // qml handle begin: operation-triggered local QML clipboard bridge
42
+ await attachQmlClipboard(client,browser,done);
43
+ // qml handle end
44
+
45
+ if(chrome ?? true)toolbar=await new WaylandUrlBar(browser.context,{
46
+ close,changed:()=>{dirty=true;if(ready)void draw();},
47
+ clipboard,
48
+ }).initialize();
49
+ window=new WaylandWindow(client,{width,height:height+(toolbar?BAR_HEIGHT:0)});
50
+ client.onClose=close;window.onClose=close;
51
+
52
+ // qml handle begin: forward declarative window constraints to xdg_toplevel
53
+ const syncQmlConstraints=qmlWindowConstraints(client,window,browser.context);
54
+ // qml handle end
55
+
56
+ const draw=async()=>{
57
+ if(closed||!ready||navigating||busy||!dirty||!window.available)return;
58
+ busy=true;dirty=false;const generation=navigation;let path;
59
+ try{
60
+ const w=window.width,h=window.height,bar=toolbar?Math.min(BAR_HEIGHT,h-1):0,z=toolbar?.zoom||1;
61
+ const size=`${w}:${h}:${z}`;
62
+ if(pageDirty||pageSize!==size||!pageBytes){
63
+ pageDirty=false;
64
+ if(pageSize!==size)await browser.context.resize(w/z,(h-bar)/z,z);
65
+ const frame=JSON.parse(await browser.context.screenshot({format:'bgra'}));path=frame.path;
66
+ if(closed||generation!==navigation)return;
67
+ if(frame.transport!=='file'||frame.format!=='bgra'||frame.stride!==frame.width*4)throw Error('Unexpected raw frame');
68
+ const pixels=readFileSync(path);
69
+ if(frame.width===w&&frame.height===h-bar)pageBytes=pixels;
70
+ else {
71
+ pageBytes=Buffer.alloc(w*(h-bar)*4,255);
72
+ for(let row=0;row<Math.min(frame.height,h-bar);row++)pixels.copy(pageBytes,row*w*4,row*frame.stride,row*frame.stride+Math.min(w,frame.width)*4);
73
+ }
74
+ pageSize=size;
75
+ }
76
+ toolbar?.updateURL(browser.context.url);
77
+ const parts=toolbar?[toolbar.pixels(w).subarray(0,w*bar*4),pageBytes]:pageBytes;
78
+ if(!closed&&!window.present(parts,w,h))dirty=true;
79
+ if(!closed){const title=String(await browser.context.title()||'Buninu Browser');if(generation===navigation&&!closed)client.request(window.toplevel,'set_title',title);}
80
+ }catch(e){if(!closed&&generation===navigation)fail(e);}finally{if(path)try{unlinkSync(path);}catch{}busy=false;if(!closed&&dirty&&window.available)setTimeout(draw,0);}
81
+ };
82
+ // Actual navigation may replace the renderer. Ignore work from the old
83
+ // document and resume drawing once navigation settles. Editing chrome does
84
+ // not enter this path and continues to present page animation normally.
85
+ const navigate=browser.context.navigate;
86
+ browser.context.navigate=async function(...args){
87
+ navigation++;navigating++;
88
+ try{return await navigate.apply(this,args);}
89
+ finally{navigating--;pageDirty=true;dirty=true;if(ready&&!closed)void draw();}
90
+ };
91
+ let readingTitle=false,lastTitle='';
92
+ titleTimer=setInterval(async()=>{
93
+ if(closed||!ready||navigating||readingTitle)return;
94
+ readingTitle=true;const generation=navigation;
95
+ try{
96
+ const title=String(await browser.context.title()||'Buninu Browser');
97
+ if(closed||generation!==navigation)return;
98
+ toolbar?.updateURL(browser.context.url);
99
+ // qml handle begin: refresh size hints alongside existing window metadata
100
+ await syncQmlConstraints();
101
+ // qml handle end
102
+
103
+ if(title!==lastTitle){lastTitle=title;client.request(window.toplevel,'set_title',title);}
104
+ }catch(e){if(!closed&&generation===navigation)fail(e);}finally{readingTitle=false;}
105
+ },500);
106
+ window.onConfigure=()=>{pageDirty=true;dirty=true;draw();};window.onReady=draw;
107
+ off=browser.context.onFrame(()=>{pageDirty=true;dirty=true;draw();});
108
+ const input=toolbar?{
109
+ press:(...args)=>toolbar.press(...args).catch(e=>toolbar.flash(e.message)),
110
+ pointerButton:(...args)=>toolbar.pointerButton(...args).catch(e=>toolbar.flash(e.message)),
111
+ move:(...args)=>toolbar.move(...args),scroll:(...args)=>toolbar.scroll(...args),
112
+ }:browser.context;
113
+ attachInput(client,window,input,fail);
114
+ await browser.context.resize(width,height,1);await browser.context.navigate(url);ready=true;draw();
115
+ // qml handle begin: send initial window size hints
116
+ await syncQmlConstraints();
117
+ // qml handle end
118
+
119
+ return {browser,client,window,toolbar,close,done};
120
+ }catch(e){close();throw e;}
121
+ }
122
+ if(import.meta.main){
123
+ const args=Bun.argv.slice(2).filter(a=>a!=='--');
124
+ if(args.some(a=>['-h','--help'].includes(a))){console.log('Usage: buninu-browser --wayland [URL or file] [--window-size=800,600]\nUses WAYLAND_DISPLAY and XDG_RUNTIME_DIR. Experimental wl_shm frontend.');process.exit(0);}
125
+ let width=800,height=600,url='about:blank';
126
+ for(const arg of args){if(arg.startsWith('--window-size=')){[width,height]=arg.slice(14).split(',').map(Number);}else if(arg.startsWith('-'))throw Error(`Unknown Wayland option: ${arg}`);else url=arg;}
127
+ if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width>8192||height>8192)throw Error('Invalid window size');
128
+ if(!/^[a-z][a-z\d+.-]*:/i.test(url))url=url.startsWith('/')||url.startsWith('.')?pathToFileURL(resolve(url)).href:`https://${url}`;
129
+ const app=await launch({url,width,height});for(const signal of ['SIGINT','SIGTERM','SIGHUP'])process.on(signal,app.close);await app.done;
130
+ }
@@ -0,0 +1,75 @@
1
+ import {readSync} from 'node:fs';
2
+ import {libc} from './transport.js';
3
+
4
+ // A deliberately bounded XKB subset, not a replacement for libxkbcommon.
5
+ export function parseKeymap(text) {
6
+ const codes=new Map(),keys=new Map();
7
+ for(const m of text.matchAll(/<([^>]+)>\s*=\s*(\d+)/g))codes.set(m[1],Number(m[2])-8);
8
+ for(const m of text.matchAll(/key\s+<([^>]+)>\s*\{[^}]*\[([^\]]+)\]/g)){
9
+ const code=codes.get(m[1]);if(code!==undefined)keys.set(code,m[2].split(',').map(s=>s.trim()));
10
+ }return keys;
11
+ }
12
+ const names={space:' ',Return:'Enter',KP_Enter:'Enter',Tab:'Tab',ISO_Left_Tab:'Tab',Escape:'Escape',BackSpace:'Backspace',Delete:'Delete',Insert:'Insert',Home:'Home',End:'End',Prior:'PageUp',Next:'PageDown',Left:'ArrowLeft',Right:'ArrowRight',Up:'ArrowUp',Down:'ArrowDown',slash:'/',period:'.',comma:',',minus:'-',equal:'=',semicolon:';',apostrophe:"'",bracketleft:'[',bracketright:']',backslash:'\\',grave:'`'};
13
+ export function keysym(symbol){
14
+ if(names[symbol]!==undefined)return names[symbol];if(!symbol)return null;
15
+ if(symbol.length===1||/^F\d+$/.test(symbol))return symbol;
16
+ if(/^(?:0x[\da-f]+|U[\da-f]+)$/i.test(symbol)){let n=parseInt(symbol.replace(/^U/i,''),16);if(n>=0xffbe&&n<=0xffe0)return 'F'+(n-0xffbd);const special={65288:'Backspace',65289:'Tab',65293:'Enter',65307:'Escape',65535:'Delete',65360:'Home',65361:'ArrowLeft',65362:'ArrowUp',65363:'ArrowRight',65364:'ArrowDown',65365:'PageUp',65366:'PageDown',65367:'End',65379:'Insert'};if(special[n])return special[n];if(n>=0xff00&&n<0x1000000)return null;if(n>=0x1000000)n-=0x1000000;return n>0&&n<=0x10ffff?String.fromCodePoint(n):null;}
17
+ return null;
18
+ }
19
+ export function attachInput(client,window,context,onError) {
20
+ if(!client.globals.has('wl_seat'))return;
21
+ let pointer,keyboard,x=0,y=0,focused=false,keys=new Map(),mods=0,locked=0;
22
+ let lastClick=null, clickCount=0;
23
+ const modifiers=()=>({modifiers:(mods&8?1:0)|(mods&4?2:0)|(mods&64?4:0)|(mods&1?8:0)});
24
+ const active=()=>!window.closed&&!client.connection.closed;
25
+ const invoke=(method,...args)=>{
26
+ if(!active())return;
27
+ // A compositor can send leave/modifier events in the same batch as close.
28
+ // Pending input is cancelled with the renderer; it must not restart/report it.
29
+ Promise.resolve().then(()=>active()?context[method](...args):undefined)
30
+ .catch(error=>{if(active())onError(error);});
31
+ };
32
+ const seat=client.bind('wl_seat',5,(event,[caps])=>{
33
+ if(event!=='capabilities')return;
34
+ if((caps&1)&&!pointer){pointer=client.object('wl_pointer',(name,args)=>{
35
+ if(name==='enter'){focused=args[1]===window.surface;[x,y]=args.slice(2);}
36
+ if(name==='leave'){focused=false;lastClick=null;clickCount=0;}
37
+ if(name==='motion'){[x,y]=args.slice(1);if(focused)invoke('move',x,y,modifiers());}
38
+
39
+ // Mouse-triggered copy also has a valid Wayland input serial.
40
+ if(name==='button'&&focused&&client.clipboard)client.clipboard.serial=args[0];
41
+
42
+ if(name==='button'&&focused){
43
+ const [,time,button,state]=args;
44
+ if(state){
45
+ // Wayland supplies monotonic uint32 milliseconds, not a click count.
46
+ // Allow modest touch jitter in surface coordinates; reset on leave.
47
+ clickCount=lastClick&&lastClick.button===button&&((time-lastClick.time)>>>0)<400
48
+ &&Math.abs(x-lastClick.x)<=12&&Math.abs(y-lastClick.y)<=12 ? clickCount+1 : 1;
49
+ lastClick={time,button,x,y};
50
+ }
51
+ const options={clickCount:clickCount||1,...modifiers()};
52
+ if(context.pointerButton)invoke('pointerButton',x,y,button,state,options);
53
+ else if(state===0&&button>=272&&button<=274)invoke('click',x,y,{...options,button:button===273?'right':button===274?'middle':'left'});
54
+ }
55
+ if(name==='axis'&&focused){
56
+ // Browser scrolling consumes pixel deltas. Land maps 100 CDP pixels
57
+ // to 10 Wayland axis units; restore that scale on the client side.
58
+ // Preserve fractional movement and apply page zoom later in the toolbar.
59
+ const delta=args[2]*10;
60
+ invoke('scroll',args[1]===1?delta:0,args[1]===0?delta:0,{x,y});
61
+ }
62
+ });client.request(seat,'get_pointer',pointer);}
63
+ if((caps&2)&&!keyboard){keyboard=client.object('wl_keyboard',(name,args)=>{
64
+
65
+ // Use the compositor's real keyboard serial for selection.
66
+ if(name==='enter'||name==='key')if(client.clipboard)client.clipboard.serial=args[0];
67
+
68
+ if(name==='keymap'){const [format,fd,size]=args;try{if(format===1&&size<=1024*1024){const bytes=Buffer.alloc(size);let n=0;while(n<size){const got=readSync(fd,bytes,n,size-n,n);if(!got)break;n+=got;}keys=parseKeymap(bytes.subarray(0,n).toString());}}finally{libc.close(fd);}}
69
+ if(name==='modifiers'){mods=args[1]|args[2];locked=args[3];}
70
+ if(name==='key'&&args[3]===1){const symbols=keys.get(args[2]);if(!symbols)return;let shift=!!(mods&1);if((locked&2)&&/^[a-z]$/.test(keysym(symbols[0])||''))shift=!shift;
71
+ const key=keysym(symbols[shift?1:0]||symbols[0]);if(key)invoke('press',key,{modifiers:(mods&8?1:0)|(mods&4?2:0)|(mods&64?4:0)|(mods&1?8:0)});
72
+ }
73
+ });client.request(seat,'get_keyboard',keyboard);}
74
+ });
75
+ }
@@ -0,0 +1,55 @@
1
+ // Adapted from buninu-land (MIT; LICENSE-land); XML files unchanged.
2
+ // Interpret upstream XML with Bun's built-in parser at runtime; no generated bindings.
3
+ import { readFileSync } from 'node:fs';
4
+ const list = value => value == null ? [] : Array.isArray(value) ? value : [value];
5
+ const attrs = node => Object.fromEntries(Object.entries(node).filter(([k]) => k.startsWith('@')).map(([k,v]) => [k.slice(1), v]));
6
+ export const interfaces = new Map();
7
+ for (const file of ['wayland', 'xdg-shell']) {
8
+ const xml = Bun.XML.parse(readFileSync(new URL(`./protocols/${file}.xml`, import.meta.url), 'utf8'));
9
+ for (const node of list(xml.protocol.interface)) {
10
+ const spec = { ...attrs(node) };
11
+ for (const [tag, key] of [['request', 'requests'], ['event', 'events']])
12
+ spec[key] = list(node[tag]).map(message => ({ ...attrs(message), args: list(message.arg).map(attrs) }));
13
+ interfaces.set(spec.name, spec);
14
+ }
15
+ }
16
+ export function decode(spec, bytes, fds) {
17
+ let offset = 8, fdOffset = 0;
18
+ const values = [];
19
+ const word = () => { if (offset + 4 > bytes.length) throw Error('Truncated argument'); const v = bytes.readUInt32LE(offset); offset += 4; return v; };
20
+ for (const arg of spec.args) {
21
+ if (arg.type === 'fd') { if (fdOffset === fds.length) return null; values.push(fds[fdOffset++]); continue; }
22
+ if (arg.type === 'new_id' && !arg.interface) {
23
+ const n = word(); if (!n || offset + n > bytes.length || bytes[offset + n - 1]) throw Error('Invalid interface string');
24
+ values.push(bytes.toString('utf8', offset, offset + n - 1)); offset += (n + 3) & ~3;
25
+ values.push(word(), word()); continue;
26
+ }
27
+ const v = word();
28
+ if (arg.type === 'string' || arg.type === 'array') {
29
+ if (offset + v > bytes.length) throw Error('Truncated array/string');
30
+ if (arg.type === 'string' && v && bytes[offset + v - 1]) throw Error('Unterminated string');
31
+ values.push(arg.type === 'string' ? (v ? bytes.toString('utf8', offset, offset + v - 1) : null) : bytes.subarray(offset, offset + v));
32
+ offset += (v + 3) & ~3;
33
+ } else values.push(arg.type === 'fixed' ? (v | 0) / 256 : arg.type === 'int' ? v | 0 : v);
34
+ }
35
+ if (offset !== bytes.length) throw Error('Invalid message size');
36
+ fds.splice(0, fdOffset);
37
+ return values;
38
+ }
39
+ export function encodeRequest(id, iface, name, values) {
40
+ const requests=interfaces.get(iface).requests, opcode=requests.findIndex(e=>e.name===name);
41
+ if(opcode<0)throw Error(`Unknown request ${iface}.${name}`);
42
+ const parts=[Buffer.alloc(8)],fds=[];
43
+ const word=v=>{const b=Buffer.alloc(4);b.writeUInt32LE(Number(v)>>>0);parts.push(b);};
44
+ const bytes=(value,string)=>{const b=string?Buffer.from(value==null?'':value+'\0'):Buffer.from(value);const p=Buffer.alloc(4+((b.length+3)&~3));p.writeUInt32LE(b.length);b.copy(p,4);parts.push(p);};
45
+ let i=0;
46
+ for(const arg of requests[opcode].args){
47
+ const v=values[i++];
48
+ if(arg.type==='fd'){fds.push(v);continue;}
49
+ if(arg.type==='new_id'&&!arg.interface){bytes(v,true);word(values[i++]);word(values[i++]);}
50
+ else if(arg.type==='string'||arg.type==='array')bytes(v,arg.type==='string');
51
+ else word(arg.type==='fixed'?Math.round(v*256):v);
52
+ }
53
+ const data=Buffer.concat(parts);if(data.length>65535)throw Error('Wayland message too large');
54
+ data.writeUInt32LE(id);data.writeUInt32LE((data.length<<16)|opcode,4);return {data,fds};
55
+ }