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,669 @@
1
+ // Copying and moving files: the `collisions` question, `transfer` and `transfercancel`
2
+ // (docs/protocol.md). Upstream: src/backend/transfer*.rs and ops.rs.
3
+ //
4
+ // Ported: copy and move of files, folders and symlinks, each target created exclusively; a
5
+ // cross-drive move copies and then removes the source; per-drive queueing; progress; cancel,
6
+ // which removes what the item in flight had written; the collide words keep, skip and refuse;
7
+ // fsync onto FAT-family drives; `replace`, which moves the item already there to flea's Trash
8
+ // first and puts it back when the transfer fails; `merge` with `mergecollisions`' second question.
9
+ // Not yet: undo's journal, `fatnames`, and fifos, sockets and device nodes, which are refused
10
+ // rather than opened.
11
+ import {trashRecord,forgetTrashRecord} from './trash-record.js';
12
+ import { trashOne } from "./trash.js";
13
+ import { constants, promises as fsp, lstatSync, statSync, accessSync, readdirSync, readlinkSync, realpathSync, renameSync, unlinkSync } from "node:fs";
14
+ import { basename, dirname, join, relative, resolve, isAbsolute, sep } from "node:path";
15
+ import { fsTypeOf } from "./fsinfo.js";
16
+ import { describeKind } from "./kinds.js";
17
+ import { capturedPaths, captureSelection, selectionItemUnchanged } from "./menu.js";
18
+
19
+ const WORDS = new Set(["keep", "replace", "skip", "refuse", "merge"]);
20
+ // A merge's second answer, for names inside the folders it folds together: no merge of its own.
21
+ const INNER = new Set(["keep", "replace", "skip", "refuse"]);
22
+ const LINK_LIMIT = 40;
23
+ const FAT = new Set(["vfat", "exfat", "fat", "fat12", "fat16", "fat32", "msdos", "ntfs", "ntfs3"]);
24
+ const CHUNK = 1 << 20,
25
+ WINDOW = 150,
26
+ NAME_TRIES = 1000,
27
+ REFUSED_CAP = 100;
28
+
29
+ class Cancelled extends Error {}
30
+
31
+ // ---- sources and destinations ----
32
+
33
+ // The paths a request names: `paths` wins, else `rows` against the listing in force.
34
+ export function sources(request, listing, stamp, command) {
35
+ // A menu's Copy to and Move to run on the selection its snapshot captured.
36
+ // A menu id is a positive count; the keyboard path sends 0 for "not from a menu".
37
+ if (request.menuId && command !== "paths") {
38
+ const captured = capturedPaths(request.menuId);
39
+ if (!captured) return { stale: { t: "error", where: command, path: "", msg: "Selected items changed; reopen the menu." } };
40
+ return { paths: captured };
41
+ }
42
+ if (Array.isArray(request.paths) && request.paths.length) return { paths: request.paths.map(String) };
43
+ if (Array.isArray(request.rows)) {
44
+ if (request.listing !== undefined && request.listing !== stamp)
45
+ return { stale: { t: "error", where: "stale", path: command, msg: "the listing changed; read the rows again" } };
46
+ const paths = [];
47
+ for (const index of request.rows) {
48
+ const row = listing?.rows[index];
49
+ if (row) paths.push(row.trash?.file ?? join(listing.path || ".", row.name));
50
+ }
51
+ return { paths };
52
+ }
53
+ return { paths: [] };
54
+ }
55
+
56
+ function destProblem(dest) {
57
+ if (typeof dest !== "string" || !isAbsolute(dest)) return "the destination is not an absolute folder";
58
+ let st;
59
+ try {
60
+ st = statSync(dest);
61
+ } catch {
62
+ return "the destination folder does not exist";
63
+ }
64
+ if (!st.isDirectory()) return "the destination is not a folder";
65
+ try {
66
+ accessSync(dest, constants.W_OK);
67
+ } catch {
68
+ return "the destination folder cannot be written";
69
+ }
70
+ return "";
71
+ }
72
+
73
+ // What a name holds: device, inode and file type, and birth time where the filesystem keeps one.
74
+ const identity = (st) => `${st.dev}:${st.ino}:${st.mode & constants.S_IFMT}:${st.birthtimeMs || 0}`;
75
+ const lstatOrNull = (path) => {
76
+ try {
77
+ return lstatSync(path);
78
+ } catch {
79
+ return null;
80
+ }
81
+ };
82
+ const sameFolder = (path, dest) => resolve(dirname(path)) === resolve(dest);
83
+
84
+ // "backup.tar.zst" -> "backup.tar copy.zst", then "backup.tar copy 2.zst"; a dotfile keeps its whole name as the stem.
85
+ export function copyName(name, n) {
86
+ const suffix = n <= 1 ? " copy" : ` copy ${n}`,
87
+ dot = name.lastIndexOf(".");
88
+ return dot > 0 ? `${name.slice(0, dot)}${suffix}${name.slice(dot)}` : `${name}${suffix}`;
89
+ }
90
+
91
+ function freeCopyPath(dest, name) {
92
+ for (let n = 1; n <= NAME_TRIES; n++) {
93
+ const candidate = join(dest, copyName(name, n));
94
+ if (!lstatOrNull(candidate)) return candidate;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ // ---- the collisions question ----
100
+
101
+ // The latest question: its id, its destination and, per colliding source, what its name held.
102
+ let question = null;
103
+
104
+ export function collisions(request, send, listing, stamp) {
105
+ question = null;
106
+ const named = sources(request, listing, stamp, "collisions");
107
+ if (named.stale) return send(!request.menuId ? named.stale : { t: "collisions", id: request.id ?? 0, total: 0, names: [], mergeable: false });
108
+ const id = request.id ?? 0,
109
+ dest = request.dest,
110
+ held = new Map(),
111
+ names = [];
112
+ let total = 0,
113
+ mergeable = false;
114
+ if (!destProblem(dest)) {
115
+ for (const path of named.paths) {
116
+ if (!isAbsolute(path) || sameFolder(path, dest)) continue;
117
+ const source = lstatOrNull(path);
118
+ if (!source) continue;
119
+ const there = lstatOrNull(join(dest, basename(path)));
120
+ if (!there) continue;
121
+ total++;
122
+ held.set(path, identity(there));
123
+ // lstat on both sides: a link to a folder is replaced or kept, never merged through.
124
+ mergeable ||= source.isDirectory() && there.isDirectory();
125
+ if (names.length < 3) {
126
+ const name = basename(path),
127
+ dir = source.isDirectory();
128
+ names.push({ n: name, d: dir, i: describeKind(name, dir, source.mode, undefined).icon });
129
+ }
130
+ }
131
+ }
132
+ question = { id, dest: typeof dest === "string" ? resolve(dest) : "", held,
133
+ menuId: request.menuId, selection: !request.menuId ? null : captureSelection(request.menuId) };
134
+ send({ t: "collisions", id, total, names, mergeable });
135
+ }
136
+
137
+ const nameEntry = (name, st) => ({ n: name, d: st.isDirectory(), i: describeKind(name, st.isDirectory(), st.mode, undefined).icon });
138
+
139
+ // After the first card answered Merge: one read-only walk of each folder that merges, gathering
140
+ // every relative name both sides hold except folders that merge further down. It follows the
141
+ // `collisions` question just kept and folds only the names that question saw, still unchanged.
142
+ export function mergeCollisions(request, send, listing, stamp) {
143
+ const outer = question;
144
+ question = null;
145
+ const id = request.id ?? 0,
146
+ dest = request.dest,
147
+ reply = (total, names) => send({ t: "mergecollisions", id, total, names });
148
+ const matches = !!outer && typeof dest === "string" && outer.dest === resolve(dest) && outer.menuId === request.menuId;
149
+ const selection = !request.menuId ? null : matches ? outer.selection : captureSelection(request.menuId);
150
+ const named = request.menuId && selection ? { paths: selection.paths } : sources(request, listing, stamp, "mergecollisions");
151
+ if (named.stale) return request.menuId ? reply(0, []) : send(named.stale);
152
+ const held = matches ? outer.held : new Map(),
153
+ inner = new Map(),
154
+ names = [];
155
+ let total = 0;
156
+ const note = (rel, source, there, name) => {
157
+ total++;
158
+ inner.set(rel, identity(there));
159
+ if (names.length < 3) names.push(nameEntry(name, source));
160
+ };
161
+ const walk = (src, dst, rel) => {
162
+ let entries;
163
+ try {
164
+ entries = readdirSync(src);
165
+ } catch {
166
+ return;
167
+ }
168
+ for (const entry of entries) {
169
+ const source = lstatOrNull(join(src, entry)), there = source && lstatOrNull(join(dst, entry));
170
+ if (!there) continue;
171
+ if (source.isDirectory() && there.isDirectory()) walk(join(src, entry), join(dst, entry), join(rel, entry));
172
+ else note(join(rel, entry), source, there, entry);
173
+ }
174
+ };
175
+ if (!destProblem(dest)) {
176
+ for (const path of named.paths) {
177
+ if (!isAbsolute(path) || sameFolder(path, dest)) continue;
178
+ const source = lstatOrNull(path), name = basename(path), there = source && lstatOrNull(join(dest, name));
179
+ if (!there || held.get(path) !== identity(there)) continue;
180
+ if (source.isDirectory() && there.isDirectory()) walk(path, join(dest, name), name);
181
+ else note(name, source, there, name);
182
+ }
183
+ }
184
+ question = { id, dest: typeof dest === "string" ? resolve(dest) : "", held, inner,
185
+ menuId: request.menuId, selection: outer && matches ? outer.selection : selection };
186
+ reply(total, names);
187
+ }
188
+
189
+ // ---- replacing ----
190
+
191
+ // Why the item at `target` may not go to Trash for `src`, or "" when it may.
192
+ function replaceProblem(src, source, target, paths) {
193
+ const there = resolve(target);
194
+ for (const path of paths) {
195
+ const full = resolve(path);
196
+ if (full === there || full.startsWith(there + sep)) return "the item already there holds the one being moved in, so it was not replaced";
197
+ }
198
+ if (source.isSymbolicLink() && linkLeadsTo(src, target)) return "the incoming link points at the item already there, so it was not replaced";
199
+ return "";
200
+ }
201
+
202
+ // Whether the link at `src`, once it sits at `target`, would name `target` itself: now, or by its
203
+ // own text looked up from there through any other link on the way. Past LINK_LIMIT links it is let
204
+ // through like a dangling link, which the kernel refuses anyway.
205
+ function linkLeadsTo(src, target) {
206
+ const there = resolve(target);
207
+ try {
208
+ const now = statSync(src), held = statSync(there);
209
+ if (now.dev === held.dev && now.ino === held.ino) return true;
210
+ } catch {}
211
+ let text;
212
+ try {
213
+ text = readlinkSync(src);
214
+ } catch {
215
+ return false;
216
+ }
217
+ let at = resolve(dirname(there), text);
218
+ for (let hops = 0; hops < LINK_LIMIT; hops++) {
219
+ let parent = dirname(at);
220
+ try {
221
+ parent = realpathSync(parent);
222
+ } catch {}
223
+ at = join(parent, basename(at));
224
+ if (at === there) return true;
225
+ let st;
226
+ try {
227
+ st = lstatSync(at);
228
+ } catch {
229
+ return false;
230
+ }
231
+ if (!st.isSymbolicLink()) return false;
232
+ at = resolve(dirname(at), readlinkSync(at));
233
+ }
234
+ return false;
235
+ }
236
+
237
+ // Puts a replaced item back from the Trash; "" when it is back, else why it stays there.
238
+ function putBack(trashed, target) {
239
+ if (lstatOrNull(target)) return "a new item now has its name";
240
+ try {
241
+ renameSync(trashed.file, target);
242
+ } catch (e) {
243
+ return reason(e);
244
+ }
245
+ try {
246
+ unlinkSync(trashed.info);
247
+ } catch {}
248
+ return "";
249
+ }
250
+
251
+ const stillInTrash = (err, why) => `${err}; the item it replaced is still in the Trash (${why})`;
252
+
253
+ // ---- copying ----
254
+
255
+ export const reason = (e) =>
256
+ e instanceof Cancelled
257
+ ? "cancelled"
258
+ : ({
259
+ EACCES: "permission denied",
260
+ EPERM: "permission denied",
261
+ ENOSPC: "the drive is full",
262
+ EDQUOT: "the drive is full",
263
+ EEXIST: "an item with that name is already there",
264
+ ENOENT: "it no longer exists",
265
+ EROFS: "the drive is read-only",
266
+ EINVAL: "this drive does not allow that name",
267
+ EILSEQ: "this drive does not allow that name",
268
+ ENAMETOOLONG: "that name is too long for this drive",
269
+ }[e?.code] ?? String(e?.message ?? e));
270
+
271
+ // An entry the destination drive refuses for that entry alone: recorded, and the walk goes on.
272
+ function refusedEntry(e, kind, fat) {
273
+ const code = e?.code;
274
+ if (kind === "link" && (code === "EOPNOTSUPP" || code === "ENOSYS" || (code === "EPERM" && fat))) return "this drive cannot hold links";
275
+ if (code === "EINVAL" || code === "EILSEQ") return "this drive does not allow that name";
276
+ if (code === "ENAMETOOLONG") return "that name is too long for this drive";
277
+ return "";
278
+ }
279
+
280
+ class Job {
281
+ constructor(id, send) {
282
+ this.id = id;
283
+ this.send = send;
284
+ this.cancelled = false;
285
+ this.lastWindow = 0;
286
+ }
287
+ check() {
288
+ if (this.cancelled) throw new Cancelled();
289
+ }
290
+ progress(index, name, bytes, total, last = false) {
291
+ const now = performance.now();
292
+ if (!last && now - this.lastWindow < WINDOW) return;
293
+ this.lastWindow = now;
294
+ this.send({ t: "transferprogress", id: this.id, index, name, bytes, total });
295
+ }
296
+ }
297
+
298
+ // One item and everything under it, into `dst`, which must not exist yet.
299
+ async function copyAny(src, dst, ctx, top = false) {
300
+ ctx.job.check();
301
+ const st = await fsp.lstat(src);
302
+ if (st.isSymbolicLink()) {
303
+ const text = await fsp.readlink(src);
304
+ // The type says whether the link names a folder; Windows needs it, every other system ignores it.
305
+ let type = "file";
306
+ try { if (statSync(resolve(dirname(src), text)).isDirectory()) type = "dir"; } catch {}
307
+ return guarded(() => fsp.symlink(text, dst, type), dst, "link", ctx, top);
308
+ }
309
+ if (st.isDirectory()) {
310
+ const made = await guarded(() => fsp.mkdir(dst, { mode: st.mode & 0o7777 | 0o700 }), dst, "dir", ctx, top);
311
+ if (made === false) return;
312
+ ctx.made.add(dst);
313
+ for (const entry of await fsp.readdir(src)) await copyAny(join(src, entry), join(dst, entry), ctx);
314
+ await fsp.chmod(dst, st.mode & 0o7777).catch(() => {});
315
+ await fsp.utimes(dst, st.atime, st.mtime).catch(() => {});
316
+ if (ctx.durable) await fsyncPath(dst, ctx);
317
+ return;
318
+ }
319
+ if (!st.isFile()) {
320
+ // A fifo, socket or device node has no contents to stream, and opening one could wait forever.
321
+ const err = "special files are not copied yet";
322
+ if (top) throw Object.assign(Error(err), { sentence: err });
323
+ if (ctx.refused.length < REFUSED_CAP) ctx.refused.push({ name: relative(ctx.root, dst), err });
324
+ ctx.refusedCount++;
325
+ return;
326
+ }
327
+ const out = await guarded(() => fsp.open(dst, "wx", st.mode & 0o7777), dst, "file", ctx, top);
328
+ if (out === false) return;
329
+ ctx.made.add(dst);
330
+ const input = await fsp.open(src, "r");
331
+ try {
332
+ const buffer = Buffer.allocUnsafe(CHUNK);
333
+ let done = 0;
334
+ if (top) ctx.job.progress(ctx.index, ctx.name, 0, st.size);
335
+ for (;;) {
336
+ ctx.job.check();
337
+ const { bytesRead } = await input.read(buffer, 0, CHUNK, null);
338
+ if (!bytesRead) break;
339
+ let written = 0;
340
+ while (written < bytesRead) written += (await out.write(buffer, written, bytesRead - written)).bytesWritten;
341
+ done += bytesRead;
342
+ if (top) ctx.job.progress(ctx.index, ctx.name, done, st.size);
343
+ }
344
+ if (ctx.durable) await out.sync();
345
+ if (top) ctx.job.progress(ctx.index, ctx.name, done, st.size, true);
346
+ } finally {
347
+ await input.close();
348
+ await out.close();
349
+ }
350
+ await fsp.utimes(dst, st.atime, st.mtime).catch(() => {});
351
+ }
352
+
353
+ // Runs one create; an entry the drive refuses inside a tree is recorded and answers false.
354
+ async function guarded(create, dst, kind, ctx, top) {
355
+ try {
356
+ return await create();
357
+ } catch (e) {
358
+ const refused = top ? "" : refusedEntry(e, kind, ctx.fat);
359
+ if (!refused) throw e;
360
+ if (ctx.refused.length < REFUSED_CAP) ctx.refused.push({ name: relative(ctx.root, dst), err: refused });
361
+ ctx.refusedCount++;
362
+ return false;
363
+ }
364
+ }
365
+
366
+ async function fsyncPath(path, ctx) {
367
+ let handle;
368
+ try {
369
+ handle = await fsp.open(path, "r");
370
+ await handle.sync();
371
+ } catch {
372
+ ctx.unconfirmed = true;
373
+ } finally {
374
+ await handle?.close().catch(() => {});
375
+ }
376
+ }
377
+
378
+ // Same drive: one rename. Another drive: a whole copy first, and only then the source goes.
379
+ async function move(src, dst, ctx) {
380
+ // rename(2) replaces a file silently, and so does Windows' MoveFileEx, so the name is checked first.
381
+ if (lstatOrNull(dst)) throw Object.assign(Error("exists"), { code: "EEXIST" });
382
+ const refusedBefore = ctx.refusedCount;
383
+ try {
384
+ await fsp.rename(src, dst);
385
+ if (ctx.durable) {
386
+ await fsyncPath(dirname(dst), ctx);
387
+ await fsyncPath(dirname(src), ctx);
388
+ }
389
+ return;
390
+ } catch (e) {
391
+ if (e.code !== "EXDEV") throw e;
392
+ }
393
+ await copyAny(src, dst, ctx, true);
394
+ if (ctx.refusedCount > refusedBefore) return;
395
+ await fsp.rm(src, { recursive: true, force: false });
396
+ }
397
+
398
+ function refuseEntry(ctx, path, err) {
399
+ if (ctx.refused.length < REFUSED_CAP) ctx.refused.push({ name: relative(ctx.root, path), err });
400
+ ctx.refusedCount++;
401
+ }
402
+
403
+ // Folds the folder `src` into the folder `dst`: what is missing lands, folders on both sides fold the
404
+ // same way down, and a name on both sides takes the inner answer when the walk saw it unchanged.
405
+ // A move moves each landed entry and removes each source folder it emptied.
406
+ async function mergeInto(src, dst, rel, ctx, m) {
407
+ ctx.job.check();
408
+ for (const entry of await fsp.readdir(src)) {
409
+ ctx.job.check();
410
+ const from = join(src, entry), at = join(dst, entry), key = join(rel, entry),
411
+ source = lstatOrNull(from);
412
+ if (!source) continue;
413
+ const there = lstatOrNull(at);
414
+ let target = at,
415
+ choice = "",
416
+ trashed = null;
417
+ if (there) {
418
+ if (source.isDirectory() && there.isDirectory()) {
419
+ await mergeInto(from, at, key, ctx, m);
420
+ continue;
421
+ }
422
+ choice = m.innerHeld.get(key) === identity(there) ? m.inner : "refuse";
423
+ if (choice === "skip") continue;
424
+ if (choice === "keep") {
425
+ target = freeCopyPath(dst, entry);
426
+ if (!target) {
427
+ refuseEntry(ctx, at, "every copy name for this item is already taken");
428
+ continue;
429
+ }
430
+ } else if (choice === "replace") {
431
+ const problem = replaceProblem(from, source, at, m.paths);
432
+ if (problem) {
433
+ refuseEntry(ctx, at, problem);
434
+ continue;
435
+ }
436
+ try {
437
+ trashed = trashOne(at);
438
+ } catch {
439
+ refuseEntry(ctx, at, "the item already there could not be moved to Trash, so nothing was replaced");
440
+ continue;
441
+ }
442
+ } else {
443
+ refuseEntry(ctx, at, "an item with that name is already there");
444
+ continue;
445
+ }
446
+ }
447
+ try {
448
+ if (m.moving) await move(from, target, ctx);
449
+ else await copyAny(from, target, ctx, true);
450
+ } catch (e) {
451
+ // Only the entry in flight is taken back on a cancel; what already landed stays.
452
+ if ((e instanceof Cancelled || trashed) && !m.moving && ctx.made.has(target)) await fsp.rm(target, { recursive: true, force: true }).catch(() => {});
453
+ const why = trashed ? putBack(trashed, at) : "";
454
+ if (e instanceof Cancelled) throw why ? Object.assign(new Cancelled(), { sentence: stillInTrash("cancelled", why) }) : e;
455
+ refuseEntry(ctx, target, why ? stillInTrash(e?.sentence ?? reason(e), why) : e?.sentence ?? reason(e));
456
+ continue;
457
+ }
458
+ m.merged++;
459
+ if (choice === "keep") m.keptBoth++;
460
+ }
461
+ if (m.moving) await fsp.rmdir(src).catch(() => {});
462
+ if (ctx.durable) await fsyncPath(dst, ctx);
463
+ }
464
+
465
+ function treeSentence(ctx, verb) {
466
+ const first = ctx.refused[0];
467
+ if (ctx.refusedCount === 1) return `${first.name} could not be ${verb}: ${first.err}`;
468
+ return `${ctx.refusedCount} items inside could not be ${verb}; the first is ${first.name}: ${first.err}`;
469
+ }
470
+
471
+ // ---- the transfer ----
472
+
473
+ let nextId = 1;
474
+ // One id space for transfers and archive jobs, so a transfercancel names exactly one of them.
475
+ export const nextJobId = () => nextId++;
476
+ const jobs = new Map(),
477
+ // The tail of each destination drive's queue.
478
+ queues = new Map();
479
+
480
+ export function transfer(request, send, listing, stamp) {
481
+ // A Copy to dialog closes its menu before it submits the collision answer.
482
+ // Keep the captured identities with that one-shot question, not with the UI's lifetime.
483
+ const asked = question;
484
+ question = null;
485
+ const dest = request.dest, problem = destProblem(dest);
486
+ const matches = asked && asked.id === request.collideId && typeof dest === "string"
487
+ && asked.dest === resolve(dest) && asked.menuId === request.menuId;
488
+ const selection = !request.menuId ? null
489
+ : matches ? asked.selection : captureSelection(request.menuId);
490
+ const named = request.menuId && selection
491
+ ? { paths: selection.paths } : sources(request, listing, stamp, "transfer");
492
+ if (named.stale) return send(named.stale);
493
+ if (problem) return send({ t: "error", where: "transfer", path: String(dest ?? ""), msg: problem });
494
+ const collide = request.collide;
495
+ if (collide !== undefined && !WORDS.has(collide))
496
+ return send({ t: "error", where: "transfer", path: dest, msg: `no collision answer is named ${collide}` });
497
+ const inner = collide === "merge" ? request.inner ?? "refuse" : "refuse";
498
+ if (!INNER.has(inner))
499
+ return send({ t: "error", where: "transfer", path: dest, msg: `no collision answer inside a merge is named ${inner}` });
500
+ const moving = request.op === "move",
501
+ paths = named.paths,
502
+ id = nextId++,
503
+ job = new Job(id, send);
504
+ const answered = collide !== undefined && matches ? asked.held : new Map();
505
+ // Inner names are answered only by the merge walk this transfer names.
506
+ const innerHeld = collide === "merge" && matches && asked.inner ? asked.inner : new Map();
507
+ jobs.set(id, job);
508
+ send({ t: "transferstarted", id, n: paths.length, moving });
509
+ let drive;
510
+ try {
511
+ drive = String(statSync(dest, { bigint: true }).dev);
512
+ } catch {
513
+ drive = dest;
514
+ }
515
+ const run = () => runTransfer(job, { paths, dest, moving, collide, answered, selection, inner, innerHeld }).finally(() => jobs.delete(id));
516
+ const tail = (queues.get(drive) ?? Promise.resolve()).then(run, run);
517
+ queues.set(drive, tail);
518
+ tail.finally(() => queues.get(drive) === tail && queues.delete(drive));
519
+ }
520
+
521
+ async function runTransfer(job, { paths, dest, moving, collide, answered, selection, inner, innerHeld }) {
522
+ const send = job.send,
523
+ fstype = (() => {
524
+ try {
525
+ return fsTypeOf(statSync(dest, { bigint: true }).dev);
526
+ } catch {
527
+ return "";
528
+ }
529
+ })(),
530
+ durable = FAT.has(fstype);
531
+ let ok = 0,
532
+ failed = 0,
533
+ skipped = 0,
534
+ unconfirmed = false;
535
+ const retryPaths = [],
536
+ refused = [],
537
+ touched = new Set([dest]),
538
+ m = { moving, inner, innerHeld, paths, merged: 0, keptBoth: 0 },
539
+ folded = [];
540
+ for (let index = 0; index < paths.length; index++) {
541
+ const src = paths[index],
542
+ name = basename(src);
543
+ if (job.cancelled) {
544
+ skipped++;
545
+ continue;
546
+ }
547
+ const fail = (err) => {
548
+ failed++;
549
+ retryPaths.push(src);
550
+ send({ t: "transferitem", id: job.id, index, name, ok: false, err });
551
+ };
552
+ if (selection && !selectionItemUnchanged(selection, index)) {
553
+ fail("Selected items changed; reopen the menu.");
554
+ continue;
555
+ }
556
+ const source = isAbsolute(src) ? lstatOrNull(src) : null;
557
+ if (!source) {
558
+ fail(isAbsolute(src) ? "it no longer exists" : "the path is not absolute");
559
+ continue;
560
+ }
561
+ let target = join(dest, name),
562
+ trashed = null,
563
+ merging = false,
564
+ choice = "";
565
+ if (sameFolder(src, dest)) {
566
+ if (collide === undefined) {
567
+ fail("already in that folder");
568
+ continue;
569
+ }
570
+ if (moving) {
571
+ skipped++;
572
+ continue;
573
+ }
574
+ target = freeCopyPath(dest, name);
575
+ } else {
576
+ const there = lstatOrNull(target);
577
+ if (there) {
578
+ choice = answered.get(src) === identity(there) ? collide : "refuse";
579
+ if (choice === "merge") {
580
+ // A folder on a folder folds in; anything else on the first card takes the inner answer.
581
+ if (source.isDirectory() && there.isDirectory()) merging = true;
582
+ else choice = innerHeld.get(name) === identity(there) ? inner : "refuse";
583
+ }
584
+ if (merging) {
585
+ } else if (choice === "skip") {
586
+ skipped++;
587
+ continue;
588
+ } else if (choice === "keep") target = freeCopyPath(dest, name);
589
+ else if (choice === "replace") {
590
+ const problem = replaceProblem(src, source, target, paths);
591
+ if (problem) {
592
+ fail(problem);
593
+ continue;
594
+ }
595
+ try {
596
+ trashed = trashOne(target);
597
+ } catch {
598
+ fail("the item already there could not be moved to Trash, so nothing was replaced");
599
+ continue;
600
+ }
601
+ } else {
602
+ fail("an item with that name is already there");
603
+ continue;
604
+ }
605
+ }
606
+ }
607
+ if (!target) {
608
+ fail("every copy name for this item is already taken");
609
+ continue;
610
+ }
611
+ if (source.isDirectory() && (resolve(target) + sep).startsWith(resolve(src) + sep)) {
612
+ fail("a folder cannot be copied into itself");
613
+ continue;
614
+ }
615
+ const ctx = { job, index, name, root: dest, made: new Set(), refused: [], refusedCount: 0, durable, fat: FAT.has(fstype), unconfirmed: false };
616
+ const trashMetadata = moving && !merging ? trashRecord(src) : null;
617
+ try {
618
+ if (merging) {
619
+ folded.push(name);
620
+ await mergeInto(src, target, name, ctx, m);
621
+ } else if (moving) await move(src, target, ctx);
622
+ else await copyAny(src, target, ctx, true);
623
+ if (!merging) {
624
+ m.merged++;
625
+ if (choice === "keep") m.keptBoth++;
626
+ }
627
+ if (moving) forgetTrashRecord(trashMetadata);
628
+ if (ctx.durable) await fsyncPath(dirname(target), ctx);
629
+ unconfirmed ||= ctx.unconfirmed;
630
+ if (moving) touched.add(dirname(src));
631
+ if (ctx.refusedCount) {
632
+ refused.push(...ctx.refused.slice(0, REFUSED_CAP - refused.length));
633
+ fail(treeSentence(ctx, moving ? "moved" : "copied"));
634
+ } else {
635
+ ok++;
636
+ send({ t: "transferitem", id: job.id, index, name, ok: true });
637
+ }
638
+ } catch (e) {
639
+ // A cancel removes what the item in flight wrote; any other failure leaves it, as upstream does for undo.
640
+ // A merge never removes the folder it folds into: mergeInto took back its own entry in flight.
641
+ if (e instanceof Cancelled && ctx.made.size && !merging) await fsp.rm(target, { recursive: true, force: true }).catch(() => {});
642
+ // A failed copy that replaced an item takes back its partial copy, so the replaced item can return.
643
+ if (trashed && !moving && ctx.made.has(target)) await fsp.rm(target, { recursive: true, force: true }).catch(() => {});
644
+ const err = e?.sentence ?? reason(e),
645
+ why = trashed ? putBack(trashed, target) : "";
646
+ fail(why ? stillInTrash(err, why) : err);
647
+ }
648
+ }
649
+ const done = { t: "transferdone", id: job.id, ok, failed, skipped, cancelled: job.cancelled, retryPaths, durable: durable && !unconfirmed && failed === 0, note: unconfirmed ? "copied, but the drive did not confirm the folder" : "" };
650
+ // `into` names the merging folder only when it was the transfer's one item; anything beside it landed in dest.
651
+ if (collide === "merge") Object.assign(done, { merged: m.merged, keptBoth: m.keptBoth, into: folded.length === 1 && paths.length === 1 ? folded[0] : basename(resolve(dest)) || resolve(dest) });
652
+ if (refused.length) done.refused = refused;
653
+ send(done);
654
+ for (const path of touched) send({ t: "changed", path });
655
+ }
656
+
657
+ // One item and everything under it copied to `dst`, which must not exist yet: duplicate's copy.
658
+ // A tree entry the drive refused fails the whole copy with the transfer's own sentence.
659
+ export async function copyTree(src, dst) {
660
+ const ctx = { job: new Job(0, () => {}), index: 0, name: basename(src), root: dirname(dst), made: new Set(),
661
+ refused: [], refusedCount: 0, durable: false, fat: false, unconfirmed: false };
662
+ await copyAny(src, dst, ctx, true);
663
+ if (ctx.refusedCount) throw Object.assign(Error(treeSentence(ctx, "copied")), { sentence: treeSentence(ctx, "copied") });
664
+ }
665
+
666
+ export function transferCancel(request) {
667
+ const job = jobs.get(request.id);
668
+ if (job) job.cancelled = true;
669
+ }