flea-qml 0.0.0-stage → 0.2.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 (637) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +511 -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 +407 -0
  51. package/src/archiveops.js +728 -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 +135 -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/terminal-server.js +95 -0
  82. package/src/terminal-shell.js +26 -0
  83. package/src/transfer.js +669 -0
  84. package/src/trash-record.js +41 -0
  85. package/src/trash.js +210 -0
  86. package/src/trashbrowse.js +327 -0
  87. package/src/uistate.js +393 -0
  88. package/src/uistore.js +168 -0
  89. package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
  90. package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
  91. package/src/vendor/LICENSES/MIT-katex.txt +21 -0
  92. package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
  93. package/src/vendor/LICENSES/MIT-pele.txt +40 -0
  94. package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
  95. package/src/vendor/LICENSES/MIT-temml.txt +21 -0
  96. package/src/vendor/pele.js +145 -0
  97. package/src/vendor/temml.mjs +14708 -0
  98. package/src/watch.js +98 -0
  99. package/src/wayland/client.js +109 -0
  100. package/src/wayland/protocol.js +55 -0
  101. package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
  102. package/src/wayland/protocols/wayland.xml +3248 -0
  103. package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
  104. package/src/wayland/resource-io.js +6 -0
  105. package/src/wayland/socket-discovery.js +26 -0
  106. package/src/wayland/transport-posix.js +120 -0
  107. package/src/wayland/transport-win32.js +11 -0
  108. package/src/wayland/transport.js +3 -0
  109. package/src/wayland/win32-resources.js +80 -0
  110. package/src/wayland/win32-wire.js +62 -0
  111. package/test/archive.test.js +80 -0
  112. package/test/bench-markdown-scroll.mjs +190 -0
  113. package/test/clip-host.test.js +113 -0
  114. package/test/code-preview.test.js +35 -0
  115. package/test/compare-ui-paths.test.js +24 -0
  116. package/test/compare.test.js +54 -0
  117. package/test/delete-trash.test.js +67 -0
  118. package/test/dirsize.test.js +138 -0
  119. package/test/file-ops.test.js +307 -0
  120. package/test/file-url.test.js +24 -0
  121. package/test/find-contents.test.js +161 -0
  122. package/test/find-input.test.js +66 -0
  123. package/test/fixtures/pele-parity.json +1006 -0
  124. package/test/highlight-launcher.test.js +35 -0
  125. package/test/html-preview.test.js +22 -0
  126. package/test/input.test.js +15 -0
  127. package/test/keymap.test.js +26 -0
  128. package/test/language-icons.test.js +36 -0
  129. package/test/launcher.test.js +51 -0
  130. package/test/manual-archive-here-ui.mjs +47 -0
  131. package/test/manual-code-scroll.mjs +28 -0
  132. package/test/manual-column-scroll.mjs +24 -0
  133. package/test/manual-compare-ui.mjs +20 -0
  134. package/test/manual-find-status.mjs +37 -0
  135. package/test/manual-folder-focus.mjs +63 -0
  136. package/test/manual-html-security.mjs +23 -0
  137. package/test/manual-html-ui.mjs +30 -0
  138. package/test/manual-syntax-preview.mjs +26 -0
  139. package/test/manual-terminal-lifecycle.mjs +42 -0
  140. package/test/manual-terminal-navigation.mjs +48 -0
  141. package/test/manual-terminal-ui.mjs +47 -0
  142. package/test/manual-trash-ui.mjs +86 -0
  143. package/test/markdown-diagrams.test.js +93 -0
  144. package/test/markdown-html.test.js +105 -0
  145. package/test/mathml.test.js +92 -0
  146. package/test/menu-transfer.test.js +57 -0
  147. package/test/office-preview.test.js +154 -0
  148. package/test/pele-parity.test.js +28 -0
  149. package/test/places.test.js +134 -0
  150. package/test/preview-keys.test.js +60 -0
  151. package/test/preview-language.test.js +22 -0
  152. package/test/status-filename.test.js +20 -0
  153. package/test/transfer-replace-merge.test.js +187 -0
  154. package/test/trash-address.test.js +23 -0
  155. package/test/trash-view.test.js +37 -0
  156. package/test/trashbrowse.test.js +135 -0
  157. package/ui/AboutFacts.qml +78 -0
  158. package/ui/ArchiveCards.qml +95 -0
  159. package/ui/ArchiveCompressDialog.qml +378 -0
  160. package/ui/ArchiveHereConfirm.qml +127 -0
  161. package/ui/ArchivePasswordDialog.qml +257 -0
  162. package/ui/Backend.qml +775 -0
  163. package/ui/BulkOldNames.qml +47 -0
  164. package/ui/BulkRenameDialog.qml +380 -0
  165. package/ui/CardFrame.qml +67 -0
  166. package/ui/CardScroll.qml +79 -0
  167. package/ui/CheckBox.qml +73 -0
  168. package/ui/ChromeBar.qml +360 -0
  169. package/ui/ChromeButton.qml +131 -0
  170. package/ui/ChromeModes.qml +43 -0
  171. package/ui/CodeLines.qml +256 -0
  172. package/ui/CodeRefusal.qml +52 -0
  173. package/ui/CollideConfirm.qml +147 -0
  174. package/ui/CollideHost.qml +167 -0
  175. package/ui/ColumnPane.qml +525 -0
  176. package/ui/ColumnRow.qml +380 -0
  177. package/ui/ColumnsArea.qml +594 -0
  178. package/ui/ColumnsLanding.qml +147 -0
  179. package/ui/CompareView.qml +321 -0
  180. package/ui/ContextMenu.qml +743 -0
  181. package/ui/ConvertDialog.qml +456 -0
  182. package/ui/CountedWorker.qml +7 -0
  183. package/ui/Crumb.qml +64 -0
  184. package/ui/CrumbDrop.qml +25 -0
  185. package/ui/CsvTable.qml +394 -0
  186. package/ui/CursorMark.qml +41 -0
  187. package/ui/DefaultClaim.qml +130 -0
  188. package/ui/DeviceLock.qml +47 -0
  189. package/ui/DeviceMounts.qml +540 -0
  190. package/ui/DialogButton.qml +150 -0
  191. package/ui/DialogField.qml +147 -0
  192. package/ui/DialogHeader.qml +31 -0
  193. package/ui/DialogScrim.qml +20 -0
  194. package/ui/DialogTitle.qml +37 -0
  195. package/ui/Divider.qml +9 -0
  196. package/ui/DropInto.qml +126 -0
  197. package/ui/DropTarget.qml +35 -0
  198. package/ui/DropboxMark.qml +60 -0
  199. package/ui/EmptyState.qml +150 -0
  200. package/ui/EscapeHint.qml +11 -0
  201. package/ui/FactsTable.qml +54 -0
  202. package/ui/FastScrollHandler.qml +476 -0
  203. package/ui/FatNamesConfirm.qml +157 -0
  204. package/ui/Favourites.qml +143 -0
  205. package/ui/FigureService.qml +120 -0
  206. package/ui/FileDrag.qml +267 -0
  207. package/ui/FilterStrip.qml +133 -0
  208. package/ui/FitMetrics.qml +22 -0
  209. package/ui/FlashDriveRows.qml +137 -0
  210. package/ui/FlashHost.qml +72 -0
  211. package/ui/FlashImageRow.qml +104 -0
  212. package/ui/FlashRunner.qml +119 -0
  213. package/ui/FlashStepRow.qml +172 -0
  214. package/ui/FlashStrip.qml +38 -0
  215. package/ui/FlashView.qml +397 -0
  216. package/ui/FleaMark.qml +88 -0
  217. package/ui/FlyoutFade.qml +23 -0
  218. package/ui/FontPane.qml +170 -0
  219. package/ui/Glass.qml +114 -0
  220. package/ui/GlassTagger.qml +59 -0
  221. package/ui/Glyph.qml +64 -0
  222. package/ui/GlyphSolid.qml +22 -0
  223. package/ui/GridArea.qml +664 -0
  224. package/ui/GridTile.qml +325 -0
  225. package/ui/GvfsBridge.qml +164 -0
  226. package/ui/Header.qml +433 -0
  227. package/ui/HoverTimers.qml +39 -0
  228. package/ui/HtmlClip.qml +30 -0
  229. package/ui/HtmlPane.qml +63 -0
  230. package/ui/HyprlandMark.qml +45 -0
  231. package/ui/Ipc.qml +1005 -0
  232. package/ui/IpcControls.qml +7 -0
  233. package/ui/IpcMenuState.qml +48 -0
  234. package/ui/IpcPreviewState.qml +185 -0
  235. package/ui/JumpPath.qml +51 -0
  236. package/ui/KeymapSheet.qml +561 -0
  237. package/ui/KeymapSheetResult.qml +89 -0
  238. package/ui/KindStack.qml +40 -0
  239. package/ui/LangMark.qml +51 -0
  240. package/ui/LangMarkLoader.qml +26 -0
  241. package/ui/List.qml +480 -0
  242. package/ui/LoadingState.qml +54 -0
  243. package/ui/LocalSend.qml +56 -0
  244. package/ui/LocalSendMark.qml +73 -0
  245. package/ui/MarkdownBlockView.qml +311 -0
  246. package/ui/MarkdownBlocks.qml +35 -0
  247. package/ui/MarkdownFigure.qml +254 -0
  248. package/ui/MarkdownImages.qml +72 -0
  249. package/ui/MarkdownList.qml +81 -0
  250. package/ui/MarkdownMathsText.qml +97 -0
  251. package/ui/MarkdownPane.qml +83 -0
  252. package/ui/MarkdownPictures.qml +41 -0
  253. package/ui/MarkdownQuote.qml +52 -0
  254. package/ui/MarkdownRemote.qml +82 -0
  255. package/ui/MarkdownSource.qml +155 -0
  256. package/ui/MarkdownTable.qml +222 -0
  257. package/ui/MarkdownTableScroll.qml +64 -0
  258. package/ui/MarkdownTableWheel.qml +126 -0
  259. package/ui/MarkdownText.qml +87 -0
  260. package/ui/MarkdownWorker.js +132 -0
  261. package/ui/MatchText.qml +99 -0
  262. package/ui/MediaSession.qml +38 -0
  263. package/ui/MediaSound.qml +26 -0
  264. package/ui/MediaStrip.qml +191 -0
  265. package/ui/MediaTrack.qml +36 -0
  266. package/ui/MediaVolume.qml +54 -0
  267. package/ui/MenuActionDialog.qml +396 -0
  268. package/ui/MenuEdgeFade.qml +24 -0
  269. package/ui/MenuHost.qml +227 -0
  270. package/ui/MenuRow.qml +351 -0
  271. package/ui/MountListing.qml +100 -0
  272. package/ui/NetworkDialog.qml +435 -0
  273. package/ui/NetworkForm.qml +426 -0
  274. package/ui/NetworkMounts.qml +913 -0
  275. package/ui/NetworkPlaces.qml +127 -0
  276. package/ui/OpenWithDialog.qml +613 -0
  277. package/ui/Opener.qml +95 -0
  278. package/ui/Pane.qml +1320 -0
  279. package/ui/PaneMenuActions.qml +527 -0
  280. package/ui/PanePath.qml +90 -0
  281. package/ui/PaneRail.qml +268 -0
  282. package/ui/PaneStates.qml +88 -0
  283. package/ui/PaneSwap.qml +339 -0
  284. package/ui/PaneWire.qml +601 -0
  285. package/ui/ParkedWait.qml +9 -0
  286. package/ui/PathEntry.qml +186 -0
  287. package/ui/PathJump.qml +400 -0
  288. package/ui/PdfViewer.qml +297 -0
  289. package/ui/PermissionsDialog.qml +658 -0
  290. package/ui/PermissionsHeadings.qml +31 -0
  291. package/ui/PhoneMounts.qml +67 -0
  292. package/ui/PickerAnswers.qml +75 -0
  293. package/ui/PickerBox.qml +26 -0
  294. package/ui/PickerChrome.qml +295 -0
  295. package/ui/PickerFilter.qml +97 -0
  296. package/ui/PickerGrid.qml +308 -0
  297. package/ui/PickerHeader.qml +28 -0
  298. package/ui/PickerLifecycle.qml +27 -0
  299. package/ui/PickerList.qml +218 -0
  300. package/ui/PickerListing.qml +149 -0
  301. package/ui/PickerPathBar.qml +127 -0
  302. package/ui/PickerPlaces.qml +189 -0
  303. package/ui/PickerRecent.qml +92 -0
  304. package/ui/PickerSave.qml +101 -0
  305. package/ui/PickerSelection.qml +155 -0
  306. package/ui/PickerWindow.qml +846 -0
  307. package/ui/PowerSectorsReader.qml +10 -0
  308. package/ui/PrebuildDriver.qml +146 -0
  309. package/ui/Preview.qml +1005 -0
  310. package/ui/PreviewAnimation.qml +24 -0
  311. package/ui/PreviewArchive.qml +91 -0
  312. package/ui/PreviewColumn.qml +553 -0
  313. package/ui/PreviewEdge.qml +63 -0
  314. package/ui/PreviewFacts.qml +30 -0
  315. package/ui/PreviewFactsLine.qml +34 -0
  316. package/ui/PreviewHtml.qml +233 -0
  317. package/ui/PreviewImage.qml +253 -0
  318. package/ui/PreviewImagePan.qml +13 -0
  319. package/ui/PreviewImageSharp.qml +32 -0
  320. package/ui/PreviewLines.qml +106 -0
  321. package/ui/PreviewMarkdown.qml +706 -0
  322. package/ui/PreviewMedia.qml +250 -0
  323. package/ui/PreviewPdf.qml +482 -0
  324. package/ui/PreviewScroll.qml +201 -0
  325. package/ui/PreviewSwap.qml +335 -0
  326. package/ui/PreviewText.qml +149 -0
  327. package/ui/ProtocolChip.qml +18 -0
  328. package/ui/QuickLookArchive.qml +41 -0
  329. package/ui/QuickLookBar.qml +103 -0
  330. package/ui/QuickLookPrepare.qml +355 -0
  331. package/ui/QuickLookSwap.qml +10 -0
  332. package/ui/QuickLookWarm.qml +65 -0
  333. package/ui/RailDrop.qml +155 -0
  334. package/ui/RailHeading.qml +19 -0
  335. package/ui/RailPlaces.qml +38 -0
  336. package/ui/RailTrashRow.qml +67 -0
  337. package/ui/RenameField.qml +353 -0
  338. package/ui/RendererRetry.qml +11 -0
  339. package/ui/ResizeHandle.qml +52 -0
  340. package/ui/Reveal.qml +54 -0
  341. package/ui/Row.qml +476 -0
  342. package/ui/RowDrag.qml +213 -0
  343. package/ui/RowDropTarget.qml +27 -0
  344. package/ui/RunCard.qml +286 -0
  345. package/ui/Scripts.qml +65 -0
  346. package/ui/SearchStrip.qml +232 -0
  347. package/ui/SelectionBand.qml +140 -0
  348. package/ui/SelectionPreview.qml +370 -0
  349. package/ui/SettingsFavourite.qml +122 -0
  350. package/ui/SettingsGroup.qml +96 -0
  351. package/ui/SettingsHint.qml +12 -0
  352. package/ui/SettingsMetrics.qml +29 -0
  353. package/ui/SettingsPane.qml +178 -0
  354. package/ui/SettingsPanel.qml +547 -0
  355. package/ui/SettingsRail.qml +128 -0
  356. package/ui/SettingsRow.qml +409 -0
  357. package/ui/SettingsRuler.qml +85 -0
  358. package/ui/SettingsSegment.qml +85 -0
  359. package/ui/ShareBrowser.qml +113 -0
  360. package/ui/ShareLink.qml +67 -0
  361. package/ui/ShelfPins.qml +101 -0
  362. package/ui/Sidebar.qml +728 -0
  363. package/ui/SidebarRow.qml +358 -0
  364. package/ui/Spinner.qml +60 -0
  365. package/ui/StableModel.qml +89 -0
  366. package/ui/StateMessage.qml +84 -0
  367. package/ui/StatusBar.qml +512 -0
  368. package/ui/StatusFileName.qml +27 -0
  369. package/ui/TabBar.qml +673 -0
  370. package/ui/TabDragGeometry.qml +60 -0
  371. package/ui/TagMenuMark.qml +52 -0
  372. package/ui/TagRailSection.qml +53 -0
  373. package/ui/TagSlot.qml +40 -0
  374. package/ui/TagState.qml +150 -0
  375. package/ui/TagSwatch.qml +28 -0
  376. package/ui/Taildrop.qml +86 -0
  377. package/ui/TailscaleMark.qml +50 -0
  378. package/ui/TerminalService.qml +69 -0
  379. package/ui/TerminalTab.qml +48 -0
  380. package/ui/TerminalTabs.qml +27 -0
  381. package/ui/Theme.qml +460 -0
  382. package/ui/ThemeColors.qml +43 -0
  383. package/ui/TransferCard.qml +239 -0
  384. package/ui/TrashConfirm.qml +129 -0
  385. package/ui/TrashDropHandler.qml +11 -0
  386. package/ui/TrashHost.qml +202 -0
  387. package/ui/TrashMonitor.qml +30 -0
  388. package/ui/TrashToolbar.qml +19 -0
  389. package/ui/TrashView.qml +492 -0
  390. package/ui/UnlockDialog.qml +450 -0
  391. package/ui/UnlockHost.qml +206 -0
  392. package/ui/UpdateCheck.qml +97 -0
  393. package/ui/UserDirs.qml +25 -0
  394. package/ui/ViewState.qml +565 -0
  395. package/ui/ViewportScrollBar.qml +195 -0
  396. package/ui/ViewportScrollBars.qml +25 -0
  397. package/ui/WebPreview.qml +33 -0
  398. package/ui/WebPreviewView.qml +26 -0
  399. package/ui/WindowBody.qml +829 -0
  400. package/ui/WindowDrag.qml +35 -0
  401. package/ui/boot/Commons/AudioNodes.qml +50 -0
  402. package/ui/boot/Commons/AudioNodesModel.js +96 -0
  403. package/ui/boot/Commons/Border.qml +243 -0
  404. package/ui/boot/Commons/BorderGeometry.js +373 -0
  405. package/ui/boot/Commons/Color.qml +258 -0
  406. package/ui/boot/Commons/I18n.qml +118 -0
  407. package/ui/boot/Commons/InputDevices.qml +112 -0
  408. package/ui/boot/Commons/InputMethodState.qml +38 -0
  409. package/ui/boot/Commons/IpcRegistry.qml +77 -0
  410. package/ui/boot/Commons/Numbers.js +19 -0
  411. package/ui/boot/Commons/Plural.js +91 -0
  412. package/ui/boot/Commons/ShellIpc.qml +12 -0
  413. package/ui/boot/Commons/Style.qml +561 -0
  414. package/ui/boot/Commons/Translations.qml +100 -0
  415. package/ui/boot/Commons/UntypedInput.qml +152 -0
  416. package/ui/boot/Commons/Util.qml +188 -0
  417. package/ui/boot/Commons/qmldir +13 -0
  418. package/ui/boot/LICENSE-omarchy +20 -0
  419. package/ui/boot/Ui/BackgroundMedia.qml +49 -0
  420. package/ui/boot/Ui/BarIconButton.qml +106 -0
  421. package/ui/boot/Ui/BarIconContent.qml +38 -0
  422. package/ui/boot/Ui/BarIndicator.qml +53 -0
  423. package/ui/boot/Ui/BarWidget.qml +50 -0
  424. package/ui/boot/Ui/BorderOverlay.qml +54 -0
  425. package/ui/boot/Ui/BorderSurface.qml +40 -0
  426. package/ui/boot/Ui/Button.qml +224 -0
  427. package/ui/boot/Ui/ButtonGroup.qml +131 -0
  428. package/ui/boot/Ui/ConfirmDialog.qml +134 -0
  429. package/ui/boot/Ui/CursorSurface.qml +42 -0
  430. package/ui/boot/Ui/Dropdown.qml +245 -0
  431. package/ui/boot/Ui/FilterInput.qml +26 -0
  432. package/ui/boot/Ui/IconContentBounds.qml +141 -0
  433. package/ui/boot/Ui/KeyboardPanel.qml +453 -0
  434. package/ui/boot/Ui/MultiSelect.qml +624 -0
  435. package/ui/boot/Ui/NumberField.qml +86 -0
  436. package/ui/boot/Ui/OpticalGlyph.qml +68 -0
  437. package/ui/boot/Ui/OverlayWindow.qml +102 -0
  438. package/ui/boot/Ui/Panel.qml +60 -0
  439. package/ui/boot/Ui/PanelActionButton.qml +105 -0
  440. package/ui/boot/Ui/PanelController.qml +16 -0
  441. package/ui/boot/Ui/PanelHero.qml +112 -0
  442. package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
  443. package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
  444. package/ui/boot/Ui/PanelSeparator.qml +19 -0
  445. package/ui/boot/Ui/PanelSlider.qml +156 -0
  446. package/ui/boot/Ui/PanelToolTip.qml +50 -0
  447. package/ui/boot/Ui/PluginBarApi.qml +89 -0
  448. package/ui/boot/Ui/PointerMoveGate.qml +54 -0
  449. package/ui/boot/Ui/PopupCard.qml +181 -0
  450. package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
  451. package/ui/boot/Ui/SearchableDropdown.qml +354 -0
  452. package/ui/boot/Ui/SizedImage.qml +125 -0
  453. package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
  454. package/ui/boot/Ui/TextField.qml +58 -0
  455. package/ui/boot/Ui/Toggle.qml +118 -0
  456. package/ui/boot/Ui/ToggleSwitch.qml +115 -0
  457. package/ui/boot/Ui/WidgetButton.qml +172 -0
  458. package/ui/boot/Ui/qmldir +41 -0
  459. package/ui/boot/fleatab.qml +50 -0
  460. package/ui/boot/picker.qml +39 -0
  461. package/ui/boot/shell.qml +131 -0
  462. package/ui/boot/tabtearoff.qml +51 -0
  463. package/ui/js/Anchor.js +379 -0
  464. package/ui/js/AnchorHold.js +162 -0
  465. package/ui/js/Archive.js +157 -0
  466. package/ui/js/ArchiveCard.js +44 -0
  467. package/ui/js/ArchiveOps.js +44 -0
  468. package/ui/js/BackReturn.js +118 -0
  469. package/ui/js/BulkRename.js +100 -0
  470. package/ui/js/Buttons.js +54 -0
  471. package/ui/js/ChromeKeys.js +116 -0
  472. package/ui/js/ClipMarks.js +73 -0
  473. package/ui/js/ClipSelection.js +67 -0
  474. package/ui/js/Clipboard.js +179 -0
  475. package/ui/js/Cloud.js +97 -0
  476. package/ui/js/CodeRuns.js +72 -0
  477. package/ui/js/Collide.js +212 -0
  478. package/ui/js/ColumnFit.js +28 -0
  479. package/ui/js/ColumnMenu.js +111 -0
  480. package/ui/js/Columns.js +298 -0
  481. package/ui/js/ColumnsCarry.js +103 -0
  482. package/ui/js/Compare.js +74 -0
  483. package/ui/js/Contrast.js +157 -0
  484. package/ui/js/ControlReader.js +42 -0
  485. package/ui/js/Convert.js +52 -0
  486. package/ui/js/CopyAs.js +52 -0
  487. package/ui/js/Crumbs.js +178 -0
  488. package/ui/js/Csv.js +231 -0
  489. package/ui/js/Density.js +44 -0
  490. package/ui/js/Devices.js +440 -0
  491. package/ui/js/DirSizes.js +180 -0
  492. package/ui/js/Drag.js +360 -0
  493. package/ui/js/DragOut.js +166 -0
  494. package/ui/js/DropDev.js +52 -0
  495. package/ui/js/Dropbox.js +41 -0
  496. package/ui/js/Eject.js +217 -0
  497. package/ui/js/Errors.js +188 -0
  498. package/ui/js/ExtThumbs.js +115 -0
  499. package/ui/js/Facts.js +402 -0
  500. package/ui/js/FigureDate.js +14 -0
  501. package/ui/js/Filter.js +285 -0
  502. package/ui/js/Find.js +46 -0
  503. package/ui/js/Flash.js +184 -0
  504. package/ui/js/FlashCard.js +79 -0
  505. package/ui/js/FlashEndings.js +125 -0
  506. package/ui/js/FlashPane.js +116 -0
  507. package/ui/js/FleaBin.js +12 -0
  508. package/ui/js/Focus.js +504 -0
  509. package/ui/js/FolderSorts.js +108 -0
  510. package/ui/js/FolderViews.js +44 -0
  511. package/ui/js/Format.js +314 -0
  512. package/ui/js/Fuzzy.js +141 -0
  513. package/ui/js/GlassTag.js +72 -0
  514. package/ui/js/Grid.js +33 -0
  515. package/ui/js/GridGeometry.js +90 -0
  516. package/ui/js/GridNames.js +153 -0
  517. package/ui/js/GvfsBridge.js +200 -0
  518. package/ui/js/HostClipboard.js +29 -0
  519. package/ui/js/Icons.js +218 -0
  520. package/ui/js/Input.js +50 -0
  521. package/ui/js/Jump.js +200 -0
  522. package/ui/js/KeyName.js +64 -0
  523. package/ui/js/Keymap.js +401 -0
  524. package/ui/js/Kinds.js +164 -0
  525. package/ui/js/LICENSES/ISC-lucide.txt +43 -0
  526. package/ui/js/LangMarkPaths.js +47 -0
  527. package/ui/js/LangMarks.js +120 -0
  528. package/ui/js/Lists.js +11 -0
  529. package/ui/js/LocalSend.js +39 -0
  530. package/ui/js/LockedMenu.js +63 -0
  531. package/ui/js/MakeDefault.js +186 -0
  532. package/ui/js/MarkBox.js +7 -0
  533. package/ui/js/Markdown.js +119 -0
  534. package/ui/js/MarkdownLists.js +86 -0
  535. package/ui/js/MarkdownMaths.js +58 -0
  536. package/ui/js/MarkdownPictures.js +32 -0
  537. package/ui/js/MarkdownPrepared.js +139 -0
  538. package/ui/js/MarkdownTableFit.js +34 -0
  539. package/ui/js/Marks.js +148 -0
  540. package/ui/js/Marquee.js +96 -0
  541. package/ui/js/Match.js +93 -0
  542. package/ui/js/Material.js +109 -0
  543. package/ui/js/MdBlocks.js +437 -0
  544. package/ui/js/MdBreak.js +71 -0
  545. package/ui/js/MdChunks.js +70 -0
  546. package/ui/js/MdContainer.js +137 -0
  547. package/ui/js/MdDocument.js +337 -0
  548. package/ui/js/MdEmph.js +97 -0
  549. package/ui/js/MdEntity.js +108 -0
  550. package/ui/js/MdEntityTable.js +223 -0
  551. package/ui/js/MdEscape.js +59 -0
  552. package/ui/js/MdFront.js +37 -0
  553. package/ui/js/MdHold.js +32 -0
  554. package/ui/js/MdHtml.js +307 -0
  555. package/ui/js/MdHtmlBlock.js +290 -0
  556. package/ui/js/MdHtmlHeading.js +53 -0
  557. package/ui/js/MdHtmlImage.js +179 -0
  558. package/ui/js/MdInline.js +154 -0
  559. package/ui/js/MdItems.js +212 -0
  560. package/ui/js/MdLeaf.js +309 -0
  561. package/ui/js/MdLink.js +196 -0
  562. package/ui/js/MdMath.js +67 -0
  563. package/ui/js/MdParagraphs.js +138 -0
  564. package/ui/js/MdRefs.js +232 -0
  565. package/ui/js/MdResolve.js +226 -0
  566. package/ui/js/MdRun.js +300 -0
  567. package/ui/js/MdUrl.js +226 -0
  568. package/ui/js/Menu.js +514 -0
  569. package/ui/js/MenuContext.js +85 -0
  570. package/ui/js/MenuFit.js +13 -0
  571. package/ui/js/MenuRefresh.js +74 -0
  572. package/ui/js/MenuWheel.js +35 -0
  573. package/ui/js/Messages.js +186 -0
  574. package/ui/js/Motion.js +16 -0
  575. package/ui/js/Mounts.js +320 -0
  576. package/ui/js/MouseNav.js +13 -0
  577. package/ui/js/Nav.js +329 -0
  578. package/ui/js/NetFs.js +26 -0
  579. package/ui/js/OpenWith.js +83 -0
  580. package/ui/js/Ops.js +585 -0
  581. package/ui/js/Palette.js +33 -0
  582. package/ui/js/PathBar.js +260 -0
  583. package/ui/js/Permissions.js +179 -0
  584. package/ui/js/Phones.js +112 -0
  585. package/ui/js/Picker.js +353 -0
  586. package/ui/js/PlaceMenu.js +96 -0
  587. package/ui/js/Places.js +265 -0
  588. package/ui/js/PreviewAsk.js +21 -0
  589. package/ui/js/PreviewKeys.js +155 -0
  590. package/ui/js/PreviewSettle.js +29 -0
  591. package/ui/js/PreviewSwap.js +101 -0
  592. package/ui/js/PropertyFacts.js +144 -0
  593. package/ui/js/Protocols.js +296 -0
  594. package/ui/js/QuickLookKinds.js +27 -0
  595. package/ui/js/QuickLookRect.js +24 -0
  596. package/ui/js/RailDrop.js +51 -0
  597. package/ui/js/RailKeys.js +87 -0
  598. package/ui/js/RailMenu.js +133 -0
  599. package/ui/js/Recent.js +135 -0
  600. package/ui/js/RecentMode.js +156 -0
  601. package/ui/js/Reload.js +60 -0
  602. package/ui/js/Renderer.js +9 -0
  603. package/ui/js/RunFile.js +143 -0
  604. package/ui/js/Scripts.js +28 -0
  605. package/ui/js/Scroll.js +304 -0
  606. package/ui/js/ScrollOff.js +69 -0
  607. package/ui/js/Search.js +478 -0
  608. package/ui/js/Selection.js +97 -0
  609. package/ui/js/Settings.js +476 -0
  610. package/ui/js/SettingsAbout.js +97 -0
  611. package/ui/js/SettingsShelf.js +52 -0
  612. package/ui/js/SheetKeys.js +42 -0
  613. package/ui/js/SheetQuery.js +281 -0
  614. package/ui/js/ShelfPile.js +27 -0
  615. package/ui/js/SlowClick.js +53 -0
  616. package/ui/js/SlowOp.js +6 -0
  617. package/ui/js/Sort.js +134 -0
  618. package/ui/js/Startup.js +63 -0
  619. package/ui/js/StateWords.js +17 -0
  620. package/ui/js/Status.js +238 -0
  621. package/ui/js/Swap.js +166 -0
  622. package/ui/js/TabMove.js +39 -0
  623. package/ui/js/Tabs.js +751 -0
  624. package/ui/js/Tags.js +134 -0
  625. package/ui/js/Taildrop.js +128 -0
  626. package/ui/js/Tap.js +180 -0
  627. package/ui/js/TextSize.js +107 -0
  628. package/ui/js/Thumbs.js +158 -0
  629. package/ui/js/Transfer.js +172 -0
  630. package/ui/js/Trash.js +84 -0
  631. package/ui/js/TrashDates.js +47 -0
  632. package/ui/js/TrashDrop.js +64 -0
  633. package/ui/js/UiState.js +438 -0
  634. package/ui/js/Unlock.js +152 -0
  635. package/ui/js/Update.js +130 -0
  636. package/ui/js/UserDirs.js +56 -0
  637. package/ui/qmldir +230 -0
@@ -0,0 +1,138 @@
1
+ import { test, expect, afterAll } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, chmodSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { dirsize, dirsizeCancel, dirsizeInit, forgetSizes, keptSize } from "../src/dirsize.js";
6
+ import { duCommand } from "../src/find.js";
7
+ import { Listing } from "../src/listing.js";
8
+
9
+ // Folder sizes only when asked (src/dirsize.js): du streams each folder's size, the answers are
10
+ // kept by path and ride later rows as z, and a change forgets a folder and those above it only.
11
+ const root = mkdtempSync(join(tmpdir(), "flea-dirsize-"));
12
+ afterAll(() => { try { chmodSync(join(root, "w", "c", "locked"), 0o755); } catch {} rmSync(root, { recursive: true, force: true }); });
13
+ const work = join(root, "w");
14
+ mkdirSync(join(work, "a", "deep"), { recursive: true });
15
+ mkdirSync(join(work, "b"));
16
+ mkdirSync(join(work, "-dash"));
17
+ mkdirSync(join(work, "c", "locked"), { recursive: true });
18
+ writeFileSync(join(work, "a", "one"), Buffer.alloc(1000));
19
+ writeFileSync(join(work, "a", "deep", "two"), Buffer.alloc(2500));
20
+ writeFileSync(join(work, "b", "three"), Buffer.alloc(7));
21
+ writeFileSync(join(work, "-dash", "x"), Buffer.alloc(11));
22
+ writeFileSync(join(work, "c", "locked", "hid"), Buffer.alloc(5));
23
+ writeFileSync(join(work, "file.txt"), "not a folder");
24
+
25
+ let listing = Listing.scan(work, false);
26
+ listing.sort({ by: "name", desc: false, foldersFirst: true, groupByKind: false, hiddenLast: false });
27
+ const messages = [];
28
+ dirsizeInit((m) => messages.push(m), () => listing);
29
+ const until = async (done) => {
30
+ for (let i = 0; i < 400 && !done(); i++) await Bun.sleep(10);
31
+ if (!done()) throw Error("timed out: " + JSON.stringify(messages));
32
+ };
33
+ const rowOf = (name) => listing.indexOfName(name);
34
+ const sizes = () => Object.fromEntries(messages.filter((m) => m.t === "dirsized").map((m) => [listing.rows[m.row].name, [m.bytes, m.partial]]));
35
+
36
+ test("du is looked for in Find's order: bunmsh first", () => {
37
+ expect(duCommand().slice(-2)).toEqual(["builtin", "du"]);
38
+ });
39
+
40
+ test("nothing is measured until asked; one row asks for that folder alone", async () => {
41
+ expect(listing.window(0, 10).rows.some((row) => row.z !== undefined)).toBe(false);
42
+ dirsize({ rows: [rowOf("a"), rowOf("file.txt")] });
43
+ await until(() => messages.some((m) => m.t === "dirsizedone"));
44
+ expect(sizes()).toEqual({ a: [3500, false] });
45
+ expect(keptSize(join(work, "a"))).toEqual({ bytes: 3500, partial: false });
46
+ });
47
+
48
+ test("all measures every folder, a name with a leading dash included, and an unreadable one is a floor", async () => {
49
+ if (process.getuid?.() !== 0 && process.platform !== "win32") chmodSync(join(work, "c", "locked"), 0);
50
+ messages.length = 0;
51
+ dirsize({ all: true });
52
+ await until(() => messages.some((m) => m.t === "dirsizedone"));
53
+ const got = sizes();
54
+ expect(got.a).toEqual([3500, false]);
55
+ expect(got.b).toEqual([7, false]);
56
+ expect(got["-dash"]).toEqual([11, false]);
57
+ if (process.getuid?.() !== 0 && process.platform !== "win32") expect(got.c[1]).toBe(true);
58
+ expect(got["file.txt"]).toBeUndefined();
59
+ chmodSync(join(work, "c", "locked"), 0o755);
60
+ });
61
+
62
+ test("kept sizes ride later rows, follow a sort, and a change forgets only its own line upward", () => {
63
+ listing = Listing.scan(work, false);
64
+ listing.sort({ by: "name", desc: true, foldersFirst: true, groupByKind: false, hiddenLast: false });
65
+ const rows = Object.fromEntries(listing.window(0, 10).rows.map((row) => [row.n, row]));
66
+ expect(rows.a.z).toBe(3500);
67
+ expect(rows.b.z).toBe(7);
68
+ expect(rows["file.txt"].z).toBeUndefined();
69
+ // A write inside a/deep: a (and everything above) is no longer known, b still is.
70
+ forgetSizes(join(work, "a", "deep"));
71
+ expect(keptSize(join(work, "a"))).toBeUndefined();
72
+ expect(keptSize(join(work, "b"))).toEqual({ bytes: 7, partial: false });
73
+ forgetSizes(join(work, "b", "three-sibling-dir-does-not-exist"));
74
+ expect(keptSize(join(work, "b"))).toBeUndefined();
75
+ expect(keptSize(join(work, "-dash"))).toEqual({ bytes: 11, partial: false });
76
+ });
77
+
78
+ test("a cancel stops the run and says so", async () => {
79
+ messages.length = 0;
80
+ dirsize({ all: true });
81
+ dirsizeCancel();
82
+ await until(() => messages.some((m) => m.t === "dirsizedone"));
83
+ expect(messages.find((m) => m.t === "dirsizedone").cancelled).toBe(true);
84
+ });
85
+
86
+ // The cell side (ui/js/DirSizes.js): ⚖️ until measured, … while a walk is out, then the size.
87
+ async function loadJs(name, names, deps = {}) {
88
+ const s = (await Bun.file(new URL("../ui/js/" + name + ".js", import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm, "");
89
+ return new Function(...Object.keys(deps), s + ";return {" + names + "};")(...Object.values(deps));
90
+ }
91
+ const Format = { size: (n) => `${n} B` };
92
+ const D = await loadJs("DirSizes", "empty,cellFor,known,folderText,asking,askingAll,remember,settled,UNMEASURED,MEASURING", { Format });
93
+
94
+ test("a folder's Size cell reads ⚖️, then … while asked, then its size; a row's z stands in after a sort", () => {
95
+ let state = D.empty();
96
+ const row = { d: true };
97
+ expect(D.folderText(D.cellFor(state, 4), row)).toBe("⚖️");
98
+ state = D.asking(state, [4]);
99
+ expect(D.folderText(D.cellFor(state, 4), row)).toBe("…");
100
+ expect(D.folderText(D.cellFor(state, 5), row)).toBe("⚖️");
101
+ state = D.remember(state, 4, 2048, false, 100);
102
+ expect(D.folderText(D.cellFor(state, 4), row)).toBe("\u2696\ufe0f 2048 B");
103
+ expect(D.folderText(D.cellFor(D.empty(), 4), { d: true, z: 9, zp: true })).toBe("\u2696\ufe0f >9 B");
104
+ });
105
+
106
+ test("the header's ⚖️ makes every folder wait until its answer or the run's end", () => {
107
+ let state = D.askingAll(D.empty());
108
+ expect(D.cellFor(state, 900)).toEqual({ waiting: true });
109
+ state = D.remember(state, 3, 10, false, 100);
110
+ expect(D.cellFor(state, 3)).toEqual({ bytes: 10, partial: false });
111
+ expect(D.cellFor(state, 7)).toEqual({ waiting: true });
112
+ state = D.settled(state);
113
+ expect(D.cellFor(state, 7)).toBe(null);
114
+ expect(D.cellFor(state, 3)).toEqual({ bytes: 10, partial: false });
115
+ });
116
+
117
+ test("a size order uses measured folders' sizes and puts unmeasured folders after them, measuring nothing", () => {
118
+ // a and b were forgotten above; -dash (11) and c (a floor) are still measured.
119
+ const fresh = Listing.scan(work, false);
120
+ fresh.sort({ by: "name", desc: false, foldersFirst: true, groupByKind: false, hiddenLast: false });
121
+ fresh.sort({ by: "size", desc: true, foldersFirst: true, groupByKind: false, hiddenLast: false });
122
+ expect(fresh.rows.filter((r) => r.dir).map((r) => r.name)).toEqual(["-dash", "c", "a", "b"]);
123
+ expect(messages.filter((m) => m.t === "dirsized").length).toBe(0);
124
+ });
125
+
126
+ test("= measures the selection or the cursor's folder, Alt+= every folder", async () => {
127
+ // Every Qt.Key_* the table names gets a distinct number; the modifiers are Qt's own.
128
+ let next = 1000;
129
+ const named = { AltModifier: 0x08000000, ControlModifier: 0x04000000, ShiftModifier: 0x02000000, MetaModifier: 0x10000000, Key_Equal: 0x3d };
130
+ const Qt = new Proxy(named, { get: (t, k) => (k in t ? t[k] : (t[k] = next++)) });
131
+ const Keymap = await loadJs("Keymap", "lookup", { Qt });
132
+ expect(Keymap.lookup(0x3d, "=", 0)).toBe("measureDirs");
133
+ expect(Keymap.lookup(0x3d, "=", named.AltModifier)).toBe("measureAllDirs");
134
+ // Ctrl+= stays the text size.
135
+ expect(Keymap.lookup(0x3d, "=", named.ControlModifier)).toBe("textSizeUp");
136
+ const focus = await Bun.file(new URL("../ui/js/Focus.js", import.meta.url)).text();
137
+ expect(focus).toContain('case "measureDirs": root.measureDirs(root.selectionCount() > 0 ? root.selectedIndices() : [root.cursorIndex])');
138
+ });
@@ -0,0 +1,307 @@
1
+ import { test, expect, afterAll } from 'bun:test';
2
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync, lstatSync, readlinkSync, symlinkSync, statSync, chmodSync, existsSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { mkdir, newfile, duplicate } from '../src/create.js';
6
+ import { bulkrename } from '../src/bulkrename.js';
7
+ import { permissions, permissionsBatch, parseMode } from '../src/permissions.js';
8
+ import { archive, archiveCancel, formats, freeName } from '../src/archiveops.js';
9
+ import { listArchive } from '../src/archive.js';
10
+ import { openSync, closeSync } from 'node:fs';
11
+
12
+ const root = mkdtempSync(join(tmpdir(), 'flea-ops-'));
13
+ afterAll(() => rmSync(root, { recursive: true, force: true }));
14
+ let n = 0;
15
+ const fresh = () => { const dir = join(root, `case-${++n}`); mkdirSync(dir); return dir; };
16
+ // PRoot resolves links itself, so an exclusive create there follows a dangling link; the kernel never does.
17
+ const exclusiveRefusesLinks = (() => {
18
+ const dir = mkdtempSync(join(tmpdir(), 'flea-excl-'));
19
+ try {
20
+ symlinkSync(join(dir, 'absent'), join(dir, 'link'));
21
+ try { closeSync(openSync(join(dir, 'link'), 'wx')); return false; } catch { return true; }
22
+ } finally { rmSync(dir, { recursive: true, force: true }); }
23
+ })();
24
+ const umask = process.umask();
25
+ const collector = () => { const lines = []; const send = (line) => lines.push(line); return { lines, send }; };
26
+ // Resolves with the first line `pick` accepts, waiting for the asynchronous operations.
27
+ async function until(lines, pick, ms = 10000) {
28
+ const end = Date.now() + ms;
29
+ for (;;) {
30
+ const found = lines.find(pick);
31
+ if (found) return found;
32
+ if (Date.now() > end) throw Error(`no line arrived: ${JSON.stringify(lines)}`);
33
+ await Bun.sleep(10);
34
+ }
35
+ }
36
+
37
+ test('mkdir takes the first free New Folder and refuses a typed name already taken', () => {
38
+ const dir = fresh(), { lines, send } = collector();
39
+ mkdir({ path: dir }, send);
40
+ mkdir({ path: dir }, send);
41
+ expect(lines.filter((l) => l.t === 'made').map((l) => l.path)).toEqual([join(dir, 'New Folder'), join(dir, 'New Folder 2')]);
42
+ writeFileSync(join(dir, '報告'), '');
43
+ mkdir({ path: dir, name: '報告' }, send);
44
+ expect(lines.at(-1)).toMatchObject({ t: 'error', where: 'mkdir', msg: 'a folder or file with that name already exists' });
45
+ mkdir({ path: dir, name: '../x' }, send);
46
+ expect(lines.at(-1)).toMatchObject({ t: 'error', where: 'mkdir' });
47
+ mkdir({ path: 'relative' }, send);
48
+ expect(lines.at(-1).t).toBe('error');
49
+ });
50
+
51
+ test('mkdir with a request id adds the creation receipt', () => {
52
+ const dir = fresh(), { lines, send } = collector();
53
+ mkdir({ path: dir, name: 'a', request: 4 }, send);
54
+ expect(lines[0]).toMatchObject({ t: 'made', ok: true, request: 4 });
55
+ expect(lines[1]).toEqual({ t: 'menuaction', op: 'creation', id: 4, kind: 'made', ok: true, path: join(dir, 'a') });
56
+ });
57
+
58
+ test('newfile creates an empty file exclusively and never follows a dangling link', () => {
59
+ const dir = fresh(), { lines, send } = collector();
60
+ newfile({ id: 3, path: dir, name: 'New File 🙂' }, send);
61
+ expect(lines[0]).toEqual({ t: 'menuaction', id: 3, op: 'newFile', ok: true, path: join(dir, 'New File 🙂') });
62
+ expect(statSync(join(dir, 'New File 🙂')).size).toBe(0);
63
+ newfile({ id: 3, path: dir, name: 'New File 🙂' }, send);
64
+ expect(lines.at(-1)).toMatchObject({ op: 'newFile', ok: false });
65
+ if (exclusiveRefusesLinks) {
66
+ symlinkSync(join(dir, 'absent'), join(dir, 'link'));
67
+ newfile({ id: 3, path: dir, name: 'link' }, send);
68
+ expect(lines.at(-1).ok).toBe(false);
69
+ expect(existsSync(join(dir, 'absent'))).toBe(false);
70
+ }
71
+ newfile({ id: 3, path: dir, name: 'a/b' }, send);
72
+ expect(lines.at(-1).ok).toBe(false);
73
+ });
74
+
75
+ test('duplicate copies a file and a tree to free copy names', async () => {
76
+ const dir = fresh(), { lines, send } = collector();
77
+ writeFileSync(join(dir, 'backup.tar.zst'), 'x');
78
+ mkdirSync(join(dir, 'tree/inner'), { recursive: true });
79
+ writeFileSync(join(dir, 'tree/inner/f'), 'deep');
80
+ duplicate({ path: join(dir, 'backup.tar.zst') }, send);
81
+ duplicate({ path: join(dir, 'backup.tar.zst') }, send);
82
+ duplicate({ path: join(dir, 'tree') }, send);
83
+ await until(lines, (l) => l.t === 'duplicated' && l.path.endsWith('tree copy'));
84
+ expect(lines.filter((l) => l.t === 'duplicated').map((l) => l.path)).toEqual(
85
+ [join(dir, 'backup.tar copy.zst'), join(dir, 'backup.tar copy 2.zst'), join(dir, 'tree copy')]);
86
+ expect(readFileSync(join(dir, 'tree copy/inner/f'), 'utf8')).toBe('deep');
87
+ });
88
+
89
+ test('bulkrename swaps names through temporary names and refuses a bad batch whole', () => {
90
+ const dir = fresh(), { lines, send } = collector();
91
+ writeFileSync(join(dir, 'a'), 'A');
92
+ writeFileSync(join(dir, 'b'), 'B');
93
+ writeFileSync(join(dir, 'c'), 'C');
94
+ bulkrename({ paths: [join(dir, 'a'), join(dir, 'b'), join(dir, 'c')], names: ['b', 'a', 'c'] }, send);
95
+ expect(lines[0]).toEqual({ t: 'bulkrenamed', ok: 2, unchanged: 1 });
96
+ expect(readFileSync(join(dir, 'a'), 'utf8')).toBe('B');
97
+ expect(readFileSync(join(dir, 'b'), 'utf8')).toBe('A');
98
+ for (const names of [['x', 'x', 'y'], ['c', 'q'], ['ok', 'a/b', 'z'], ['', 'm', 'n']]) {
99
+ bulkrename({ paths: [join(dir, 'a'), join(dir, 'b'), join(dir, 'c')].slice(0, names.length), names }, send);
100
+ expect(lines.at(-1)).toMatchObject({ t: 'error', where: 'bulkrename' });
101
+ }
102
+ expect(readdirSync(dir).sort()).toEqual(['a', 'b', 'c']);
103
+ });
104
+
105
+ test('permissions inspects, applies and refuses a replaced item', () => {
106
+ if (process.platform === 'win32') return;
107
+ const dir = fresh(), path = join(dir, 'f'), { lines, send } = collector();
108
+ writeFileSync(path, '');
109
+ chmodSync(path, 0o644);
110
+ permissions({ op: 'inspect', id: 1, path }, send);
111
+ expect(lines[0]).toMatchObject({ t: 'permissions', id: 1, op: 'inspect', ok: true, mode: '0644', directory: false, reason: '' });
112
+ permissions({ op: 'apply', id: 1, mode: '600' }, send);
113
+ expect(lines.at(-1)).toEqual({ t: 'permissions', id: 1, op: 'apply', ok: true, mode: '0600' });
114
+ expect(statSync(path).mode & 0o777).toBe(0o600);
115
+ permissions({ op: 'apply', id: 1, mode: '9' }, send);
116
+ expect(lines.at(-1).error).toMatch(/octal/);
117
+ rmSync(path);
118
+ writeFileSync(path, '');
119
+ permissions({ op: 'apply', id: 1, mode: '644' }, send);
120
+ expect(lines.at(-1)).toMatchObject({ ok: false, error: 'Selected item changed; reopen Permissions.' });
121
+ expect(() => parseMode('1644')).toThrow();
122
+ expect(parseMode('0755')).toBe(0o755);
123
+ });
124
+
125
+ test('permissionsBatch changes every item or none, and u+x adds the owner execute bit', () => {
126
+ if (process.platform === 'win32') return;
127
+ const dir = fresh(), a = join(dir, 'a'), b = join(dir, 'b'), { lines, send } = collector();
128
+ writeFileSync(a, '');
129
+ writeFileSync(b, '');
130
+ chmodSync(a, 0o644);
131
+ chmodSync(b, 0o640);
132
+ permissionsBatch({ id: 7, paths: [a, b], modes: ['u+x', 'u+x'] }, send);
133
+ expect(lines.at(-1)).toMatchObject({ op: 'applyMany', ok: true });
134
+ expect([statSync(a).mode & 0o777, statSync(b).mode & 0o777]).toEqual([0o744, 0o740]);
135
+ permissionsBatch({ id: 8, paths: [a, join(dir, 'gone')], modes: ['600', '600'] }, send);
136
+ expect(lines.at(-1)).toMatchObject({ op: 'applyMany', ok: false });
137
+ expect(statSync(a).mode & 0o777).toBe(0o744);
138
+ });
139
+
140
+ test('formats names what this port writes and reads, and no password', () => {
141
+ const { lines, send } = collector();
142
+ formats({ id: 2 }, send);
143
+ expect(lines[0]).toMatchObject({ t: 'formats', archive: ['zip', 'tar', 'tar.gz', 'tar.zst'], convert: false,
144
+ extract: { archive: true, sevenZip: false, zip: true } });
145
+ });
146
+
147
+ test('free archive names keep the format tail', () => {
148
+ expect(freeName('/a/pack.tar.gz', 'tar.gz', 1)).toBe('/a/pack copy.tar.gz');
149
+ expect(freeName('/a/pack.tar.gz', 'tar.gz', 2)).toBe('/a/pack copy 2.tar.gz');
150
+ expect(freeName('/a/folder', null, 1)).toBe('/a/folder copy');
151
+ });
152
+
153
+ // A tree with Chinese and emoji names, a long path, a symlink, an empty folder and a non-default mode.
154
+ function sampleTree(dir) {
155
+ const top = join(dir, '專案 📦');
156
+ const long = join(top, 'd'.repeat(60), 'e'.repeat(60));
157
+ mkdirSync(long, { recursive: true });
158
+ mkdirSync(join(top, 'empty'));
159
+ writeFileSync(join(top, '說明.txt'), '你好 👋\n');
160
+ writeFileSync(join(long, 'f'.repeat(90) + '.bin'), Buffer.alloc(300000, 7));
161
+ writeFileSync(join(top, 'run.sh'), '#!/bin/sh\n');
162
+ chmodSync(join(top, 'run.sh'), 0o755);
163
+ symlinkSync('說明.txt', join(top, 'readme'));
164
+ return top;
165
+ }
166
+ // Extracted modes pass through the umask, as bsdtar's do for anyone but root.
167
+ function expectSameTree(a, b) {
168
+ const walk = (base, rel = '', mask = 0) => readdirSync(join(base, rel)).sort().flatMap((name) => {
169
+ const path = join(base, rel, name), st = lstatSync(path), key = join(rel, name);
170
+ if (st.isSymbolicLink()) return [[key, 'link', readlinkSync(path)]];
171
+ if (st.isDirectory()) return [[key, 'dir'], ...walk(base, key, mask)];
172
+ return [[key, (st.mode & 0o777 & ~mask).toString(8), readFileSync(path).toString('base64')]];
173
+ });
174
+ expect(walk(b)).toEqual(walk(a, '', umask));
175
+ }
176
+
177
+ for (const format of ['zip', 'tar', 'tar.gz', 'tar.zst']) {
178
+ test(`compress to .${format} and extract it back to the same tree`, async () => {
179
+ const dir = fresh(), top = sampleTree(dir), { lines, send } = collector();
180
+ archive({ op: 'compress', paths: [top], dest: join(dir, `專案 📦.${format}`), format }, send);
181
+ const done = await until(lines, (l) => l.t === 'archivedone');
182
+ expect(done).toMatchObject({ ok: true, err: '', path: join(dir, `專案 📦.${format}`) });
183
+ // The preview's own reader lists what was written.
184
+ const names = [];
185
+ await listArchive(done.path, done.path, (entry) => names.push(entry.name));
186
+ expect(names).toContain('專案 📦/說明.txt');
187
+ expect(names.some((name) => name.endsWith('f'.repeat(90) + '.bin'))).toBe(true);
188
+ const out = join(dir, 'out');
189
+ archive({ op: 'extract', path: done.path, dest: out }, send);
190
+ const extracted = await until(lines, (l) => l.t === 'archivedone' && l.dest);
191
+ expect(extracted).toMatchObject({ ok: true, dest: out });
192
+ expect(lines.some((l) => l.t === 'extractstarted')).toBe(true);
193
+ expectSameTree(top, join(out, '專案 📦'));
194
+ // A second compress of the same name lands under its copy name.
195
+ archive({ op: 'compress', paths: [top], dest: join(dir, `專案 📦.${format}`), format }, send);
196
+ const again = await until(lines, (l) => l.t === 'archivedone' && l.path?.includes('copy'));
197
+ expect(again.path).toBe(join(dir, `專案 📦 copy.${format}`));
198
+ expect(readdirSync(dir).filter((name) => name.startsWith('.flea-work'))).toEqual([]);
199
+ });
200
+ }
201
+
202
+ test('an archive that names a path outside its folder extracts nothing', async () => {
203
+ const dir = fresh(), { lines, send } = collector();
204
+ // A tar whose only member is ../escape.
205
+ const header = Buffer.alloc(512);
206
+ header.write('../escape', 0);
207
+ header.write('0000644\0', 100); header.write('0000000\0', 108); header.write('0000000\0', 116);
208
+ header.write('00000000003\0', 124); header.write('00000000000\0', 136);
209
+ header.fill(0x20, 148, 156); header[156] = 0x30; header.write('ustar\0', 257); header.write('00', 263);
210
+ let sum = 0; for (const byte of header) sum += byte;
211
+ header.write(sum.toString(8).padStart(6, '0') + '\0 ', 148);
212
+ const data = Buffer.alloc(512); data.write('bad');
213
+ writeFileSync(join(dir, 'evil.tar'), Buffer.concat([header, data, Buffer.alloc(1024)]));
214
+ archive({ op: 'extract', path: join(dir, 'evil.tar'), dest: join(dir, 'evil') }, send);
215
+ const done = await until(lines, (l) => l.t === 'archivedone');
216
+ expect(done.ok).toBe(false);
217
+ expect(done.err).toMatch(/outside its folder/);
218
+ expect(readdirSync(dir).sort()).toEqual(['evil.tar']);
219
+ expect(existsSync(join(root, 'escape'))).toBe(false);
220
+ const here = collector();
221
+ archive({ op: 'extract', path: join(dir, 'evil.tar'), dest: dir, here: true, confirmed: true }, here.send);
222
+ expect((await until(here.lines, l => l.t === 'archivedone')).ok).toBe(false);
223
+ expect(readdirSync(dir)).toEqual(['evil.tar']);
224
+ expect(existsSync(join(root, 'escape'))).toBe(false);
225
+ });
226
+
227
+ test('a password, a format it cannot read and a disk image are refused without starting', () => {
228
+ const { lines, send } = collector();
229
+ archive({ op: 'extract', path: '/x/a.zip', dest: '/x/a', password: 'p' }, send);
230
+ archive({ op: 'extract', path: '/x/a.7z', dest: '/x/a' }, send);
231
+ archive({ op: 'extract', path: '/x/a.iso', dest: '/x/a' }, send);
232
+ expect(lines.every((l) => l.t === 'archivedone' && l.ok === false)).toBe(true);
233
+ expect(lines.length).toBe(3);
234
+ });
235
+
236
+ test('cancelling a compress removes its staging file', async () => {
237
+ const dir = fresh(), { lines, send } = collector();
238
+ mkdirSync(join(dir, 'big'));
239
+ for (let i = 0; i < 20; i++) writeFileSync(join(dir, 'big', `f${i}`), Buffer.alloc(2 << 20, i));
240
+ archive({ op: 'compress', paths: [join(dir, 'big')], dest: join(dir, 'big.zip'), format: 'zip' }, send);
241
+ const started = await until(lines, (l) => l.t === 'archivestarted');
242
+ expect(archiveCancel(started.id)).toBe(true);
243
+ const done = await until(lines, (l) => l.t === 'archivedone');
244
+ expect(done).toMatchObject({ ok: false, err: 'cancelled' });
245
+ expect(readdirSync(dir)).toEqual(['big']);
246
+ });
247
+
248
+ for (const count of [5, 6]) test(`extract here ${count} items requires confirmation only above five`, async () => {
249
+ const dir = fresh(), input = join(dir, 'input'), dest = join(dir, 'destination');
250
+ mkdirSync(input); mkdirSync(dest);
251
+ const paths = Array.from({ length: count }, (_, i) => {
252
+ const path = join(input, `item${i}.txt`); writeFileSync(path, `content ${i}`); return path;
253
+ });
254
+ const pack = collector();
255
+ archive({ op: 'compress', paths, dest: join(dir, 'pack.zip'), format: 'zip' }, pack.send);
256
+ const packed = await until(pack.lines, l => l.t === 'archivedone');
257
+ expect(packed.ok).toBe(true);
258
+ writeFileSync(join(dest, 'item0.txt'), 'keep');
259
+ const run = async confirmed => {
260
+ const c = collector();
261
+ archive({ op: 'extract', path: packed.path, dest, here: true, confirmed }, c.send);
262
+ return await until(c.lines, l => l.t === 'archivedone');
263
+ };
264
+ const first = await run(false);
265
+ expect(first.ok).toBe(count <= 5);
266
+ if (count > 5) {
267
+ expect(first.err).toContain('confirmation');
268
+ expect(readdirSync(dest)).toEqual(['item0.txt']);
269
+ expect((await run(true)).ok).toBe(true);
270
+ }
271
+ expect(readFileSync(join(dest, 'item0.txt'), 'utf8')).toBe('keep');
272
+ expect(readdirSync(dest).length).toBe(count + 1);
273
+ expect(readdirSync(dest).some(n => n.startsWith('.flea-work'))).toBe(false);
274
+ expect(readdirSync(dest).filter(n => n !== 'item0.txt').map(n => readFileSync(join(dest, n), 'utf8')).sort()).toEqual(paths.map((_, i) => `content ${i}`).sort());
275
+ });
276
+
277
+ test('extract here never merges an existing folder; folder extraction keeps copy naming', async () => {
278
+ const dir = fresh(), input = join(dir, 'folder'), dest = join(dir, 'out');
279
+ mkdirSync(input); writeFileSync(join(input, 'new.txt'), 'new');
280
+ mkdirSync(dest); mkdirSync(join(dest, 'folder')); writeFileSync(join(dest, 'folder', 'old.txt'), 'old');
281
+ const c = collector();
282
+ archive({ op: 'compress', paths: [input], dest: join(dir, 'pack.tar'), format: 'tar' }, c.send);
283
+ const packed = await until(c.lines, l => l.t === 'archivedone');
284
+ for (const here of [true, false]) {
285
+ const result = collector();
286
+ archive({ op: 'extract', path: packed.path, dest, here, confirmed: true }, result.send);
287
+ expect((await until(result.lines, l => l.t === 'archivedone')).ok).toBe(true);
288
+ }
289
+ expect(readFileSync(join(dest, 'folder', 'old.txt'), 'utf8')).toBe('old');
290
+ expect(existsSync(join(dest, 'folder', 'new.txt'))).toBe(false);
291
+ expect(readFileSync(join(dest, 'folder copy', 'new.txt'), 'utf8')).toBe('new');
292
+ expect(readFileSync(join(dir, 'out copy', 'folder', 'new.txt'), 'utf8')).toBe('new');
293
+ });
294
+
295
+ for (const format of ['zip','tar']) test(`extract here counts top-level names, not nested members (${format})`, async () => {
296
+ const dir=fresh(), input=join(dir,'one-folder'), dest=join(dir,'destination');
297
+ mkdirSync(input);mkdirSync(dest);
298
+ for(let i=0;i<8;i++)writeFileSync(join(input,`item${i}.txt`),`content ${i}`);
299
+ const pack=collector();
300
+ archive({op:'compress',paths:[input],dest:join(dir,'pack.'+format),format},pack.send);
301
+ const packed=await until(pack.lines,l=>l.t==='archivedone');expect(packed.ok).toBe(true);
302
+ const result=collector();
303
+ archive({op:'extract',path:packed.path,dest,here:true,confirmed:false},result.send);
304
+ expect((await until(result.lines,l=>l.t==='archivedone')).ok).toBe(true);
305
+ expect(readdirSync(dest)).toEqual(['one-folder']);
306
+ expect(readdirSync(join(dest,'one-folder')).length).toBe(8);
307
+ });
@@ -0,0 +1,24 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {readFileSync} from 'node:fs';
3
+
4
+ function loadFunction(file,name,scope={}) {
5
+ const source=readFileSync(new URL(file,import.meta.url),'utf8');
6
+ const match=source.match(new RegExp('^( *)function '+name+'\\(path\\) \\{([\\s\\S]*?)^\\1\\}', 'm'));
7
+ if(!match)throw Error('Missing '+name);
8
+ return new Function(...Object.keys(scope),`return (${match[0].trim()})`)(...Object.values(scope));
9
+ }
10
+ const format=loadFunction('../ui/js/Format.js','fileUri');
11
+ const boot=loadFunction('../ui/boot/shell.qml','fileUrl');
12
+ const drag=loadFunction('../ui/js/Drag.js','uriFor',{Format:{fileUri:format}});
13
+ const cases=[
14
+ ['C:\\Users\\wxk\\AppData\\Local\\Temp\\flea\\ui\\boot/../WindowBody.qml','file:///C:/Users/wxk/AppData/Local/Temp/flea/ui/boot/../WindowBody.qml'],
15
+ ['C:/Users/a b/中文#?%.qml','file:///C:/Users/a%20b/%E4%B8%AD%E6%96%87%23%3F%25.qml'],
16
+ ['\\\\server\\share\\a b.png','file://server/share/a%20b.png'],
17
+ ['/home/a b/#?%.png','file:///home/a%20b/%23%3F%25.png'],
18
+ ['/home/a\\b.png','file:///home/a%5Cb.png'],
19
+ ];
20
+ for(const [name,convert] of [['boot',boot],['preview',format],['drag',drag]]) {
21
+ test(name+' file URLs preserve Windows drives, UNC and POSIX path bytes',()=>{
22
+ for(const [path,url] of cases){expect(convert(path)).toBe(url);expect(new URL(convert(path)).protocol).toBe('file:');}
23
+ });
24
+ }
@@ -0,0 +1,161 @@
1
+ import { test, expect, afterAll } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { grepCommand } from "../src/find.js";
6
+ import { matchOf, MATCH_TEXT_LIMIT } from "../src/backend.js";
7
+ import { Listing } from "../src/listing.js";
8
+
9
+ // Ctrl+G's content search: ui/js/Find.js builds grep's arguments and reads its lines, ui/js/Search.js
10
+ // opens and turns the mode, and the backend carries each row's matched line as `g`.
11
+ async function load(name, names, deps = {}) {
12
+ const s = (await Bun.file(new URL("../ui/js/" + name + ".js", import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm, "");
13
+ return new Function(...Object.keys(deps), s + ";return {" + names + "};")(...Object.values(deps));
14
+ }
15
+ const Find = await load("Find", "grepArguments,grepLine,snippet,CONTEXT,SNIPPET_LIMIT");
16
+ const Qt = { Key_Return: 16777220, Key_Enter: 16777221, Key_Escape: 16777216, Key_Backspace: 16777219,
17
+ Key_Tab: 16777217, Key_Backtab: 16777218, Key_G: 71, ControlModifier: 0x04000000, AltModifier: 0x08000000, MetaModifier: 0x10000000 };
18
+ const Search = await load("Search", "start,contents,commit,rescope,abort,typeKey,placeholder,sendQuery,TYPING,RESULTS",
19
+ { Qt, Input: { isPasteKey: () => false }, Format: {} });
20
+
21
+ const root = mkdtempSync(join(tmpdir(), "flea-grep-"));
22
+ afterAll(() => rmSync(root, { recursive: true, force: true }));
23
+
24
+ test("grep runs over the scope with the query as literal text, hidden names skipped unless shown", () => {
25
+ const off = Find.grepArguments("a.b*(c)", false);
26
+ expect(off).toEqual(["-r", "-n", "-o", "-a", "-i", "-m", "1", "-Z", "--line-buffered", "-E",
27
+ "--exclude=.*", "--exclude-dir=.*", "-e", ".{0,80}a\\.b\\*\\(c\\).{0,80}"]);
28
+ // No path operand: grep walks "." and the bare "." is never excluded.
29
+ expect(off.some((arg) => arg === "." || arg === "./")).toBe(false);
30
+ expect(Find.grepArguments("x", true)).not.toContain("--exclude=.*");
31
+ });
32
+
33
+ test("a grep line parses into its file, line number and a clean bounded snippet", () => {
34
+ expect(Find.grepLine("dir/a: b.txt\u000012: let x\t= 1")).toEqual({ path: "dir/a: b.txt", line: 12, text: "let x = 1" });
35
+ // A binary's control bytes become dots; the text is cut at SNIPPET_LIMIT.
36
+ expect(Find.grepLine("bin.dat\u00003:ab\u0000\u0001cd").text).toBe("ab··cd");
37
+ expect(Find.grepLine("long\u00001:" + "y".repeat(5000)).text.length).toBe(Find.SNIPPET_LIMIT);
38
+ for (const junk of ["", "no separator", "\u00001:starts with nul", "a\u0000not a number"]) expect(Find.grepLine(junk)).toBe(null);
39
+ });
40
+
41
+ for (const [label, argv] of [["bunmsh's grep", grepCommand()], ["the system grep", Bun.which("grep") ? [Bun.which("grep")] : null]]) {
42
+ test(`${label} answers the arguments: binary files match, hidden ones follow the setting, long lines stay short`, async () => {
43
+ if (!argv) return;
44
+ const scope = mkdtempSync(join(root, "scope-"));
45
+ mkdirSync(join(scope, "sub"));
46
+ mkdirSync(join(scope, ".hid"));
47
+ writeFileSync(join(scope, "sub", "notes:1.txt"), "first\nSecond Needle here\nneedle again\n");
48
+ writeFileSync(join(scope, "image.bin"), Buffer.from([0, 1, 2, ...Buffer.from("xxNEEDLExx"), 0, 255]));
49
+ writeFileSync(join(scope, "min.js"), "a".repeat(200000) + "needle" + "b".repeat(200000));
50
+ writeFileSync(join(scope, ".hidden.txt"), "needle\n");
51
+ writeFileSync(join(scope, ".hid", "in.txt"), "needle\n");
52
+ writeFileSync(join(scope, "none.txt"), "nothing\n");
53
+ const run = async (hidden) => {
54
+ const child = Bun.spawn([...argv, ...Find.grepArguments("needle", hidden)], { cwd: scope, stdout: "pipe", stderr: "pipe" });
55
+ const out = await new Response(child.stdout).text();
56
+ await child.exited;
57
+ const rows = new Map();
58
+ for (const line of out.split("\n")) {
59
+ if (line.length > 2 * Find.CONTEXT + "needle".length + 400) throw new Error(`an output line of ${line.length} characters`);
60
+ const hit = Find.grepLine(line);
61
+ if (hit && !rows.has(hit.path)) rows.set(hit.path, hit);
62
+ }
63
+ return rows;
64
+ };
65
+ const shown = await run(false);
66
+ expect([...shown.keys()].sort()).toEqual(["image.bin", "min.js", "sub/notes:1.txt"]);
67
+ expect(shown.get("sub/notes:1.txt")).toMatchObject({ line: 2, text: "Second Needle here" });
68
+ expect(shown.get("image.bin").text).toContain("NEEDLE");
69
+ expect(shown.get("min.js").text).toBe("a".repeat(Find.CONTEXT) + "needle" + "b".repeat(Find.CONTEXT));
70
+ expect([...(await run(true)).keys()].sort()).toEqual([".hid/in.txt", ".hidden.txt", "image.bin", "min.js", "sub/notes:1.txt"]);
71
+ }, 30000);
72
+ }
73
+
74
+ test("the backend keeps a checked, bounded match and hands it out as the row's g", () => {
75
+ expect(matchOf({ l: 4, t: "x" })).toEqual({ l: 4, t: "x" });
76
+ expect(matchOf({ l: 2, t: "z".repeat(MATCH_TEXT_LIMIT + 50) }).t.length).toBe(MATCH_TEXT_LIMIT);
77
+ for (const bad of [null, "x", { l: 0, t: "x" }, { l: 1.5, t: "x" }, { l: 3 }]) expect(matchOf(bad)).toBe(null);
78
+ const dir = mkdtempSync(join(root, "rows-"));
79
+ writeFileSync(join(dir, "a.txt"), "x");
80
+ writeFileSync(join(dir, "b.txt"), "x");
81
+ const listing = new Listing(dir, [{ name: "a.txt", dir: false, link: false, match: { l: 3, t: "hit" } }, { name: "b.txt", dir: false, link: false }]);
82
+ const { rows } = listing.window(0, 2);
83
+ expect(rows[0].g).toEqual({ l: 3, t: "hit" });
84
+ expect(rows[1].g).toBeUndefined();
85
+ });
86
+
87
+ // A pane as Search.js reads it, recording what it asks the backend.
88
+ function pane(extra = {}) {
89
+ const calls = [];
90
+ const root = {
91
+ calls, recentMode: "", searchMode: "", searchQuery: "", searchSent: "", searchGen: 0, searchStep: 0,
92
+ path: "/work", home: "/home/u", showHidden: false, windowSize: 40, sizeWish: false, cursorIndex: 0,
93
+ rowFor: () => null, join: (a, b) => a + "/" + b,
94
+ backend: {
95
+ searchOpen: (scope, folder, hidden) => calls.push(["open", scope]),
96
+ searchQuery: (query, gen, count, sizes, contents) => calls.push(["query", query, contents]),
97
+ searchCancel: () => calls.push(["cancel"]), searchClose: () => calls.push(["close"]),
98
+ },
99
+ openWithoutHistory: (path) => calls.push(["relist", path]),
100
+ ...extra,
101
+ };
102
+ return root;
103
+ }
104
+ const key = (code, modifiers = 0) => ({ key: code, modifiers, text: "" });
105
+
106
+ test("Ctrl+G opens a content search that waits for Enter, as f opens a name search", () => {
107
+ const root = pane();
108
+ Search.contents(root);
109
+ expect(root.searchMode).toBe(Search.TYPING);
110
+ expect(root.searchContents).toBe(true);
111
+ expect(root.calls).toEqual([["open", "/work"]]);
112
+ expect(Search.placeholder(root.path, root.home, root.searchStep, true)).toBe("grep -ri ");
113
+ root.searchQuery = "needle";
114
+ Search.typeKey(key(Qt.Key_Return), root);
115
+ expect(root.calls.at(-1)).toEqual(["query", "needle", true]);
116
+ expect(root.searchMode).toBe(Search.RESULTS);
117
+ // f starts a name search afresh.
118
+ const named = pane();
119
+ Search.start(named);
120
+ expect(named.searchContents).toBe(false);
121
+ expect(Search.placeholder(named.path, named.home, 0, false)).toBe("find . -iname ");
122
+ });
123
+
124
+ test("Ctrl+G inside Find only turns the mode; the query runs at the next Enter", () => {
125
+ const root = pane();
126
+ Search.start(root);
127
+ root.searchQuery = "draft";
128
+ Search.typeKey(key(Qt.Key_G, Qt.ControlModifier), root);
129
+ expect(root.searchContents).toBe(true);
130
+ expect(root.calls.filter((c) => c[0] === "query")).toEqual([]);
131
+ Search.typeKey(key(Qt.Key_Return), root);
132
+ expect(root.calls.at(-1)).toEqual(["query", "draft", true]);
133
+ expect(root.searchMode).toBe(Search.RESULTS);
134
+ // From the results (a query already run) it still waits, back on the query line.
135
+ const sent = root.calls.length;
136
+ Search.contents(root);
137
+ expect(root.searchContents).toBe(false);
138
+ expect(root.searchMode).toBe(Search.TYPING);
139
+ expect(root.calls.length).toBe(sent);
140
+ root.searchEnterAt = 0;
141
+ Search.typeKey(key(Qt.Key_Return), root);
142
+ expect(root.calls.at(-1)).toEqual(["query", "draft", false]);
143
+ });
144
+
145
+ test("Tab, Enter and Esc keep their meanings in a content search", () => {
146
+ const root = pane();
147
+ Search.contents(root);
148
+ root.searchQuery = "needle";
149
+ Search.typeKey(key(Qt.Key_Return), root);
150
+ // Tab turns the scope and reruns, still over contents.
151
+ Search.typeKey(key(Qt.Key_Tab), root);
152
+ expect(root.calls.slice(-2)).toEqual([["open", "/home/u"], ["query", "needle", true]]);
153
+ // Esc while the walk runs stops it and keeps the results; the next Esc leaves.
154
+ root.searchRunning = true;
155
+ Search.typeKey(key(Qt.Key_Escape), root);
156
+ expect(root.calls.at(-1)).toEqual(["cancel"]);
157
+ expect(root.searchMode).not.toBe("");
158
+ root.searchRunning = false;
159
+ Search.typeKey(key(Qt.Key_Escape), root);
160
+ expect(root.calls.slice(-2)).toEqual([["close"], ["relist", "/work"]]);
161
+ });