flea-qml 0.0.0-stage → 0.1.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 (625) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +478 -2
  3. package/THIRD-PARTY-NOTICES.md +29 -0
  4. package/demos/App.java +5 -0
  5. package/demos/Dockerfile +4 -0
  6. package/demos/Main.kt +3 -0
  7. package/demos/Program.cs +1 -0
  8. package/demos/README.md +12 -0
  9. package/demos/app.dart +3 -0
  10. package/demos/app.deb +0 -0
  11. package/demos/app.js +1 -0
  12. package/demos/backup.tar +0 -0
  13. package/demos/budget.xlsx +0 -0
  14. package/demos/build.sh +2 -0
  15. package/demos/bundle.zip +0 -0
  16. package/demos/clip.mp4 +0 -0
  17. package/demos/data.sqlite +0 -0
  18. package/demos/disk.iso +0 -0
  19. package/demos/engine.cpp +5 -0
  20. package/demos/firmware.bin +0 -0
  21. package/demos/flake.nix +4 -0
  22. package/demos/folder/inside.txt +1 -0
  23. package/demos/font.ttf +0 -0
  24. package/demos/index.html +5 -0
  25. package/demos/index.php +2 -0
  26. package/demos/init.lua +1 -0
  27. package/demos/letter.docx +0 -0
  28. package/demos/lib.rs +3 -0
  29. package/demos/main.c +6 -0
  30. package/demos/main.h +3 -0
  31. package/demos/math-and-diagrams.md +200 -0
  32. package/demos/notes.tar.gz +0 -0
  33. package/demos/notes.txt +3 -0
  34. package/demos/photo.png +0 -0
  35. package/demos/program +2 -0
  36. package/demos/report.pdf +32 -0
  37. package/demos/script.py +1 -0
  38. package/demos/server.go +7 -0
  39. package/demos/settings.json +4 -0
  40. package/demos/slides.pptx +0 -0
  41. package/demos/song.mp3 +0 -0
  42. package/demos/style.css +4 -0
  43. package/demos/tool.rb +1 -0
  44. package/demos/types.ts +2 -0
  45. package/docs/pele.md +23 -0
  46. package/docs/protocol.md +3420 -0
  47. package/docs/temml.md +63 -0
  48. package/index.js +107 -0
  49. package/package.json +34 -4
  50. package/src/archive.js +402 -0
  51. package/src/archiveops.js +697 -0
  52. package/src/backend.js +369 -0
  53. package/src/bulkrename.js +95 -0
  54. package/src/clip-host.js +76 -0
  55. package/src/clip.js +462 -0
  56. package/src/code.js +168 -0
  57. package/src/compare.js +74 -0
  58. package/src/create.js +128 -0
  59. package/src/dirsize.js +147 -0
  60. package/src/figure-svg.js +530 -0
  61. package/src/figure-thread.js +23 -0
  62. package/src/figure.js +132 -0
  63. package/src/find.js +55 -0
  64. package/src/flea.js +39 -0
  65. package/src/fsinfo.js +39 -0
  66. package/src/highlight.js +141 -0
  67. package/src/html.js +52 -0
  68. package/src/kinds.js +89 -0
  69. package/src/listing.js +130 -0
  70. package/src/markdown-html.js +317 -0
  71. package/src/mathml.js +186 -0
  72. package/src/menu.js +180 -0
  73. package/src/meta.js +129 -0
  74. package/src/office.js +810 -0
  75. package/src/order.js +100 -0
  76. package/src/peek.js +49 -0
  77. package/src/pele-wrapper.js +695 -0
  78. package/src/permissions.js +180 -0
  79. package/src/places.js +313 -0
  80. package/src/preview-language.js +10 -0
  81. package/src/transfer.js +669 -0
  82. package/src/trash-record.js +41 -0
  83. package/src/trash.js +210 -0
  84. package/src/trashbrowse.js +326 -0
  85. package/src/uistate.js +393 -0
  86. package/src/uistore.js +168 -0
  87. package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
  88. package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
  89. package/src/vendor/LICENSES/MIT-katex.txt +21 -0
  90. package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
  91. package/src/vendor/LICENSES/MIT-pele.txt +40 -0
  92. package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
  93. package/src/vendor/LICENSES/MIT-temml.txt +21 -0
  94. package/src/vendor/pele.js +145 -0
  95. package/src/vendor/temml.mjs +14708 -0
  96. package/src/watch.js +98 -0
  97. package/src/wayland/client.js +109 -0
  98. package/src/wayland/protocol.js +55 -0
  99. package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
  100. package/src/wayland/protocols/wayland.xml +3248 -0
  101. package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
  102. package/src/wayland/resource-io.js +6 -0
  103. package/src/wayland/socket-discovery.js +26 -0
  104. package/src/wayland/transport-posix.js +120 -0
  105. package/src/wayland/transport-win32.js +11 -0
  106. package/src/wayland/transport.js +3 -0
  107. package/src/wayland/win32-resources.js +80 -0
  108. package/src/wayland/win32-wire.js +62 -0
  109. package/test/archive.test.js +80 -0
  110. package/test/bench-markdown-scroll.mjs +190 -0
  111. package/test/clip-host.test.js +113 -0
  112. package/test/code-preview.test.js +35 -0
  113. package/test/compare-ui-paths.test.js +24 -0
  114. package/test/compare.test.js +54 -0
  115. package/test/delete-trash.test.js +67 -0
  116. package/test/dirsize.test.js +138 -0
  117. package/test/file-ops.test.js +241 -0
  118. package/test/find-contents.test.js +161 -0
  119. package/test/find-input.test.js +66 -0
  120. package/test/fixtures/pele-parity.json +1006 -0
  121. package/test/highlight-launcher.test.js +35 -0
  122. package/test/html-preview.test.js +22 -0
  123. package/test/input.test.js +15 -0
  124. package/test/keymap.test.js +26 -0
  125. package/test/language-icons.test.js +36 -0
  126. package/test/launcher.test.js +51 -0
  127. package/test/manual-code-scroll.mjs +28 -0
  128. package/test/manual-column-scroll.mjs +24 -0
  129. package/test/manual-compare-ui.mjs +20 -0
  130. package/test/manual-find-status.mjs +37 -0
  131. package/test/manual-html-security.mjs +23 -0
  132. package/test/manual-html-ui.mjs +30 -0
  133. package/test/manual-syntax-preview.mjs +26 -0
  134. package/test/manual-trash-ui.mjs +86 -0
  135. package/test/markdown-diagrams.test.js +93 -0
  136. package/test/markdown-html.test.js +105 -0
  137. package/test/mathml.test.js +92 -0
  138. package/test/menu-transfer.test.js +57 -0
  139. package/test/office-preview.test.js +154 -0
  140. package/test/pele-parity.test.js +28 -0
  141. package/test/places.test.js +134 -0
  142. package/test/preview-keys.test.js +60 -0
  143. package/test/preview-language.test.js +22 -0
  144. package/test/status-filename.test.js +20 -0
  145. package/test/transfer-replace-merge.test.js +187 -0
  146. package/test/trash-address.test.js +23 -0
  147. package/test/trash-view.test.js +37 -0
  148. package/test/trashbrowse.test.js +135 -0
  149. package/ui/AboutFacts.qml +78 -0
  150. package/ui/ArchiveCards.qml +68 -0
  151. package/ui/ArchiveCompressDialog.qml +378 -0
  152. package/ui/ArchivePasswordDialog.qml +257 -0
  153. package/ui/Backend.qml +771 -0
  154. package/ui/BulkOldNames.qml +47 -0
  155. package/ui/BulkRenameDialog.qml +380 -0
  156. package/ui/CardFrame.qml +67 -0
  157. package/ui/CardScroll.qml +79 -0
  158. package/ui/CheckBox.qml +73 -0
  159. package/ui/ChromeBar.qml +360 -0
  160. package/ui/ChromeButton.qml +131 -0
  161. package/ui/ChromeModes.qml +43 -0
  162. package/ui/CodeLines.qml +256 -0
  163. package/ui/CodeRefusal.qml +52 -0
  164. package/ui/CollideConfirm.qml +147 -0
  165. package/ui/CollideHost.qml +167 -0
  166. package/ui/ColumnPane.qml +523 -0
  167. package/ui/ColumnRow.qml +380 -0
  168. package/ui/ColumnsArea.qml +594 -0
  169. package/ui/ColumnsLanding.qml +147 -0
  170. package/ui/CompareView.qml +321 -0
  171. package/ui/ContextMenu.qml +743 -0
  172. package/ui/ConvertDialog.qml +456 -0
  173. package/ui/CountedWorker.qml +7 -0
  174. package/ui/Crumb.qml +64 -0
  175. package/ui/CrumbDrop.qml +25 -0
  176. package/ui/CsvTable.qml +394 -0
  177. package/ui/CursorMark.qml +41 -0
  178. package/ui/DefaultClaim.qml +130 -0
  179. package/ui/DeviceLock.qml +47 -0
  180. package/ui/DeviceMounts.qml +540 -0
  181. package/ui/DialogButton.qml +150 -0
  182. package/ui/DialogField.qml +147 -0
  183. package/ui/DialogHeader.qml +31 -0
  184. package/ui/DialogScrim.qml +20 -0
  185. package/ui/DialogTitle.qml +37 -0
  186. package/ui/Divider.qml +9 -0
  187. package/ui/DropInto.qml +126 -0
  188. package/ui/DropTarget.qml +35 -0
  189. package/ui/DropboxMark.qml +60 -0
  190. package/ui/EmptyState.qml +150 -0
  191. package/ui/EscapeHint.qml +11 -0
  192. package/ui/FactsTable.qml +54 -0
  193. package/ui/FastScrollHandler.qml +476 -0
  194. package/ui/FatNamesConfirm.qml +157 -0
  195. package/ui/Favourites.qml +143 -0
  196. package/ui/FigureService.qml +120 -0
  197. package/ui/FileDrag.qml +267 -0
  198. package/ui/FilterStrip.qml +133 -0
  199. package/ui/FitMetrics.qml +22 -0
  200. package/ui/FlashDriveRows.qml +137 -0
  201. package/ui/FlashHost.qml +72 -0
  202. package/ui/FlashImageRow.qml +104 -0
  203. package/ui/FlashRunner.qml +119 -0
  204. package/ui/FlashStepRow.qml +172 -0
  205. package/ui/FlashStrip.qml +38 -0
  206. package/ui/FlashView.qml +397 -0
  207. package/ui/FleaMark.qml +88 -0
  208. package/ui/FlyoutFade.qml +23 -0
  209. package/ui/FontPane.qml +170 -0
  210. package/ui/Glass.qml +114 -0
  211. package/ui/GlassTagger.qml +59 -0
  212. package/ui/Glyph.qml +64 -0
  213. package/ui/GlyphSolid.qml +22 -0
  214. package/ui/GridArea.qml +664 -0
  215. package/ui/GridTile.qml +325 -0
  216. package/ui/GvfsBridge.qml +164 -0
  217. package/ui/Header.qml +433 -0
  218. package/ui/HoverTimers.qml +39 -0
  219. package/ui/HtmlClip.qml +30 -0
  220. package/ui/HtmlPane.qml +63 -0
  221. package/ui/HyprlandMark.qml +45 -0
  222. package/ui/Ipc.qml +1005 -0
  223. package/ui/IpcControls.qml +7 -0
  224. package/ui/IpcMenuState.qml +48 -0
  225. package/ui/IpcPreviewState.qml +185 -0
  226. package/ui/JumpPath.qml +51 -0
  227. package/ui/KeymapSheet.qml +561 -0
  228. package/ui/KeymapSheetResult.qml +89 -0
  229. package/ui/KindStack.qml +40 -0
  230. package/ui/LangMark.qml +51 -0
  231. package/ui/LangMarkLoader.qml +26 -0
  232. package/ui/List.qml +480 -0
  233. package/ui/LoadingState.qml +54 -0
  234. package/ui/LocalSend.qml +56 -0
  235. package/ui/LocalSendMark.qml +73 -0
  236. package/ui/MarkdownBlockView.qml +311 -0
  237. package/ui/MarkdownBlocks.qml +35 -0
  238. package/ui/MarkdownFigure.qml +254 -0
  239. package/ui/MarkdownImages.qml +72 -0
  240. package/ui/MarkdownList.qml +81 -0
  241. package/ui/MarkdownMathsText.qml +97 -0
  242. package/ui/MarkdownPane.qml +83 -0
  243. package/ui/MarkdownPictures.qml +41 -0
  244. package/ui/MarkdownQuote.qml +52 -0
  245. package/ui/MarkdownRemote.qml +82 -0
  246. package/ui/MarkdownSource.qml +155 -0
  247. package/ui/MarkdownTable.qml +222 -0
  248. package/ui/MarkdownTableScroll.qml +64 -0
  249. package/ui/MarkdownTableWheel.qml +126 -0
  250. package/ui/MarkdownText.qml +87 -0
  251. package/ui/MarkdownWorker.js +132 -0
  252. package/ui/MatchText.qml +99 -0
  253. package/ui/MediaSession.qml +38 -0
  254. package/ui/MediaSound.qml +26 -0
  255. package/ui/MediaStrip.qml +191 -0
  256. package/ui/MediaTrack.qml +36 -0
  257. package/ui/MediaVolume.qml +54 -0
  258. package/ui/MenuActionDialog.qml +396 -0
  259. package/ui/MenuEdgeFade.qml +24 -0
  260. package/ui/MenuHost.qml +227 -0
  261. package/ui/MenuRow.qml +351 -0
  262. package/ui/MountListing.qml +100 -0
  263. package/ui/NetworkDialog.qml +435 -0
  264. package/ui/NetworkForm.qml +426 -0
  265. package/ui/NetworkMounts.qml +913 -0
  266. package/ui/NetworkPlaces.qml +127 -0
  267. package/ui/OpenWithDialog.qml +613 -0
  268. package/ui/Opener.qml +115 -0
  269. package/ui/Pane.qml +1296 -0
  270. package/ui/PaneMenuActions.qml +527 -0
  271. package/ui/PanePath.qml +90 -0
  272. package/ui/PaneRail.qml +239 -0
  273. package/ui/PaneStates.qml +88 -0
  274. package/ui/PaneSwap.qml +339 -0
  275. package/ui/PaneWire.qml +605 -0
  276. package/ui/ParkedWait.qml +9 -0
  277. package/ui/PathEntry.qml +186 -0
  278. package/ui/PathJump.qml +400 -0
  279. package/ui/PdfViewer.qml +297 -0
  280. package/ui/PermissionsDialog.qml +658 -0
  281. package/ui/PermissionsHeadings.qml +31 -0
  282. package/ui/PhoneMounts.qml +67 -0
  283. package/ui/PickerAnswers.qml +75 -0
  284. package/ui/PickerBox.qml +26 -0
  285. package/ui/PickerChrome.qml +295 -0
  286. package/ui/PickerFilter.qml +97 -0
  287. package/ui/PickerGrid.qml +308 -0
  288. package/ui/PickerHeader.qml +28 -0
  289. package/ui/PickerLifecycle.qml +27 -0
  290. package/ui/PickerList.qml +218 -0
  291. package/ui/PickerListing.qml +149 -0
  292. package/ui/PickerPathBar.qml +127 -0
  293. package/ui/PickerPlaces.qml +189 -0
  294. package/ui/PickerRecent.qml +92 -0
  295. package/ui/PickerSave.qml +101 -0
  296. package/ui/PickerSelection.qml +155 -0
  297. package/ui/PickerWindow.qml +846 -0
  298. package/ui/PowerSectorsReader.qml +10 -0
  299. package/ui/PrebuildDriver.qml +146 -0
  300. package/ui/Preview.qml +1004 -0
  301. package/ui/PreviewAnimation.qml +24 -0
  302. package/ui/PreviewArchive.qml +66 -0
  303. package/ui/PreviewColumn.qml +551 -0
  304. package/ui/PreviewEdge.qml +63 -0
  305. package/ui/PreviewFacts.qml +30 -0
  306. package/ui/PreviewFactsLine.qml +34 -0
  307. package/ui/PreviewHtml.qml +233 -0
  308. package/ui/PreviewImage.qml +253 -0
  309. package/ui/PreviewImagePan.qml +13 -0
  310. package/ui/PreviewImageSharp.qml +32 -0
  311. package/ui/PreviewLines.qml +106 -0
  312. package/ui/PreviewMarkdown.qml +706 -0
  313. package/ui/PreviewMedia.qml +250 -0
  314. package/ui/PreviewPdf.qml +482 -0
  315. package/ui/PreviewScroll.qml +201 -0
  316. package/ui/PreviewSwap.qml +335 -0
  317. package/ui/PreviewText.qml +149 -0
  318. package/ui/ProtocolChip.qml +18 -0
  319. package/ui/QuickLookArchive.qml +38 -0
  320. package/ui/QuickLookBar.qml +103 -0
  321. package/ui/QuickLookPrepare.qml +355 -0
  322. package/ui/QuickLookSwap.qml +10 -0
  323. package/ui/QuickLookWarm.qml +65 -0
  324. package/ui/RailDrop.qml +155 -0
  325. package/ui/RailHeading.qml +19 -0
  326. package/ui/RailPlaces.qml +38 -0
  327. package/ui/RailTrashRow.qml +67 -0
  328. package/ui/RenameField.qml +353 -0
  329. package/ui/RendererRetry.qml +11 -0
  330. package/ui/ResizeHandle.qml +52 -0
  331. package/ui/Reveal.qml +54 -0
  332. package/ui/Row.qml +476 -0
  333. package/ui/RowDrag.qml +213 -0
  334. package/ui/RowDropTarget.qml +27 -0
  335. package/ui/RunCard.qml +286 -0
  336. package/ui/Scripts.qml +65 -0
  337. package/ui/SearchStrip.qml +232 -0
  338. package/ui/SelectionBand.qml +140 -0
  339. package/ui/SelectionPreview.qml +370 -0
  340. package/ui/SettingsFavourite.qml +122 -0
  341. package/ui/SettingsGroup.qml +96 -0
  342. package/ui/SettingsHint.qml +12 -0
  343. package/ui/SettingsMetrics.qml +29 -0
  344. package/ui/SettingsPane.qml +178 -0
  345. package/ui/SettingsPanel.qml +547 -0
  346. package/ui/SettingsRail.qml +128 -0
  347. package/ui/SettingsRow.qml +409 -0
  348. package/ui/SettingsRuler.qml +85 -0
  349. package/ui/SettingsSegment.qml +85 -0
  350. package/ui/ShareBrowser.qml +113 -0
  351. package/ui/ShareLink.qml +67 -0
  352. package/ui/ShelfPins.qml +101 -0
  353. package/ui/Sidebar.qml +732 -0
  354. package/ui/SidebarRow.qml +358 -0
  355. package/ui/Spinner.qml +60 -0
  356. package/ui/StableModel.qml +89 -0
  357. package/ui/StateMessage.qml +84 -0
  358. package/ui/StatusBar.qml +512 -0
  359. package/ui/StatusFileName.qml +27 -0
  360. package/ui/TabBar.qml +673 -0
  361. package/ui/TabDragGeometry.qml +60 -0
  362. package/ui/TagMenuMark.qml +52 -0
  363. package/ui/TagRailSection.qml +53 -0
  364. package/ui/TagSlot.qml +40 -0
  365. package/ui/TagState.qml +150 -0
  366. package/ui/TagSwatch.qml +28 -0
  367. package/ui/Taildrop.qml +86 -0
  368. package/ui/TailscaleMark.qml +50 -0
  369. package/ui/Theme.qml +460 -0
  370. package/ui/ThemeColors.qml +43 -0
  371. package/ui/TransferCard.qml +239 -0
  372. package/ui/TrashConfirm.qml +129 -0
  373. package/ui/TrashDropHandler.qml +11 -0
  374. package/ui/TrashHost.qml +202 -0
  375. package/ui/TrashMonitor.qml +30 -0
  376. package/ui/TrashToolbar.qml +19 -0
  377. package/ui/TrashView.qml +492 -0
  378. package/ui/UnlockDialog.qml +450 -0
  379. package/ui/UnlockHost.qml +206 -0
  380. package/ui/UpdateCheck.qml +97 -0
  381. package/ui/UserDirs.qml +25 -0
  382. package/ui/ViewState.qml +565 -0
  383. package/ui/ViewportScrollBar.qml +195 -0
  384. package/ui/ViewportScrollBars.qml +25 -0
  385. package/ui/WebPreview.qml +33 -0
  386. package/ui/WebPreviewView.qml +26 -0
  387. package/ui/WindowBody.qml +827 -0
  388. package/ui/WindowDrag.qml +35 -0
  389. package/ui/boot/Commons/AudioNodes.qml +50 -0
  390. package/ui/boot/Commons/AudioNodesModel.js +96 -0
  391. package/ui/boot/Commons/Border.qml +243 -0
  392. package/ui/boot/Commons/BorderGeometry.js +373 -0
  393. package/ui/boot/Commons/Color.qml +258 -0
  394. package/ui/boot/Commons/I18n.qml +118 -0
  395. package/ui/boot/Commons/InputDevices.qml +112 -0
  396. package/ui/boot/Commons/InputMethodState.qml +38 -0
  397. package/ui/boot/Commons/IpcRegistry.qml +77 -0
  398. package/ui/boot/Commons/Numbers.js +19 -0
  399. package/ui/boot/Commons/Plural.js +91 -0
  400. package/ui/boot/Commons/ShellIpc.qml +12 -0
  401. package/ui/boot/Commons/Style.qml +561 -0
  402. package/ui/boot/Commons/Translations.qml +100 -0
  403. package/ui/boot/Commons/UntypedInput.qml +152 -0
  404. package/ui/boot/Commons/Util.qml +188 -0
  405. package/ui/boot/Commons/qmldir +13 -0
  406. package/ui/boot/LICENSE-omarchy +20 -0
  407. package/ui/boot/Ui/BackgroundMedia.qml +49 -0
  408. package/ui/boot/Ui/BarIconButton.qml +106 -0
  409. package/ui/boot/Ui/BarIconContent.qml +38 -0
  410. package/ui/boot/Ui/BarIndicator.qml +53 -0
  411. package/ui/boot/Ui/BarWidget.qml +50 -0
  412. package/ui/boot/Ui/BorderOverlay.qml +54 -0
  413. package/ui/boot/Ui/BorderSurface.qml +40 -0
  414. package/ui/boot/Ui/Button.qml +224 -0
  415. package/ui/boot/Ui/ButtonGroup.qml +131 -0
  416. package/ui/boot/Ui/ConfirmDialog.qml +134 -0
  417. package/ui/boot/Ui/CursorSurface.qml +42 -0
  418. package/ui/boot/Ui/Dropdown.qml +245 -0
  419. package/ui/boot/Ui/FilterInput.qml +26 -0
  420. package/ui/boot/Ui/IconContentBounds.qml +141 -0
  421. package/ui/boot/Ui/KeyboardPanel.qml +453 -0
  422. package/ui/boot/Ui/MultiSelect.qml +624 -0
  423. package/ui/boot/Ui/NumberField.qml +86 -0
  424. package/ui/boot/Ui/OpticalGlyph.qml +68 -0
  425. package/ui/boot/Ui/OverlayWindow.qml +102 -0
  426. package/ui/boot/Ui/Panel.qml +60 -0
  427. package/ui/boot/Ui/PanelActionButton.qml +105 -0
  428. package/ui/boot/Ui/PanelController.qml +16 -0
  429. package/ui/boot/Ui/PanelHero.qml +112 -0
  430. package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
  431. package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
  432. package/ui/boot/Ui/PanelSeparator.qml +19 -0
  433. package/ui/boot/Ui/PanelSlider.qml +156 -0
  434. package/ui/boot/Ui/PanelToolTip.qml +50 -0
  435. package/ui/boot/Ui/PluginBarApi.qml +89 -0
  436. package/ui/boot/Ui/PointerMoveGate.qml +54 -0
  437. package/ui/boot/Ui/PopupCard.qml +181 -0
  438. package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
  439. package/ui/boot/Ui/SearchableDropdown.qml +354 -0
  440. package/ui/boot/Ui/SizedImage.qml +125 -0
  441. package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
  442. package/ui/boot/Ui/TextField.qml +58 -0
  443. package/ui/boot/Ui/Toggle.qml +118 -0
  444. package/ui/boot/Ui/ToggleSwitch.qml +115 -0
  445. package/ui/boot/Ui/WidgetButton.qml +172 -0
  446. package/ui/boot/Ui/qmldir +41 -0
  447. package/ui/boot/fleatab.qml +50 -0
  448. package/ui/boot/picker.qml +39 -0
  449. package/ui/boot/shell.qml +122 -0
  450. package/ui/boot/tabtearoff.qml +51 -0
  451. package/ui/js/Anchor.js +379 -0
  452. package/ui/js/AnchorHold.js +162 -0
  453. package/ui/js/Archive.js +157 -0
  454. package/ui/js/ArchiveCard.js +44 -0
  455. package/ui/js/ArchiveOps.js +44 -0
  456. package/ui/js/BackReturn.js +118 -0
  457. package/ui/js/BulkRename.js +100 -0
  458. package/ui/js/Buttons.js +54 -0
  459. package/ui/js/ChromeKeys.js +116 -0
  460. package/ui/js/ClipMarks.js +73 -0
  461. package/ui/js/ClipSelection.js +67 -0
  462. package/ui/js/Clipboard.js +179 -0
  463. package/ui/js/Cloud.js +97 -0
  464. package/ui/js/CodeRuns.js +72 -0
  465. package/ui/js/Collide.js +212 -0
  466. package/ui/js/ColumnFit.js +28 -0
  467. package/ui/js/ColumnMenu.js +111 -0
  468. package/ui/js/Columns.js +298 -0
  469. package/ui/js/ColumnsCarry.js +103 -0
  470. package/ui/js/Compare.js +74 -0
  471. package/ui/js/Contrast.js +157 -0
  472. package/ui/js/ControlReader.js +42 -0
  473. package/ui/js/Convert.js +52 -0
  474. package/ui/js/CopyAs.js +52 -0
  475. package/ui/js/Crumbs.js +178 -0
  476. package/ui/js/Csv.js +231 -0
  477. package/ui/js/Density.js +44 -0
  478. package/ui/js/Devices.js +440 -0
  479. package/ui/js/DirSizes.js +180 -0
  480. package/ui/js/Drag.js +364 -0
  481. package/ui/js/DragOut.js +166 -0
  482. package/ui/js/DropDev.js +52 -0
  483. package/ui/js/Dropbox.js +41 -0
  484. package/ui/js/Eject.js +217 -0
  485. package/ui/js/Errors.js +188 -0
  486. package/ui/js/ExtThumbs.js +115 -0
  487. package/ui/js/Facts.js +402 -0
  488. package/ui/js/FigureDate.js +14 -0
  489. package/ui/js/Filter.js +285 -0
  490. package/ui/js/Find.js +46 -0
  491. package/ui/js/Flash.js +184 -0
  492. package/ui/js/FlashCard.js +79 -0
  493. package/ui/js/FlashEndings.js +125 -0
  494. package/ui/js/FlashPane.js +116 -0
  495. package/ui/js/FleaBin.js +12 -0
  496. package/ui/js/Focus.js +502 -0
  497. package/ui/js/FolderSorts.js +108 -0
  498. package/ui/js/FolderViews.js +44 -0
  499. package/ui/js/Format.js +305 -0
  500. package/ui/js/Fuzzy.js +141 -0
  501. package/ui/js/GlassTag.js +72 -0
  502. package/ui/js/Grid.js +33 -0
  503. package/ui/js/GridGeometry.js +90 -0
  504. package/ui/js/GridNames.js +153 -0
  505. package/ui/js/GvfsBridge.js +200 -0
  506. package/ui/js/HostClipboard.js +29 -0
  507. package/ui/js/Icons.js +218 -0
  508. package/ui/js/Input.js +50 -0
  509. package/ui/js/Jump.js +200 -0
  510. package/ui/js/KeyName.js +64 -0
  511. package/ui/js/Keymap.js +401 -0
  512. package/ui/js/Kinds.js +164 -0
  513. package/ui/js/LICENSES/ISC-lucide.txt +43 -0
  514. package/ui/js/LangMarkPaths.js +47 -0
  515. package/ui/js/LangMarks.js +120 -0
  516. package/ui/js/Lists.js +11 -0
  517. package/ui/js/LocalSend.js +39 -0
  518. package/ui/js/LockedMenu.js +63 -0
  519. package/ui/js/MakeDefault.js +186 -0
  520. package/ui/js/MarkBox.js +7 -0
  521. package/ui/js/Markdown.js +119 -0
  522. package/ui/js/MarkdownLists.js +86 -0
  523. package/ui/js/MarkdownMaths.js +58 -0
  524. package/ui/js/MarkdownPictures.js +32 -0
  525. package/ui/js/MarkdownPrepared.js +139 -0
  526. package/ui/js/MarkdownTableFit.js +34 -0
  527. package/ui/js/Marks.js +148 -0
  528. package/ui/js/Marquee.js +96 -0
  529. package/ui/js/Match.js +93 -0
  530. package/ui/js/Material.js +109 -0
  531. package/ui/js/MdBlocks.js +437 -0
  532. package/ui/js/MdBreak.js +71 -0
  533. package/ui/js/MdChunks.js +70 -0
  534. package/ui/js/MdContainer.js +137 -0
  535. package/ui/js/MdDocument.js +337 -0
  536. package/ui/js/MdEmph.js +97 -0
  537. package/ui/js/MdEntity.js +108 -0
  538. package/ui/js/MdEntityTable.js +223 -0
  539. package/ui/js/MdEscape.js +59 -0
  540. package/ui/js/MdFront.js +37 -0
  541. package/ui/js/MdHold.js +32 -0
  542. package/ui/js/MdHtml.js +307 -0
  543. package/ui/js/MdHtmlBlock.js +290 -0
  544. package/ui/js/MdHtmlHeading.js +53 -0
  545. package/ui/js/MdHtmlImage.js +179 -0
  546. package/ui/js/MdInline.js +154 -0
  547. package/ui/js/MdItems.js +212 -0
  548. package/ui/js/MdLeaf.js +309 -0
  549. package/ui/js/MdLink.js +196 -0
  550. package/ui/js/MdMath.js +67 -0
  551. package/ui/js/MdParagraphs.js +138 -0
  552. package/ui/js/MdRefs.js +232 -0
  553. package/ui/js/MdResolve.js +226 -0
  554. package/ui/js/MdRun.js +300 -0
  555. package/ui/js/MdUrl.js +226 -0
  556. package/ui/js/Menu.js +514 -0
  557. package/ui/js/MenuContext.js +85 -0
  558. package/ui/js/MenuFit.js +13 -0
  559. package/ui/js/MenuRefresh.js +74 -0
  560. package/ui/js/MenuWheel.js +35 -0
  561. package/ui/js/Messages.js +186 -0
  562. package/ui/js/Motion.js +16 -0
  563. package/ui/js/Mounts.js +320 -0
  564. package/ui/js/MouseNav.js +13 -0
  565. package/ui/js/Nav.js +329 -0
  566. package/ui/js/NetFs.js +26 -0
  567. package/ui/js/OpenWith.js +83 -0
  568. package/ui/js/Ops.js +585 -0
  569. package/ui/js/Palette.js +33 -0
  570. package/ui/js/PathBar.js +260 -0
  571. package/ui/js/Permissions.js +179 -0
  572. package/ui/js/Phones.js +112 -0
  573. package/ui/js/Picker.js +353 -0
  574. package/ui/js/PlaceMenu.js +96 -0
  575. package/ui/js/Places.js +265 -0
  576. package/ui/js/PreviewAsk.js +21 -0
  577. package/ui/js/PreviewKeys.js +155 -0
  578. package/ui/js/PreviewSettle.js +29 -0
  579. package/ui/js/PreviewSwap.js +101 -0
  580. package/ui/js/PropertyFacts.js +144 -0
  581. package/ui/js/Protocols.js +296 -0
  582. package/ui/js/QuickLookKinds.js +27 -0
  583. package/ui/js/QuickLookRect.js +24 -0
  584. package/ui/js/RailDrop.js +51 -0
  585. package/ui/js/RailKeys.js +87 -0
  586. package/ui/js/RailMenu.js +133 -0
  587. package/ui/js/Recent.js +135 -0
  588. package/ui/js/RecentMode.js +156 -0
  589. package/ui/js/Reload.js +60 -0
  590. package/ui/js/Renderer.js +9 -0
  591. package/ui/js/RunFile.js +143 -0
  592. package/ui/js/Scripts.js +28 -0
  593. package/ui/js/Scroll.js +304 -0
  594. package/ui/js/ScrollOff.js +69 -0
  595. package/ui/js/Search.js +478 -0
  596. package/ui/js/Selection.js +97 -0
  597. package/ui/js/Settings.js +476 -0
  598. package/ui/js/SettingsAbout.js +97 -0
  599. package/ui/js/SettingsShelf.js +52 -0
  600. package/ui/js/SheetKeys.js +42 -0
  601. package/ui/js/SheetQuery.js +281 -0
  602. package/ui/js/ShelfPile.js +27 -0
  603. package/ui/js/SlowClick.js +53 -0
  604. package/ui/js/SlowOp.js +6 -0
  605. package/ui/js/Sort.js +134 -0
  606. package/ui/js/Startup.js +63 -0
  607. package/ui/js/StateWords.js +17 -0
  608. package/ui/js/Status.js +238 -0
  609. package/ui/js/Swap.js +166 -0
  610. package/ui/js/TabMove.js +39 -0
  611. package/ui/js/Tabs.js +736 -0
  612. package/ui/js/Tags.js +134 -0
  613. package/ui/js/Taildrop.js +128 -0
  614. package/ui/js/Tap.js +174 -0
  615. package/ui/js/TextSize.js +107 -0
  616. package/ui/js/Thumbs.js +158 -0
  617. package/ui/js/Transfer.js +172 -0
  618. package/ui/js/Trash.js +81 -0
  619. package/ui/js/TrashDates.js +47 -0
  620. package/ui/js/TrashDrop.js +64 -0
  621. package/ui/js/UiState.js +438 -0
  622. package/ui/js/Unlock.js +152 -0
  623. package/ui/js/Update.js +130 -0
  624. package/ui/js/UserDirs.js +56 -0
  625. package/ui/qmldir +228 -0
@@ -0,0 +1,35 @@
1
+ import {test,expect,afterAll} from 'bun:test';
2
+ import {mkdtempSync,mkdirSync,copyFileSync,writeFileSync,rmSync} from 'node:fs';
3
+ import {join} from 'node:path';
4
+ import {tmpdir} from 'node:os';
5
+ const root=mkdtempSync(join(tmpdir(),'flea-highlight-'));
6
+ afterAll(()=>rmSync(root,{recursive:true,force:true}));
7
+ let serial=0;
8
+ async function run(name,{failure=false,sibling=false,filetype=null}={}) {
9
+ const dir=join(root,String(++serial)),src=join(dir,'flea','src'),bin=join(dir,'commands');
10
+ mkdirSync(src,{recursive:true});mkdirSync(bin);
11
+ // Isolate discovery from the real sibling checkout without changing it or using links.
12
+ copyFileSync(new URL('../src/highlight.js',import.meta.url),join(src,'highlight.js'));
13
+ const path=join(dir,'sample with spaces.js');writeFileSync(path,'const n = 1;');
14
+ const args=['--cat','--edit',...(filetype?['-filetype',filetype]:[]),path];
15
+ const body=`if (JSON.stringify(process.argv.slice(2)) !== JSON.stringify(${JSON.stringify(args)})) process.exit(9);\nprocess.stdout.write('\\x1b[31mconst\\x1b[0m n = 1;\\n');\n`;
16
+ writeFileSync(join(bin,name),`#!${process.execPath}\n${failure?'process.exit(7);':body}`,{mode:0o755});
17
+ if(sibling){const target=join(dir,'jsmdcui','src');mkdirSync(target,{recursive:true});writeFileSync(join(target,'index.js'),body);}
18
+ const script=join(src,'probe.js');
19
+ writeFileSync(script,`import {highlightFile,jsmdcui} from './highlight.js'; console.log(JSON.stringify({command:jsmdcui(),lines:await highlightFile(${JSON.stringify(path)},undefined,${JSON.stringify(filetype)})}));`);
20
+ const proc=Bun.spawn([process.execPath,script],{env:{...process.env,PATH:bin},stdout:'pipe',stderr:'pipe'});
21
+ const [text,err,code]=await Promise.all([new Response(proc.stdout).text(),new Response(proc.stderr).text(),proc.exited]);
22
+ expect({code,err}).toEqual({code:0,err:''});
23
+ return {result:JSON.parse(text),bin};
24
+ }
25
+ for(const name of ['jmi','jsmdcui'])test(`executes PATH ${name} launcher without guessing its install layout`,async()=>{
26
+ const {result,bin}=await run(name);expect(result.command).toEqual([join(bin,name)]);expect(result.lines[0]).toEqual([1,'const',0,' n = 1;']);
27
+ });
28
+ test('failed launcher preserves plain-text fallback',async()=>{expect((await run('jmi',{failure:true})).result.lines).toBeNull();});
29
+ test('sibling checkout still runs through Bun before PATH launchers',async()=>{
30
+ const {result}=await run('jmi',{failure:true,sibling:true});expect(result.command.length).toBe(2);expect(result.command[1]).toEndWith('/jsmdcui/src/index.js');expect(result.lines[0][0]).toBe(1);
31
+ });
32
+
33
+ test('passes a language override as the single-dash jsmdcui setting',async()=>{
34
+ expect((await run('jmi',{filetype:'v'})).result.lines[0][0]).toBe(1);
35
+ });
@@ -0,0 +1,22 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {previewHtml,html,HTML_LIMIT} from '../src/html.js';
3
+ import {mkdtemp,writeFile,rm} from 'node:fs/promises';
4
+ import {tmpdir} from 'node:os';
5
+ import {join} from 'node:path';
6
+ test('HTML is rebuilt with fixed tags, no source attributes, nested divs and literal entities',()=>{
7
+ expect(previewHtml('<div id=x><div onclick="evil()">A &amp; B</div></div>')).toBe('<div><div>A &amp; B</div></div>');
8
+ expect(previewHtml('<textarea><img src=x onerror="evil()"></textarea>')).toBe('&lt;img src=x onerror="evil()"&gt;');
9
+ expect(previewHtml('<script>globalThis.__htmlProbe++</script><style>bad</style><p>OK</p>')).toBe('<p>OK</p>');
10
+ expect(previewHtml('<svg><foreignObject><b>hidden</b></foreignObject></svg><p>OK</p>')).toBe('<p>OK</p>');
11
+ expect(()=>previewHtml('x'.repeat(HTML_LIMIT+1))).toThrow();
12
+ });
13
+ test('HTML file replies are bounded and cancellation produces no result',async()=>{
14
+ const dir=await mkdtemp(join(tmpdir(),'flea-html-'));
15
+ try{
16
+ const path=join(dir,'test.html');await writeFile(path,'<h1 onmouseover="evil()">Hello</h1>');
17
+ expect(await html({path,token:4,slot:'look'})).toMatchObject({state:'ready',html:'<h1>Hello</h1>',token:4,slot:'look'});
18
+ expect(await html({path,cancel:true})).toBe(null);
19
+ expect((await html({path:dir})).state).toBe('failed');
20
+ await writeFile(path,'x'.repeat(HTML_LIMIT+1));expect((await html({path})).state).toBe('failed');
21
+ }finally{await rm(dir,{recursive:true,force:true})}
22
+ });
@@ -0,0 +1,15 @@
1
+ import { test, expect } from 'bun:test';
2
+ const source=(await Bun.file(new URL('../ui/js/Input.js',import.meta.url)).text()).replace(/^\.pragma library\s*/,'');
3
+ const warnings=[];
4
+ const {pasteInto}=new Function('console',source+'; return {pasteInto};')({warn: e=>warnings.push(e)});
5
+ test('paste synchronous and asynchronous clipboard text',async()=>{
6
+ const values=[];pasteInto({clipboardText:()=> 'hello\nother'},v=>values.push(v));
7
+ pasteInto({clipboardText:()=>Promise.resolve('中文😀')},v=>values.push(v));
8
+ await Bun.sleep(0);expect(values).toEqual(['hello','中文😀']);
9
+ });
10
+ test('clipboard exceptions and rejections are caught',async()=>{
11
+ warnings.length=0;
12
+ pasteInto({clipboardText:()=>{throw Error('sync')}},()=>{});
13
+ pasteInto({clipboardText:()=>Promise.reject(Error('async'))},()=>{});
14
+ await Bun.sleep(0);expect(warnings.length).toBe(2);
15
+ });
@@ -0,0 +1,26 @@
1
+ import { test, expect } from "bun:test";
2
+
3
+ // flea-qml's own changes to the generated key table (ui/js/Keymap.js), checked in every preset.
4
+ const source = (await Bun.file(new URL("../ui/js/Keymap.js", import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm, "");
5
+ let next = 1000;
6
+ const named = { ControlModifier: 0x04000000, AltModifier: 0x08000000, ShiftModifier: 0x02000000, MetaModifier: 0x10000000,
7
+ Key_Return: 16777220, Key_Enter: 16777221, Key_D: 68, Key_I: 73, Key_Equal: 0x3d };
8
+ const Qt = new Proxy(named, { get: (t, k) => (k in t ? t[k] : (t[k] = next++)) });
9
+ const { lookupFor } = new Function("Qt", source + ";return {lookupFor};")(Qt);
10
+ const PRESETS = ["default", "vim", "mac", "windows"];
11
+
12
+ test("Alt+Enter opens Properties in every preset, as Windows, KDE and GNOME do", () => {
13
+ for (const preset of PRESETS) {
14
+ expect(lookupFor(preset, named.Key_Return, "", named.AltModifier)).toBe("properties");
15
+ expect(lookupFor(preset, named.Key_Enter, "", named.AltModifier)).toBe("properties");
16
+ }
17
+ expect(lookupFor("mac", named.Key_I, "", named.MetaModifier)).toBe("properties");
18
+ });
19
+
20
+ test("= and Alt+= measure folders in every preset; Ctrl+= stays the text size", () => {
21
+ for (const preset of PRESETS) {
22
+ expect(lookupFor(preset, named.Key_Equal, "=", 0)).toBe("measureDirs");
23
+ expect(lookupFor(preset, named.Key_Equal, "", named.AltModifier)).toBe("measureAllDirs");
24
+ expect(lookupFor(preset, named.Key_Equal, "", named.ControlModifier)).toBe("textSizeUp");
25
+ }
26
+ });
@@ -0,0 +1,36 @@
1
+ import {test,expect} from 'bun:test';
2
+ async function load(name,exports,deps={}) {
3
+ const source=(await Bun.file(new URL('../ui/js/'+name+'.js',import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm,'');
4
+ return new Function(...Object.keys(deps),source+';return {'+exports+'};')(...Object.values(deps));
5
+ }
6
+ const Icons=await load('Icons','isEmoji');
7
+ const Format=await load('Format','isSymlink');
8
+
9
+ test('emoji and directory rows never load language components, even during Find',async()=>{
10
+ const Lang=await load('LangMarks','slugForRow,syncMark',{Icons,Format,Qt:{createComponent(){throw Error('unnecessary component load')}}});
11
+ for(const row of [{n:'main.js',i:'📄',p:33188},{n:'code.py',i:'🐍',p:33188},{n:'folder.rs',d:true}]) {
12
+ const owner={langSlug:Lang.slugForRow(row),thumbDrawn:false,langLoader:null};
13
+ expect(owner.langSlug).toBe('');
14
+ Lang.syncMark(owner,{parent:{}},'LangMarkLoader.qml',1);
15
+ expect(owner.langLoader).toBeNull();
16
+ }
17
+ });
18
+
19
+ test('visible language logos load once and release when replaced by emoji or thumbnails',async()=>{
20
+ let loads=0,released=0,componentReleased=0;
21
+ const Lang=await load('LangMarks','slugForRow,syncMark',{Icons,Format,Qt:{createComponent(){loads++;return {
22
+ status:1,createObject(){return {active:true,destroy(){released++;}}},destroy(){componentReleased++;}
23
+ };}}});
24
+ const owner={langSlug:Lang.slugForRow({n:'main.js',i:'file',p:33188}),thumbDrawn:false,langLoader:null};
25
+ const glyph={parent:{}};
26
+ expect(owner.langSlug).toBe('javascript');
27
+ Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
28
+ Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
29
+ expect(loads).toBe(1);expect(componentReleased).toBe(1);
30
+ owner.langSlug=Lang.slugForRow({n:'main.js',i:'📄',p:33188});
31
+ Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
32
+ expect(released).toBe(1);expect(owner.langLoader).toBeNull();
33
+ owner.langSlug='javascript';owner.thumbDrawn=true;
34
+ Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
35
+ expect(loads).toBe(1);
36
+ });
@@ -0,0 +1,51 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {EventEmitter} from 'node:events';
3
+ import {browserPackage,browserCommand,selectFrontend,detectKitty,entryURL} from '../index.js';
4
+ import {fileURLToPath} from 'node:url';
5
+ import info from '../package.json';
6
+ import {isAbsolute} from 'node:path';
7
+
8
+ test('launcher follows browser package manager order',()=>{
9
+ const which = names => name => names.includes(name) ? '/bin/'+name : null;
10
+ expect(browserCommand(['file:///a%20b.qml'],which(['npx','bun','bunx']))).toEqual(['/bin/npx','--loglevel=error','-p',browserPackage(),'--','buninu-browser','file:///a%20b.qml']);
11
+ expect(browserCommand([],which(['npx','bunx']))).toEqual(['/bin/bunx',browserPackage()]);
12
+ expect(browserCommand([],which(['bun']))).toEqual(['/bin/bun','x',browserPackage()]);
13
+ expect(browserCommand([],which([]),'/running/bun')).toEqual(['/running/bun','x',browserPackage()]);
14
+ expect(isAbsolute(fileURLToPath(entryURL))).toBe(true);
15
+ });
16
+ test('explicit frontends override detection on Windows and Linux',()=>{
17
+ expect(selectFrontend([],true,'win32').frontend).toBe('--casty');
18
+ expect(selectFrontend([],false,'win32').frontend).toBe('--win32');
19
+ for(const platform of ['linux','android']) expect(selectFrontend([],false,platform).frontend).toBe('--wayland');
20
+ for(const flag of ['--casty','--wayland','--win32','--headless']) expect(selectFrontend([flag,'--help'],true,'win32')).toEqual({frontend:flag,args:['--help']});
21
+ expect(selectFrontend(['--future-frontend','arg'],false)).toEqual({frontend:'--future-frontend',args:['arg']});
22
+ });
23
+ function terminal(reply) {
24
+ const input=new EventEmitter(),output=new EventEmitter();
25
+ input.isTTY=output.isTTY=true;input.isRaw=false;input.paused=true;
26
+ input.isPaused=()=>input.paused;input.pause=()=>{input.paused=true;};input.resume=()=>{input.paused=false;};input.setRawMode=v=>{input.isRaw=v;};input.unshift=()=>{};
27
+ output.write=query=>{const id=/i=(\d+)/.exec(query)[1];if(reply!==null)queueMicrotask(()=>{input.emit('data',Buffer.from(`\x1b_Gi=${id};${reply}\x1b`));input.emit('data',Buffer.from('\\'));});};
28
+ return {input,output};
29
+ }
30
+ test('graphics query handles fragmented replies and restores terminal on timeout',async()=>{
31
+ for(const [reply,expected] of [['OK',true],['OK Buninu1 bgra',true],['EINVAL',false],[null,false]]) {
32
+ const {input,output}=terminal(reply);
33
+ expect(await detectKitty(input,output,5)).toBe(expected);
34
+ expect(input.isRaw).toBe(false);expect(input.paused).toBe(true);expect(input.listenerCount('data')).toBe(0);
35
+ }
36
+ expect(await detectKitty({isTTY:false},{isTTY:true})).toBe(false);
37
+ });
38
+
39
+ test('browser package comes from the script without shell execution',()=>{
40
+ expect(browserPackage()).toBe(info.scripts.browser.split(/\s+/)[1]);
41
+ expect(browserPackage('bunx buninu-browser@9.8.7')).toBe('buninu-browser@9.8.7');
42
+ expect(()=>browserPackage('bunx buninu-browser@1; echo bad')).toThrow();
43
+ });
44
+
45
+ test('version flags report the package and pinned browser versions', async()=>{
46
+ for (const flag of ['--version','-V']) {
47
+ const child = Bun.spawn([process.execPath, fileURLToPath(new URL('../index.js', import.meta.url)), flag], {stdout:'pipe',stderr:'pipe'});
48
+ expect(await new Response(child.stdout).text()).toBe(`flea-qml ${info.version}\n${browserPackage()} (pinned)\n`);
49
+ expect(await child.exited).toBe(0);
50
+ }
51
+ });
@@ -0,0 +1,28 @@
1
+ // bun test/manual-code-scroll.mjs (requires sibling buninu-browser).
2
+ import {rm} from 'node:fs/promises';
3
+ import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
4
+ const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
5
+ const qml=repo+'ui/boot/.scroll-check-'+process.pid+'.qml';
6
+ const text='x'.repeat(512)+'/system/bin/linker64\n'+Array.from({length:150},(_,i)=>'line '+i).join('\n');
7
+ await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow {width:500;height:300;color:"#171b22";Flea.CodeLines {id:code;anchors.fill:parent;fromText:true;text:${JSON.stringify(text)};active:true;scrolls:true;Component.onCompleted:globalThis.code=code;Flea.FastScrollHandler {flickable:code.viewport}}}`);
8
+ const browser=await createBrowser({spareRenderer:false});
9
+ try{
10
+ await browser.context.navigate(Bun.pathToFileURL(qml).href);
11
+ await Bun.sleep(300);
12
+ const run=s=>browser.context.evaluate(s);
13
+ const before=await run('({w:code.viewport.width,cw:code.viewport.contentWidth,h:code.viewport.height,ch:code.viewport.contentHeight})');
14
+ if(before.cw<=before.w||before.ch<=before.h)throw Error(JSON.stringify(before));
15
+ await run('document.elementFromPoint(200,100).dispatchEvent(new WheelEvent("wheel",{bubbles:true,cancelable:true,clientX:200,clientY:100,deltaX:200,deltaY:0}))');
16
+ await Bun.sleep(100);
17
+ if(!(await run('code.viewport.contentX > 0')))throw Error('Horizontal wheel failed');
18
+ await run('document.elementFromPoint(200,100).dispatchEvent(new WheelEvent("wheel",{bubbles:true,cancelable:true,clientX:200,clientY:100,deltaX:0,deltaY:150}))');
19
+ await Bun.sleep(100);
20
+ if(!(await run('code.viewport.contentY > 0')))throw Error('Vertical wheel failed');
21
+ await run('code.viewport.contentY=0;code.viewport.contentX=code.viewport.contentWidth-code.viewport.width');
22
+ await Bun.write('/tmp/flea-code-scroll.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
23
+ await run('code.wrap=true');await Bun.sleep(100);
24
+ if(!await run('code.viewport.contentX===0 && code.viewport.contentWidth===code.viewport.width'))throw Error('Wrap did not reset horizontal lane');
25
+ await run('code.wrap=false;code.text="short"');await Bun.sleep(100);
26
+ if(!await run('code.viewport.contentX===0 && code.viewport.contentWidth===code.viewport.width'))throw Error('New file did not reset width');
27
+ console.log('PASS horizontal/vertical wheel, wrap and new file reset',before);
28
+ }finally{await browser.close();await rm(qml,{force:true})}
@@ -0,0 +1,24 @@
1
+ // bun test/manual-column-scroll.mjs: complete Flea preview column, not bare CodeLines.
2
+ import {mkdtempSync,mkdirSync,rmSync} from 'node:fs';
3
+ import {join} from 'node:path';import {tmpdir} from 'node:os';
4
+ const repo=Bun.fileURLToPath(new URL('../',import.meta.url));const dir=mkdtempSync(join(tmpdir(),'flea-column-scroll-'));
5
+ for(const [key,sub] of [['HOME','home'],['XDG_DATA_HOME','data'],['XDG_CONFIG_HOME','config'],['XDG_CACHE_HOME','cache']]){process.env[key]=join(dir,sub);mkdirSync(process.env[key])}
6
+ process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
7
+ await Bun.write(join(dir,'long.txt'),'x'.repeat(512)+'/system/bin/linker64\n'+Array.from({length:200},(_,i)=>'line '+i).join('\n'));
8
+ const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
9
+ const qml=join(repo,'ui/boot/.column-scroll-'+process.pid+'.qml');
10
+ await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow {id:win;width:900;height:600;Loader{anchors.fill:parent;Component.onCompleted:setSource("${Bun.pathToFileURL(join(repo,'ui/WindowBody.qml')).href}");onLoaded:{item.host=win;globalThis.body=item}}}`);
11
+ const browser=await createBrowser({spareRenderer:false});const run=s=>browser.context.evaluate(s);
12
+ const wait=async s=>{for(let i=0;i<150;i++){if(await run(s))return;await Bun.sleep(100)}throw Error('Timeout '+s)};
13
+ try{
14
+ await browser.context.navigate(Bun.pathToFileURL(qml).href);
15
+ await wait('!!globalThis.body && !body.currentPane.listInFlight');
16
+ await run('body.currentPane.viewMode="columns"');await Bun.sleep(500);await run('body.currentPane.setCursor(body.currentPane.rows.findIndex(r=>r.n==="long.txt"))');
17
+ await wait('!!body.currentPane.columnsArea?.previewColumn?.linesItem?.code?.firstScreen');
18
+ await run('globalThis.col=body.currentPane.columnsArea.previewColumn;globalThis.v=col.linesItem.viewport');
19
+ const rect=await run('(()=>{const r=__qmlRuntime.element(col.frameItem).getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()');
20
+ const wheel=async(dx,dy)=>{await run(`document.elementFromPoint(${rect.x},${rect.y}).dispatchEvent(new WheelEvent("wheel",{bubbles:true,cancelable:true,clientX:${rect.x},clientY:${rect.y},deltaX:${dx},deltaY:${dy}}))`);await Bun.sleep(200)};
21
+ await wheel(180,0);if(!await run('v.contentX>0'))throw Error('Column horizontal wheel failed');
22
+ await wheel(0,100);await wait('v.contentY>0');
23
+ console.log('PASS complete preview column horizontal and vertical wheel',await run('({x:v.contentX,y:v.contentY,cw:v.contentWidth,ch:v.contentHeight})'));
24
+ }finally{await browser.close();rmSync(qml,{force:true});rmSync(dir,{recursive:true,force:true})}
@@ -0,0 +1,20 @@
1
+ // Actual JS backend -> bunmsh -cc builtin diff -> QML CompareView.
2
+ import {mkdtemp,rm} from 'node:fs/promises';import {join} from 'node:path';import {tmpdir} from 'node:os';
3
+ import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
4
+ const dir=await mkdtemp(join(tmpdir(),'flea-compare-ui-'));
5
+ const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
6
+ const qml=join(repo,'ui/boot/.compare-'+process.pid+'.qml');
7
+ const a=join(dir,'before.txt'),b=join(dir,'after.txt');
8
+ process.env.BUNINU_BUN=process.execPath;
9
+ await Bun.write(a,'first\nold\nlast\n');await Bun.write(b,'first\nnew\nlast\n');
10
+ await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow{width:640;height:480;color:"#171b22";Flea.Backend{id:backend} Flea.CompareView{id:view;anchors.fill:parent;backend:backend;active:true;pathA:${JSON.stringify(a)};pathB:${JSON.stringify(b)};Component.onCompleted:globalThis.comparison=view}}`);
11
+ const browser=await createBrowser({spareRenderer:false});
12
+ try{
13
+ await browser.context.navigate(Bun.pathToFileURL(qml).href);
14
+ let state;for(let i=0;i<100;i++){state=await browser.context.evaluate('globalThis.comparison?.answer');if(state)break;await Bun.sleep(100)}
15
+ if(state!=='diff')throw Error('Compare answer: '+state);
16
+ const kinds=await browser.context.evaluate('comparison.rows.map(r=>r.kind)');
17
+ if(!kinds.includes('removed')||!kinds.includes('added'))throw Error(JSON.stringify(kinds));
18
+ await Bun.write('/tmp/flea-compare.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
19
+ console.log('PASS real backend and QML CompareView',kinds);
20
+ }finally{await browser.close();await rm(qml,{force:true});await rm(dir,{recursive:true,force:true})}
@@ -0,0 +1,37 @@
1
+ // Exercise the real status bar bindings, including completed Find and ordinary lists.
2
+ import {rm} from 'node:fs/promises';
3
+ import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
4
+ const qml=new URL('../ui/boot/.find-status-'+process.pid+'.qml',import.meta.url);
5
+ process.env.BUNINU_BUN=process.execPath;
6
+ await Bun.write(qml,`import QtQuick
7
+ import Quickshell
8
+ import ".." as Flea
9
+ FloatingWindow {
10
+ width: 1000; height: 100; color: "#171b22"
11
+ Flea.StatusBar {
12
+ id: bar; width: parent.width
13
+ listingState: "ready"; total: 2
14
+ searchLine: "2 found in 0.4 s"; searchRunning: false
15
+ pane: ({
16
+ selectionVersion: 1, selection: {count: function(){return 0}, indices: function(){return []}},
17
+ cursorIndex: 0, shown: null, shownTotal: 2, rows: [{n:"sub/完整的 long filename.txt",s:123}],
18
+ cursorRow: {n:"sub/完整的 long filename.txt",s:123},
19
+ rowFor: function(i){return this.rows[i]}
20
+ })
21
+ Component.onCompleted: globalThis.bar = bar
22
+ }
23
+ }`);
24
+ const browser=await createBrowser({spareRenderer:false});
25
+ function assert(ok,message){if(!ok)throw Error(message)}
26
+ try{
27
+ await browser.context.navigate(Bun.pathToFileURL(Bun.fileURLToPath(qml)).href);
28
+ for(let i=0;i<100;i++){if(await browser.context.evaluate('!!globalThis.bar?.fileNameItem'))break;await Bun.sleep(50)}
29
+ let state=await browser.context.evaluate('({shown:bar.showFileName,label:bar.fileLabel,count:bar.countText(),visible:bar.fileNameItem?.visible})');
30
+ assert(state.shown&&state.visible&&state.label.includes('123')&&state.label.endsWith('完整的 long filename.txt'),JSON.stringify(state));
31
+ assert(state.count==='2 found in 0.4 s',JSON.stringify(state));
32
+ await browser.context.evaluate('bar.searchRunning=true');
33
+ assert(await browser.context.evaluate('!bar.showFileName && bar.centreText()===bar.searchLine'),'live progress lost');
34
+ await browser.context.evaluate('bar.searchRunning=false; bar.searchLine=""');
35
+ assert(await browser.context.evaluate('bar.showFileName && bar.countText()==="2 items"'),'ordinary listing regressed');
36
+ console.log('PASS Find filename, file size, completed summary, running progress and ordinary listing');
37
+ }finally{await browser.close();await rm(qml,{force:true})}
@@ -0,0 +1,23 @@
1
+ // bun test/manual-html-security.mjs; uses sibling browser, with scripts enabled.
2
+ // The attack only increments a harmless global counter. Never run raw fixtures.
3
+ import {previewHtml,HTML_TAGS} from '../src/html.js';
4
+ import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
5
+ const probe='globalThis.__htmlProbe=(globalThis.__htmlProbe||0)+1';
6
+ const payloads=[`<img src=x onerror="${probe}">`,`<script>${probe}</script>`,`<svg onload="${probe}"></svg>`,`<iframe srcdoc="<script>${probe}</script>"></iframe>`,`<p onclick="${probe}">click</p>`,'&lt;img src=x onerror=alert(1)&gt;','<!--><img src=x onerror="'+probe+'">'];
7
+ const wrappers=['div','textarea','plaintext','xmp','title','noscript','select','option','table','svg','math','template','script','style','iframe'];
8
+ const inputs=[];
9
+ for(const a of wrappers)for(const b of wrappers)for(const p of payloads)inputs.push(`<${a}><${b}>${p}</${a}></${b}><p>tail</p>`);
10
+ inputs.push('<math><mtext><table><mglyph><style><!--</style><img title="--><img src=x onerror='+probe+'>">');
11
+ const outputs=inputs.map(previewHtml);
12
+ const browser=await createBrowser({spareRenderer:false});
13
+ try{
14
+ await browser.context.navigate('about:blank');
15
+ // First prove the harmless probe works in this execution context.
16
+ await browser.context.evaluate(probe);
17
+ if(await browser.context.evaluate('globalThis.__htmlProbe')!==1)throw Error('Probe did not execute');
18
+ await browser.context.evaluate(`globalThis.__htmlProbe=0;globalThis.__bad=[];const allowed=new Set(${JSON.stringify([...HTML_TAGS])});for(const html of ${JSON.stringify(outputs)}){const host=document.createElement('div');host.innerHTML=html;document.body.appendChild(host);for(const el of host.querySelectorAll('*')){if(!allowed.has(el.localName)||el.attributes.length||el.namespaceURI!=='http://www.w3.org/1999/xhtml')__bad.push(el.outerHTML);el.dispatchEvent(new Event('click'));el.dispatchEvent(new Event('error'));el.dispatchEvent(new Event('load'));}host.remove();}`);
19
+ await Bun.sleep(100);
20
+ const result=await browser.context.evaluate('({executed:__htmlProbe,bad:__bad})');
21
+ if(result.executed||result.bad.length)throw Error(JSON.stringify(result));
22
+ console.log('PASS',outputs.length,'adversarial fragments; no illegal elements/attributes or script execution');
23
+ }finally{await browser.close()}
@@ -0,0 +1,30 @@
1
+ // bun test/manual-html-ui.mjs; real backend + QML rich-text preview, no external resources.
2
+ import {mkdtemp,rm} from 'node:fs/promises';
3
+ import {join} from 'node:path';
4
+ import {tmpdir} from 'node:os';
5
+ import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
6
+ const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
7
+ const dir=await mkdtemp(join(tmpdir(),'flea-html-ui-'));
8
+ const qml=join(repo,'ui/boot/.html-check-'+process.pid+'.qml');
9
+ const path=join(dir,'preview.html');
10
+ process.env.BUNINU_BUN=process.execPath;
11
+ await Bun.write(path,'<h1 onclick="globalThis.__htmlProbe=1">HTML preview</h1><div><b>Bold</b> &amp; <i>italic</i><div>Nested div</div></div><textarea><img src=x onerror="globalThis.__htmlProbe=1"></textarea>'+ '<p>Scrollable text</p>'.repeat(80));
12
+ await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow {width:640;height:480;color:"#171b22";Flea.Backend{id:backend} Loader {anchors.fill:parent;Component.onCompleted:setSource("${Bun.pathToFileURL(join(repo,'ui/PreviewHtml.qml')).href}");onLoaded:{item.backend=backend;item.path=${JSON.stringify(path)};item.active=true;globalThis.preview=item}}}`);
13
+ const browser=await createBrowser({spareRenderer:false});
14
+ try{
15
+ await browser.context.navigate(Bun.pathToFileURL(qml).href);
16
+ let ready=false;
17
+ for(let i=0;i<150;i++){ready=await browser.context.evaluate('!!globalThis.preview && preview.contentReady');if(ready)break;await Bun.sleep(100)}
18
+ if(!ready)throw Error('Preview never ready: '+await browser.context.evaluate('document.body.innerText'));
19
+ const result=await browser.context.evaluate('({text:document.body.innerText,probe:globalThis.__htmlProbe||0,height:preview.viewport.contentHeight,viewport:preview.viewport.height})');
20
+ if(!result.text.includes('Nested div')||result.probe||result.height<=result.viewport)throw Error(JSON.stringify(result));
21
+ await browser.context.evaluate('preview.scrollBy(5)');
22
+ if(!(await browser.context.evaluate('preview.scrollY > 0')))throw Error('Scroll failed');
23
+ await browser.context.evaluate('preview.viewport.contentY=0');
24
+ await Bun.write('/tmp/flea-html-preview.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
25
+ await browser.context.evaluate('preview.width=240');
26
+ const boxes=await browser.context.evaluate(`(()=>{const e=document.querySelector('h1'),p=document.querySelector('p');return {heading:e.getBoundingClientRect(),paragraph:p.getBoundingClientRect(),content:preview.viewport.contentHeight}})()`);
27
+ if(boxes.paragraph.top<boxes.heading.bottom)throw Error('Heading overlaps paragraph: '+JSON.stringify(boxes));
28
+ await Bun.write('/tmp/flea-html-preview-narrow.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
29
+ console.log('PASS backend HTML, nested rich text, literal attack text and scrolling',result.height,result.viewport);
30
+ }finally{await browser.close();await rm(qml,{force:true});await rm(dir,{recursive:true,force:true})}
@@ -0,0 +1,26 @@
1
+ // Run: bun test/manual-syntax-preview.mjs [path/to/jsmdcui]
2
+ // Uses that checkout's runtime/syntax definitions and syntax-parity samples.
3
+ import {resolve,join} from 'node:path';
4
+ const base=resolve(process.argv[2] || new URL('../../jsmdcui',import.meta.url).pathname);
5
+ const {SyntaxDefinition,parseHeaderYaml,detectSyntax}=await import(Bun.pathToFileURL(join(base,'src/highlight/parser.js')).href);
6
+ import {code} from '../src/code.js';
7
+ import {runsOf} from '../src/highlight.js';
8
+ import {tmpdir} from 'node:os';
9
+ import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';
10
+ const defs=[];
11
+ for(const file of new Bun.Glob('*.yaml').scanSync(base+'/runtime/syntax')) {const text=await Bun.file(base+'/runtime/syntax/'+file).text(),src=Bun.YAML.parse(text);defs.push({file,def:new SyntaxDefinition(parseHeaderYaml(src),src,text)});}
12
+ const dir=mkdtempSync(join(tmpdir(),'flea-syntax-'));const results=[];
13
+ const words=new Set(['Dockerfile','Containerfile','.bashrc','Makefile','CMakeLists.txt','README.md','Cargo.lock','sample.dats','sample.f90','sample.ebuild','sample.eclass','gitconfig','git-rebase-todo','go.mod','meson.build','sample.mm','sample.pl','sample.ps1','Xresources','sample.yml','sample.zig','sample.zc']);
14
+ for(const {def} of defs) for(const w of (def.header.filename||'').match(/[A-Za-z0-9_-]+/g)||[]) {words.add(w);words.add('.'+w);words.add('sample.'+w);words.add(w+'.conf');}
15
+ let index=0;
16
+ try {await Promise.all(Array.from({length:4},async()=>{for(;;){const i=index++;if(i>=defs.length)return;const {file,def}=defs[i];if(/^html[45]?$/.test(def.filetype))continue;
17
+ const sample=Bun.file(base+'/tests/syntax-parity/samples/'+file.replace(/yaml$/,'txt'));if(!await sample.exists()){results.push({file,error:'missing sample'});continue;}
18
+ const text=await sample.text();
19
+ const child=Bun.spawn([process.execPath,join(base,'src/index.js'),'--cat','--edit','-filetype',def.filetype,sample.name],{stdout:'pipe',stderr:'pipe'});
20
+ const [ansi,stderr,exit]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
21
+ const explicitColour=exit===0 && ansi.split('\n').some(l=>runsOf(l).some((v,i)=>i%2===0&&v!==0));
22
+ const path=[...words].find(p=>def.header.matchFileName(p)&&detectSyntax(defs.map(x=>x.def),{path:p,firstLine:text.split('\n')[0],lines:text.split('\n').slice(0,50)})?.filetype===def.filetype);
23
+ if(!path){results.push({file,explicitColour,error:'no automatic filename witness',pattern:def.header.filename});continue;}
24
+ const target=dir+'/'+i;await Bun.write(target+'/'+path,text);const r=await code({path:target+'/'+path});results.push({file,explicitColour,path,state:r.state,lines:r.total});
25
+ }}));}finally{rmSync(dir,{recursive:true,force:true});}
26
+ results.sort((a,b)=>a.file.localeCompare(b.file));await Bun.write(join(tmpdir(),'flea-syntax-results.json'),JSON.stringify(results,null,2));console.log(JSON.stringify({total:results.length,explicitColour:results.filter(r=>r.explicitColour).length,styled:results.filter(r=>r.state==='styled').length,other:results.filter(r=>r.state!=='styled')},null,2));
@@ -0,0 +1,86 @@
1
+ // Run: bun test/manual-trash-ui.mjs (requires ../buninu-browser). Uses temporary fixtures only.
2
+ import {mkdtempSync,mkdirSync,writeFileSync,rmSync} from 'node:fs';
3
+ import {join} from 'node:path';
4
+ import {tmpdir} from 'node:os';
5
+ const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
6
+ const dir=mkdtempSync(join(tmpdir(),'flea-trash-ui-'));
7
+ process.env.XDG_DATA_HOME=join(dir,'data');process.env.XDG_CONFIG_HOME=join(dir,'config');process.env.XDG_CACHE_HOME=join(dir,'cache');process.env.HOME=dir;process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
8
+ for(const p of ['data','config','cache'])mkdirSync(join(dir,p));
9
+ const {trashOne}=await import('../src/trash.js');
10
+ writeFileSync(join(dir,'preview.txt'),'HELLO TRASH PREVIEW\nline two');trashOne(join(dir,'preview.txt'));
11
+ writeFileSync(join(dir,'other.txt'),'SECOND');trashOne(join(dir,'other.txt'));
12
+ mkdirSync(join(dir,'folder'));writeFileSync(join(dir,'folder','child'),'child');trashOne(join(dir,'folder'));
13
+ const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
14
+ const file=join(repo,'ui/boot/.trash-check-'+process.pid+'.qml');
15
+ await Bun.write(file,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow { id: win; width:900; height:600; Loader {id:body; anchors.fill:parent; Component.onCompleted:{globalThis.testLoader=body;setSource("${Bun.pathToFileURL(join(repo,"ui/WindowBody.qml")).href}")} onStatusChanged:console.log("TESTLOADER",status) ; onLoaded:{item.host=win;globalThis.testBody=item}} }`);
16
+ const browser=await createBrowser({spareRenderer:false});
17
+ const run=s=>browser.context.evaluate(s);
18
+ try {
19
+ await browser.context.addInitScript(`globalThis.compileErrors=[];const compile=__qmlHost.compile;__qmlHost.compile=(...a)=>{try{return compile(...a)}catch(e){compileErrors.push(String(e));throw e}}`);
20
+ await browser.context.navigate(Bun.pathToFileURL(file).href);
21
+ console.log('loaded',await run('document.body.innerText.slice(0,5000)'));
22
+ for(let i=0;i<30;i++){if(await run('!!globalThis.testBody && !testBody.currentPane.listInFlight'))break;await Bun.sleep(100)}
23
+ if(!await run('!!globalThis.testBody'))throw Error('testBody missing '+JSON.stringify(await run('({status:globalThis.testLoader?.status,source:globalThis.testLoader?.source,text:document.body.innerText,errors:globalThis.compileErrors})'))) ;
24
+ console.log('initial',await run('({text:document.body.innerText.slice(0,150),mode:testBody.currentPane.recentMode})'));
25
+ for(const address of ['trash:/','trash://','trash:///']) {
26
+ if(await run('testBody.currentPane.recentMode === "trash"')) {
27
+ await run('testBody.currentPane.closeRecent()');
28
+ for(let i=0;i<150;i++){if(await run('!testBody.currentPane.listInFlight'))break;await Bun.sleep(100)}
29
+ }
30
+ await run(`testBody.currentPane.chromeBar.startEdit();testBody.currentPane.chromeBar.pathField.text=${JSON.stringify(address)};testBody.currentPane.chromeBar.commitEdit()`);
31
+ for(let i=0;i<150;i++){if(await run('!testBody.currentPane.listInFlight'))break;await Bun.sleep(100)}
32
+ if(!await run('testBody.currentPane.recentMode === "trash" && testBody.currentPane.path === "/" && testBody.currentPane.chromeBar.path === "trash:///"'))throw Error('Trash address failed: '+address);
33
+ }
34
+ await run('testBody.currentPane.chromeBar.startEdit()');
35
+ if(await run('testBody.currentPane.chromeBar.editText')!=='trash:///')throw Error('Trash edit address acquired an extra slash');
36
+ await run('testBody.currentPane.chromeBar.closeEdit()');
37
+ console.log('PASS all Trash address aliases, display and unchanged backing path');
38
+ const paths=await run('testBody.currentPane.rows.map(r=>r.trash.original)');
39
+ if(paths.length!==3 || paths.some(p=>!p.startsWith(dir+'/')))throw Error('External trash detected; refusing mutation');
40
+ console.log('trash',await run('({text:document.body.innerText,mode:testBody.currentPane.recentMode,rows:testBody.currentPane.total,cursor:testBody.currentPane.cursorRow})'));
41
+ await run('testBody.currentPane.setCursor(testBody.currentPane.rows.findIndex(r=>r.n.endsWith("preview.txt")))');
42
+ if(await run('testBody.currentPane.statusBar.selectedName')!=='preview.txt')throw Error('Trash status filename is not the original name');
43
+ if(!await run('testBody.currentPane.statusBar.fileNameItem.visible && testBody.currentPane.statusBar.fileNameItem.width > 0'))throw Error('Status filename has no visible space');
44
+ console.log('PASS status filename uses the original Trash name');
45
+ // Exercise only the presentation component with a name wider than the window;
46
+ // keep the actual fixture paths and operation identities untouched.
47
+ await run('testBody.currentPane.statusBar.fileNameItem.name="完整檔案名稱-".repeat(40)+".txt"');
48
+ await Bun.sleep(100);
49
+ if(!await run('testBody.currentPane.statusBar.fileNameItem.contentWidth > testBody.currentPane.statusBar.fileNameItem.width'))throw Error('Long status filename cannot scroll');
50
+ await run('testBody.currentPane.statusBar.fileNameItem.contentX=testBody.currentPane.statusBar.fileNameItem.contentWidth-testBody.currentPane.statusBar.fileNameItem.width');
51
+ if(!await run('testBody.currentPane.statusBar.fileNameItem.contentX > 0'))throw Error('Long status filename did not scroll');
52
+ if(process.env.FLEA_STATUS_SCREENSHOT)await Bun.write(process.env.FLEA_STATUS_SCREENSHOT,Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
53
+ await run('testBody.currentPane.statusBar.fileNameItem.name=testBody.currentPane.statusBar.fileLabel');
54
+ if(await run('testBody.currentPane.statusBar.fileNameItem.contentX')!==0)throw Error('New filename did not reset scrolling');
55
+ console.log('PASS long status filename scrolling and reset');
56
+ await run('testBody.currentPane.act("preview")');await Bun.sleep(2000);
57
+ if(!await run('testBody.quickLookActive && document.body.innerText.includes("HELLO TRASH PREVIEW")'))throw Error('preview missing');
58
+ if(process.env.FLEA_TRASH_SCREENSHOT)await Bun.write(process.env.FLEA_TRASH_SCREENSHOT,Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
59
+ console.log('preview',await run('({active:testBody.quickLookActive,text:document.body.innerText.slice(-900)})'));
60
+ await run('testBody.currentPane.quickLook().close()');
61
+ await run('testBody.currentPane.act("filter");testBody.currentPane.filterQuery="preview"');
62
+ await Bun.sleep(150);
63
+ if(await run('testBody.currentPane.shown.length')!==1)throw Error('filter failed');
64
+ await run('testBody.currentPane.filterQuery="";testBody.currentPane.filterTyping=false');
65
+ const wait=async predicate=>{for(let i=0;i<200;i++){if(await run(predicate))return;await Bun.sleep(50)}throw Error('Timed out: '+predicate)};
66
+ await run('testBody.currentPane.open(testBody.currentPane.join("/",testBody.currentPane.rows.find(r=>r.d).n))');
67
+ await wait('!testBody.currentPane.listInFlight && testBody.currentPane.recentMode===""');
68
+ await run('testBody.currentPane.goBack()');
69
+ await wait('!testBody.currentPane.listInFlight && testBody.currentPane.recentMode==="trash"');
70
+ console.log('PASS filter and directory Back');
71
+ await run('testBody.currentPane.setCursor(testBody.currentPane.rows.findIndex(r=>r.n.endsWith("preview.txt")));testBody.currentPane.trash.action("deletePermanently")');
72
+ await wait('testBody.currentPane.trash.confirmationOpen');
73
+ await run('testBody.currentPane.trash.confirmation.cancel()');
74
+ await wait('!testBody.currentPane.trash.busy');
75
+ await run('testBody.currentPane.trash.action("restoreTrashSelection")');
76
+ await wait('!testBody.currentPane.trash.busy && !testBody.currentPane.listInFlight && testBody.currentPane.total===2');
77
+ if(!await Bun.file(join(dir,'preview.txt')).exists())throw Error('restore missing');
78
+ console.log('PASS cancel deletion and restore selected');
79
+ await run('testBody.currentPane.trash.action("emptyTrash")');
80
+ await wait('testBody.currentPane.trash.confirmationOpen');
81
+ // Opening the review is safe; never activate Empty Trash in a test, because
82
+ // mount-local trashes can exist outside the isolated HOME/XDG directories.
83
+ await run('testBody.currentPane.trash.confirmation.cancel()');
84
+ await wait('!testBody.currentPane.trash.busy');
85
+ console.log('PASS Empty Trash review and cancellation');
86
+ }finally{browser.close();rmSync(file,{force:true});rmSync(dir,{recursive:true,force:true})}
@@ -0,0 +1,93 @@
1
+ // Mermaid diagrams in the Markdown previews: fences held out of the page and drawn by src/figure.js
2
+ // (Pele in a Bun Worker), embedded as SVG pictures, and their source kept with a reason when not.
3
+ import { afterAll, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { DIAGRAMS_PER_PAGE, drawDiagrams, holdFormulas, markdownHtml, markdownPage } from "../src/markdown-html.js";
8
+ import { figureTheme, renderDiagram, stopFigures } from "../src/figure.js";
9
+
10
+ const folder = mkdtempSync(join(tmpdir(), "flea-diagrams-"));
11
+ afterAll(() => { stopFigures(); rmSync(folder, { recursive: true, force: true }); });
12
+
13
+ const FLOW = "flowchart TD\n A[開始 😀] --> B[結束]";
14
+ const pictures = (html) => [...html.matchAll(/<div class="diagram"><img alt="Mermaid diagram" src="data:image\/svg\+xml;base64,([^"]+)"><\/div>/g)]
15
+ .map((match) => Buffer.from(match[1], "base64").toString());
16
+
17
+ test("Mermaid fences are held as the block view picks them, and other code stays code", () => {
18
+ const source = [
19
+ "```mermaid", "flowchart TD", " A --> B", "```",
20
+ "~~~ Mermaid title", "pie", "~~~",
21
+ " ```mermaid", " sequenceDiagram", " A->>B: hi", " ```",
22
+ "```js", "const x = 1;", "```",
23
+ "```mermaid`", "not a fence opener", "```",
24
+ "$x$ and `$y$`",
25
+ "```mermaid", "flowchart LR", " unclosed --> end",
26
+ ].join("\n");
27
+ const { text, diagrams, formulas } = holdFormulas(source);
28
+ expect(diagrams).toEqual(["flowchart TD\n A --> B", "pie", "sequenceDiagram\n A->>B: hi", "flowchart LR\n unclosed --> end"]);
29
+ expect(text).toContain("```js\nconst x = 1;\n```");
30
+ expect(text).not.toContain("flowchart");
31
+ expect(formulas).toEqual([{ tex: "x", display: false }]);
32
+ });
33
+
34
+ test("a drawn diagram becomes a picture; a failed one keeps its source and says why", () => {
35
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><text>中文</text></svg>';
36
+ const source = "# T\n\n```mermaid\nA\n```\n\n```mermaid\nB & <b>\n```\n\n```mermaid\nC\n```\n";
37
+ const html = markdownPage(source, folder, {}, [{ svg }, { error: "Parse error <here>" }]);
38
+ expect(pictures(html)).toEqual([svg]);
39
+ expect(html).toContain("<pre><code>B &amp; &lt;b&gt;</code></pre><p class=\"diagram-error\">Diagram not drawn · Parse error &lt;here&gt;</p>");
40
+ // A page built without drawing (no answer for the fence) shows the plain source.
41
+ expect(html).toContain("<pre><code>C</code></pre>");
42
+ expect(html).not.toMatch(/[]|<p><div|<script/);
43
+ });
44
+
45
+ test("diagrams share the pictures' budget", () => {
46
+ const big = `<svg xmlns="http://www.w3.org/2000/svg">${"x".repeat((1 << 20) + 1)}</svg>`;
47
+ const html = markdownPage("```mermaid\nA\n```\n", folder, {}, [{ svg: big }]);
48
+ expect(pictures(html)).toEqual([]);
49
+ expect(html).toContain("Diagram not drawn · over the page's picture budget");
50
+ });
51
+
52
+ test("the HTML preview draws Chinese and emoji labels, and the page stays passive", async () => {
53
+ const path = join(folder, "doc.md");
54
+ writeFileSync(path, `# 圖\n\n\`\`\`mermaid\n${FLOW}\n\`\`\`\n\n\`\`\`mermaid\nflowchart TD\n A -->\n\`\`\`\n`);
55
+ const answer = await markdownHtml({ path, token: 3, slot: "s", theme: { foreground: "#c0caf5", background: "#101315" } });
56
+ expect(answer).toMatchObject({ t: "markdownhtml", token: 3, slot: "s", state: "ready" });
57
+ const [svg] = pictures(answer.html);
58
+ expect(svg).toContain("開始 😀");
59
+ expect(svg).toContain("#c0caf5");
60
+ expect(svg).not.toMatch(/<script|<foreignObject|\son\w+=/i);
61
+ expect(answer.html).toMatch(/Diagram not drawn · Parse error on line 3: Expecting/);
62
+ expect(answer.html).not.toMatch(/<script/i);
63
+ });
64
+
65
+ test(`at most ${DIAGRAMS_PER_PAGE} diagrams are drawn for one page`, async () => {
66
+ const source = Array.from({ length: DIAGRAMS_PER_PAGE + 1 }, () => "```mermaid\nflowchart TD\n A --> B\n```").join("\n\n");
67
+ const drawn = await drawDiagrams(source);
68
+ expect(drawn.slice(0, DIAGRAMS_PER_PAGE).every((answer) => answer.svg)).toBe(true);
69
+ expect(drawn.at(-1)).toEqual({ error: `more than ${DIAGRAMS_PER_PAGE} diagrams on the page` });
70
+ });
71
+
72
+ test("renderDiagram caches, reports errors, and recovers from a deadline", async () => {
73
+ const first = await renderDiagram(FLOW);
74
+ expect(first.svg).toContain("結束");
75
+ expect(await renderDiagram(FLOW)).toBe(first);
76
+ expect((await renderDiagram("flowchart TD\n A -->")).error).toMatch(/^Parse error on line 3: Expecting/);
77
+ expect(await renderDiagram(" ")).toEqual({ error: "empty diagram" });
78
+ expect(await renderDiagram("x".repeat(32769))).toEqual({ error: "diagram over 32 KiB" });
79
+ const slow = "flowchart LR\n" + Array.from({ length: 300 }, (_, i) => ` N${i} --> N${(i * 7 + 3) % 300}`).join("\n");
80
+ expect(await renderDiagram(slow, {}, { timeout: 1 })).toEqual({ error: "diagram took over 0.001 s" });
81
+ // The stuck Worker was terminated; the next diagram starts a fresh one.
82
+ expect((await renderDiagram("sequenceDiagram\n 甲->>乙: 你好")).svg).toContain("你好");
83
+ });
84
+
85
+ test("figureTheme keeps colours, a plain font name and whole advance tables, and dates a Gantt chart", () => {
86
+ const advances = Array.from({ length: 95 }, () => 500);
87
+ const theme = figureTheme({ fg: '"><script>', background: "#101315", font: "Noto Sans CJK TC", advances, boldAdvances: [1, 2] },
88
+ "gantt\n title 計畫", new Date(2026, 10, 15, 23));
89
+ expect(theme).toMatchObject({ fg: "#d8dee9", bg: "#101315", font: "Noto Sans CJK TC", advances, boldAdvances: [] });
90
+ expect(theme.today).toBe(Date.UTC(2026, 10, 15, 12));
91
+ expect(figureTheme({ font: 'x" onload="y' }).font).toBe("sans-serif");
92
+ expect(figureTheme({}, "flowchart TD").today).toBeUndefined();
93
+ });