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,9 @@
1
+ export const QML_MIME = 'text/x-qml';
2
+ export function isQmlDocument(url, contentType = '') {
3
+ const mime = contentType.split(';')[0].trim().toLowerCase();
4
+ if (['text/x-qml', 'application/x-qml'].includes(mime)) return true;
5
+ // An explicit HTML response must retain HTML behavior, even at a .qml URL.
6
+ return ['', 'text/plain', 'application/octet-stream'].includes(mime)
7
+ && /\.qml$/i.test(new URL(url, 'file:///').pathname);
8
+ }
9
+
@@ -0,0 +1,324 @@
1
+ import {readClipboard, writeClipboard} from '../clipboard.js';
2
+ import { Worker } from 'node:worker_threads';
3
+ import { loadQmlGraphSync } from './loader.js';
4
+ import { createQmlDocument } from './document.js';
5
+ // Local QML application capabilities. No imports from this module are exposed to HTML.
6
+ import {
7
+ readFileSync,
8
+ writeFileSync,
9
+ renameSync,
10
+ unlinkSync,
11
+ statSync,
12
+ watch,
13
+ } from 'node:fs';
14
+ import { resolve, delimiter } from 'node:path';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ const spawn = Bun.spawn.bind(Bun);
17
+ const hostFetch = globalThis.fetch.bind(globalThis);
18
+ const hostTimeout = AbortSignal.timeout.bind(AbortSignal);
19
+ const environment = { ...process.env };
20
+ const active = new Set();
21
+ let installed = false;
22
+ export function createQmlHost(url) {
23
+ if (new URL(url).protocol !== 'file:')
24
+ throw Error('QML host requires a local document');
25
+ if (!installed) {
26
+ installed = true;
27
+ process.on('exit', () => {
28
+ for (const host of active) host.dispose();
29
+ });
30
+ process.on('SIGTERM', () => {
31
+ for (const host of active) host.dispose();
32
+ process.exit(0);
33
+ });
34
+ }
35
+ let closed = false;
36
+ const resources = new Set(),
37
+ base = new URL('.', url);
38
+ const check = () => {
39
+ if (closed) throw Error('QML host is closed');
40
+ };
41
+ const path = (value) =>
42
+ String(value).startsWith('file:')
43
+ ? fileURLToPath(value)
44
+ : resolve(fileURLToPath(base), String(value));
45
+ let clipboardEndpoint;
46
+ const clipboardRequest = async (operation, text) => {
47
+ check();
48
+ if (!clipboardEndpoint) return operation==='read' ? readClipboard() : writeClipboard(text);
49
+ const response=await hostFetch(clipboardEndpoint,{method:'POST',body:JSON.stringify({operation,text}),signal:hostTimeout(4000)});
50
+ if(!response.ok)throw Error('Wayland clipboard unavailable');
51
+ return (await response.json()).text;
52
+ };
53
+ const host = {
54
+ setClipboardEndpoint(endpoint) {
55
+ check(); const u=new URL(endpoint);
56
+ if(u.protocol!=='http:' || u.hostname!=='127.0.0.1')throw Error('Invalid clipboard endpoint');
57
+ clipboardEndpoint=u.href;
58
+ },
59
+ clipboardRead() {return clipboardRequest('read');},
60
+ clipboardWrite(text) {return clipboardRequest('write',String(text));},
61
+ worker(url, onMessage, onReady, onError) {
62
+ check();
63
+ if (new URL(url).protocol !== 'file:')
64
+ throw Error('WorkerScript requires a local source');
65
+ const worker = new Worker(
66
+ new URL('./worker-thread.js', import.meta.url),
67
+ { workerData: { url } },
68
+ );
69
+ let stopped = false;
70
+ worker.on('message', (data) => {
71
+ if (stopped) return;
72
+ if (data.kind === 'ready') onReady();
73
+ else if (data.kind === 'message') onMessage(data.message);
74
+ else if (data.kind === 'error') onError(data.error);
75
+ });
76
+ worker.on('error', (error) => {
77
+ if (!stopped) onError(String(error.stack || error));
78
+ });
79
+ const stop = () => {
80
+ if (stopped) return;
81
+ stopped = true;
82
+ resources.delete(stop);
83
+ void worker.terminate();
84
+ };
85
+ resources.add(stop);
86
+ stop.send = (message) => {
87
+ if (stopped) throw Error('WorkerScript has stopped');
88
+ worker.postMessage(message);
89
+ };
90
+ return stop;
91
+ },
92
+ markdown: (source) => {
93
+ check();
94
+ return Bun.markdown.html(String(source));
95
+ },
96
+ shellDir: fileURLToPath(base).replace(/\/$/, ''),
97
+ processId: process.pid,
98
+ compile(href, externalSingletons = []) {
99
+ check();
100
+ if (new URL(href).protocol !== 'file:')
101
+ throw Error('Synchronous application components must be local files');
102
+ const source = readFileSync(fileURLToPath(href), 'utf8');
103
+ const fetchResource = (url) => {
104
+ if (new URL(url).protocol !== 'file:')
105
+ throw Error('Local component imports must be local files');
106
+ try {
107
+ return { status: 200, body: readFileSync(fileURLToPath(url)) };
108
+ } catch (e) {
109
+ if (e.code === 'ENOENT' || e.code === 'ENOTDIR')
110
+ return { status: 404 };
111
+ throw e;
112
+ }
113
+ };
114
+ const graph = loadQmlGraphSync(source, href, fetchResource, {
115
+ importRoots: host.importRoots,
116
+ });
117
+ const { document } = createQmlDocument(source, href, {
118
+ graph,
119
+ component: true,
120
+ externalSingletons,
121
+ });
122
+ const program = document.querySelector('script')?.textContent;
123
+ if (!program)
124
+ throw Error(
125
+ document.querySelector('p')?.textContent ||
126
+ 'Component compilation failed',
127
+ );
128
+ return program;
129
+ },
130
+ env(name) {
131
+ check();
132
+ return environment[String(name)] ?? null;
133
+ },
134
+ hyprEvents(callback) {
135
+ const signature = environment.HYPRLAND_INSTANCE_SIGNATURE;
136
+ if (!signature) return () => {};
137
+ const socketPath = resolve(
138
+ environment.XDG_RUNTIME_DIR || '/tmp',
139
+ 'hypr',
140
+ signature,
141
+ '.socket2.sock',
142
+ );
143
+ let socket,
144
+ buffer = '',
145
+ stopped = false;
146
+ const stop = () => {
147
+ stopped = true;
148
+ socket?.end();
149
+ resources.delete(stop);
150
+ };
151
+ resources.add(stop);
152
+ Bun.connect({
153
+ unix: socketPath,
154
+ socket: {
155
+ open(s) {
156
+ socket = s;
157
+ if (stopped) s.end();
158
+ },
159
+ data(s, bytes) {
160
+ buffer += new TextDecoder().decode(bytes);
161
+ let end;
162
+ while ((end = buffer.indexOf('\n')) >= 0) {
163
+ const line = buffer.slice(0, end);
164
+ buffer = buffer.slice(end + 1);
165
+ const at = line.indexOf('>>');
166
+ if (at >= 0 && !closed)
167
+ callback({ name: line.slice(0, at), data: line.slice(at + 2) });
168
+ }
169
+ },
170
+ error() {},
171
+ close() {
172
+ resources.delete(stop);
173
+ },
174
+ },
175
+ }).catch(() => resources.delete(stop));
176
+ return stop;
177
+ },
178
+ importRoots: (environment.QML_IMPORT_PATH || '')
179
+ .split(delimiter)
180
+ .filter(Boolean)
181
+ .map((p) => pathToFileURL(resolve(p) + '/').href),
182
+ write(name, data, atomic = true) {
183
+ check();
184
+ const target = path(name);
185
+ let temporary;
186
+ try {
187
+ if (atomic) {
188
+ let mode = 0o600;
189
+ try {
190
+ mode = statSync(target).mode & 0o777;
191
+ } catch (error) {
192
+ if (error.code !== 'ENOENT') throw error;
193
+ }
194
+ temporary =
195
+ target + '.qml-write-' + process.pid + '-' + crypto.randomUUID();
196
+ writeFileSync(temporary, data, { flag: 'wx', mode });
197
+ renameSync(temporary, target);
198
+ temporary = null;
199
+ } else writeFileSync(target, data);
200
+ return { error: null };
201
+ } catch (error) {
202
+ return { error: error.code || String(error) };
203
+ } finally {
204
+ if (temporary)
205
+ try {
206
+ unlinkSync(temporary);
207
+ } catch {}
208
+ }
209
+ },
210
+ read(name) {
211
+ check();
212
+ try {
213
+ return { text: readFileSync(path(name), 'utf8'), error: null };
214
+ } catch (e) {
215
+ return { text: '', error: e.code || String(e) };
216
+ }
217
+ },
218
+ watch(name, callback) {
219
+ check();
220
+ const target = path(name);
221
+ let watcher;
222
+ try {
223
+ watcher = watch(target, () => {
224
+ if (!closed) callback();
225
+ });
226
+ } catch {
227
+ return () => {};
228
+ }
229
+ const stop = () => {
230
+ watcher.close();
231
+ resources.delete(stop);
232
+ };
233
+ resources.add(stop);
234
+ return stop;
235
+ },
236
+ run(command, callback, options = {}) {
237
+ check();
238
+ if (!Array.isArray(command) || !command.length)
239
+ throw Error('Process.command must be a nonempty array');
240
+ let child;
241
+ try {
242
+ child = spawn({
243
+ cmd: command.map(String),
244
+ cwd: options.cwd ? path(options.cwd) : fileURLToPath(base),
245
+ env: {
246
+ ...(options.clearEnvironment ? {} : environment),
247
+ ...(options.environment || {}),
248
+ },
249
+ stdin: options.stdin ? 'pipe' : 'ignore',
250
+ stdout: 'pipe',
251
+ stderr: 'pipe',
252
+ });
253
+ } catch (error) {
254
+ queueMicrotask(() => {
255
+ if (!closed)
256
+ callback({
257
+ exitCode: 127,
258
+ stdout: '',
259
+ stderr: String(error),
260
+ error: String(error),
261
+ });
262
+ });
263
+ return () => {};
264
+ }
265
+ const stop = () => {
266
+ child.kill();
267
+ resources.delete(stop);
268
+ };
269
+ stop.write = (text) => {
270
+ if (!options.stdin) throw Error('Process stdin is disabled');
271
+ child.stdin.write(String(text));
272
+ child.stdin.flush();
273
+ };
274
+ stop.closeWriteChannel = () => {
275
+ if (options.stdin) child.stdin.end();
276
+ };
277
+ stop.signal = (signal) => child.kill(Number(signal));
278
+ stop.processId = child.pid;
279
+ resources.add(stop);
280
+ const read = async (stream, onData) => {
281
+ let text = '';
282
+ const decoder = new TextDecoder();
283
+ for await (const bytes of stream) {
284
+ const chunk = decoder.decode(bytes, { stream: true });
285
+ text += chunk;
286
+ if (!closed) onData?.(chunk, bytes);
287
+ }
288
+ const tail = decoder.decode();
289
+ text += tail;
290
+ if (tail && !closed) onData?.(tail);
291
+ return text;
292
+ };
293
+ Promise.all([
294
+ read(child.stdout, options.stdout),
295
+ read(child.stderr, options.stderr),
296
+ child.exited,
297
+ ])
298
+ .then(([stdout, stderr, exitCode]) => {
299
+ resources.delete(stop);
300
+ if (!closed) callback({ stdout, stderr, exitCode });
301
+ })
302
+ .catch((error) => {
303
+ resources.delete(stop);
304
+ if (!closed)
305
+ callback({
306
+ exitCode: 1,
307
+ stdout: '',
308
+ stderr: String(error),
309
+ error: String(error),
310
+ });
311
+ });
312
+ return stop;
313
+ },
314
+ dispose() {
315
+ if (closed) return;
316
+ closed = true;
317
+ for (const stop of [...resources]) stop();
318
+ resources.clear();
319
+ active.delete(host);
320
+ },
321
+ };
322
+ active.add(host);
323
+ return host;
324
+ }
@@ -0,0 +1,199 @@
1
+ // QML Image uses existing raster decoding or an inert inline SVG DOM. The SVG
2
+ // subset matches the browser's renderer; it does not rasterize/re-encode SVG.
3
+ export function createQmlImages(api) {
4
+ const { document, effect, host, cleanup } = api;
5
+ const ns = 'http://www.w3.org/2000/svg';
6
+ function svg(source) {
7
+ if (source.length > 4 * 1024 * 1024) throw Error('QML SVG exceeds 4 MiB');
8
+ const parsed = new DOMParser().parseFromString(source, 'image/svg+xml');
9
+ const root = parsed.documentElement;
10
+ if (root?.localName !== 'svg' || parsed.querySelector('parsererror'))
11
+ throw Error('Invalid SVG image');
12
+ const supported = new Set([
13
+ 'svg',
14
+ 'g',
15
+ 'path',
16
+ 'rect',
17
+ 'circle',
18
+ 'ellipse',
19
+ 'line',
20
+ 'polyline',
21
+ 'polygon',
22
+ ]);
23
+ const ignored = new Set(['title', 'desc', 'metadata']);
24
+ let nodes = 0;
25
+ function copy(from) {
26
+ if (++nodes > 10000) throw Error('QML SVG element limit');
27
+ if (!supported.has(from.localName))
28
+ throw Error('Unsupported QML SVG element: ' + from.localName);
29
+ const to = document.createElementNS(ns, from.localName);
30
+ for (const attr of from.attributes) {
31
+ if (
32
+ /^on/i.test(attr.name) ||
33
+ ['href', 'xlink:href'].includes(attr.name) ||
34
+ /url\s*\(/i.test(attr.value)
35
+ )
36
+ throw Error('Unsupported active/external QML SVG attribute');
37
+ to.setAttribute(attr.name, attr.value);
38
+ }
39
+ for (const child of from.children) {
40
+ if (!ignored.has(child.localName)) to.appendChild(copy(child));
41
+ }
42
+ return to;
43
+ }
44
+ const element = copy(root),
45
+ box = (root.getAttribute('viewBox') || '')
46
+ .trim()
47
+ .split(/[\s,]+/)
48
+ .map(Number);
49
+ const width = parseFloat(root.getAttribute('width')) || box[2],
50
+ height = parseFloat(root.getAttribute('height')) || box[3];
51
+ if (!(width > 0 && height > 0)) throw Error('SVG image has no dimensions');
52
+ if (!element.hasAttribute('viewBox'))
53
+ element.setAttribute('viewBox', `0 0 ${width} ${height}`);
54
+ return { element, width, height };
55
+ }
56
+ return (v, element, url) => {
57
+ let generation = 0,
58
+ current,
59
+ node = null,
60
+ pendingImage = null;
61
+ const remove = () => {
62
+ node?.remove();
63
+ node = null;
64
+ };
65
+ const fit = () => {
66
+ v.status;
67
+ if (!node) return;
68
+ node.style.position = 'absolute';
69
+ node.style.pointerEvents = 'none';
70
+ node.style.width = v.width + 'px';
71
+ node.style.height = v.height + 'px';
72
+ node.style.left = '0';
73
+ node.style.top = '0';
74
+ node.style.objectFit =
75
+ v.fillMode === 1
76
+ ? 'contain'
77
+ : v.fillMode === 2
78
+ ? 'cover'
79
+ : v.fillMode === 6
80
+ ? 'none'
81
+ : 'fill';
82
+ node.style.imageRendering = v.smooth ? 'auto' : 'pixelated';
83
+ node.style.transform = v.mirror ? 'scaleX(-1)' : '';
84
+ const ratio =
85
+ v.fillMode === 6
86
+ ? 1
87
+ : (v.fillMode === 2 ? Math.max : Math.min)(
88
+ v.width / (v.implicitWidth || 1),
89
+ v.height / (v.implicitHeight || 1),
90
+ );
91
+ v.paintedWidth = [1, 2, 6].includes(v.fillMode)
92
+ ? v.implicitWidth * ratio
93
+ : v.width;
94
+ v.paintedHeight = [1, 2, 6].includes(v.fillMode)
95
+ ? v.implicitHeight * ratio
96
+ : v.height;
97
+ if (node.localName === 'svg') {
98
+ // The inline SVG renderer always preserves its viewBox ratio. Fit by
99
+ // sizing the DOM box, then let the QML wrapper clip crop/pad overflow.
100
+ node.style.width = v.paintedWidth + 'px';
101
+ node.style.height = v.paintedHeight + 'px';
102
+ node.style.left = (v.width - v.paintedWidth) / 2 + 'px';
103
+ node.style.top = (v.height - v.paintedHeight) / 2 + 'px';
104
+ }
105
+ element.style.overflow = 'hidden';
106
+ };
107
+ effect(() => {
108
+ if (current === v.source) return;
109
+ current = v.source;
110
+ const token = ++generation;
111
+ pendingImage?.remove();
112
+ pendingImage = null;
113
+ if (!current) {
114
+ remove();
115
+ v.status = 0;
116
+ v.progress = 0;
117
+ v.implicitWidth = 0;
118
+ v.implicitHeight = 0;
119
+ return;
120
+ }
121
+ if (!v.retainWhileLoading) remove();
122
+ v.status = 2;
123
+ v.progress = 0;
124
+ const source = new URL(current, url).href;
125
+ const ready = (next, width, height) => {
126
+ if (token !== generation) {
127
+ next.remove();
128
+ return;
129
+ }
130
+ remove();
131
+ node = next;
132
+ element.appendChild(node);
133
+ v.implicitWidth = width;
134
+ v.implicitHeight = height;
135
+ v.status = 1;
136
+ v.progress = 1;
137
+ fit();
138
+ };
139
+ const failed = () => {
140
+ if (token === generation) {
141
+ remove();
142
+ v.status = 3;
143
+ v.progress = 0;
144
+ }
145
+ };
146
+ if (
147
+ /\.svg(?:[?#]|$)/i.test(source) ||
148
+ /^data:image\/svg\+xml/i.test(source)
149
+ ) {
150
+ (async () => {
151
+ let text;
152
+ if (source.startsWith('file:')) {
153
+ if (!host) throw Error('Local SVG requires local QML');
154
+ const result = host.read(source);
155
+ if (result.error) throw Error(result.error);
156
+ text = result.text;
157
+ } else {
158
+ const response = await fetch(source);
159
+ if (!response.ok) throw Error('SVG HTTP ' + response.status);
160
+ text = await response.text();
161
+ }
162
+ const result = svg(text);
163
+ ready(result.element, result.width, result.height);
164
+ })().catch(failed);
165
+ } else {
166
+ const image = document.createElement('img');
167
+ pendingImage = image;
168
+ image.style.display = 'none';
169
+ element.appendChild(image);
170
+ image.addEventListener(
171
+ 'load',
172
+ () => {
173
+ if (pendingImage === image) pendingImage = null;
174
+ image.style.display = 'block';
175
+ ready(image, image.naturalWidth, image.naturalHeight);
176
+ },
177
+ { once: true },
178
+ );
179
+ image.addEventListener(
180
+ 'error',
181
+ () => {
182
+ if (pendingImage === image) pendingImage = null;
183
+ image.remove();
184
+ failed();
185
+ },
186
+ { once: true },
187
+ );
188
+ image.src = source;
189
+ }
190
+ });
191
+ effect(fit);
192
+ cleanup(() => {
193
+ generation++;
194
+ pendingImage?.remove();
195
+ pendingImage = null;
196
+ remove();
197
+ });
198
+ };
199
+ }
@@ -0,0 +1,132 @@
1
+ import { statSync } from 'node:fs';
2
+ import { resolve, join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ export function parseArgs(args, env = process.env) {
6
+ let path = env.QS_CONFIG_PATH || '',
7
+ config = env.QS_CONFIG_NAME || 'default';
8
+ let help = false;
9
+ for (let i = 0; i < args.length; i++) {
10
+ const arg = args[i];
11
+ const next = () => {
12
+ if (!args[i + 1] || args[i + 1].startsWith('-'))
13
+ throw Error(`${arg} needs a value`);
14
+ return args[++i];
15
+ };
16
+ if (arg === '-h' || arg === '--help') help = true;
17
+ else if (arg === '-p' || arg === '--path') path = next();
18
+ else if (arg.startsWith('--path=')) path = arg.slice(7);
19
+ else if (arg === '-c' || arg === '--config') {
20
+ config = next();
21
+ path = '';
22
+ } else throw Error(`Unsupported Quickshell option: ${arg}`);
23
+ }
24
+ if (help) return { help: true };
25
+ if (!path) {
26
+ if (!/^[\w.-]+$/.test(config) || config === '..')
27
+ throw Error('Invalid configuration name');
28
+ const base = env.XDG_CONFIG_HOME || (env.HOME && join(env.HOME, '.config'));
29
+ if (!base) throw Error('Set --path, XDG_CONFIG_HOME or HOME');
30
+ path = join(
31
+ base,
32
+ 'quickshell',
33
+ ...(config === 'default' ? [] : [config]),
34
+ 'shell.qml',
35
+ );
36
+ }
37
+ path = resolve(path);
38
+ if (statSync(path).isDirectory()) path = join(path, 'shell.qml');
39
+ if (!statSync(path).isFile()) throw Error(`Not a QML file: ${path}`);
40
+ return { path };
41
+ }
42
+
43
+ export async function main(args) {
44
+ const options = parseArgs(args);
45
+ if (options.help) {
46
+ console.log(
47
+ `Usage: qs -p FILE_OR_DIRECTORY\n qs -c CONFIG\n\nExperimental Buninu QML/Wayland frontend, not the native Quickshell runtime.\nUses WAYLAND_DISPLAY, XDG_RUNTIME_DIR and QML_IMPORT_PATH.\n-p, --path QML file or directory containing shell.qml\n-c, --config Name under $XDG_CONFIG_HOME/quickshell\n-h, --help Show this help\n\nOnly supported QML features run. Native Quickshell IPC, Qt plugins and\nmultiple native windows are not implemented. Unknown arguments fail explicitly.`,
48
+ );
49
+ return;
50
+ }
51
+ const { launch } = await import('../../frontends/wayland/index.js');
52
+ const app = await launch({
53
+ chrome: false,
54
+ url: pathToFileURL(options.path).href,
55
+ width: 900,
56
+ height: 600,
57
+ });
58
+ const stop = () => app.close();
59
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
60
+ process.on(signal, stop);
61
+ try {
62
+ await app.done;
63
+ } finally {
64
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
65
+ process.off(signal, stop);
66
+ app.close();
67
+ }
68
+ }
69
+
70
+ // Package entry adapters share QS configuration resolution with the standalone qs.
71
+ export function backendArgs(args, env = process.env) {
72
+ const separator = args.indexOf('--');
73
+ const options = parseArgs(separator < 0 ? args : args.slice(0, separator), env);
74
+ if (options.help) return options;
75
+ return { args: [pathToFileURL(options.path).href, ...(separator < 0 ? [] : args.slice(separator + 1))] };
76
+ }
77
+
78
+ export function backendUsage(backend) {
79
+ return `Usage: qs${backend} [-p FILE_OR_DIRECTORY | -c CONFIG] [-- BACKEND_ARGS...]
80
+ buninu-browser --quickshell-${backend} [-p FILE_OR_DIRECTORY | -c CONFIG] [-- BACKEND_ARGS...]
81
+
82
+ Experimental Buninu QML runtime using the ${backend} backend, not native Quickshell.
83
+ -p, --path QML file or directory containing shell.qml
84
+ -c, --config Name under $XDG_CONFIG_HOME/quickshell
85
+ -h, --help Show this help
86
+ Uses QS_CONFIG_PATH, QS_CONFIG_NAME and QML_IMPORT_PATH.
87
+ Without -p/-c, uses the same default configuration as standalone qs.
88
+ Arguments after -- are forwarded unchanged to ${backend}.
89
+ For casty, another -- separates Casty options from browser engine options.
90
+ Only supported QML features run; native Quickshell IPC and Qt plugins are not implemented.`;
91
+ }
92
+
93
+ // Independent executables and the package routes use the same backend adapter.
94
+ export async function runBackend(backend, args = Bun.argv.slice(2)) {
95
+ try {
96
+ if (!['wayland', 'headless', 'casty'].includes(backend))
97
+ throw Error(`Unknown Quickshell backend: ${backend}`);
98
+ const forwarded = backendArgs(args);
99
+ if (forwarded.help) {
100
+ console.log(backendUsage(backend));
101
+ return;
102
+ }
103
+ const engine = resolve(import.meta.dir, '../../buninu-browser.js');
104
+ let cmd;
105
+ if (backend === 'casty') {
106
+ const { castyCommand } = await import('../../index.js');
107
+ cmd = castyCommand(forwarded.args);
108
+ } else {
109
+ cmd = backend === 'wayland'
110
+ ? [process.execPath, resolve(import.meta.dir, '../../frontends/wayland/index.js'), '--window-size=900,600', ...forwarded.args]
111
+ : [process.execPath, engine, ...forwarded.args];
112
+ }
113
+ const child = Bun.spawn({
114
+ cmd,
115
+ env: backend === 'casty' ? { ...process.env, CASTY_BROWSER: engine } : process.env,
116
+ stdio: ['inherit', 'inherit', 'inherit'],
117
+ });
118
+ const handlers = ['SIGINT', 'SIGTERM', 'SIGHUP'].map(signal => {
119
+ const handler = () => child.kill(signal);
120
+ process.on(signal, handler);
121
+ return [signal, handler];
122
+ });
123
+ try {
124
+ process.exitCode = (await child.exited) ?? 1;
125
+ } finally {
126
+ for (const [signal, handler] of handlers) process.off(signal, handler);
127
+ }
128
+ } catch (error) {
129
+ console.error(`buninu qs${backend}: ${error.message}`);
130
+ process.exitCode = 1;
131
+ }
132
+ }