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,41 @@
1
+ // Metadata follows real file operations, never the original (restoration) path.
2
+ import {lstatSync,readFileSync,unlinkSync,writeFileSync,renameSync} from 'node:fs';
3
+ import {basename,dirname,join,resolve} from 'node:path';
4
+ import {homeTrash,knownTrashes,trashInfoOriginal,trashInfoPath} from './trash.js';
5
+ const identity=st=>`${st.dev}:${st.ino}:${st.mode}:${st.birthtimeMs}`;
6
+ export function trashRecord(path) {
7
+ const files=dirname(resolve(path)),root=dirname(files);
8
+ if(basename(files)!=='files')return null;
9
+ const known=[homeTrash(),...knownTrashes()].map(resolvePath=>resolve(resolvePath));
10
+ if(!known.includes(root) && !/^\.Trash-\d+$/.test(basename(root)) && basename(dirname(root))!=='.Trash')return null;
11
+ try {
12
+ for(const dir of [root,files,join(root,'info')]) {
13
+ const st=lstatSync(dir);
14
+ if(!st.isDirectory() || (process.getuid && (st.uid!==process.getuid() || (st.mode&0o077))))return null;
15
+ }
16
+ const info=join(root,'info',basename(path)+'.trashinfo'),st=lstatSync(info);
17
+ if(!st.isFile()||st.size>1024*1024)return null;
18
+ return {path:resolve(path),info,text:readFileSync(info,'utf8'),identity:identity(st),root};
19
+ }catch{return null}
20
+ }
21
+ export function forgetTrashRecord(record) {
22
+ if(!record)return;
23
+ try {lstatSync(record.path);return} catch(e){if(e.code!=='ENOENT')return}
24
+ try {
25
+ if(identity(lstatSync(record.info))===record.identity && readFileSync(record.info,'utf8')===record.text)unlinkSync(record.info);
26
+ }catch(e){if(e.code!=='ENOENT')throw e}
27
+ }
28
+ // Reserve the new metadata name before moving; refuse collisions rather than overwrite.
29
+ export function renameWithTrashRecord(source,destination) {
30
+ try {lstatSync(destination);throw Error('An item with that name already exists')}catch(e){if(e.code!=='ENOENT')throw e}
31
+ const record=trashRecord(source);
32
+ if(!record){renameSync(source,destination);return}
33
+ if(dirname(resolve(destination))!==dirname(record.path))throw Error('Rename must stay in the same directory');
34
+ const top=record.root===resolve(homeTrash())?'':basename(dirname(record.root))==='.Trash'?dirname(dirname(record.root)):dirname(record.root);
35
+ const original=trashInfoOriginal((/^Path=(.*)$/m.exec(record.text)?.[1]??'').trim(),top);
36
+ const text=original?record.text.replace(/^Path=.*$/m,()=> 'Path='+trashInfoPath(join(dirname(original),basename(destination)),top)):record.text;
37
+ const info=join(record.root,'info',basename(destination)+'.trashinfo');
38
+ writeFileSync(info,text,{flag:'wx',mode:0o600});
39
+ try {renameSync(source,destination)}catch(e){unlinkSync(info);throw e}
40
+ forgetTrashRecord(record);
41
+ }
package/src/trash.js ADDED
@@ -0,0 +1,210 @@
1
+ // The `trash` request (docs/protocol.md "trash"): each path moves into the freedesktop Trash.
2
+ // Upstream runs `gio trash`; flea-qml runs no outside program, so this follows the FreeDesktop.org
3
+ // Trash specification 1.0 itself: the item is renamed into `files/`, after a `.trashinfo` record
4
+ // naming its original path is created exclusively in `info/`, which also reserves the name.
5
+ //
6
+ // Which Trash: the home Trash ($XDG_DATA_HOME/Trash) for an item on the same filesystem; otherwise
7
+ // the top directory's `.Trash/$uid` when that `.Trash` is a real, sticky folder, else
8
+ // `.Trash-$uid`, made if missing. An item is never copied between filesystems: a drive with no
9
+ // usable Trash fails that item and leaves it in place. Every system keeps this same Trash,
10
+ // Windows included: flea's Trash is this folder, not the Recycle Bin.
11
+ import { appendFileSync, closeSync, constants, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import { storePaths } from "./uistore.js";
15
+ import { basename, dirname, join, resolve, sep, isAbsolute } from "node:path";
16
+
17
+ const NAME_TRIES = 1000;
18
+
19
+ export const homeTrash = () => join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "Trash");
20
+
21
+ // Path= as the specification asks: the item's path in URI form, through Bun's pathToFileURL on
22
+ // every system, so a Windows drive letter is written /C:/... like any other path. In a per-drive
23
+ // Trash it is relative to the drive's top: the same conversion, with the top's own part cut off.
24
+ // Sample: /media/usb/照片/x.jpg under /media/usb gives %E7%85%A7%E7%89%87/x.jpg.
25
+ export function trashInfoPath(full, base) {
26
+ const url = pathToFileURL(full),
27
+ path = url.host ? `//${url.host}${url.pathname}` : url.pathname;
28
+ if (!base) return path;
29
+ const top = pathToFileURL(base.endsWith(sep) ? base : base + sep).pathname;
30
+ return url.pathname.startsWith(top) ? url.pathname.slice(top.length) : path;
31
+ }
32
+
33
+ // Decode relative paths as filesystem paths, not URL schemes (a name may contain ':').
34
+ // Invalid metadata cannot supply a restore destination outside its declared volume.
35
+ export function trashInfoOriginal(path, base) {
36
+ if (!path) return "";
37
+ try {
38
+ if (path.startsWith("/")) return fileURLToPath(path.startsWith("//") ? `file:${path}` : `file://${path}`);
39
+ if (!base) return "";
40
+ const decoded = decodeURIComponent(path);
41
+ if (decoded.includes("\0") || isAbsolute(decoded)) return "";
42
+ const top = resolve(base), full = resolve(top, decoded);
43
+ return full !== top && full.startsWith(top.endsWith(sep) ? top : top + sep) ? full : "";
44
+ } catch { return ""; }
45
+ }
46
+
47
+ // Local time without a zone, YYYY-MM-DDThh:mm:ss, as the specification's DeletionDate.
48
+ function deletionDate(now = new Date()) {
49
+ const two = (n) => String(n).padStart(2, "0");
50
+ return `${now.getFullYear()}-${two(now.getMonth() + 1)}-${two(now.getDate())}T${two(now.getHours())}:${two(now.getMinutes())}:${two(now.getSeconds())}`;
51
+ }
52
+
53
+ // The highest folder above `path` still on its device: the mount's top directory.
54
+ function topDirectory(path, dev) {
55
+ let top = dirname(path);
56
+ for (;;) {
57
+ const up = dirname(top);
58
+ if (up === top) return top;
59
+ try {
60
+ if (statSync(up).dev !== dev) return top;
61
+ } catch {
62
+ return top;
63
+ }
64
+ top = up;
65
+ }
66
+ }
67
+
68
+ export function usableTrash(trash) {
69
+ // Never follow a pre-existing symlink for a user-owned Trash or its children.
70
+ for (const path of [trash, join(trash, "files"), join(trash, "info")]) {
71
+ try { mkdirSync(path, { mode: 0o700 }); }
72
+ catch (error) { if (error.code !== "EEXIST") throw error; }
73
+ const st = lstatSync(path);
74
+ if (!st.isDirectory() || (process.getuid && (st.uid !== process.getuid() || (st.mode & 0o077))))
75
+ throw Object.assign(new Error("Trash directory is not private and owned by this user"), {code: "EUNSAFETRASH"});
76
+ }
77
+ return trash;
78
+ }
79
+
80
+ // Every per-drive Trash flea has written into, one path a line beside ui.json, so the Trash view
81
+ // finds them again on any system, whether or not that system has a mount table to read.
82
+ export const knownTrashesFile = () => join(dirname(storePaths().file), "trash-folders");
83
+ export function knownTrashes() {
84
+ try {
85
+ return readFileSync(knownTrashesFile(), "utf8").split("\n").filter(Boolean);
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+ function remember(trash) {
91
+ if (knownTrashes().includes(trash)) return;
92
+ try {
93
+ mkdirSync(dirname(knownTrashesFile()), { recursive: true });
94
+ appendFileSync(knownTrashesFile(), `${trash}\n`);
95
+ } catch {}
96
+ }
97
+
98
+ // { trash, base } for an item: base is what Path= is relative to ("" for the home Trash).
99
+ function trashFor(path, st) {
100
+ // The home Trash is made first, so its own device is what the item is compared with; on Windows
101
+ // ~/.local/share does not exist until flea makes it.
102
+ try {
103
+ const destination = homeTrash();
104
+ mkdirSync(dirname(destination), {recursive: true, mode: 0o700});
105
+ const home = usableTrash(destination);
106
+ if (statSync(home).dev === st.dev) return { trash: home, base: "" };
107
+ } catch (error) { if (error.code === "EUNSAFETRASH") throw error; }
108
+ // Windows has no uid; one user per profile, so the per-drive folder is .Trash-0 there.
109
+ const uid = process.getuid?.() ?? 0,
110
+ top = topDirectory(path, st.dev);
111
+ // $topdir/.Trash/$uid only when .Trash is a real folder with the sticky bit, never a link.
112
+ try {
113
+ const shared = lstatSync(join(top, ".Trash"));
114
+ if (shared.isDirectory() && shared.mode & constants.S_ISVTX) {
115
+ const trash = usableTrash(join(top, ".Trash", String(uid)));
116
+ remember(trash);
117
+ return { trash, base: top };
118
+ }
119
+ } catch {}
120
+ const trash = usableTrash(join(top, `.Trash-${uid}`));
121
+ remember(trash);
122
+ return { trash, base: top };
123
+ }
124
+
125
+ // Reserves a name in the Trash by creating its .trashinfo exclusively, then writes the record.
126
+ function reserve(trash, name, record) {
127
+ for (let n = 1; n <= NAME_TRIES; n++) {
128
+ const candidate = n === 1 ? name : `${name}.${n}`;
129
+ let fd;
130
+ try {
131
+ fd = openSync(join(trash, "info", `${candidate}.trashinfo`), "wx", 0o600);
132
+ } catch (e) {
133
+ if (e.code === "EEXIST") continue;
134
+ throw e;
135
+ }
136
+ const info = join(trash, "info", `${candidate}.trashinfo`);
137
+ try {
138
+ const bytes = Buffer.from(record);
139
+ for (let offset = 0; offset < bytes.length;) {
140
+ const written = writeSync(fd, bytes, offset, bytes.length - offset);
141
+ if (!written) throw new Error("Could not write Trash metadata");
142
+ offset += written;
143
+ }
144
+ } catch (error) {
145
+ try { unlinkSync(info); } catch {}
146
+ throw error;
147
+ } finally { closeSync(fd); }
148
+ // An orphan file still owns its name. Only ENOENT means it is free.
149
+ let exists = false;
150
+ try { lstatSync(join(trash, "files", candidate)); exists = true; }
151
+ catch (error) {
152
+ if (error.code !== "ENOENT") { unlinkSync(info); throw error; }
153
+ }
154
+ if (exists) { unlinkSync(info); continue; }
155
+ return candidate;
156
+ }
157
+ throw new Error("every Trash name for this item is taken");
158
+ }
159
+
160
+ // Moves one item to the Trash and answers where it went, { file, info }; throws with a sentence when it cannot.
161
+ export function trashOne(path) {
162
+ if (!isAbsolute(path) || resolve(path) === dirname(resolve(path))) throw new Error("Refusing to trash a filesystem root or relative path");
163
+ const full = resolve(path),
164
+ st = lstatSync(full);
165
+ let where;
166
+ try {
167
+ where = trashFor(full, st);
168
+ } catch {
169
+ throw new Error("This drive has no Trash that could be used, so the item was left in place");
170
+ }
171
+ if (full === where.trash || full.startsWith(where.trash + sep) || where.trash.startsWith(full + sep)) throw new Error("That item is already in the Trash");
172
+ const record = `[Trash Info]\nPath=${trashInfoPath(full, where.base)}\nDeletionDate=${deletionDate()}\n`;
173
+ const name = reserve(where.trash, basename(full), record);
174
+ try {
175
+ const current = lstatSync(full);
176
+ if (current.dev !== st.dev || current.ino !== st.ino || (current.mode & constants.S_IFMT) !== (st.mode & constants.S_IFMT))
177
+ throw new Error("Selected item changed; try again");
178
+ renameSync(full, join(where.trash, "files", name));
179
+ } catch (e) {
180
+ unlinkSync(join(where.trash, "info", `${name}.trashinfo`));
181
+ throw e.code === "EXDEV" ? new Error("This drive has no Trash that could be used, so the item was left in place") : e;
182
+ }
183
+ return { file: join(where.trash, "files", name), info: join(where.trash, "info", `${name}.trashinfo`) };
184
+ }
185
+
186
+ const reason = (e) =>
187
+ ({ EACCES: "permission denied", EPERM: "permission denied", ENOENT: "it no longer exists", EROFS: "the drive is read-only", EBUSY: "the item is in use" })[e?.code] ??
188
+ String(e?.message ?? e);
189
+
190
+ // The whole request: trashstarted, then one trashed line, then changed for each folder touched.
191
+ export function trash(request, send, named) {
192
+ const id = request.id ?? 0,
193
+ paths = named.paths;
194
+ send({ t: "trashstarted", id, n: paths.length, cancellable: false });
195
+ const moved = [],
196
+ folders = new Set();
197
+ let firstError = "";
198
+ for (const path of paths) {
199
+ try {
200
+ trashOne(path);
201
+ moved.push(path);
202
+ folders.add(dirname(path));
203
+ } catch (e) {
204
+ firstError ||= reason(e);
205
+ }
206
+ }
207
+ const failed = paths.length - moved.length;
208
+ send({ t: "trashed", id, ok: moved.length, failed, moved, ...(failed ? { err: firstError } : {}) });
209
+ for (const path of folders) send({ t: "changed", path });
210
+ }
@@ -0,0 +1,327 @@
1
+ // Reading the Trash back: the sidebar's count (a `trashcount` line from the backend) and the Trash view's
2
+ // `trashbrowse` requests (list, window, select, summary, prepare, check, cancel, restore, delete).
3
+ // Upstream asks `gio`; flea-qml reads the same FreeDesktop.org Trash that src/trash.js writes:
4
+ // every `info/<name>.trashinfo` whose `files/<name>` is still there is one item.
5
+ //
6
+ // The Trash folders read are the home Trash, each mount's `.Trash/$uid` and `.Trash-$uid` when a
7
+ // mount table can be read, and every per-drive Trash flea has written into: the same on every system.
8
+ import { lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, watch } from "node:fs";
9
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
10
+ import { describeKind } from "./kinds.js";
11
+ import { homeTrash, knownTrashes, trashInfoOriginal, usableTrash } from "./trash.js";
12
+
13
+ import { reviewTrees } from "./menu.js";
14
+
15
+ const identity = (st) => `${st.dev}:${st.ino}:${st.mode & 0o170000}`;
16
+
17
+ // Every Trash folder that exists now: [{ trash, top }], top "" for the home Trash.
18
+ export function trashFolders() {
19
+ const out = [],
20
+ seen = new Set();
21
+ const add = (trash, top) => {
22
+ const key = resolve(trash);
23
+ if (seen.has(key)) return;
24
+ try {
25
+ if (top && basename(dirname(trash)) === ".Trash") {
26
+ const shared = lstatSync(dirname(trash));
27
+ if (!shared.isDirectory() || !(shared.mode & 0o1000)) return;
28
+ }
29
+ for (const path of [trash, join(trash, "info"), join(trash, "files")]) {
30
+ const st = lstatSync(path);
31
+ if (!st.isDirectory() || (process.getuid && (st.uid !== process.getuid() || (st.mode & 0o077)))) return;
32
+ }
33
+ } catch {
34
+ return;
35
+ }
36
+ seen.add(key);
37
+ out.push({ trash: key, top });
38
+ };
39
+ add(homeTrash(), "");
40
+ // Every system takes the same three sources: the home Trash, each mount's Trash when a mount
41
+ // table can be read (Linux and Android have one; elsewhere the read fails and adds nothing), and
42
+ // every per-drive Trash flea itself has written into.
43
+ let mounts = "";
44
+ try {
45
+ mounts = readFileSync("/proc/self/mountinfo", "utf8");
46
+ } catch {}
47
+ const uid = process.getuid?.() ?? 0;
48
+ for (const line of mounts.split("\n")) {
49
+ // Field 5 is the mount point, with spaces and the like written as octal escapes.
50
+ const top = line.split(" ")[4]?.replace(/\\([0-7]{3})/g, (_, o) => String.fromCharCode(parseInt(o, 8)));
51
+ if (!top || !isAbsolute(top)) continue;
52
+ add(join(top, ".Trash", String(uid)), top);
53
+ add(join(top, `.Trash-${uid}`), top);
54
+ }
55
+ // A per-drive Trash's Path= is relative to the drive's top, the folder that holds the Trash.
56
+ for (const trash of knownTrashes()) add(trash, basename(dirname(trash)) === ".Trash" ? dirname(dirname(trash)) : dirname(trash));
57
+ return out;
58
+ }
59
+
60
+ // Every item now in the Trash, newest first.
61
+ export function readTrash() {
62
+ const items = [];
63
+ for (const { trash, top } of trashFolders()) {
64
+ let names = [];
65
+ try {
66
+ names = readdirSync(join(trash, "info"));
67
+ } catch {
68
+ continue;
69
+ }
70
+ for (const entry of names) {
71
+ if (!entry.endsWith(".trashinfo") || entry === ".trashinfo" || entry === "..trashinfo" || entry === "...trashinfo") continue;
72
+ const name = entry.slice(0, -".trashinfo".length),
73
+ file = join(trash, "files", name);
74
+ let st, text;
75
+ try {
76
+ const metadata = lstatSync(join(trash, "info", entry));
77
+ if (!metadata.isFile() || metadata.size > 1024 * 1024) continue;
78
+ st = lstatSync(file);
79
+ text = readFileSync(join(trash, "info", entry), "utf8");
80
+ } catch {
81
+ continue;
82
+ }
83
+ const original = trashInfoOriginal((/^Path=(.*)$/m.exec(text)?.[1] ?? "").trim(), top);
84
+ const deleted = /^DeletionDate=(.*)$/m.exec(text)?.[1]?.trim() ?? "";
85
+ const directory = st.isDirectory();
86
+ items.push({
87
+ uri: file,
88
+ name,
89
+ original,
90
+ deleted,
91
+ directory,
92
+ icon: describeKind(original.split(/[\\/]/).pop() || name, directory, st.mode, undefined).icon,
93
+ identity: `${identity(st)}:${JSON.stringify(text)}`,
94
+ fileIdentity: identity(st),
95
+ file,
96
+ info: join(trash, "info", entry),
97
+ });
98
+ }
99
+ }
100
+ return items.sort((a, b) => (a.deleted < b.deleted ? 1 : a.deleted > b.deleted ? -1 : 0));
101
+ }
102
+
103
+ // ---- the Trash count the backend reports: once at start, then on every change ----
104
+
105
+ // Calls onCount with the item count now and whenever a Trash folder changes. Runs inside the
106
+ // backend, so it lives exactly as long as the interface and adds no process of its own.
107
+ export function watchTrashCount(onCount) {
108
+ let last = "",
109
+ timer = null;
110
+ const report = () => {
111
+ timer = null;
112
+ const items = readTrash();
113
+ const signature = JSON.stringify(items.map(item => [item.uri, item.identity]));
114
+ if (signature !== last) {
115
+ last = signature;
116
+ onCount(items.length);
117
+ }
118
+ };
119
+ const watched = new Map();
120
+ const schedule = () => {
121
+ timer ??= setTimeout(report, 120);
122
+ timer.unref?.();
123
+ };
124
+ const arm = () => {
125
+ const dirs = new Set(trashFolders().flatMap(({trash}) => [join(trash, "files"), join(trash, "info")]));
126
+ for (const [dir, watcher] of watched) {
127
+ if (!dirs.has(dir)) { watcher.close(); watched.delete(dir); }
128
+ }
129
+ for (const { trash } of trashFolders())
130
+ for (const sub of ["files", "info"]) {
131
+ const dir = join(trash, sub);
132
+ if (watched.has(dir)) continue;
133
+ try {
134
+ const watcher = watch(dir, { persistent: false }, schedule);
135
+ watcher.on("error", () => { watcher.close(); watched.delete(dir); schedule(); });
136
+ watched.set(dir, watcher);
137
+ } catch {}
138
+ }
139
+ };
140
+ // The home Trash is made first, as the specification allows, so there is a folder to watch
141
+ // before anything has ever been trashed.
142
+ try {
143
+ usableTrash(homeTrash());
144
+ } catch {}
145
+ arm();
146
+ report();
147
+ // A drive mounted later, with a Trash of its own, is picked up within a few seconds.
148
+ const interval = setInterval(() => {
149
+ arm();
150
+ schedule();
151
+ }, 5000);
152
+ interval.unref?.();
153
+ return () => { clearInterval(interval); clearTimeout(timer); for (const watcher of watched.values()) watcher.close(); watched.clear(); };
154
+ }
155
+
156
+ // ---- the Trash view ----
157
+
158
+ let snapshot = [],
159
+ selection = null,
160
+ selectionTokens = 0,
161
+ review = null,
162
+ reviewTokens = 0;
163
+
164
+ const rowOf = (item) => ({
165
+ uri: item.uri,
166
+ name: item.name,
167
+ original: item.original,
168
+ deleted: item.deleted,
169
+ directory: item.directory,
170
+ icon: item.icon,
171
+ identity: item.identity,
172
+ ...(selection?.has(item.uri) ? { bulkSelected: true } : {}),
173
+ });
174
+
175
+ // The items a restore or delete names: all of them, the bulk selection minus exclusions, or URIs,
176
+ // each still holding the identity the client saw.
177
+ function chosen(request, items) {
178
+ const byUri = new Map(items.map((item) => [item.uri, item]));
179
+ if (request.all) return { items };
180
+ const exclude = new Set(request.exclude ?? []);
181
+ let picked = [];
182
+ if (request.selectionToken) {
183
+ if (!selection || selection.token !== request.selectionToken) return { stale: true };
184
+ picked = [...selection.keys()].filter((uri) => !exclude.has(uri)).map((uri) => {
185
+ const item = byUri.get(uri);
186
+ return item?.identity === selection.get(uri) ? item : null;
187
+ });
188
+ } else {
189
+ picked = (request.uris ?? []).map((uri, i) => {
190
+ const item = byUri.get(uri);
191
+ const want = request.identities?.[i];
192
+ return item && (!want || want === item.identity) ? item : null;
193
+ });
194
+ }
195
+ if (picked.some((item) => !item)) return { stale: true };
196
+ return { items: picked };
197
+ }
198
+
199
+ function removeItem(item) {
200
+ rmSync(item.file, { recursive: true, force: false });
201
+ try {
202
+ unlinkSync(item.info);
203
+ } catch {}
204
+ }
205
+
206
+ function restoreItem(item) {
207
+ if (!item.original || !isAbsolute(item.original)) throw new Error("the Trash does not say where it came from");
208
+ try {
209
+ lstatSync(item.original);
210
+ throw Object.assign(new Error("an item with that name is already there"), { taken: true });
211
+ } catch (e) {
212
+ if (e.taken || e.code !== "ENOENT") throw e;
213
+ }
214
+ mkdirSync(dirname(item.original), { recursive: true });
215
+ renameSync(item.file, item.original);
216
+ try {
217
+ unlinkSync(item.info);
218
+ } catch {}
219
+ }
220
+
221
+ const reason = (e) =>
222
+ ({ EACCES: "permission denied", EPERM: "permission denied", EXDEV: "it would have to cross drives", EROFS: "the drive is read-only" })[e?.code] ??
223
+ String(e?.message ?? e);
224
+
225
+ export function trashbrowse(request, send) {
226
+ try { return handleTrashBrowse(request, send); }
227
+ catch (error) { send({t: "trashbrowse", op: request.op ?? "", id: request.id ?? 0, ok: false, error: reason(error)}); }
228
+ }
229
+
230
+ function validReview() {
231
+ if (!review) return false;
232
+ const now = new Map(readTrash().map(item => [item.uri, item.identity]));
233
+ if (!review.items.every(item => now.get(item.uri) === item.identity)) return false;
234
+ try { return JSON.stringify(reviewTrees(review.items.map(item => item.file)).rows) === JSON.stringify(review.tree.rows); }
235
+ catch { return false; }
236
+ }
237
+
238
+ function handleTrashBrowse(request, send) {
239
+ const id = request.id ?? 0,
240
+ op = request.op ?? "",
241
+ answer = (fields) => send({ t: "trashbrowse", op, id, ok: true, ...fields });
242
+ const windowOf = (start, count) => {
243
+ const from = Math.max(0, Math.min(start | 0, snapshot.length));
244
+ return snapshot.slice(from, from + Math.max(0, count | 0)).map(rowOf);
245
+ };
246
+ const selectionFields = () => (selection ? { selectionToken: selection.token, selectionCount: selection.size, selectionStale: false } : {});
247
+ if (op === "list") {
248
+ snapshot = readTrash();
249
+ if (selection) for (const uri of [...selection.keys()]) if (!snapshot.some((item) => item.uri === uri)) selection.delete(uri);
250
+ const present = new Set(snapshot.map((item) => item.uri));
251
+ return answer({
252
+ total: snapshot.length,
253
+ start: request.start ?? 0,
254
+ rows: windowOf(request.start ?? 0, request.count ?? 0),
255
+ present: (request.uris ?? []).filter((uri) => present.has(uri)),
256
+ recoveredCount: 0,
257
+ recoveryErrors: [],
258
+ ...selectionFields(),
259
+ });
260
+ }
261
+ if (op === "window") return answer({ total: snapshot.length, start: request.start ?? 0, rows: windowOf(request.start ?? 0, request.count ?? 0), ...selectionFields() });
262
+ if (op === "select") {
263
+ selection = new Map(snapshot.map((item) => [item.uri, item.identity]));
264
+ selection.token = ++selectionTokens;
265
+ return answer({ selectionToken: selection.token, selectionCount: selection.size });
266
+ }
267
+ if (op === "cancel") {
268
+ review = null;
269
+ if (request.clearSelection) selection = null;
270
+ return answer({});
271
+ }
272
+ if (op === "summary") return answer({ bytes: reviewTrees(snapshot.map(item => item.file)).bytes, partial: false });
273
+ if (op === "prepare") {
274
+ review = null;
275
+ const picked = chosen(request, readTrash());
276
+ if (picked.stale) return send({ t: "trashbrowse", op, id, ok: false, stale: true, error: "The Trash changed; review it again." });
277
+ const tree = reviewTrees(picked.items.map(item => item.file));
278
+ review = { token: ++reviewTokens, items: picked.items.map((item) => ({ ...item })), tree };
279
+ return answer({
280
+ token: review.token,
281
+ all: request.emptyTrash === true || request.all === true,
282
+ count: review.items.length,
283
+ name: review.items.length === 1 ? (review.items[0].original.split(/[\\/]/).pop() || review.items[0].name) : "",
284
+ bytes: tree.bytes,
285
+ partial: false,
286
+ });
287
+ }
288
+ if (op === "check") {
289
+ const valid = !!review && review.token === request.token && validReview();
290
+ return answer({ valid });
291
+ }
292
+ if (op === "delete" || op === "restore") {
293
+ let items;
294
+ if (op === "delete") {
295
+ // Only the reviewed snapshot deletes, once.
296
+ if (!review || review.token !== request.token || !validReview()) return send({ t: "trashbrowse", op, id, ok: false, stale: true, error: "The Trash changed; review it again." });
297
+ items = review.items;
298
+ review = null;
299
+ } else {
300
+ const picked = chosen(request, readTrash());
301
+ if (picked.stale) return send({ t: "trashbrowse", op, id, ok: false, stale: true, error: "The Trash changed; select the items again." });
302
+ items = picked.items;
303
+ }
304
+ let done = 0;
305
+ const failures = [],
306
+ folders = new Set();
307
+ for (const item of items) {
308
+ try {
309
+ // Checked again just before acting, so an item swapped since it was seen is left alone.
310
+ if (identity(lstatSync(item.file)) !== item.fileIdentity) throw new Error("it changed in the Trash");
311
+ if (op === "delete") removeItem(item);
312
+ else {
313
+ restoreItem(item);
314
+ folders.add(dirname(item.original));
315
+ }
316
+ done++;
317
+ } catch (e) {
318
+ failures.push({ uri: item.uri, name: item.original.split(/[\\/]/).pop() || item.name, error: reason(e), identity: item.identity });
319
+ }
320
+ }
321
+ selection = null;
322
+ answer({ done, failed: failures.length, failures });
323
+ for (const path of folders) send({ t: "changed", path });
324
+ return;
325
+ }
326
+ send({ t: "trashbrowse", op, id, ok: false, error: `${op || "That Trash action"} is not ported to flea-qml yet.` });
327
+ }