buninu-browser 0.2.4 → 0.4.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 (249) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +102 -2
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -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 +116 -0
  20. package/frontends/wayland/input.js +56 -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 +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +57 -0
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +78 -13
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/paint/frame.js +35 -3
  35. package/lib/paint/raw-frames.js +47 -0
  36. package/lib/qml/README.md +971 -0
  37. package/lib/qml/builtins.js +700 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/document.js +1176 -0
  40. package/lib/qml/format.js +9 -0
  41. package/lib/qml/host.js +324 -0
  42. package/lib/qml/images.js +199 -0
  43. package/lib/qml/launcher.js +132 -0
  44. package/lib/qml/loader.js +517 -0
  45. package/lib/qml/media-renderer.js +49 -0
  46. package/lib/qml/media.js +86 -0
  47. package/lib/qml/models.js +622 -0
  48. package/lib/qml/parser.js +381 -0
  49. package/lib/qml/qs +5 -0
  50. package/lib/qml/qscasty +3 -0
  51. package/lib/qml/qsheadless +3 -0
  52. package/lib/qml/qswayland +3 -0
  53. package/lib/qml/reactive.js +159 -0
  54. package/lib/qml/runtime.js +3381 -0
  55. package/lib/qml/shapes.js +243 -0
  56. package/lib/qml/text.js +109 -0
  57. package/lib/qml/textures.js +123 -0
  58. package/lib/qml/wayland-clipboard.js +32 -0
  59. package/lib/qml/worker-thread.js +70 -0
  60. package/lib/render-tree/index.js +16 -1
  61. package/lib/renderer/bindings.js +11 -0
  62. package/lib/renderer/canvas-2d.js +13 -0
  63. package/lib/renderer/compositor-channel.js +103 -0
  64. package/lib/renderer/compositor.js +25 -33
  65. package/lib/renderer/flags.js +1 -1
  66. package/lib/renderer/host.js +128 -92
  67. package/lib/renderer/main.js +38 -4
  68. package/lib/renderer/page-realm.js +32 -1
  69. package/lib/renderer/page-renderer.js +250 -21
  70. package/lib/renderer/text-input-paint.js +54 -0
  71. package/lib/renderer/text-input.js +57 -0
  72. package/lib/style/computed-style.js +2 -2
  73. package/package.json +1 -1
  74. package/test/clipboard.test.js +54 -0
  75. package/test/fixtures/browser-raw-demo.html +36 -0
  76. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  77. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  78. package/test/fixtures/flea-helpers/bin/bat +3 -0
  79. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  80. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  81. package/test/fixtures/flea-helpers/bin/glow +3 -0
  82. package/test/fixtures/flea-helpers/bin/qs +3 -0
  83. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  84. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  85. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  86. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  87. package/test/fixtures/flea-helpers/check.mjs +36 -0
  88. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  89. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  90. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  91. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  92. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  93. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  94. package/test/fixtures/qml/flea/LICENSE +21 -0
  95. package/test/fixtures/qml/flea/README.md +25 -0
  96. package/test/fixtures/qml/flea/Theme.qml +8 -0
  97. package/test/fixtures/qml/flea/demo.qml +24 -0
  98. package/test/fixtures/qml/flea/qmldir +3 -0
  99. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  100. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  101. package/test/fixtures/qml/flea-theme/README.md +70 -0
  102. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  103. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  104. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  107. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  108. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  109. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  110. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  111. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  112. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  113. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  114. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  115. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  116. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  117. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  118. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  119. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  120. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  121. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  122. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  123. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  124. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  125. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  126. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  127. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  128. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  173. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  174. package/test/fixtures/qml/hello.qml +26 -0
  175. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  176. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  177. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  178. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  179. package/test/fixtures/qml/imports/logic/base.js +2 -0
  180. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  181. package/test/fixtures/qml/imports/logic/math.js +3 -0
  182. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  183. package/test/fixtures/qml/imports/main.qml +21 -0
  184. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  185. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  186. package/test/fixtures/qml/multiply.qml +61 -0
  187. package/test/fixtures/qml-canvas-dom.js +90 -0
  188. package/test/fixtures/qml-media-audio.js +39 -0
  189. package/test/fixtures/raw-capture-benchmark.json +76 -0
  190. package/test/helpers/flea-sandbox.js +48 -0
  191. package/test/helpers/quickshell-fixture.js +125 -0
  192. package/test/html-clipboard-wayland.test.js +47 -0
  193. package/test/index.test.js +13 -0
  194. package/test/manual-casty-without-qml.js +87 -0
  195. package/test/manual-flea-application.js +49 -0
  196. package/test/manual-flea-binary.js +62 -0
  197. package/test/manual-flea-qml.js +9 -0
  198. package/test/manual-flea-settings.js +40 -0
  199. package/test/manual-flea-theme.js +40 -0
  200. package/test/manual-qml-removal.js +46 -0
  201. package/test/manual-without-qml.js +45 -0
  202. package/test/qml-canvas-dom.test.js +37 -0
  203. package/test/qml-clipboard-bridge.test.js +25 -0
  204. package/test/qml-clipboard-wayland.test.js +40 -0
  205. package/test/qml-connections-timing.test.js +73 -0
  206. package/test/qml-dynamic.test.js +401 -0
  207. package/test/qml-flea-app.test.js +146 -0
  208. package/test/qml-flea-backend.test.js +78 -0
  209. package/test/qml-flea-columns-backend.test.js +38 -0
  210. package/test/qml-flea-compile.test.js +78 -0
  211. package/test/qml-flea-launcher.test.js +88 -0
  212. package/test/qml-flea-markdown-backend.test.js +31 -0
  213. package/test/qml-flea-media.test.js +28 -0
  214. package/test/qml-flea-navigation-backend.test.js +35 -0
  215. package/test/qml-flea-navigation-timing.test.js +147 -0
  216. package/test/qml-flea-preview-backend.test.js +53 -0
  217. package/test/qml-flea-previews.test.js +163 -0
  218. package/test/qml-flea-rows.test.js +101 -0
  219. package/test/qml-flea-textures.test.js +59 -0
  220. package/test/qml-flea-view-switch.test.js +33 -0
  221. package/test/qml-flea-wayland.test.js +68 -0
  222. package/test/qml-flea.test.js +34 -0
  223. package/test/qml-host.test.js +238 -0
  224. package/test/qml-imports.test.js +102 -0
  225. package/test/qml-inherited-handlers.test.js +51 -0
  226. package/test/qml-input-selection.test.js +66 -0
  227. package/test/qml-interactions.test.js +309 -0
  228. package/test/qml-language.test.js +167 -0
  229. package/test/qml-launcher.test.js +54 -0
  230. package/test/qml-layout-text.test.js +205 -0
  231. package/test/qml-media.test.js +63 -0
  232. package/test/qml-persisted-delegates.test.js +45 -0
  233. package/test/qml-quickshell-controls.test.js +65 -0
  234. package/test/qml-quickshell-settings.test.js +62 -0
  235. package/test/qml-repeater.test.js +72 -0
  236. package/test/qml-style-updates.test.js +124 -0
  237. package/test/qml-textures.test.js +66 -0
  238. package/test/qml-views.test.js +332 -0
  239. package/test/qml-worker.test.js +69 -0
  240. package/test/qml.test.js +83 -0
  241. package/test/raw-capture.test.js +152 -0
  242. package/test/renderer-host.test.js +11 -0
  243. package/test/text-input-pointer.test.js +31 -0
  244. package/test/text-input-scroll.test.js +29 -0
  245. package/test/text-input.test.js +47 -0
  246. package/test/wayland-frontend.test.js +83 -0
  247. package/test/wayland-urlbar.test.js +38 -0
  248. package/test/window-resize.test.js +36 -0
  249. package/tools/bench-raw-capture.js +32 -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,116 @@
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 {attachQmlClipboard} from '../../lib/qml/wayland-clipboard.js';
13
+ // qml handle end
14
+
15
+ export async function launch({url='about:blank',width=800,height=600,socket,chrome}={}) {
16
+ // qml handle begin: local application windows default to no browser chrome
17
+ chrome ??= !/\.qml(?:[?#]|$)/i.test(url);
18
+ // qml handle end
19
+
20
+ let browser,client,window,off,titleTimer,toolbar,closed=false,busy=false,dirty=true,ready=false;
21
+ let pageDirty=true,pageBytes,pageSize='',navigation=0,navigating=0;
22
+ let resolveDone;const done=new Promise(resolve=>resolveDone=resolve);
23
+ const close=()=>{if(closed)return;closed=true;clearInterval(titleTimer);off?.();toolbar?.dispose();try{window?.close();}finally{client?.close();browser?.close();resolveDone();}};
24
+ const fail=e=>{console.error(`Wayland frontend: ${e.message}`);close();};
25
+ try{
26
+ client=new WaylandClient(socket);await client.initialize();
27
+ attachClipboard(client,done);
28
+ const clipboard={
29
+ read:async()=>{
30
+ if(client.clipboard?.selection)return client.clipboard.read();
31
+ const text=await readClipboard();
32
+ // A successful host paste seeds the compositor's text cache too.
33
+ if(text&&client.clipboard?.serial)await client.clipboard.write(text);
34
+ return text;
35
+ },
36
+ write:text=>client.clipboard?client.clipboard.write(text):writeClipboard(text),
37
+ };
38
+ browser=await createBrowser({spareRenderer:false,clipboard});
39
+
40
+ // qml handle begin: operation-triggered local QML clipboard bridge
41
+ await attachQmlClipboard(client,browser,done);
42
+ // qml handle end
43
+
44
+ if(chrome ?? true)toolbar=await new WaylandUrlBar(browser.context,{
45
+ close,changed:()=>{dirty=true;if(ready)void draw();},
46
+ clipboard,
47
+ }).initialize();
48
+ window=new WaylandWindow(client,{width,height:height+(toolbar?BAR_HEIGHT:0)});
49
+ client.onClose=close;window.onClose=close;
50
+ const draw=async()=>{
51
+ if(closed||!ready||navigating||busy||!dirty||!window.available)return;
52
+ busy=true;dirty=false;const generation=navigation;let path;
53
+ try{
54
+ const w=window.width,h=window.height,bar=toolbar?Math.min(BAR_HEIGHT,h-1):0,z=toolbar?.zoom||1;
55
+ const size=`${w}:${h}:${z}`;
56
+ if(pageDirty||pageSize!==size||!pageBytes){
57
+ pageDirty=false;
58
+ if(pageSize!==size)await browser.context.resize(w/z,(h-bar)/z,z);
59
+ const frame=JSON.parse(await browser.context.screenshot({format:'bgra'}));path=frame.path;
60
+ if(closed||generation!==navigation)return;
61
+ if(frame.transport!=='file'||frame.format!=='bgra'||frame.stride!==frame.width*4)throw Error('Unexpected raw frame');
62
+ const pixels=readFileSync(path);
63
+ if(frame.width===w&&frame.height===h-bar)pageBytes=pixels;
64
+ else {
65
+ pageBytes=Buffer.alloc(w*(h-bar)*4,255);
66
+ 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);
67
+ }
68
+ pageSize=size;
69
+ }
70
+ toolbar?.updateURL(browser.context.url);
71
+ const parts=toolbar?[toolbar.pixels(w).subarray(0,w*bar*4),pageBytes]:pageBytes;
72
+ if(!closed&&!window.present(parts,w,h))dirty=true;
73
+ if(!closed){const title=String(await browser.context.title()||'Buninu Browser');if(generation===navigation&&!closed)client.request(window.toplevel,'set_title',title);}
74
+ }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);}
75
+ };
76
+ // Actual navigation may replace the renderer. Ignore work from the old
77
+ // document and resume drawing once navigation settles. Editing chrome does
78
+ // not enter this path and continues to present page animation normally.
79
+ const navigate=browser.context.navigate;
80
+ browser.context.navigate=async function(...args){
81
+ navigation++;navigating++;
82
+ try{return await navigate.apply(this,args);}
83
+ finally{navigating--;pageDirty=true;dirty=true;if(ready&&!closed)void draw();}
84
+ };
85
+ let readingTitle=false,lastTitle='';
86
+ titleTimer=setInterval(async()=>{
87
+ if(closed||!ready||navigating||readingTitle)return;
88
+ readingTitle=true;const generation=navigation;
89
+ try{
90
+ const title=String(await browser.context.title()||'Buninu Browser');
91
+ if(closed||generation!==navigation)return;
92
+ toolbar?.updateURL(browser.context.url);
93
+ if(title!==lastTitle){lastTitle=title;client.request(window.toplevel,'set_title',title);}
94
+ }catch(e){if(!closed&&generation===navigation)fail(e);}finally{readingTitle=false;}
95
+ },500);
96
+ window.onConfigure=()=>{pageDirty=true;dirty=true;draw();};window.onReady=draw;
97
+ off=browser.context.onFrame(()=>{pageDirty=true;dirty=true;draw();});
98
+ const input=toolbar?{
99
+ press:(...args)=>toolbar.press(...args).catch(e=>toolbar.flash(e.message)),
100
+ pointerButton:(...args)=>toolbar.pointerButton(...args).catch(e=>toolbar.flash(e.message)),
101
+ move:(...args)=>toolbar.move(...args),scroll:(...args)=>toolbar.scroll(...args),
102
+ }:browser.context;
103
+ attachInput(client,window,input,fail);
104
+ await browser.context.resize(width,height,1);await browser.context.navigate(url);ready=true;draw();
105
+ return {browser,client,window,toolbar,close,done};
106
+ }catch(e){close();throw e;}
107
+ }
108
+ if(import.meta.main){
109
+ const args=Bun.argv.slice(2).filter(a=>a!=='--');
110
+ 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);}
111
+ let width=800,height=600,url='about:blank';
112
+ 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;}
113
+ if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width>8192||height>8192)throw Error('Invalid window size');
114
+ if(!/^[a-z][a-z\d+.-]*:/i.test(url))url=url.startsWith('/')||url.startsWith('.')?pathToFileURL(resolve(url)).href:`https://${url}`;
115
+ const app=await launch({url,width,height});for(const signal of ['SIGINT','SIGTERM','SIGHUP'])process.on(signal,app.close);await app.done;
116
+ }
@@ -0,0 +1,56 @@
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
+ const active=()=>!window.closed&&!client.connection.closed;
23
+ const invoke=(method,...args)=>{
24
+ if(!active())return;
25
+ // A compositor can send leave/modifier events in the same batch as close.
26
+ // Pending input is cancelled with the renderer; it must not restart/report it.
27
+ Promise.resolve().then(()=>active()?context[method](...args):undefined)
28
+ .catch(error=>{if(active())onError(error);});
29
+ };
30
+ const seat=client.bind('wl_seat',5,(event,[caps])=>{
31
+ if(event!=='capabilities')return;
32
+ if((caps&1)&&!pointer){pointer=client.object('wl_pointer',(name,args)=>{
33
+ if(name==='enter'){focused=args[1]===window.surface;[x,y]=args.slice(2);}
34
+ if(name==='leave')focused=false;
35
+ if(name==='motion'){[x,y]=args.slice(1);if(focused)invoke('move',x,y);}
36
+
37
+ // Mouse-triggered copy also has a valid Wayland input serial.
38
+ if(name==='button'&&focused&&client.clipboard)client.clipboard.serial=args[0];
39
+
40
+ if(name==='button'&&focused&&context.pointerButton)invoke('pointerButton',x,y,args[2],args[3]);
41
+ else if(name==='button'&&focused&&args[2]===272&&args[3]===0)invoke('click',x,y);
42
+ if(name==='axis'&&focused)invoke('scroll',args[1]===1?args[2]:0,args[1]===0?args[2]:0,{x,y});
43
+ });client.request(seat,'get_pointer',pointer);}
44
+ if((caps&2)&&!keyboard){keyboard=client.object('wl_keyboard',(name,args)=>{
45
+
46
+ // Use the compositor's real keyboard serial for selection.
47
+ if(name==='enter'||name==='key')if(client.clipboard)client.clipboard.serial=args[0];
48
+
49
+ 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);}}
50
+ if(name==='modifiers'){mods=args[1]|args[2];locked=args[3];}
51
+ 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;
52
+ 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)});
53
+ }
54
+ });client.request(seat,'get_keyboard',keyboard);}
55
+ });
56
+ }
@@ -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
+ }