buninu-browser 0.3.0 → 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 (239) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +19 -4
  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 +37 -2
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +47 -8
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/qml/README.md +971 -0
  35. package/lib/qml/builtins.js +700 -0
  36. package/lib/qml/canvas-dom.js +115 -0
  37. package/lib/qml/document.js +1176 -0
  38. package/lib/qml/format.js +9 -0
  39. package/lib/qml/host.js +324 -0
  40. package/lib/qml/images.js +199 -0
  41. package/lib/qml/launcher.js +132 -0
  42. package/lib/qml/loader.js +517 -0
  43. package/lib/qml/media-renderer.js +49 -0
  44. package/lib/qml/media.js +86 -0
  45. package/lib/qml/models.js +622 -0
  46. package/lib/qml/parser.js +381 -0
  47. package/lib/qml/qs +5 -0
  48. package/lib/qml/qscasty +3 -0
  49. package/lib/qml/qsheadless +3 -0
  50. package/lib/qml/qswayland +3 -0
  51. package/lib/qml/reactive.js +159 -0
  52. package/lib/qml/runtime.js +3381 -0
  53. package/lib/qml/shapes.js +243 -0
  54. package/lib/qml/text.js +109 -0
  55. package/lib/qml/textures.js +123 -0
  56. package/lib/qml/wayland-clipboard.js +32 -0
  57. package/lib/qml/worker-thread.js +70 -0
  58. package/lib/render-tree/index.js +16 -1
  59. package/lib/renderer/bindings.js +11 -0
  60. package/lib/renderer/canvas-2d.js +13 -0
  61. package/lib/renderer/host.js +31 -6
  62. package/lib/renderer/main.js +19 -6
  63. package/lib/renderer/page-realm.js +16 -1
  64. package/lib/renderer/page-renderer.js +230 -13
  65. package/lib/renderer/text-input-paint.js +54 -0
  66. package/lib/renderer/text-input.js +57 -0
  67. package/lib/style/computed-style.js +2 -2
  68. package/package.json +1 -1
  69. package/test/clipboard.test.js +19 -0
  70. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  71. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  72. package/test/fixtures/flea-helpers/bin/bat +3 -0
  73. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  74. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  75. package/test/fixtures/flea-helpers/bin/glow +3 -0
  76. package/test/fixtures/flea-helpers/bin/qs +3 -0
  77. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  78. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  79. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  80. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  81. package/test/fixtures/flea-helpers/check.mjs +36 -0
  82. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  83. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  84. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  85. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  86. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  87. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  88. package/test/fixtures/qml/flea/LICENSE +21 -0
  89. package/test/fixtures/qml/flea/README.md +25 -0
  90. package/test/fixtures/qml/flea/Theme.qml +8 -0
  91. package/test/fixtures/qml/flea/demo.qml +24 -0
  92. package/test/fixtures/qml/flea/qmldir +3 -0
  93. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  94. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  95. package/test/fixtures/qml/flea-theme/README.md +70 -0
  96. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  97. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  98. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  99. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  100. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  101. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  102. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  103. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  104. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  107. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  108. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  109. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  110. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  111. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  112. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  113. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  114. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  115. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  116. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  117. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  118. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  119. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  120. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  121. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  122. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  123. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  124. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  125. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  126. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  127. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  128. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  167. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  168. package/test/fixtures/qml/hello.qml +26 -0
  169. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  170. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  171. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  172. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  173. package/test/fixtures/qml/imports/logic/base.js +2 -0
  174. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  175. package/test/fixtures/qml/imports/logic/math.js +3 -0
  176. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  177. package/test/fixtures/qml/imports/main.qml +21 -0
  178. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  179. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  180. package/test/fixtures/qml/multiply.qml +61 -0
  181. package/test/fixtures/qml-canvas-dom.js +90 -0
  182. package/test/fixtures/qml-media-audio.js +39 -0
  183. package/test/helpers/flea-sandbox.js +48 -0
  184. package/test/helpers/quickshell-fixture.js +125 -0
  185. package/test/html-clipboard-wayland.test.js +47 -0
  186. package/test/index.test.js +13 -0
  187. package/test/manual-casty-without-qml.js +87 -0
  188. package/test/manual-flea-application.js +49 -0
  189. package/test/manual-flea-binary.js +62 -0
  190. package/test/manual-flea-qml.js +9 -0
  191. package/test/manual-flea-settings.js +40 -0
  192. package/test/manual-flea-theme.js +40 -0
  193. package/test/manual-qml-removal.js +46 -0
  194. package/test/manual-without-qml.js +45 -0
  195. package/test/qml-canvas-dom.test.js +37 -0
  196. package/test/qml-clipboard-bridge.test.js +25 -0
  197. package/test/qml-clipboard-wayland.test.js +40 -0
  198. package/test/qml-connections-timing.test.js +73 -0
  199. package/test/qml-dynamic.test.js +401 -0
  200. package/test/qml-flea-app.test.js +146 -0
  201. package/test/qml-flea-backend.test.js +78 -0
  202. package/test/qml-flea-columns-backend.test.js +38 -0
  203. package/test/qml-flea-compile.test.js +78 -0
  204. package/test/qml-flea-launcher.test.js +88 -0
  205. package/test/qml-flea-markdown-backend.test.js +31 -0
  206. package/test/qml-flea-media.test.js +28 -0
  207. package/test/qml-flea-navigation-backend.test.js +35 -0
  208. package/test/qml-flea-navigation-timing.test.js +147 -0
  209. package/test/qml-flea-preview-backend.test.js +53 -0
  210. package/test/qml-flea-previews.test.js +163 -0
  211. package/test/qml-flea-rows.test.js +101 -0
  212. package/test/qml-flea-textures.test.js +59 -0
  213. package/test/qml-flea-view-switch.test.js +33 -0
  214. package/test/qml-flea-wayland.test.js +68 -0
  215. package/test/qml-flea.test.js +34 -0
  216. package/test/qml-host.test.js +238 -0
  217. package/test/qml-imports.test.js +102 -0
  218. package/test/qml-inherited-handlers.test.js +51 -0
  219. package/test/qml-input-selection.test.js +66 -0
  220. package/test/qml-interactions.test.js +309 -0
  221. package/test/qml-language.test.js +167 -0
  222. package/test/qml-launcher.test.js +54 -0
  223. package/test/qml-layout-text.test.js +205 -0
  224. package/test/qml-media.test.js +63 -0
  225. package/test/qml-persisted-delegates.test.js +45 -0
  226. package/test/qml-quickshell-controls.test.js +65 -0
  227. package/test/qml-quickshell-settings.test.js +62 -0
  228. package/test/qml-repeater.test.js +72 -0
  229. package/test/qml-style-updates.test.js +124 -0
  230. package/test/qml-textures.test.js +66 -0
  231. package/test/qml-views.test.js +332 -0
  232. package/test/qml-worker.test.js +69 -0
  233. package/test/qml.test.js +83 -0
  234. package/test/renderer-host.test.js +11 -0
  235. package/test/text-input-pointer.test.js +31 -0
  236. package/test/text-input-scroll.test.js +29 -0
  237. package/test/text-input.test.js +47 -0
  238. package/test/wayland-frontend.test.js +83 -0
  239. package/test/wayland-urlbar.test.js +38 -0
@@ -0,0 +1,54 @@
1
+ import { computeElementStyle } from '../style/computed-style.js';
2
+ export const inputPresentation = new WeakMap();
3
+ export const inputOwner = new WeakMap();
4
+
5
+ const spans = new WeakMap();
6
+ const carets = new WeakMap();
7
+ export const inputCaretNode = element => carets.get(element);
8
+
9
+ // Form controls share native selection and caret presentation across frontends.
10
+ export function textInputSelection(element, text, style, make) {
11
+ const options=inputPresentation.get(element)||{};
12
+ const focused=element.ownerDocument.activeElement===element;
13
+ const value=String(element.value??'');
14
+ const position=at=>element.type==='password'?[...value.slice(0,at)].length:at;
15
+ const start=position(element.selectionStart??0),end=position(element.selectionEnd??0);
16
+ const raw=(focused||options.persistent)&&end>start ? {start,end,background:options.background||'#3874f2',color:options.selectedColor||'white'}:null;
17
+ const caretAt = (at, selected=false) => {
18
+ let span=carets.get(element);
19
+ if(!span){span=element.ownerDocument.createElement('span');carets.set(element,span);inputOwner.set(span,element);}
20
+ const height=options.height||parseFloat(style.fontSize)||16;
21
+ const color=options.color||style.color;
22
+ const on=!selected&&options.on!==false&&options.visible!==false;
23
+ span.style.cssText=`display:inline-block;width:0;height:${height}px;border-left:1px solid ${on?color:'transparent'};margin-right:-1px;vertical-align:text-bottom;pointer-events:none`;
24
+ return make('caret','inline-block',{tagName:'SPAN',style:computeElementStyle(span,style),children:Object.freeze([])},span);
25
+ };
26
+ if (!raw) {
27
+ if(!focused||element.disabled||options.visible===false)return null;
28
+ const node=(part,text)=>make(part,'text',{text,style,valueStart:part==='after'?start:0,pseudo:'value',children:Object.freeze([])},element);
29
+ return [node('before',text.slice(0,start)),caretAt(start),node('after',text.slice(start))];
30
+ }
31
+ const { background, color } = raw;
32
+ const from = Math.max(0, Math.min(text.length, start)),
33
+ to = Math.max(from, Math.min(text.length, end));
34
+ if (from === to) return null;
35
+ const textNode = (part, text, textStyle = style, domNode = element) => make(part, 'text', {
36
+ text, style: textStyle, valueStart:part==='selected'?from:part==='after'?to:0, pseudo: 'value', children: Object.freeze([]),
37
+ }, domNode);
38
+ let span = spans.get(element);
39
+ if (!span) { span = element.ownerDocument.createElement('span'); spans.set(element, span); inputOwner.set(span,element); }
40
+ span.style.pointerEvents = 'none';
41
+ span.style.backgroundColor = background;
42
+ span.style.color = color;
43
+ const selectedStyle = computeElementStyle(span, style);
44
+ const children = [];
45
+ if (from) children.push(textNode('before', text.slice(0, from)));
46
+ if(focused&&element.selectionDirection==='backward')children.push(caretAt(from,true));
47
+ children.push(make('selection', 'inline', {
48
+ tagName: 'SPAN', style: selectedStyle,
49
+ children: Object.freeze([textNode('selected', text.slice(from, to), selectedStyle, span)]),
50
+ }, span));
51
+ if(focused&&element.selectionDirection!=='backward')children.push(caretAt(to,true));
52
+ if (to < text.length) children.push(textNode('after', text.slice(to)));
53
+ return children;
54
+ }
@@ -0,0 +1,57 @@
1
+ // Shared editing defaults for HTML and QML's real input/textarea elements.
2
+ export function isTextInput(element) {
3
+ return !!element && !element.disabled && (element.tagName==='TEXTAREA' ||
4
+ element.tagName==='INPUT' && ['text','search','url','email','tel','password'].includes(element.type));
5
+ }
6
+ export const inputRange = element => ({text:String(element.value),start:element.selectionStart??0,end:element.selectionEnd??0});
7
+ export function selectInput(element,start,end=start) {
8
+ element.setSelectionRange(Math.min(start,end),Math.max(start,end),end<start?'backward':'forward');
9
+ }
10
+ const columns=new WeakMap();
11
+ export function moveInputSelection(element,event) {
12
+ if(!isTextInput(element)||event.altKey)return false;
13
+ const {text,start,end}=inputRange(element),ctrl=event.ctrlKey||event.metaKey,key=event.key;
14
+ if(ctrl&&key.toLowerCase()==='a'){selectInput(element,0,text.length);return true;}
15
+ if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(key))return false;
16
+ let at=element.selectionDirection==='backward'?start:end;
17
+ const anchor=at===start?end:start,multi=element.tagName==='TEXTAREA';
18
+ const lineStart=text.lastIndexOf('\n',at-1)+1;
19
+ if(key==='ArrowUp'||key==='ArrowDown') {
20
+ if(!multi)return false;
21
+ const previous=columns.get(element),column=previous?.at===at?previous.column:at-lineStart;
22
+ if(key==='ArrowUp') {
23
+ const stop=lineStart-1;at=stop<0?0:Math.min(text.lastIndexOf('\n',stop-1)+1+column,stop);
24
+ } else {
25
+ const next=text.indexOf('\n',at),stop=next<0?-1:text.indexOf('\n',next+1);
26
+ at=next<0?text.length:Math.min(next+1+column,stop<0?text.length:stop);
27
+ }
28
+ columns.set(element,{at,column});
29
+ } else {
30
+ columns.delete(element);
31
+ if(key==='Home')at=multi&&!ctrl?lineStart:0;
32
+ else if(key==='End'){const stop=text.indexOf('\n',at);at=multi&&!ctrl&&stop>=0?stop:text.length;}
33
+ else if(!event.shiftKey&&start!==end)at=key==='ArrowLeft'?start:end;
34
+ else if(key==='ArrowLeft') {
35
+ at-=[...text.slice(0,at)].pop()?.length??0;
36
+ if(ctrl){while(at>0&&/\s/u.test(text[at]))at--;while(at>0&&/[\p{L}\p{N}_]/u.test(text[at-1]))at--;}
37
+ } else {
38
+ at+=text.codePointAt(at)>0xffff?2:at<text.length?1:0;
39
+ if(ctrl){while(at<text.length&&/[\p{L}\p{N}_]/u.test(text[at]))at++;while(at<text.length&&/\s/u.test(text[at]))at++;}
40
+ }
41
+ }
42
+ selectInput(element,event.shiftKey?anchor:at,at);return true;
43
+ }
44
+ export async function inputClipboard(element,operation,{read,write,replace}) {
45
+ if(!isTextInput(element)||operation!=='copy'&&element.readOnly)return;
46
+ const range=inputRange(element);
47
+ if(operation==='copy'||operation==='cut') {
48
+ if(element.type==='password'||range.start===range.end)return;
49
+ await write(range.text.slice(range.start,range.end));
50
+ if(operation==='copy')return;
51
+ }
52
+ let text=operation==='paste'?await read():'';
53
+ if(String(element.value)!==range.text||element.selectionStart!==range.start||element.selectionEnd!==range.end||element.readOnly||!element.isConnected)return;
54
+ if(element.tagName!=='TEXTAREA')text=text.replace(/[\r\n]+/g,'');
55
+ if(element.maxLength>=0)text=text.slice(0,Math.max(0,element.maxLength-(range.text.length-range.end+range.start)));
56
+ replace(text,operation==='paste'?'insertFromPaste':'deleteByCut');
57
+ }
@@ -284,8 +284,8 @@ const CONTROL_FONT = "font-size: 13.333px; font-family: sans-serif";
284
284
  const FIELD = `${CONTROL_FONT}; border: 1px solid #767676; background-color: white`;
285
285
  const BUTTON_LIKE = `${CONTROL_FONT}; border: 1px solid #767676; border-radius: 2px; padding: 1px 6px; background-color: #efefef; text-align: center`;
286
286
  const UA_CONTROL_DECLARATIONS = Object.freeze({
287
- textarea: parseDeclarations(`${FIELD}; font-family: monospace; padding: 2px; white-space: pre-wrap; overflow-wrap: break-word`),
288
- text: parseDeclarations(`${FIELD}; padding: 1px 2px; white-space: pre`),
287
+ textarea: parseDeclarations(`${FIELD}; font-family: monospace; padding: 2px; white-space: pre-wrap; overflow-wrap: break-word; overflow: auto`),
288
+ text: parseDeclarations(`${FIELD}; padding: 1px 2px; white-space: pre; overflow: hidden`),
289
289
  checkbox: parseDeclarations("width: 13px; height: 13px; margin: 3px 3px 3px 4px; border: 1px solid #767676; border-radius: 2px; background-color: white; box-sizing: border-box"),
290
290
  radio: parseDeclarations("width: 13px; height: 13px; margin: 3px 3px 0 5px; border: 1px solid #767676; border-radius: 50%; background-color: white; box-sizing: border-box"),
291
291
  button: parseDeclarations(`${BUTTON_LIKE}; white-space: pre`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-browser",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Buninu Browser is a browser engine written in JavaScript and WebAssembly for Bun.",
5
5
  "keywords": [
6
6
  "buninu",
@@ -33,3 +33,22 @@ test('a real button click writes through controller; page-only calls cannot over
33
33
  expect(await host.call('evaluate','window.copyStatus')).toBe('copied');
34
34
  }finally{host.close();}
35
35
  },20000);
36
+
37
+ test('Windows also uses Bun.which-resolved xclip before its platform fallback',async()=>{
38
+ const seen=[];
39
+ await writeClipboard('Windows 中文',{platform:'win32',which:name=>{seen.push(name);return name==='xclip'?'C:/bin/xclip.exe':null;},spawn:(args,opts)=>{expect(args).toEqual(['C:/bin/xclip.exe','-selection','clipboard']);expect(opts.stdin.toString()).toBe('Windows 中文');return {exited:Promise.resolve(0),kill(){}};}});
40
+ expect(seen).toEqual(['xclip']);
41
+ });
42
+
43
+ test('clipboard read is one-shot, preserves empty/text values, and prefers xclip on Windows too',async()=>{
44
+ const {readClipboard}=await import('../lib/clipboard.js');
45
+ const calls=[];let value='中文\n';
46
+ const options={platform:'win32',which:name=>name==='xclip'?'/bin/xclip':null,spawn:(args)=>{
47
+ calls.push(args);return {stdout:new Response(value).body,exited:Promise.resolve(0),kill(){}};
48
+ }};
49
+ expect(await readClipboard(options)).toBe('中文\n');
50
+ expect(calls).toEqual([['/bin/xclip','-selection','clipboard','-o']]);
51
+ value='';expect(await readClipboard(options)).toBe('');
52
+ await Bun.sleep(30);expect(calls).toHaveLength(2);
53
+ await expect(readClipboard({...options,which:()=>null})).rejects.toThrow('No working');
54
+ });
@@ -0,0 +1,36 @@
1
+ # Build context: buninu-browser repository root. Compatible with js-udocker.
2
+ FROM oven/bun:1.4.4
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+ ENV LC_ALL=C.UTF-8
5
+ RUN set -eu; if [ -f /usr/bin/perl ] && [ ! -e /usr/bin/perl5.40.1 ]; then cp /usr/bin/perl /usr/bin/perl5.40.1; fi
6
+ RUN set -eu; apt-get update; apt-get install -y --no-install-recommends ca-certificates curl zstd shared-mime-info util-linux libgcc-s1; apt-get clean
7
+ RUN set -eu; case "$(uname -m)" in aarch64|arm64) arch=aarch64 ;; x86_64) arch=x86_64 ;; *) exit 1 ;; esac; curl -fL --retry 3 "https://pkgs.omarchy.org/stable/$arch/flea-0.3.12-1-$arch.pkg.tar.zst" -o /tmp/flea.pkg.tar.zst; tar --zstd -xf /tmp/flea.pkg.tar.zst -C / usr; /usr/bin/flea --version
8
+ RUN set -eu; mkdir -p /usr/share/omarchy; curl -fL --retry 3 https://codeload.github.com/basecamp/omarchy/tar.gz/3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d -o /tmp/omarchy.tar.gz; tar -xzf /tmp/omarchy.tar.gz --strip-components=1 -C /usr/share/omarchy omarchy-3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d/shell/Commons omarchy-3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d/shell/Ui omarchy-3c7027db2a4fa9fa6a49d7af7f87c27ac48e677d/LICENSE; ln -sfn shell /usr/share/omarchy/qs
9
+ COPY buninu-browser.js /opt/buninu-browser/buninu-browser.js
10
+ COPY frontends /opt/buninu-browser/frontends
11
+ COPY usr /opt/buninu-browser/usr
12
+ COPY lib /opt/buninu-browser/lib
13
+ COPY vendor /opt/buninu-browser/vendor
14
+ COPY LICENSES /opt/buninu-browser/LICENSES
15
+ COPY LICENSE /opt/buninu-browser/LICENSE
16
+ COPY THIRD_PARTY_NOTICES.md /opt/buninu-browser/THIRD_PARTY_NOTICES.md
17
+ COPY package.json /opt/buninu-browser/package.json
18
+ COPY test/fixtures/flea-helpers /usr/local/lib/flea-helpers
19
+ COPY test/fixtures/flea-thumbnail-stub /opt/flea-thumbnail-stub
20
+ COPY test/fixtures/tone.mp3 /root/flea-demo/tone.mp3
21
+ COPY test/fixtures/long-video.mp4 /root/flea-demo/long-video.mp4
22
+ WORKDIR /usr/local/lib/flea-helpers
23
+ RUN set -eu; bun i --backend=copyfile jsmdcui@0.19.2; chmod +x /usr/local/lib/flea-helpers/bin/*; for tool in qs bat glow bwrap; do ln -sfn /usr/local/lib/flea-helpers/bin/$tool /usr/local/bin/$tool; done; install -Dm755 /opt/flea-thumbnail-stub/flea-thumbnail-stub /usr/local/bin/flea-thumbnail-stub; install -Dm644 /opt/flea-thumbnail-stub/flea-placeholder.thumbnailer /usr/share/thumbnailers/flea-placeholder.thumbnailer
24
+ RUN set -eu; install -Dm644 source-mime.xml /usr/share/mime/packages/buninu-source.xml; update-mime-database /usr/share/mime
25
+ RUN set -eu; printf 'const answer = 42;\n' > /root/flea-demo/hello.js; bun /usr/local/lib/flea-helpers/check.mjs
26
+ ENV HOME=/root
27
+ ENV XDG_RUNTIME_DIR=/tmp
28
+ ENV WAYLAND_DISPLAY=wayland-buninu
29
+ ENV XDG_CONFIG_HOME=/root/.config
30
+ ENV XDG_DATA_HOME=/root/.local/share
31
+ ENV XDG_STATE_HOME=/root/.local/state
32
+ ENV XDG_CACHE_HOME=/root/.cache
33
+ ENV QSG_RHI_BACKEND=opengl
34
+ WORKDIR /root
35
+ ENTRYPOINT []
36
+ CMD ["/usr/bin/flea", "--gui", "/root/flea-demo"]
@@ -0,0 +1,43 @@
1
+ #!/usr/local/bin/bun
2
+ // Experimental Flea bat argv adapter, using the unchanged bundled glow highlighter.
3
+ import {RuntimeRegistry} from './node_modules/jsmdcui/src/runtime/registry.js';
4
+ import {Colorscheme} from './node_modules/jsmdcui/src/config/colorscheme.js';
5
+ import {SyntaxDefinition,detectSyntax,loadSyntaxDefinitions} from './node_modules/jsmdcui/src/highlight/parser.js';
6
+ import {Highlighter} from './node_modules/jsmdcui/src/highlight/highlighter.js';
7
+ import {styleToAnsi} from './node_modules/jsmdcui/src/display/ansi-style.js';
8
+ import {fileURLToPath} from 'node:url';
9
+ import {adaptSyntaxRules} from './syntax-compat.mjs';
10
+ let name='',lang='',file=null,first=1,last=Infinity;
11
+ for(const arg of Bun.argv.slice(2)){
12
+ if(['--no-config','--no-custom-assets','--theme=ansi','--color=always','--style=plain','--paging=never','--wrap=never'].includes(arg))continue;
13
+ if(arg.startsWith('--file-name='))name=arg.slice(12);
14
+ else if(arg.startsWith('--language='))lang=arg.slice(11);
15
+ else if(arg.startsWith('--line-range=')){const m=/^--line-range=(\d+):(\d+)$/.exec(arg);if(!m)throw Error('Unsupported line range');first=+m[1];last=+m[2];}
16
+ else if(arg.startsWith('-')&&arg!=='-')throw Error('Unsupported bat option: '+arg);
17
+ else if(file!==null)throw Error('Only one input is supported');else file=arg;
18
+ }
19
+ const text=await (file&&file!=='-'?Bun.file(file):Bun.stdin).text();
20
+ if(Buffer.byteLength(text)>1048576)throw Error('Preview exceeds 1 MiB');
21
+ const runtime=new RuntimeRegistry({repoRoot:fileURLToPath(new URL('./node_modules/jsmdcui',import.meta.url)),configDir:'/tmp'});
22
+ await runtime.init({user:false});
23
+ const scheme=await new Colorscheme(runtime).load('simple'),defs=await loadSyntaxDefinitions(runtime);
24
+ adaptSyntaxRules(defs,SyntaxDefinition);
25
+ const lines=text.replace(/\r\n?/g,'\n').split('\n');if(lines.at(-1)==='')lines.pop();
26
+ const aliases={js:'javascript',jsx:'javascript',mjs:'javascript',cjs:'javascript',node:'javascript',nodejs:'javascript',ts:'typescript',tsx:'typescript',mts:'typescript',cts:'typescript',rs:'rust',py:'python',python3:'python',sh:'shell',bash:'shell',shellscript:'shell',md:'markdown',yml:'yaml',jsonc:'json',json5:'json',jsonl:'json',ndjson:'json',cpp:'c++',cc:'c++',cxx:'c++',hpp:'c++',cs:'csharp','c#':'csharp',rb:'ruby',golang:'go',kt:'kotlin',kts:'kotlin',ps1:'powershell',pwsh:'powershell',bat:'batch',cmd:'batch',docker:'dockerfile',make:'makefile',cmakelists:'cmake',tf:'terraform',proto:'proto',protobuf:'proto',tex:'tex',latex:'tex',diff:'patch',dotenv:'ini'};
27
+ const byType=new Map(defs.map(d=>[d.filetype.toLowerCase(),d]));
28
+ const lookup=key=>byType.get(aliases[key.toLowerCase()]||key.toLowerCase());
29
+ const path=name||file||'',ext=path.split('.').at(-1).toLowerCase();
30
+ const def=lang?lookup(lang):lookup(({jsx:'jsx',mts:'ts',cts:'ts',jsonc:'json',json5:'json',jsonl:'json',ndjson:'json',env:'ini'})[ext]||'')||detectSyntax(defs,{path,firstLine:lines[0]||'',lines:lines.slice(0,50)});
31
+ if(lang&&!def)process.exit(1);
32
+ const highlighter=def?new Highlighter(def,defs):null;let state=null;
33
+ for(let i=0;i<Math.min(lines.length,last);i++){
34
+ const line=lines[i],result=highlighter?.highlightLine(line,state);if(result)state=result.state;
35
+ if(i+1<first)continue;
36
+ if(!result){process.stdout.write(line+'\n');continue;}
37
+ const changes=[...result.changes.entries()];let output='';
38
+ for(let j=0;j<changes.length-1;j++){
39
+ const [from,group]=changes[j],to=changes[j+1][0];
40
+ output+=styleToAnsi(scheme.get(group)).replace(/38;5;(\d+)/g,(_,n)=>String(+n<8?30+(+n):90+(+n)-8))+line.slice(from,to);
41
+ }
42
+ process.stdout.write(output+'\x1b[0m\n');
43
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ sd=$(dirname "$(realpath "$0")")
3
+ exec bun "$sd/../bat.mjs" "$@"
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ # Experiment only: no namespace/filesystem isolation; outer prlimit remains.
3
+ envargs=()
4
+ while (($#)); do
5
+ case "$1" in
6
+ --unshare-*|--die-with-parent|--new-session|--clearenv) shift ;; --setenv) envargs+=("$2=$3"); shift 3 ;; --ro-bind|--bind|--symlink) shift 3 ;; --proc|--dev|--tmpfs|--dir) shift 2 ;;
7
+ --json-status-fd) fd=$2; [[ $fd =~ ^[0-9]+$ ]] || exit 2; eval "exec ${fd}>&-"; shift 2 ;; --) shift; break ;; --*) echo "Unsupported experimental bwrap option: $1" >&2; exit 2 ;; *) break ;;
8
+ esac
9
+ done
10
+ exec env "${envargs[@]}" "$@"
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # Diagnostic only: retains real bwrap; currently fails under bunproot.
3
+ args=()
4
+ for arg in "$@"; do
5
+ case "$arg" in
6
+ --unshare-all) args+=(--unshare-user --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup-try) ;; --unshare-net) ;; *) args+=("$arg") ;;
7
+ esac
8
+ done
9
+ exec /usr/bin/bwrap-real "${args[@]}"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ sd=$(dirname "$(realpath "$0")")
3
+ exec bun "$sd/../node_modules/jsmdcui/src/index.js" --cat "$@"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ export QML_IMPORT_PATH="/usr/share/omarchy${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}"
3
+ exec bun /opt/buninu-browser/lib/qml/qswayland "$@"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ echo "${0##*/}: unavailable in this experiment" >&2
3
+ exit 1
@@ -0,0 +1,47 @@
1
+ // Exercise the official backend, including bat path validation, sandbox and SGR parsing.
2
+ import cases from './highlight-cases.json' with {type:'json'};
3
+ import {mkdir,realpath} from 'node:fs/promises';
4
+ const base='/root/flea-demo/code';
5
+ await mkdir(base,{recursive:true});
6
+ if(!(await realpath('/usr/local/bin/bat')).startsWith('/usr/'))throw Error('Flea requires bat realpath under /usr');
7
+ const p=Bun.spawn(['/usr/bin/flea','--backend'],{stdin:'pipe',stdout:'pipe',stderr:'pipe'});
8
+ const errors=new Response(p.stderr).text();
9
+ let pending,token=0;
10
+ const reader=(async()=>{
11
+ let buffer='';const decoder=new TextDecoder();
12
+ for await(const chunk of p.stdout){
13
+ buffer+=decoder.decode(chunk,{stream:true});let i;
14
+ while((i=buffer.indexOf('\n'))>=0){
15
+ const line=buffer.slice(0,i);buffer=buffer.slice(i+1);
16
+ const msg=JSON.parse(line);if(msg.t==='code'&&msg.token===token)pending?.(msg);
17
+ }
18
+ }
19
+ })();
20
+ async function ask(params){
21
+ token++;
22
+ let timer;
23
+ try{return await new Promise((resolve,reject)=>{
24
+ pending=resolve;timer=setTimeout(()=>reject(Error('Timed out: '+JSON.stringify(params))),15000);
25
+ p.stdin.write(JSON.stringify({c:'code',token,slot:'verify',first:1,count:400,...params})+'\n');
26
+ });}finally{clearTimeout(timer);pending=null;}
27
+ }
28
+ let checks=0;
29
+ try{
30
+ for(const sample of cases){
31
+ const path=base+'/'+sample.name;await Bun.write(path,sample.text);
32
+ for(const query of [{path},{text:sample.text,name:sample.name,lang:sample.lang}]){
33
+ const result=await ask(query);
34
+ const colored=result.lines?.some(line=>line.some((v,i)=>i%2===0&&v>0));
35
+ if(result.state!=='styled'||!colored)throw Error(`${sample.name} (${query.path?'file':sample.lang}): ${JSON.stringify(result)}`);
36
+ const text=result.lines.map(line=>line.filter((_,i)=>i%2===1).join('')).join('\n');
37
+ if(text!==sample.text.trimEnd())throw Error('Text changed: '+sample.name);
38
+ checks++;
39
+ }
40
+ }
41
+ const plain=await ask({text:'plain text\n',name:'sample.unknown',lang:'not-a-real-language'});
42
+ if(plain.state!=='plain')throw Error('Unknown language must fall back to plain text');
43
+ console.log(`PASS: official Flea colored ${cases.length} formats, ${checks} file/language requests; unknown language falls back to plain`);
44
+ }finally{
45
+ p.stdin.write('{"c":"quit"}\n');p.stdin.end();await p.exited;await reader;
46
+ const stderr=await errors;if(stderr)console.error(stderr);
47
+ }
@@ -0,0 +1,29 @@
1
+ // Real Flea media component, decoder and host PulseAudio connection.
2
+ import {createBrowser} from '/opt/buninu-browser/lib/headless-shell.js';
3
+ import {PulseClient} from '/opt/buninu-browser/lib/audio/pulse-client.js';
4
+ import {mkdtemp,rm} from 'node:fs/promises';
5
+ const dir=await mkdtemp('/tmp/flea-media-check-');
6
+ Object.assign(process.env,{HOME:dir,XDG_CONFIG_HOME:dir+'/config',XDG_CACHE_HOME:dir+'/cache',QML_IMPORT_PATH:'/usr/share/omarchy',FLEA_BIN:'/usr/bin/flea',FLEA_PATH:'/root/flea-demo'});
7
+ let b;
8
+ try{
9
+ const pulse=await Promise.race([PulseClient.connect(),Bun.sleep(5000).then(()=>{throw Error('Pulse connection timeout')})]);
10
+ console.log('Pulse connected');pulse.close();
11
+ b=await createBrowser({spareRenderer:false});
12
+ await b.context.addInitScript(`globalThis.players=[];globalThis.panes=[];let rt;Object.defineProperty(globalThis,'__qmlRuntime',{get:()=>rt,set:r=>{rt=r;const d=r.describe;r.describe=(v,t,...a)=>{if(t.includes('Pane'))panes.push(v);if(t.includes('MediaPlayer'))players.push(v);return d(v,t,...a)}}});`);
13
+ await b.context.navigate('file:///usr/share/flea/ui/boot/shell.qml');
14
+ await Bun.sleep(1500);
15
+ await b.context.press('2',{code:'Digit2',modifiers:2});
16
+ await Bun.sleep(300);
17
+ const point=await b.context.evaluate(`(()=>{const e=[...document.querySelectorAll('[data-qml-type=Text]')].find(e=>e.textContent==='tone.mp3'&&e.getBoundingClientRect().width>0);const r=e.getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})()`);
18
+ await b.context.click(...point);await Bun.sleep(500);
19
+ console.log('before',await b.context.evaluate(`panes.map(p=>({mode:p.viewMode,path:p.previewColumnItem?.path,visible:p.previewColumnItem?.visible,allowed:p.previewColumnItem?.playerAllowed}))`));
20
+ await b.context.evaluate(`panes[0].previewColumnItem.mediaStripItem().toggled()`);
21
+
22
+ for(let i=0;i<8;i++){
23
+ await Bun.sleep(400);
24
+ console.log('column',await b.context.evaluate(`panes.map(p=>({wants:p.previewColumnItem?.wantsPlayback,loaded:p.previewColumnItem?.playerLoaded(),playing:p.previewColumnItem?.mediaPlaying(),position:p.previewColumnItem?.mediaPosition()}))`));
25
+ console.log(await b.context.evaluate(`players.map(p=>({source:p.source,error:p.error,message:p.errorString,position:p.position,duration:p.duration,state:p.playbackState,volume:p.audioOutput?.volume,muted:p.audioOutput?.muted}))`));
26
+ }
27
+ }finally{b?.close();await rm(dir,{recursive:true,force:true});}
28
+
29
+ process.exit(0);
@@ -0,0 +1,27 @@
1
+ // Actual Flea UI + backend, isolated settings, without changing the user's Land.
2
+ import {mkdtemp,rm} from 'node:fs/promises';
3
+ import {createBrowser} from '/opt/buninu-browser/lib/headless-shell.js';
4
+ const dir=await mkdtemp('/tmp/flea-color-ui-');
5
+ Object.assign(process.env,{HOME:dir,XDG_CONFIG_HOME:dir+'/config',XDG_CACHE_HOME:dir+'/cache',XDG_DATA_HOME:dir+'/data',XDG_STATE_HOME:dir+'/state',QML_IMPORT_PATH:'/usr/share/omarchy',FLEA_BIN:'/usr/bin/flea',FLEA_PATH:'/root/flea-demo'});
6
+ let browser;
7
+ try{
8
+ browser=await createBrowser({spareRenderer:false,rendererTimeout:30000});
9
+ await browser.context.resize(900,600);
10
+ await browser.context.addInitScript(`globalThis.codeReaders=[];let rt;Object.defineProperty(globalThis,'__qmlRuntime',{get:()=>rt,set:r=>{rt=r;const d=r.describe;r.describe=(v,t,...a)=>{if(t.includes('CodeLines'))codeReaders.push(v);return d(v,t,...a)}}});`);
11
+ await browser.context.navigate('file:///usr/share/flea/ui/boot/shell.qml');
12
+ async function waitFor(expression){for(let i=0;i<200;i++){if(await browser.context.evaluate(expression))return;await Bun.sleep(50)}throw Error('UI timeout: '+expression);}
13
+ await waitFor(`document.body.innerText.includes('hello.js')`);
14
+ for(const [name,text] of [['sample.jsonl','{"answer":42}\n'],['sample.html','<html><body><h1>Hello</h1></body></html>\n']])await Bun.write('/root/flea-demo/'+name,text);
15
+ for(const name of ['hello.js','sample.jsonl','sample.html']){
16
+ await waitFor(`document.body.innerText.includes(${JSON.stringify(name)})`);
17
+ const point=await browser.context.evaluate(`(()=>{const e=[...document.querySelectorAll('[data-qml-type=Text]')].find(e=>e.textContent===${JSON.stringify(name)}&&e.getBoundingClientRect().width>0);const r=e.getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})()`);
18
+ await browser.context.click(...point);
19
+ await browser.context.press(' ',{code:'Space'});
20
+ if(name.endsWith('.html')){await Bun.sleep(800);await browser.context.press('r',{code:'KeyR'});}
21
+ if(name.endsWith('.html')) await waitFor(`Array.from(document.querySelectorAll('[data-qml-type=Text]')).some(e=>e.textContent.includes('<html>')&&Array.from(e.querySelectorAll('span')).some(s=>s.style.color))`);
22
+ else await waitFor(`codeReaders.some(v=>v.active&&v.path.endsWith(${JSON.stringify(name)})&&v.answer==='styled'&&Object.values(v.held).some(l=>l.some((r,i)=>i%2===0&&r>0)))`);
23
+ await Bun.write('/tmp/flea-js-preview.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
24
+ console.log('PASS: official Flea UI preview styled: '+name);
25
+ await browser.context.press('Escape',{code:'Escape'});
26
+ }
27
+ }catch(error){await Bun.write('/tmp/flea-html-preview.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));console.error(await browser.context.evaluate(`({text:document.body.innerText,html:Array.from(document.querySelectorAll('[data-qml-type=Text]')).filter(e=>e.textContent.includes('<html>')).map(e=>e.innerHTML),readers:codeReaders.map(v=>({active:v.active,path:v.path,answer:v.answer}))})`));throw error;}finally{browser?.close();await rm(dir,{recursive:true,force:true});}
@@ -0,0 +1,36 @@
1
+ // Build smoke test: actual tools, real Flea backend, no compositor required.
2
+ import {mkdtemp,rm} from 'node:fs/promises';
3
+ const assert=(ok,message)=>{if(!ok)throw Error(message);};
4
+ async function run(args,input='',env={}){
5
+ const p=Bun.spawn(args,{env:{...process.env,PATH:'/usr/local/bin:/usr/bin:/bin',...env},stdin:'pipe',stdout:'pipe',stderr:'pipe'});
6
+ p.stdin.write(input);
7
+ p.stdin.end();
8
+ const [out,err,code]=await Promise.all([new Response(p.stdout).text(),new Response(p.stderr).text(),p.exited]);
9
+ assert(code===0,`${args.join(' ')} exited ${code}: ${err}`);return out;
10
+ }
11
+ assert(Bun.version==='1.4.4',`Expected Bun 1.4.4, got ${Bun.version}`);
12
+ assert(await Bun.file('/usr/share/mime/globs2').exists(),'Missing MIME database');
13
+ assert(await Bun.file('/usr/share/omarchy/qs/Commons/Style.qml').exists(),'Missing Omarchy Commons');
14
+ const ansi=await run(['bat','--color=always','--style=plain','--paging=never','--file-name=sample.js','-'],'const answer = 42;\n');
15
+ assert(ansi.includes('\x1b[')&&Bun.stripANSI(ansi).includes('const answer = 42;'),'bat adapter did not highlight input');
16
+ await run([process.execPath,new URL('./check-highlighting.mjs',import.meta.url).pathname]).then(text=>process.stdout.write(text));
17
+ const cache=await mkdtemp('/tmp/flea-build-check-');
18
+ try{
19
+ const out=await run(['/usr/bin/flea','--backend'],[
20
+ {c:'list',path:'/root/flea-demo',first:20},{c:'thumb',rows:[0,1,2]},{c:'quit'}
21
+ ].map(x=>JSON.stringify(x)+'\n').join(''),{XDG_CACHE_HOME:cache});
22
+ const replies=out.trim().split('\n').map(x=>JSON.parse(x));
23
+ const thumb=replies.find(x=>x.t==='thumbed'&&x.file);
24
+ assert(thumb,'Flea did not accept placeholder thumbnail: '+out);
25
+ assert((await Bun.file(thumb.file).bytes()).subarray(1,4).toString()==='80,78,71','Invalid PNG');
26
+ const {createBrowser}=await import('/opt/buninu-browser/lib/headless-shell.js');
27
+ const page=await createBrowser({spareRenderer:false});
28
+ try {
29
+ const qml=cache+'/check.qml';
30
+ await Bun.write(qml,'import QtQuick\nRectangle {width:320;height:120;color:"white";Text {text:"Flea QML ready";color:"black"}}');
31
+ await page.context.navigate(Bun.pathToFileURL(qml).href);
32
+ assert((await page.context.evaluate('document.body.innerText')).includes('Flea QML ready'),'QML runtime did not render text');
33
+ assert((await page.context.screenshot({format:'png'})).length>100,'QML screenshot is empty');
34
+ } finally {page.close();}
35
+ console.log('PASS: Bun 1.4.4, MIME, Omarchy imports, jsmdcui ANSI and official Flea thumbnail backend, QML rendering');
36
+ }finally{await rm(cache,{recursive:true,force:true});}