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,381 @@
1
+ import {
2
+ Parser,
3
+ parse,
4
+ parseExpressionAt,
5
+ tokenizer,
6
+ } from '../../vendor/acorn/dist/acorn.mjs';
7
+
8
+ // Small, strict QML subset. Acorn owns JavaScript tokenization/parsing, including
9
+ // strings, comments, regexes and template literals; unsupported QML fails loudly.
10
+ export function parseQml(source) {
11
+ // Preserve offsets while Acorn parses QML's cast operator as a binary operator.
12
+ // The compiler recognizes the original 'as' spelling and emits a checked cast.
13
+ const js = (code) => {
14
+ const parts = [...tokenizer(code, { ecmaVersion: 'latest' })];
15
+ for (let n = parts.length - 2; n >= 0; n--) {
16
+ const t = parts[n],
17
+ next = parts[n + 1];
18
+ if (
19
+ code.slice(t.start, t.end) === 'as' &&
20
+ /^[A-Z]/.test(code.slice(next.start, next.end))
21
+ )
22
+ code = code.slice(0, t.start) + 'in' + code.slice(t.end);
23
+ }
24
+ return code;
25
+ };
26
+ if (source.length > 256 * 1024)
27
+ throw new SyntaxError('QML experiment: source exceeds 256 KiB');
28
+ const tokens = [...tokenizer(source, { ecmaVersion: 'latest' })];
29
+ const javascriptSource = js(source);
30
+ let i = 0,
31
+ count = 0;
32
+ const text = () => source.slice(tokens[i]?.start, tokens[i]?.end);
33
+ const fail = (message) => {
34
+ throw new SyntaxError(
35
+ `QML: ${message} at offset ${tokens[i]?.start ?? source.length}`,
36
+ );
37
+ };
38
+ const take = (value) => {
39
+ if (text() !== value) fail(`expected ${value}, got ${text()}`);
40
+ i++;
41
+ };
42
+ const name = () => {
43
+ if (!/^[A-Za-z_$][\w$]*$/.test(text())) fail('expected identifier');
44
+ let result = text();
45
+ i++;
46
+ while (text() === '.') {
47
+ i++;
48
+ result += '.' + text();
49
+ i++;
50
+ }
51
+ return result;
52
+ };
53
+ const imports = [],
54
+ pragmas = [];
55
+ while (text() === 'pragma') {
56
+ const start = tokens[i++].end;
57
+ while (
58
+ i < tokens.length &&
59
+ !/[\r\n]/.test(source.slice(start, tokens[i].start)) &&
60
+ text() !== ';'
61
+ )
62
+ i++;
63
+ pragmas.push(source.slice(start, tokens[i - 1]?.end ?? start).trim());
64
+ if (text() === ';') i++;
65
+ }
66
+ while (text() === 'import') {
67
+ const start = tokens[i++].end;
68
+ while (
69
+ i < tokens.length &&
70
+ !/[\r\n]/.test(source.slice(start, tokens[i].start)) &&
71
+ text() !== ';'
72
+ )
73
+ i++;
74
+ imports.push(source.slice(start, tokens[i - 1]?.end ?? start).trim());
75
+ if (text() === ';') i++;
76
+ }
77
+ function object(depth = 0) {
78
+ if (depth > 64 || ++count > 2000) fail('object limit exceeded');
79
+ const type = name();
80
+ let bindingTarget = null;
81
+ if (text() === 'on') {
82
+ i++;
83
+ bindingTarget = name();
84
+ }
85
+ take('{');
86
+ const properties = [],
87
+ children = [],
88
+ components = [];
89
+ while (i < tokens.length && text() !== '}') {
90
+ if (text() === ';') {
91
+ i++;
92
+ continue;
93
+ }
94
+ if (text() === 'component') {
95
+ i++;
96
+ const componentName = name();
97
+ take(':');
98
+ components.push({ name: componentName, object: object(depth + 1) });
99
+ continue;
100
+ }
101
+ let afterName = i + 1;
102
+ while (tokens[afterName]?.type.label === '.') afterName += 2;
103
+ if (
104
+ tokens[afterName]?.type.label === '{' ||
105
+ source.slice(tokens[afterName]?.start, tokens[afterName]?.end) === 'on'
106
+ ) {
107
+ const child = object(depth + 1);
108
+ if (/^[a-z]/.test(child.type)) {
109
+ if (child.children.length || child.components.length)
110
+ fail('unsupported nested property group');
111
+ for (const p of child.properties)
112
+ properties.push({ ...p, key: child.type + '.' + p.key });
113
+ } else children.push(child);
114
+ continue;
115
+ }
116
+ if (text() === 'function') {
117
+ const start = tokens[i].start;
118
+ // Acorn parses the complete remaining JS prefix; QML's outer closing
119
+ // brace is not JavaScript, so locate the function body with tokens first.
120
+ let params = i;
121
+ while (params < tokens.length && tokens[params].type.label !== '(')
122
+ params++;
123
+ let endParams = params,
124
+ paren = 0;
125
+ do {
126
+ const label = tokens[endParams++]?.type.label;
127
+ if (label === '(') paren++;
128
+ if (label === ')') paren--;
129
+ } while (endParams < tokens.length && paren);
130
+ let body = endParams;
131
+ while (body < tokens.length && tokens[body].type.label !== '{') body++;
132
+ if (body === tokens.length) fail('missing function body');
133
+ let j = body,
134
+ level = 0;
135
+ do {
136
+ const label = tokens[j++]?.type.label;
137
+ if (label === '{' || label === '${') level++;
138
+ if (label === '}') level--;
139
+ } while (j < tokens.length && level);
140
+ let code = source.slice(start, tokens[j - 1].end);
141
+ // Erase QML annotations only at parameter/return-type positions. Braces
142
+ // in destructuring and default values are JavaScript, not the body.
143
+ const erase = (from, to) => {
144
+ code =
145
+ code.slice(0, from - start) +
146
+ ' '.repeat(to - from) +
147
+ code.slice(to - start);
148
+ };
149
+ let depth = 0,
150
+ braces = 0,
151
+ brackets = 0,
152
+ hasDefault = false;
153
+ for (let at = params; at < endParams; at++) {
154
+ const label = tokens[at].type.label;
155
+ if (label === '(') depth++;
156
+ else if (label === ')') depth--;
157
+ else if (label === '{' || label === '${') braces++;
158
+ else if (label === '}') braces--;
159
+ else if (label === '[') brackets++;
160
+ else if (label === ']') brackets--;
161
+ if (depth !== 1 || braces || brackets) continue;
162
+ if (label === ',') hasDefault = false;
163
+ if (label === '=') hasDefault = true;
164
+ if (label === ':' && !hasDefault) {
165
+ let end = at + 1;
166
+ while (
167
+ end < endParams &&
168
+ !['=', ',', ')'].includes(tokens[end].type.label)
169
+ )
170
+ end++;
171
+ erase(tokens[at].start, tokens[end].start);
172
+ at = end - 1;
173
+ }
174
+ }
175
+ if (tokens[endParams]?.type.label === ':')
176
+ erase(tokens[endParams].start, tokens[body].start);
177
+ const ast = parse(js(code), { ecmaVersion: 'latest' });
178
+ if (ast.body.length !== 1 || ast.body[0].type !== 'FunctionDeclaration')
179
+ fail('invalid function');
180
+ properties.push({ key: ast.body[0].id.name, code, ast, method: true });
181
+ i = j;
182
+ continue;
183
+ }
184
+ if (text() === 'signal') {
185
+ i++;
186
+ const key = name();
187
+ const args = [];
188
+ if (text() !== '(') {
189
+ properties.push({ key, signal: true, args });
190
+ continue;
191
+ }
192
+ take('(');
193
+ while (text() !== ')') {
194
+ const type = name(),
195
+ argument = name();
196
+ args.push(argument);
197
+ if (text() === ',') i++;
198
+ else break;
199
+ }
200
+ take(')');
201
+ properties.push({ key, signal: true, args });
202
+ continue;
203
+ }
204
+ let custom = false,
205
+ propertyType = null,
206
+ readonly = false,
207
+ required = false,
208
+ defaultProperty = false;
209
+ if (text() === 'default') {
210
+ defaultProperty = true;
211
+ i++;
212
+ }
213
+ if (text() === 'required') {
214
+ required = true;
215
+ i++;
216
+ }
217
+ if (text() === 'readonly') {
218
+ readonly = true;
219
+ i++;
220
+ }
221
+ if (
222
+ text() === 'property' &&
223
+ source.slice(tokens[i + 1]?.start, tokens[i + 1]?.end) !== ':'
224
+ ) {
225
+ i++;
226
+ custom = true;
227
+ propertyType = name();
228
+ }
229
+ const key = name();
230
+ if (custom && text() !== ':') {
231
+ const code = required
232
+ ? 'undefined'
233
+ : ({
234
+ int: '0',
235
+ real: '0',
236
+ double: '0',
237
+ bool: 'false',
238
+ string: '""',
239
+ url: '""',
240
+ var: 'undefined',
241
+ point: '({x:0,y:0})',
242
+ size: '({width:0,height:0})',
243
+ rect: '({x:0,y:0,width:0,height:0})',
244
+ color: '"transparent"',
245
+ }[propertyType] ?? 'null');
246
+ properties.push({
247
+ key,
248
+ custom,
249
+ propertyType,
250
+ required,
251
+ readonly,
252
+ defaultProperty,
253
+ code,
254
+ ast: parseExpressionAt(code, 0, { ecmaVersion: 'latest' }),
255
+ });
256
+ continue;
257
+ }
258
+ take(':');
259
+ let objectEnd = i + 1;
260
+ while (tokens[objectEnd]?.type.label === '.') objectEnd += 2;
261
+ if (
262
+ /^[A-Za-z_$][\w$]*$/.test(text()) &&
263
+ tokens[objectEnd]?.type.label === '{'
264
+ ) {
265
+ properties.push({
266
+ key,
267
+ custom,
268
+ propertyType,
269
+ readonly,
270
+ required,
271
+ defaultProperty,
272
+ object: object(depth + 1),
273
+ });
274
+ continue;
275
+ }
276
+ const start = tokens[i]?.start;
277
+ if (start === undefined) fail('missing value');
278
+ let ast,
279
+ end,
280
+ block = false;
281
+ if (
282
+ /^(?:.*\.)?on[A-Z_]/.test(key) &&
283
+ [
284
+ 'if',
285
+ 'for',
286
+ 'while',
287
+ 'do',
288
+ 'switch',
289
+ 'try',
290
+ 'throw',
291
+ 'return',
292
+ ].includes(text())
293
+ ) {
294
+ const parser = new Parser(
295
+ { ecmaVersion: 'latest', allowReturnOutsideFunction: true },
296
+ javascriptSource,
297
+ start,
298
+ );
299
+ parser.nextToken();
300
+ const statement = parser.parseStatement(null);
301
+ end = statement.end;
302
+ const code = '{' + source.slice(start, end) + '}';
303
+ ast = parse(js(code), {
304
+ ecmaVersion: 'latest',
305
+ allowReturnOutsideFunction: true,
306
+ });
307
+ properties.push({
308
+ key,
309
+ custom,
310
+ propertyType,
311
+ code,
312
+ ast,
313
+ block: true,
314
+ readonly,
315
+ defaultProperty,
316
+ });
317
+ while (tokens[i]?.start < end) i++;
318
+ continue;
319
+ }
320
+ if (text() === '{') {
321
+ block = true;
322
+ let nesting = 0,
323
+ j = i;
324
+ do {
325
+ const label = tokens[j]?.type.label;
326
+ if (label === '{' || label === '${') nesting++;
327
+ if (label === '}') nesting--;
328
+ j++;
329
+ } while (j < tokens.length && nesting);
330
+ if (nesting) fail('unterminated block');
331
+ end = tokens[j - 1].end;
332
+ const body = source.slice(start, end);
333
+ let literal;
334
+ if (!/^(?:.*\.)?on[A-Z_]/.test(key))
335
+ try {
336
+ literal = parseExpressionAt(js(body), 0, { ecmaVersion: 'latest' });
337
+ } catch {}
338
+ if (
339
+ literal?.type === 'ObjectExpression' &&
340
+ literal.end === body.length
341
+ ) {
342
+ ast = literal;
343
+ block = false;
344
+ } else
345
+ ast = parse(js(body), {
346
+ ecmaVersion: 'latest',
347
+ allowReturnOutsideFunction: true,
348
+ });
349
+ } else {
350
+ ast = parseExpressionAt(javascriptSource, start, {
351
+ ecmaVersion: 'latest',
352
+ preserveParens: true,
353
+ });
354
+ end = ast.end;
355
+ }
356
+ const code = source.slice(start, end);
357
+ // Normalize offsets to each expression for the compiler's identifier rewrite.
358
+ if (!block)
359
+ ast = parseExpressionAt(js(code), 0, {
360
+ ecmaVersion: 'latest',
361
+ preserveParens: true,
362
+ });
363
+ properties.push({
364
+ key,
365
+ custom,
366
+ propertyType,
367
+ code,
368
+ ast,
369
+ block,
370
+ readonly,
371
+ defaultProperty,
372
+ });
373
+ while (tokens[i]?.start < end) i++;
374
+ }
375
+ take('}');
376
+ return { type, properties, children, components, bindingTarget };
377
+ }
378
+ const root = object();
379
+ if (i !== tokens.length) fail('unexpected trailing input');
380
+ return { imports, pragmas, root };
381
+ }
package/lib/qml/qs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ // Experimental Quickshell-compatible launcher. Keep it opt-in via PATH.
3
+ import { main } from './launcher.js';
4
+ try { await main(Bun.argv.slice(2)); }
5
+ catch (error) { console.error(`buninu qs: ${error.message}`); process.exitCode = 1; }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import { runBackend } from './launcher.js';
3
+ await runBackend('casty');
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import { runBackend } from './launcher.js';
3
+ await runBackend('headless');
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import { runBackend } from './launcher.js';
3
+ await runBackend('wayland');
@@ -0,0 +1,159 @@
1
+ // Serialized into the existing page realm; no host callbacks or native APIs.
2
+ export function createReactive() {
3
+ let revision = 0;
4
+ let active = null,
5
+ pending = new Set(),
6
+ scheduled = false;
7
+ const track = new WeakMap(),
8
+ raw = new WeakMap();
9
+ const mark = (job) => {
10
+ if (job.stopped || job.dirty || job.running) return;
11
+ job.dirty = true;
12
+ pending.add(job);
13
+ job.invalidate?.();
14
+ };
15
+ const invalidate = (target, key) => {
16
+ for (const job of track.get(raw.get(target) ?? target)?.get(key) ?? [])
17
+ mark(job);
18
+ };
19
+ const clean = (job) => {
20
+ for (const set of job.deps) set.delete(job);
21
+ job.deps.clear();
22
+ };
23
+ const run = (job) => {
24
+ if (job.stopped || !job.dirty) return;
25
+ if (job.running) throw Error('QML binding cycle');
26
+ job.dirty = false;
27
+ job.running = true;
28
+ clean(job);
29
+ const previous = active;
30
+ active = job;
31
+ try {
32
+ job.fn();
33
+ } catch (error) {
34
+ job.dirty = true;
35
+ throw error;
36
+ } finally {
37
+ job.running = false;
38
+ active = previous;
39
+ }
40
+ };
41
+ const flush = (allowIncomplete = false) => {
42
+ scheduled = false;
43
+ let passes = 0,
44
+ last;
45
+ while (pending.size) {
46
+ if (++passes > 100) throw new Error('QML binding cycle');
47
+ const jobs = [...pending],
48
+ failed = [];
49
+ pending.clear();
50
+ for (const job of jobs) {
51
+ if (job.running) {
52
+ pending.add(job);
53
+ continue;
54
+ }
55
+ try {
56
+ run(job);
57
+ } catch (error) {
58
+ if (error.qmlFatal) throw error;
59
+ last = error;
60
+ failed.push(job);
61
+ }
62
+ }
63
+ if (failed.length && !pending.size) {
64
+ if (allowIncomplete) {
65
+ for (const job of failed) pending.add(job);
66
+ return last;
67
+ }
68
+ throw last;
69
+ }
70
+ for (const job of failed) pending.add(job);
71
+ if (pending.size && [...pending].every((job) => job.running)) return null;
72
+ }
73
+ return null;
74
+ };
75
+ const state = (initial, interceptSet) => {
76
+ const proxy = new Proxy(initial, {
77
+ get(target, key) {
78
+ if (active) {
79
+ let map = track.get(target);
80
+ if (!map) track.set(target, (map = new Map()));
81
+ let set = map.get(key);
82
+ if (!set) map.set(key, (set = new Set()));
83
+ set.add(active);
84
+ active.deps.add(set);
85
+ }
86
+ return target[key];
87
+ },
88
+ set(target, key, value) {
89
+ if (interceptSet?.(proxy, target, key, value)) return true;
90
+ // A setter may detach a binding even when its value does not change.
91
+ if (Object.hasOwn(target, key) && Object.is(target[key], value)) {
92
+ if (Object.getOwnPropertyDescriptor(target, key)?.set) target[key] = value;
93
+ return true;
94
+ }
95
+ target[key] = value;
96
+ revision++;
97
+ invalidate(target, key);
98
+ if (pending.size && !scheduled) {
99
+ scheduled = true;
100
+ queueMicrotask(flush);
101
+ }
102
+ return true;
103
+ },
104
+ });
105
+ raw.set(proxy, initial);
106
+ return proxy;
107
+ };
108
+ const effect = (fn) => {
109
+ const job = {
110
+ fn,
111
+ deps: new Set(),
112
+ stopped: false,
113
+ dirty: true,
114
+ running: false,
115
+ };
116
+ try {
117
+ run(job);
118
+ } catch (error) {
119
+ job.stopped = true;
120
+ clean(job);
121
+ pending.delete(job);
122
+ throw error;
123
+ }
124
+ const stop = () => {
125
+ job.stopped = true;
126
+ clean(job);
127
+ pending.delete(job);
128
+ };
129
+ stop.run = () => run(job);
130
+ Object.defineProperty(stop, 'onInvalidate', {
131
+ set: (fn) => {
132
+ job.invalidate = fn;
133
+ },
134
+ });
135
+ Object.defineProperty(stop, 'dirty', {
136
+ get: () => job.dirty && !job.stopped,
137
+ });
138
+ return stop;
139
+ };
140
+ const untrack = (fn) => {
141
+ const previous = active;
142
+ active = null;
143
+ try {
144
+ return fn();
145
+ } finally {
146
+ active = previous;
147
+ }
148
+ };
149
+ return {
150
+ state,
151
+ effect,
152
+ flush,
153
+ untrack,
154
+ invalidate,
155
+ get revision() {
156
+ return revision;
157
+ },
158
+ };
159
+ }