flea-qml 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (625) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +478 -2
  3. package/THIRD-PARTY-NOTICES.md +29 -0
  4. package/demos/App.java +5 -0
  5. package/demos/Dockerfile +4 -0
  6. package/demos/Main.kt +3 -0
  7. package/demos/Program.cs +1 -0
  8. package/demos/README.md +12 -0
  9. package/demos/app.dart +3 -0
  10. package/demos/app.deb +0 -0
  11. package/demos/app.js +1 -0
  12. package/demos/backup.tar +0 -0
  13. package/demos/budget.xlsx +0 -0
  14. package/demos/build.sh +2 -0
  15. package/demos/bundle.zip +0 -0
  16. package/demos/clip.mp4 +0 -0
  17. package/demos/data.sqlite +0 -0
  18. package/demos/disk.iso +0 -0
  19. package/demos/engine.cpp +5 -0
  20. package/demos/firmware.bin +0 -0
  21. package/demos/flake.nix +4 -0
  22. package/demos/folder/inside.txt +1 -0
  23. package/demos/font.ttf +0 -0
  24. package/demos/index.html +5 -0
  25. package/demos/index.php +2 -0
  26. package/demos/init.lua +1 -0
  27. package/demos/letter.docx +0 -0
  28. package/demos/lib.rs +3 -0
  29. package/demos/main.c +6 -0
  30. package/demos/main.h +3 -0
  31. package/demos/math-and-diagrams.md +200 -0
  32. package/demos/notes.tar.gz +0 -0
  33. package/demos/notes.txt +3 -0
  34. package/demos/photo.png +0 -0
  35. package/demos/program +2 -0
  36. package/demos/report.pdf +32 -0
  37. package/demos/script.py +1 -0
  38. package/demos/server.go +7 -0
  39. package/demos/settings.json +4 -0
  40. package/demos/slides.pptx +0 -0
  41. package/demos/song.mp3 +0 -0
  42. package/demos/style.css +4 -0
  43. package/demos/tool.rb +1 -0
  44. package/demos/types.ts +2 -0
  45. package/docs/pele.md +23 -0
  46. package/docs/protocol.md +3420 -0
  47. package/docs/temml.md +63 -0
  48. package/index.js +107 -0
  49. package/package.json +34 -4
  50. package/src/archive.js +402 -0
  51. package/src/archiveops.js +697 -0
  52. package/src/backend.js +369 -0
  53. package/src/bulkrename.js +95 -0
  54. package/src/clip-host.js +76 -0
  55. package/src/clip.js +462 -0
  56. package/src/code.js +168 -0
  57. package/src/compare.js +74 -0
  58. package/src/create.js +128 -0
  59. package/src/dirsize.js +147 -0
  60. package/src/figure-svg.js +530 -0
  61. package/src/figure-thread.js +23 -0
  62. package/src/figure.js +132 -0
  63. package/src/find.js +55 -0
  64. package/src/flea.js +39 -0
  65. package/src/fsinfo.js +39 -0
  66. package/src/highlight.js +141 -0
  67. package/src/html.js +52 -0
  68. package/src/kinds.js +89 -0
  69. package/src/listing.js +130 -0
  70. package/src/markdown-html.js +317 -0
  71. package/src/mathml.js +186 -0
  72. package/src/menu.js +180 -0
  73. package/src/meta.js +129 -0
  74. package/src/office.js +810 -0
  75. package/src/order.js +100 -0
  76. package/src/peek.js +49 -0
  77. package/src/pele-wrapper.js +695 -0
  78. package/src/permissions.js +180 -0
  79. package/src/places.js +313 -0
  80. package/src/preview-language.js +10 -0
  81. package/src/transfer.js +669 -0
  82. package/src/trash-record.js +41 -0
  83. package/src/trash.js +210 -0
  84. package/src/trashbrowse.js +326 -0
  85. package/src/uistate.js +393 -0
  86. package/src/uistore.js +168 -0
  87. package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
  88. package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
  89. package/src/vendor/LICENSES/MIT-katex.txt +21 -0
  90. package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
  91. package/src/vendor/LICENSES/MIT-pele.txt +40 -0
  92. package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
  93. package/src/vendor/LICENSES/MIT-temml.txt +21 -0
  94. package/src/vendor/pele.js +145 -0
  95. package/src/vendor/temml.mjs +14708 -0
  96. package/src/watch.js +98 -0
  97. package/src/wayland/client.js +109 -0
  98. package/src/wayland/protocol.js +55 -0
  99. package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
  100. package/src/wayland/protocols/wayland.xml +3248 -0
  101. package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
  102. package/src/wayland/resource-io.js +6 -0
  103. package/src/wayland/socket-discovery.js +26 -0
  104. package/src/wayland/transport-posix.js +120 -0
  105. package/src/wayland/transport-win32.js +11 -0
  106. package/src/wayland/transport.js +3 -0
  107. package/src/wayland/win32-resources.js +80 -0
  108. package/src/wayland/win32-wire.js +62 -0
  109. package/test/archive.test.js +80 -0
  110. package/test/bench-markdown-scroll.mjs +190 -0
  111. package/test/clip-host.test.js +113 -0
  112. package/test/code-preview.test.js +35 -0
  113. package/test/compare-ui-paths.test.js +24 -0
  114. package/test/compare.test.js +54 -0
  115. package/test/delete-trash.test.js +67 -0
  116. package/test/dirsize.test.js +138 -0
  117. package/test/file-ops.test.js +241 -0
  118. package/test/find-contents.test.js +161 -0
  119. package/test/find-input.test.js +66 -0
  120. package/test/fixtures/pele-parity.json +1006 -0
  121. package/test/highlight-launcher.test.js +35 -0
  122. package/test/html-preview.test.js +22 -0
  123. package/test/input.test.js +15 -0
  124. package/test/keymap.test.js +26 -0
  125. package/test/language-icons.test.js +36 -0
  126. package/test/launcher.test.js +51 -0
  127. package/test/manual-code-scroll.mjs +28 -0
  128. package/test/manual-column-scroll.mjs +24 -0
  129. package/test/manual-compare-ui.mjs +20 -0
  130. package/test/manual-find-status.mjs +37 -0
  131. package/test/manual-html-security.mjs +23 -0
  132. package/test/manual-html-ui.mjs +30 -0
  133. package/test/manual-syntax-preview.mjs +26 -0
  134. package/test/manual-trash-ui.mjs +86 -0
  135. package/test/markdown-diagrams.test.js +93 -0
  136. package/test/markdown-html.test.js +105 -0
  137. package/test/mathml.test.js +92 -0
  138. package/test/menu-transfer.test.js +57 -0
  139. package/test/office-preview.test.js +154 -0
  140. package/test/pele-parity.test.js +28 -0
  141. package/test/places.test.js +134 -0
  142. package/test/preview-keys.test.js +60 -0
  143. package/test/preview-language.test.js +22 -0
  144. package/test/status-filename.test.js +20 -0
  145. package/test/transfer-replace-merge.test.js +187 -0
  146. package/test/trash-address.test.js +23 -0
  147. package/test/trash-view.test.js +37 -0
  148. package/test/trashbrowse.test.js +135 -0
  149. package/ui/AboutFacts.qml +78 -0
  150. package/ui/ArchiveCards.qml +68 -0
  151. package/ui/ArchiveCompressDialog.qml +378 -0
  152. package/ui/ArchivePasswordDialog.qml +257 -0
  153. package/ui/Backend.qml +771 -0
  154. package/ui/BulkOldNames.qml +47 -0
  155. package/ui/BulkRenameDialog.qml +380 -0
  156. package/ui/CardFrame.qml +67 -0
  157. package/ui/CardScroll.qml +79 -0
  158. package/ui/CheckBox.qml +73 -0
  159. package/ui/ChromeBar.qml +360 -0
  160. package/ui/ChromeButton.qml +131 -0
  161. package/ui/ChromeModes.qml +43 -0
  162. package/ui/CodeLines.qml +256 -0
  163. package/ui/CodeRefusal.qml +52 -0
  164. package/ui/CollideConfirm.qml +147 -0
  165. package/ui/CollideHost.qml +167 -0
  166. package/ui/ColumnPane.qml +523 -0
  167. package/ui/ColumnRow.qml +380 -0
  168. package/ui/ColumnsArea.qml +594 -0
  169. package/ui/ColumnsLanding.qml +147 -0
  170. package/ui/CompareView.qml +321 -0
  171. package/ui/ContextMenu.qml +743 -0
  172. package/ui/ConvertDialog.qml +456 -0
  173. package/ui/CountedWorker.qml +7 -0
  174. package/ui/Crumb.qml +64 -0
  175. package/ui/CrumbDrop.qml +25 -0
  176. package/ui/CsvTable.qml +394 -0
  177. package/ui/CursorMark.qml +41 -0
  178. package/ui/DefaultClaim.qml +130 -0
  179. package/ui/DeviceLock.qml +47 -0
  180. package/ui/DeviceMounts.qml +540 -0
  181. package/ui/DialogButton.qml +150 -0
  182. package/ui/DialogField.qml +147 -0
  183. package/ui/DialogHeader.qml +31 -0
  184. package/ui/DialogScrim.qml +20 -0
  185. package/ui/DialogTitle.qml +37 -0
  186. package/ui/Divider.qml +9 -0
  187. package/ui/DropInto.qml +126 -0
  188. package/ui/DropTarget.qml +35 -0
  189. package/ui/DropboxMark.qml +60 -0
  190. package/ui/EmptyState.qml +150 -0
  191. package/ui/EscapeHint.qml +11 -0
  192. package/ui/FactsTable.qml +54 -0
  193. package/ui/FastScrollHandler.qml +476 -0
  194. package/ui/FatNamesConfirm.qml +157 -0
  195. package/ui/Favourites.qml +143 -0
  196. package/ui/FigureService.qml +120 -0
  197. package/ui/FileDrag.qml +267 -0
  198. package/ui/FilterStrip.qml +133 -0
  199. package/ui/FitMetrics.qml +22 -0
  200. package/ui/FlashDriveRows.qml +137 -0
  201. package/ui/FlashHost.qml +72 -0
  202. package/ui/FlashImageRow.qml +104 -0
  203. package/ui/FlashRunner.qml +119 -0
  204. package/ui/FlashStepRow.qml +172 -0
  205. package/ui/FlashStrip.qml +38 -0
  206. package/ui/FlashView.qml +397 -0
  207. package/ui/FleaMark.qml +88 -0
  208. package/ui/FlyoutFade.qml +23 -0
  209. package/ui/FontPane.qml +170 -0
  210. package/ui/Glass.qml +114 -0
  211. package/ui/GlassTagger.qml +59 -0
  212. package/ui/Glyph.qml +64 -0
  213. package/ui/GlyphSolid.qml +22 -0
  214. package/ui/GridArea.qml +664 -0
  215. package/ui/GridTile.qml +325 -0
  216. package/ui/GvfsBridge.qml +164 -0
  217. package/ui/Header.qml +433 -0
  218. package/ui/HoverTimers.qml +39 -0
  219. package/ui/HtmlClip.qml +30 -0
  220. package/ui/HtmlPane.qml +63 -0
  221. package/ui/HyprlandMark.qml +45 -0
  222. package/ui/Ipc.qml +1005 -0
  223. package/ui/IpcControls.qml +7 -0
  224. package/ui/IpcMenuState.qml +48 -0
  225. package/ui/IpcPreviewState.qml +185 -0
  226. package/ui/JumpPath.qml +51 -0
  227. package/ui/KeymapSheet.qml +561 -0
  228. package/ui/KeymapSheetResult.qml +89 -0
  229. package/ui/KindStack.qml +40 -0
  230. package/ui/LangMark.qml +51 -0
  231. package/ui/LangMarkLoader.qml +26 -0
  232. package/ui/List.qml +480 -0
  233. package/ui/LoadingState.qml +54 -0
  234. package/ui/LocalSend.qml +56 -0
  235. package/ui/LocalSendMark.qml +73 -0
  236. package/ui/MarkdownBlockView.qml +311 -0
  237. package/ui/MarkdownBlocks.qml +35 -0
  238. package/ui/MarkdownFigure.qml +254 -0
  239. package/ui/MarkdownImages.qml +72 -0
  240. package/ui/MarkdownList.qml +81 -0
  241. package/ui/MarkdownMathsText.qml +97 -0
  242. package/ui/MarkdownPane.qml +83 -0
  243. package/ui/MarkdownPictures.qml +41 -0
  244. package/ui/MarkdownQuote.qml +52 -0
  245. package/ui/MarkdownRemote.qml +82 -0
  246. package/ui/MarkdownSource.qml +155 -0
  247. package/ui/MarkdownTable.qml +222 -0
  248. package/ui/MarkdownTableScroll.qml +64 -0
  249. package/ui/MarkdownTableWheel.qml +126 -0
  250. package/ui/MarkdownText.qml +87 -0
  251. package/ui/MarkdownWorker.js +132 -0
  252. package/ui/MatchText.qml +99 -0
  253. package/ui/MediaSession.qml +38 -0
  254. package/ui/MediaSound.qml +26 -0
  255. package/ui/MediaStrip.qml +191 -0
  256. package/ui/MediaTrack.qml +36 -0
  257. package/ui/MediaVolume.qml +54 -0
  258. package/ui/MenuActionDialog.qml +396 -0
  259. package/ui/MenuEdgeFade.qml +24 -0
  260. package/ui/MenuHost.qml +227 -0
  261. package/ui/MenuRow.qml +351 -0
  262. package/ui/MountListing.qml +100 -0
  263. package/ui/NetworkDialog.qml +435 -0
  264. package/ui/NetworkForm.qml +426 -0
  265. package/ui/NetworkMounts.qml +913 -0
  266. package/ui/NetworkPlaces.qml +127 -0
  267. package/ui/OpenWithDialog.qml +613 -0
  268. package/ui/Opener.qml +115 -0
  269. package/ui/Pane.qml +1296 -0
  270. package/ui/PaneMenuActions.qml +527 -0
  271. package/ui/PanePath.qml +90 -0
  272. package/ui/PaneRail.qml +239 -0
  273. package/ui/PaneStates.qml +88 -0
  274. package/ui/PaneSwap.qml +339 -0
  275. package/ui/PaneWire.qml +605 -0
  276. package/ui/ParkedWait.qml +9 -0
  277. package/ui/PathEntry.qml +186 -0
  278. package/ui/PathJump.qml +400 -0
  279. package/ui/PdfViewer.qml +297 -0
  280. package/ui/PermissionsDialog.qml +658 -0
  281. package/ui/PermissionsHeadings.qml +31 -0
  282. package/ui/PhoneMounts.qml +67 -0
  283. package/ui/PickerAnswers.qml +75 -0
  284. package/ui/PickerBox.qml +26 -0
  285. package/ui/PickerChrome.qml +295 -0
  286. package/ui/PickerFilter.qml +97 -0
  287. package/ui/PickerGrid.qml +308 -0
  288. package/ui/PickerHeader.qml +28 -0
  289. package/ui/PickerLifecycle.qml +27 -0
  290. package/ui/PickerList.qml +218 -0
  291. package/ui/PickerListing.qml +149 -0
  292. package/ui/PickerPathBar.qml +127 -0
  293. package/ui/PickerPlaces.qml +189 -0
  294. package/ui/PickerRecent.qml +92 -0
  295. package/ui/PickerSave.qml +101 -0
  296. package/ui/PickerSelection.qml +155 -0
  297. package/ui/PickerWindow.qml +846 -0
  298. package/ui/PowerSectorsReader.qml +10 -0
  299. package/ui/PrebuildDriver.qml +146 -0
  300. package/ui/Preview.qml +1004 -0
  301. package/ui/PreviewAnimation.qml +24 -0
  302. package/ui/PreviewArchive.qml +66 -0
  303. package/ui/PreviewColumn.qml +551 -0
  304. package/ui/PreviewEdge.qml +63 -0
  305. package/ui/PreviewFacts.qml +30 -0
  306. package/ui/PreviewFactsLine.qml +34 -0
  307. package/ui/PreviewHtml.qml +233 -0
  308. package/ui/PreviewImage.qml +253 -0
  309. package/ui/PreviewImagePan.qml +13 -0
  310. package/ui/PreviewImageSharp.qml +32 -0
  311. package/ui/PreviewLines.qml +106 -0
  312. package/ui/PreviewMarkdown.qml +706 -0
  313. package/ui/PreviewMedia.qml +250 -0
  314. package/ui/PreviewPdf.qml +482 -0
  315. package/ui/PreviewScroll.qml +201 -0
  316. package/ui/PreviewSwap.qml +335 -0
  317. package/ui/PreviewText.qml +149 -0
  318. package/ui/ProtocolChip.qml +18 -0
  319. package/ui/QuickLookArchive.qml +38 -0
  320. package/ui/QuickLookBar.qml +103 -0
  321. package/ui/QuickLookPrepare.qml +355 -0
  322. package/ui/QuickLookSwap.qml +10 -0
  323. package/ui/QuickLookWarm.qml +65 -0
  324. package/ui/RailDrop.qml +155 -0
  325. package/ui/RailHeading.qml +19 -0
  326. package/ui/RailPlaces.qml +38 -0
  327. package/ui/RailTrashRow.qml +67 -0
  328. package/ui/RenameField.qml +353 -0
  329. package/ui/RendererRetry.qml +11 -0
  330. package/ui/ResizeHandle.qml +52 -0
  331. package/ui/Reveal.qml +54 -0
  332. package/ui/Row.qml +476 -0
  333. package/ui/RowDrag.qml +213 -0
  334. package/ui/RowDropTarget.qml +27 -0
  335. package/ui/RunCard.qml +286 -0
  336. package/ui/Scripts.qml +65 -0
  337. package/ui/SearchStrip.qml +232 -0
  338. package/ui/SelectionBand.qml +140 -0
  339. package/ui/SelectionPreview.qml +370 -0
  340. package/ui/SettingsFavourite.qml +122 -0
  341. package/ui/SettingsGroup.qml +96 -0
  342. package/ui/SettingsHint.qml +12 -0
  343. package/ui/SettingsMetrics.qml +29 -0
  344. package/ui/SettingsPane.qml +178 -0
  345. package/ui/SettingsPanel.qml +547 -0
  346. package/ui/SettingsRail.qml +128 -0
  347. package/ui/SettingsRow.qml +409 -0
  348. package/ui/SettingsRuler.qml +85 -0
  349. package/ui/SettingsSegment.qml +85 -0
  350. package/ui/ShareBrowser.qml +113 -0
  351. package/ui/ShareLink.qml +67 -0
  352. package/ui/ShelfPins.qml +101 -0
  353. package/ui/Sidebar.qml +732 -0
  354. package/ui/SidebarRow.qml +358 -0
  355. package/ui/Spinner.qml +60 -0
  356. package/ui/StableModel.qml +89 -0
  357. package/ui/StateMessage.qml +84 -0
  358. package/ui/StatusBar.qml +512 -0
  359. package/ui/StatusFileName.qml +27 -0
  360. package/ui/TabBar.qml +673 -0
  361. package/ui/TabDragGeometry.qml +60 -0
  362. package/ui/TagMenuMark.qml +52 -0
  363. package/ui/TagRailSection.qml +53 -0
  364. package/ui/TagSlot.qml +40 -0
  365. package/ui/TagState.qml +150 -0
  366. package/ui/TagSwatch.qml +28 -0
  367. package/ui/Taildrop.qml +86 -0
  368. package/ui/TailscaleMark.qml +50 -0
  369. package/ui/Theme.qml +460 -0
  370. package/ui/ThemeColors.qml +43 -0
  371. package/ui/TransferCard.qml +239 -0
  372. package/ui/TrashConfirm.qml +129 -0
  373. package/ui/TrashDropHandler.qml +11 -0
  374. package/ui/TrashHost.qml +202 -0
  375. package/ui/TrashMonitor.qml +30 -0
  376. package/ui/TrashToolbar.qml +19 -0
  377. package/ui/TrashView.qml +492 -0
  378. package/ui/UnlockDialog.qml +450 -0
  379. package/ui/UnlockHost.qml +206 -0
  380. package/ui/UpdateCheck.qml +97 -0
  381. package/ui/UserDirs.qml +25 -0
  382. package/ui/ViewState.qml +565 -0
  383. package/ui/ViewportScrollBar.qml +195 -0
  384. package/ui/ViewportScrollBars.qml +25 -0
  385. package/ui/WebPreview.qml +33 -0
  386. package/ui/WebPreviewView.qml +26 -0
  387. package/ui/WindowBody.qml +827 -0
  388. package/ui/WindowDrag.qml +35 -0
  389. package/ui/boot/Commons/AudioNodes.qml +50 -0
  390. package/ui/boot/Commons/AudioNodesModel.js +96 -0
  391. package/ui/boot/Commons/Border.qml +243 -0
  392. package/ui/boot/Commons/BorderGeometry.js +373 -0
  393. package/ui/boot/Commons/Color.qml +258 -0
  394. package/ui/boot/Commons/I18n.qml +118 -0
  395. package/ui/boot/Commons/InputDevices.qml +112 -0
  396. package/ui/boot/Commons/InputMethodState.qml +38 -0
  397. package/ui/boot/Commons/IpcRegistry.qml +77 -0
  398. package/ui/boot/Commons/Numbers.js +19 -0
  399. package/ui/boot/Commons/Plural.js +91 -0
  400. package/ui/boot/Commons/ShellIpc.qml +12 -0
  401. package/ui/boot/Commons/Style.qml +561 -0
  402. package/ui/boot/Commons/Translations.qml +100 -0
  403. package/ui/boot/Commons/UntypedInput.qml +152 -0
  404. package/ui/boot/Commons/Util.qml +188 -0
  405. package/ui/boot/Commons/qmldir +13 -0
  406. package/ui/boot/LICENSE-omarchy +20 -0
  407. package/ui/boot/Ui/BackgroundMedia.qml +49 -0
  408. package/ui/boot/Ui/BarIconButton.qml +106 -0
  409. package/ui/boot/Ui/BarIconContent.qml +38 -0
  410. package/ui/boot/Ui/BarIndicator.qml +53 -0
  411. package/ui/boot/Ui/BarWidget.qml +50 -0
  412. package/ui/boot/Ui/BorderOverlay.qml +54 -0
  413. package/ui/boot/Ui/BorderSurface.qml +40 -0
  414. package/ui/boot/Ui/Button.qml +224 -0
  415. package/ui/boot/Ui/ButtonGroup.qml +131 -0
  416. package/ui/boot/Ui/ConfirmDialog.qml +134 -0
  417. package/ui/boot/Ui/CursorSurface.qml +42 -0
  418. package/ui/boot/Ui/Dropdown.qml +245 -0
  419. package/ui/boot/Ui/FilterInput.qml +26 -0
  420. package/ui/boot/Ui/IconContentBounds.qml +141 -0
  421. package/ui/boot/Ui/KeyboardPanel.qml +453 -0
  422. package/ui/boot/Ui/MultiSelect.qml +624 -0
  423. package/ui/boot/Ui/NumberField.qml +86 -0
  424. package/ui/boot/Ui/OpticalGlyph.qml +68 -0
  425. package/ui/boot/Ui/OverlayWindow.qml +102 -0
  426. package/ui/boot/Ui/Panel.qml +60 -0
  427. package/ui/boot/Ui/PanelActionButton.qml +105 -0
  428. package/ui/boot/Ui/PanelController.qml +16 -0
  429. package/ui/boot/Ui/PanelHero.qml +112 -0
  430. package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
  431. package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
  432. package/ui/boot/Ui/PanelSeparator.qml +19 -0
  433. package/ui/boot/Ui/PanelSlider.qml +156 -0
  434. package/ui/boot/Ui/PanelToolTip.qml +50 -0
  435. package/ui/boot/Ui/PluginBarApi.qml +89 -0
  436. package/ui/boot/Ui/PointerMoveGate.qml +54 -0
  437. package/ui/boot/Ui/PopupCard.qml +181 -0
  438. package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
  439. package/ui/boot/Ui/SearchableDropdown.qml +354 -0
  440. package/ui/boot/Ui/SizedImage.qml +125 -0
  441. package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
  442. package/ui/boot/Ui/TextField.qml +58 -0
  443. package/ui/boot/Ui/Toggle.qml +118 -0
  444. package/ui/boot/Ui/ToggleSwitch.qml +115 -0
  445. package/ui/boot/Ui/WidgetButton.qml +172 -0
  446. package/ui/boot/Ui/qmldir +41 -0
  447. package/ui/boot/fleatab.qml +50 -0
  448. package/ui/boot/picker.qml +39 -0
  449. package/ui/boot/shell.qml +122 -0
  450. package/ui/boot/tabtearoff.qml +51 -0
  451. package/ui/js/Anchor.js +379 -0
  452. package/ui/js/AnchorHold.js +162 -0
  453. package/ui/js/Archive.js +157 -0
  454. package/ui/js/ArchiveCard.js +44 -0
  455. package/ui/js/ArchiveOps.js +44 -0
  456. package/ui/js/BackReturn.js +118 -0
  457. package/ui/js/BulkRename.js +100 -0
  458. package/ui/js/Buttons.js +54 -0
  459. package/ui/js/ChromeKeys.js +116 -0
  460. package/ui/js/ClipMarks.js +73 -0
  461. package/ui/js/ClipSelection.js +67 -0
  462. package/ui/js/Clipboard.js +179 -0
  463. package/ui/js/Cloud.js +97 -0
  464. package/ui/js/CodeRuns.js +72 -0
  465. package/ui/js/Collide.js +212 -0
  466. package/ui/js/ColumnFit.js +28 -0
  467. package/ui/js/ColumnMenu.js +111 -0
  468. package/ui/js/Columns.js +298 -0
  469. package/ui/js/ColumnsCarry.js +103 -0
  470. package/ui/js/Compare.js +74 -0
  471. package/ui/js/Contrast.js +157 -0
  472. package/ui/js/ControlReader.js +42 -0
  473. package/ui/js/Convert.js +52 -0
  474. package/ui/js/CopyAs.js +52 -0
  475. package/ui/js/Crumbs.js +178 -0
  476. package/ui/js/Csv.js +231 -0
  477. package/ui/js/Density.js +44 -0
  478. package/ui/js/Devices.js +440 -0
  479. package/ui/js/DirSizes.js +180 -0
  480. package/ui/js/Drag.js +364 -0
  481. package/ui/js/DragOut.js +166 -0
  482. package/ui/js/DropDev.js +52 -0
  483. package/ui/js/Dropbox.js +41 -0
  484. package/ui/js/Eject.js +217 -0
  485. package/ui/js/Errors.js +188 -0
  486. package/ui/js/ExtThumbs.js +115 -0
  487. package/ui/js/Facts.js +402 -0
  488. package/ui/js/FigureDate.js +14 -0
  489. package/ui/js/Filter.js +285 -0
  490. package/ui/js/Find.js +46 -0
  491. package/ui/js/Flash.js +184 -0
  492. package/ui/js/FlashCard.js +79 -0
  493. package/ui/js/FlashEndings.js +125 -0
  494. package/ui/js/FlashPane.js +116 -0
  495. package/ui/js/FleaBin.js +12 -0
  496. package/ui/js/Focus.js +502 -0
  497. package/ui/js/FolderSorts.js +108 -0
  498. package/ui/js/FolderViews.js +44 -0
  499. package/ui/js/Format.js +305 -0
  500. package/ui/js/Fuzzy.js +141 -0
  501. package/ui/js/GlassTag.js +72 -0
  502. package/ui/js/Grid.js +33 -0
  503. package/ui/js/GridGeometry.js +90 -0
  504. package/ui/js/GridNames.js +153 -0
  505. package/ui/js/GvfsBridge.js +200 -0
  506. package/ui/js/HostClipboard.js +29 -0
  507. package/ui/js/Icons.js +218 -0
  508. package/ui/js/Input.js +50 -0
  509. package/ui/js/Jump.js +200 -0
  510. package/ui/js/KeyName.js +64 -0
  511. package/ui/js/Keymap.js +401 -0
  512. package/ui/js/Kinds.js +164 -0
  513. package/ui/js/LICENSES/ISC-lucide.txt +43 -0
  514. package/ui/js/LangMarkPaths.js +47 -0
  515. package/ui/js/LangMarks.js +120 -0
  516. package/ui/js/Lists.js +11 -0
  517. package/ui/js/LocalSend.js +39 -0
  518. package/ui/js/LockedMenu.js +63 -0
  519. package/ui/js/MakeDefault.js +186 -0
  520. package/ui/js/MarkBox.js +7 -0
  521. package/ui/js/Markdown.js +119 -0
  522. package/ui/js/MarkdownLists.js +86 -0
  523. package/ui/js/MarkdownMaths.js +58 -0
  524. package/ui/js/MarkdownPictures.js +32 -0
  525. package/ui/js/MarkdownPrepared.js +139 -0
  526. package/ui/js/MarkdownTableFit.js +34 -0
  527. package/ui/js/Marks.js +148 -0
  528. package/ui/js/Marquee.js +96 -0
  529. package/ui/js/Match.js +93 -0
  530. package/ui/js/Material.js +109 -0
  531. package/ui/js/MdBlocks.js +437 -0
  532. package/ui/js/MdBreak.js +71 -0
  533. package/ui/js/MdChunks.js +70 -0
  534. package/ui/js/MdContainer.js +137 -0
  535. package/ui/js/MdDocument.js +337 -0
  536. package/ui/js/MdEmph.js +97 -0
  537. package/ui/js/MdEntity.js +108 -0
  538. package/ui/js/MdEntityTable.js +223 -0
  539. package/ui/js/MdEscape.js +59 -0
  540. package/ui/js/MdFront.js +37 -0
  541. package/ui/js/MdHold.js +32 -0
  542. package/ui/js/MdHtml.js +307 -0
  543. package/ui/js/MdHtmlBlock.js +290 -0
  544. package/ui/js/MdHtmlHeading.js +53 -0
  545. package/ui/js/MdHtmlImage.js +179 -0
  546. package/ui/js/MdInline.js +154 -0
  547. package/ui/js/MdItems.js +212 -0
  548. package/ui/js/MdLeaf.js +309 -0
  549. package/ui/js/MdLink.js +196 -0
  550. package/ui/js/MdMath.js +67 -0
  551. package/ui/js/MdParagraphs.js +138 -0
  552. package/ui/js/MdRefs.js +232 -0
  553. package/ui/js/MdResolve.js +226 -0
  554. package/ui/js/MdRun.js +300 -0
  555. package/ui/js/MdUrl.js +226 -0
  556. package/ui/js/Menu.js +514 -0
  557. package/ui/js/MenuContext.js +85 -0
  558. package/ui/js/MenuFit.js +13 -0
  559. package/ui/js/MenuRefresh.js +74 -0
  560. package/ui/js/MenuWheel.js +35 -0
  561. package/ui/js/Messages.js +186 -0
  562. package/ui/js/Motion.js +16 -0
  563. package/ui/js/Mounts.js +320 -0
  564. package/ui/js/MouseNav.js +13 -0
  565. package/ui/js/Nav.js +329 -0
  566. package/ui/js/NetFs.js +26 -0
  567. package/ui/js/OpenWith.js +83 -0
  568. package/ui/js/Ops.js +585 -0
  569. package/ui/js/Palette.js +33 -0
  570. package/ui/js/PathBar.js +260 -0
  571. package/ui/js/Permissions.js +179 -0
  572. package/ui/js/Phones.js +112 -0
  573. package/ui/js/Picker.js +353 -0
  574. package/ui/js/PlaceMenu.js +96 -0
  575. package/ui/js/Places.js +265 -0
  576. package/ui/js/PreviewAsk.js +21 -0
  577. package/ui/js/PreviewKeys.js +155 -0
  578. package/ui/js/PreviewSettle.js +29 -0
  579. package/ui/js/PreviewSwap.js +101 -0
  580. package/ui/js/PropertyFacts.js +144 -0
  581. package/ui/js/Protocols.js +296 -0
  582. package/ui/js/QuickLookKinds.js +27 -0
  583. package/ui/js/QuickLookRect.js +24 -0
  584. package/ui/js/RailDrop.js +51 -0
  585. package/ui/js/RailKeys.js +87 -0
  586. package/ui/js/RailMenu.js +133 -0
  587. package/ui/js/Recent.js +135 -0
  588. package/ui/js/RecentMode.js +156 -0
  589. package/ui/js/Reload.js +60 -0
  590. package/ui/js/Renderer.js +9 -0
  591. package/ui/js/RunFile.js +143 -0
  592. package/ui/js/Scripts.js +28 -0
  593. package/ui/js/Scroll.js +304 -0
  594. package/ui/js/ScrollOff.js +69 -0
  595. package/ui/js/Search.js +478 -0
  596. package/ui/js/Selection.js +97 -0
  597. package/ui/js/Settings.js +476 -0
  598. package/ui/js/SettingsAbout.js +97 -0
  599. package/ui/js/SettingsShelf.js +52 -0
  600. package/ui/js/SheetKeys.js +42 -0
  601. package/ui/js/SheetQuery.js +281 -0
  602. package/ui/js/ShelfPile.js +27 -0
  603. package/ui/js/SlowClick.js +53 -0
  604. package/ui/js/SlowOp.js +6 -0
  605. package/ui/js/Sort.js +134 -0
  606. package/ui/js/Startup.js +63 -0
  607. package/ui/js/StateWords.js +17 -0
  608. package/ui/js/Status.js +238 -0
  609. package/ui/js/Swap.js +166 -0
  610. package/ui/js/TabMove.js +39 -0
  611. package/ui/js/Tabs.js +736 -0
  612. package/ui/js/Tags.js +134 -0
  613. package/ui/js/Taildrop.js +128 -0
  614. package/ui/js/Tap.js +174 -0
  615. package/ui/js/TextSize.js +107 -0
  616. package/ui/js/Thumbs.js +158 -0
  617. package/ui/js/Transfer.js +172 -0
  618. package/ui/js/Trash.js +81 -0
  619. package/ui/js/TrashDates.js +47 -0
  620. package/ui/js/TrashDrop.js +64 -0
  621. package/ui/js/UiState.js +438 -0
  622. package/ui/js/Unlock.js +152 -0
  623. package/ui/js/Update.js +130 -0
  624. package/ui/js/UserDirs.js +56 -0
  625. package/ui/qmldir +228 -0
@@ -0,0 +1,180 @@
1
+ // The Permissions card: `permissions` (inspect, apply, close) and `permissionsBatch`
2
+ // (docs/protocol.md "permissionsBatch"). Upstream: src/backend/permissions.rs.
3
+ // Upstream holds an O_PATH descriptor between inspect and apply; Node has none, so the item's
4
+ // device and inode are read again just before the chmod instead, and a link is never followed.
5
+ // Not yet: the undo journal.
6
+ import { chmodSync, lstatSync, readFileSync } from "node:fs";
7
+ import { isAbsolute } from "node:path";
8
+ import { userInfo } from "node:os";
9
+ import { fsTypeOf } from "./fsinfo.js";
10
+ import { reason as ioReason } from "./transfer.js";
11
+
12
+ const SPECIAL = 0o7000;
13
+ const CHANGED = "Selected item changed; reopen Permissions.";
14
+ const euid = () => (typeof process.geteuid === "function" ? process.geteuid() : -1);
15
+
16
+ /** "644" or "0644"; anything else refuses with the card's sentence. */
17
+ export function parseMode(text) {
18
+ const ordinary = typeof text === "string" && (text.length === 3 || (text.length === 4 && text[0] === "0"));
19
+ if (!ordinary || !/^[0-7]+$/.test(text)) throw Error("Enter three octal digits or a leading-zero four-digit mode.");
20
+ return Number.parseInt(text, 8);
21
+ }
22
+
23
+ // Why an item is shown read-only, or "" when its owner may change it.
24
+ function readOnly(st) {
25
+ for (const [bit, label] of [[0o4000, "setuid"], [0o2000, "setgid"], [0o1000, "sticky"]])
26
+ if (st.mode & bit) return `Read-only: ${label} bit is present.`;
27
+ const me = euid();
28
+ if (me >= 0 && st.uid !== me) return "Read-only: you are not the owner.";
29
+ return "";
30
+ }
31
+
32
+ // Sample input, /etc/passwd: "gm:x:1000:1000::/home/gm:/bin/bash"; field 2 is the uid, as in /etc/group the gid.
33
+ function nameIn(file, id) {
34
+ try {
35
+ for (const line of readFileSync(file, "utf8").split("\n")) {
36
+ const fields = line.split(":");
37
+ if (fields.length > 2 && Number(fields[2]) === id) return fields[0];
38
+ }
39
+ } catch {}
40
+ return "";
41
+ }
42
+ function ownerName(uid) {
43
+ const found = nameIn("/etc/passwd", uid);
44
+ if (found) return found;
45
+ try {
46
+ const me = userInfo();
47
+ if (me.uid === uid) return me.username;
48
+ } catch {}
49
+ return "";
50
+ }
51
+
52
+ const fmt = (mode) => (mode & 0o7777).toString(8).padStart(4, "0");
53
+ const sameItem = (a, b) => a.dev === b.dev && a.ino === b.ino;
54
+ const shape = (st) =>
55
+ !st.isFile() && !st.isDirectory() ? "Permissions takes one item, not a link."
56
+ : st.mode & SPECIAL ? "Special permissions cannot be edited." : "";
57
+ const sentence = (e) => (e?.code ? ioReason(e) : String(e?.message ?? e).replace(/\.$/, ""));
58
+
59
+ // chmod follows links, so the name is read again right before it and must still be the reviewed item.
60
+ function chmodChecked(path, held, target) {
61
+ const now = lstatSync(path);
62
+ if (!sameItem(now, held) || now.isSymbolicLink()) throw Error("the item was replaced, so its mode was left in place");
63
+ if ((now.mode & 0o777) !== (held.mode & 0o777)) throw Error("the mode changed since, so it was left in place");
64
+ chmodSync(path, target);
65
+ if ((lstatSync(path).mode & 0o777) !== (target & 0o777)) {
66
+ let fs = "";
67
+ try {
68
+ fs = fsTypeOf(lstatSync(path, { bigint: true }).dev);
69
+ } catch {}
70
+ // A fixed-mask filesystem answers success and keeps its mode.
71
+ throw Error(`This ${fs ? fs + " " : ""}drive ignores permission changes, so nothing was changed`);
72
+ }
73
+ }
74
+
75
+ // The card holds one reviewed item per request id.
76
+ const held = new Map();
77
+
78
+ export function permissions(request, send) {
79
+ const id = request.id ?? 0,
80
+ op = String(request.op ?? "");
81
+ const answer = (fields) => send({ t: "permissions", id, op, ...fields });
82
+ const fail = (error) => answer({ ok: false, error });
83
+ if (op === "close") {
84
+ held.delete(id);
85
+ return answer({ ok: true });
86
+ }
87
+ if (op === "inspect") {
88
+ held.delete(id);
89
+ const path = request.path;
90
+ if (!id || typeof path !== "string" || !isAbsolute(path)) return fail("Permissions requires an absolute path and request identity.");
91
+ let st;
92
+ try {
93
+ st = lstatSync(path);
94
+ } catch (e) {
95
+ return fail(`Could not inspect permissions: ${ioReason(e)}.`);
96
+ }
97
+ if (!st.isFile() && !st.isDirectory()) return fail("Permissions takes one item, not a link.");
98
+ held.set(id, { path, st });
99
+ return answer({ ok: true, path, directory: st.isDirectory(), mode: fmt(st.mode), uid: st.uid, gid: st.gid,
100
+ owner: ownerName(st.uid), group: nameIn("/etc/group", st.gid), reason: readOnly(st) });
101
+ }
102
+ if (op === "apply") {
103
+ let target;
104
+ try {
105
+ target = parseMode(request.mode);
106
+ } catch (e) {
107
+ return fail(e.message);
108
+ }
109
+ const item = held.get(id);
110
+ if (!item) return fail(CHANGED);
111
+ let now;
112
+ try {
113
+ now = lstatSync(item.path);
114
+ } catch {
115
+ return fail("Selected item moved or disappeared; reopen Permissions.");
116
+ }
117
+ if (!sameItem(now, item.st) || now.isSymbolicLink()) return fail(CHANGED);
118
+ const why = readOnly(now) || shape(now);
119
+ if (why) return fail(why);
120
+ try {
121
+ chmodChecked(item.path, now, target);
122
+ } catch (e) {
123
+ return fail(`Could not change mode: ${sentence(e)}. No change was applied.`);
124
+ }
125
+ held.set(id, { path: item.path, st: lstatSync(item.path) });
126
+ send({ t: "changed", path: item.path.replace(/\/[^/]*$/, "") || "/" });
127
+ return answer({ ok: true, mode: fmt(target) });
128
+ }
129
+ fail("Unknown permissions operation.");
130
+ }
131
+
132
+ // Every path to its own mode, all or none: a failure partway puts back what changed, newest first.
133
+ // "u+x" adds the owner's execute bit to whatever mode the item has.
134
+ export function permissionsBatch(request, send) {
135
+ const id = request.id ?? 0;
136
+ const answer = (ok, mode, error) => send({ t: "permissions", id, op: "applyMany", ok, mode, error });
137
+ const { paths, modes } = request;
138
+ if (!Array.isArray(paths) || !Array.isArray(modes) || paths.length === 0 || paths.length !== modes.length)
139
+ return answer(false, "", "Permissions needs at least one selected item.");
140
+ const checked = [];
141
+ try {
142
+ for (let i = 0; i < paths.length; i++) {
143
+ const path = paths[i],
144
+ text = modes[i];
145
+ if (typeof path !== "string" || !isAbsolute(path)) throw Error("Permissions requires an absolute path and request identity.");
146
+ const absolute = text === "u+x" ? null : parseMode(text);
147
+ let st;
148
+ try {
149
+ st = lstatSync(path);
150
+ } catch (e) {
151
+ throw Error(`Could not inspect permissions: ${ioReason(e)}.`);
152
+ }
153
+ const why = shape(st) || readOnly(st);
154
+ if (why) throw Error(why);
155
+ checked.push({ path, st, target: absolute ?? ((st.mode & 0o777) | 0o100) });
156
+ }
157
+ } catch (e) {
158
+ return answer(false, "", e.message);
159
+ }
160
+ const applied = [];
161
+ for (const item of checked) {
162
+ try {
163
+ chmodChecked(item.path, item.st, item.target);
164
+ applied.push(item);
165
+ } catch (e) {
166
+ const failure = `Could not change mode: ${sentence(e)}.`;
167
+ let stuck = 0;
168
+ for (const done of applied.reverse()) {
169
+ try {
170
+ chmodSync(done.path, done.st.mode & 0o777);
171
+ } catch {
172
+ stuck++;
173
+ }
174
+ }
175
+ return answer(false, "", stuck ? `${failure} ${stuck} of ${checked.length} items stayed changed.` : `${failure} No change was applied.`);
176
+ }
177
+ }
178
+ for (const folder of new Set(checked.map((item) => item.path.replace(/\/[^/]*$/, "") || "/"))) send({ t: "changed", path: folder });
179
+ answer(true, fmt(checked[0].target), "");
180
+ }
package/src/places.js ADDED
@@ -0,0 +1,313 @@
1
+ // The sidebar's probed places (docs/protocol.md "flea-qml probed places"): the folders under
2
+ // PLACES and the drives under DEVICES that this machine really has, found by looking at the
3
+ // filesystem and nothing else. Upstream reads user-dirs.dirs for the first and asks lsblk for the
4
+ // second, so a system with neither shows Home alone and a folder the file names is drawn whether
5
+ // or not it exists. Here every system is asked the same questions and the rows are what answers.
6
+ // No outside program runs, and the looking lives in the backend, so it ends with it.
7
+ import { lstatSync } from "node:fs";
8
+ import { lstat, mkdir, readFile, readdir, stat, statfs } from "node:fs/promises";
9
+ import { homedir, tmpdir } from "node:os";
10
+ import { basename, isAbsolute, join, resolve, sep } from "node:path";
11
+
12
+ const POLL_MS = 5000,
13
+ // How long one look may take before its row is left out of this answer; the look itself runs
14
+ // on, and its row arrives with the answer after it lands.
15
+ LOOK_MS = 2000,
16
+ // A folder known to hold a file is listed again at most this often while it keeps changing: a
17
+ // camera folder of thousands of photos costs a tenth of a second to list.
18
+ RELIST_MS = 60_000;
19
+
20
+ // The folders a user's files go by, under home and under Android's shared storage alike. The
21
+ // first name that passes is the row; `role` is user-dirs.dirs' own word, whose path is tried
22
+ // before the names. A folder is a row only once it directly holds a regular file: an empty one,
23
+ // or one holding only folders, is somewhere nobody keeps anything yet.
24
+ const FOLDERS = [
25
+ { role: "DESKTOP", glyph: "folder", names: ["Desktop"] },
26
+ { role: "DOWNLOAD", glyph: "download", names: ["Downloads", "Download"] },
27
+ { role: "DOCUMENTS", glyph: "file-text", names: ["Documents"] },
28
+ { role: "PICTURES", glyph: "image", names: ["Pictures"] },
29
+ { role: "CAMERA", glyph: "camera", names: ["DCIM/Camera", "DCIM", "Pictures/Camera Roll"] },
30
+ { role: "SCREENSHOTS", glyph: "image", names: ["Pictures/Screenshots", "DCIM/Screenshots"] },
31
+ { role: "MUSIC", glyph: "music", names: ["Music"] },
32
+ { role: "VIDEOS", glyph: "film", names: ["Videos", "Movies"] },
33
+ { role: "TEMPLATES", glyph: "folder", names: [] },
34
+ { role: "PUBLICSHARE", glyph: "folder", names: [] },
35
+ ];
36
+
37
+ // Where removable drives are mounted, by each system's convention, and how many folders lie
38
+ // between the name and the drive: /Volumes/NAME, /media/NAME or /media/USER/NAME, /run/media/USER/NAME.
39
+ const MOUNTS = [
40
+ { dir: "/Volumes", depth: 0 },
41
+ { dir: "/media", depth: 1 },
42
+ { dir: "/run/media", depth: 1 },
43
+ ];
44
+ // Android's own two entries under /storage, which are the shared storage and not a card.
45
+ const STORAGE_OWN = ["emulated", "self"];
46
+
47
+ // One look at a path at a time, and none awaited past LOOK_MS: a drive that stopped answering (a
48
+ // network share gone away) leaves its own row out instead of holding every other row back, and a
49
+ // look still out is joined, never started again. A look that fails answers null.
50
+ const looks = new Map();
51
+ function look(key, run) {
52
+ let flight = looks.get(key);
53
+ if (!flight) {
54
+ flight = run()
55
+ .catch(() => null)
56
+ .finally(() => looks.delete(key));
57
+ looks.set(key, flight);
58
+ }
59
+ let timer;
60
+ const late = new Promise((done) => {
61
+ timer = setTimeout(done, LOOK_MS, null);
62
+ });
63
+ return Promise.race([flight, late]).finally(() => clearTimeout(timer));
64
+ }
65
+
66
+ // A folder's identity, or null for anything else. bigint, because a Windows file index does not
67
+ // fit a double and two folders must never round into one; a filesystem that numbers nothing is
68
+ // told apart by path.
69
+ const folder = (path) =>
70
+ look(`folder:${path}`, async () => {
71
+ const st = await stat(path, { bigint: true });
72
+ if (!st.isDirectory()) return null;
73
+ return { dev: st.dev, id: st.ino ? `${st.dev}:${st.ino}` : `path:${path}`, stamp: `${st.dev}:${st.ino}:${st.mtimeNs}` };
74
+ });
75
+
76
+ // Whether a folder directly holds a regular file: a folder of folders does not count, nor a link.
77
+ async function anyFile(path) {
78
+ let asked = 0;
79
+ for (const entry of await readdir(path, { withFileTypes: true })) {
80
+ if (entry.isFile()) return true;
81
+ // A filesystem that names no types leaves every test false; a few of those are asked directly.
82
+ const typed = entry.isDirectory() || entry.isSymbolicLink() || entry.isFIFO() || entry.isSocket() || entry.isCharacterDevice() || entry.isBlockDevice();
83
+ if (typed || asked++ >= 16) continue;
84
+ try {
85
+ if ((await lstat(join(path, entry.name))).isFile()) return true;
86
+ } catch {}
87
+ }
88
+ return false;
89
+ }
90
+
91
+ // The folder when it holds a file, else null. The answer is kept against the folder's own change
92
+ // time, so a quiet folder costs one stat however large it is; an empty one is always read again.
93
+ const fileful = new Map();
94
+ async function holdsFile(path) {
95
+ const here = await folder(path);
96
+ if (!here) return null;
97
+ const known = fileful.get(path),
98
+ now = Date.now();
99
+ if (known?.has && (known.stamp === here.stamp || now - known.read < RELIST_MS)) return here;
100
+ const has = await look(`files:${path}`, () => anyFile(path));
101
+ // A listing that ran out of time keeps the last answer rather than dropping a row that was there.
102
+ if (has === null) return known?.has ? here : null;
103
+ fileful.set(path, { stamp: here.stamp, has, read: now });
104
+ return has ? here : null;
105
+ }
106
+
107
+ // Sample input: XDG_DOWNLOAD_DIR="$HOME/下載" names DOWNLOAD as /home/a/下載. The last valid line
108
+ // of a role wins, as upstream's ui/js/UserDirs.js reads it; a relative path names nothing.
109
+ export function userDirs(text, home) {
110
+ const roles = new Map();
111
+ for (const line of String(text ?? "").split("\n")) {
112
+ const pair = /^[ \t]*XDG_([A-Z]+)_DIR[ \t]*=[ \t]*"(\$HOME(?=\/|"))?((?:\\[\s\S]|[^"\\])*)"/.exec(line);
113
+ if (!pair) continue;
114
+ const rest = pair[3].replace(/\\([\s\S])/g, "$1"),
115
+ path = pair[2] ? home + rest : rest;
116
+ if (isAbsolute(path)) roles.set(pair[1], resolve(path));
117
+ }
118
+ return roles;
119
+ }
120
+
121
+ // The Android app whose private folder holds this temporary directory, which is how an app's
122
+ // own shell names itself. Sample input: /data/data/com.termux/files/usr/tmp gives com.termux in
123
+ // /data/data/com.termux, and /data/user/0/org.example.app/cache the same under /data/user/0.
124
+ export function appPackage(tmp) {
125
+ const found = /^(\/data\/(?:data|user\/\d+)\/([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+))(?:\/|$)/.exec(String(tmp ?? ""));
126
+ return found ? { name: found[2], dir: found[1] } : null;
127
+ }
128
+
129
+ // Every root this system's paths can start from: "/" where paths are POSIX, the drive letters
130
+ // where they are not. node:path decides which, so one list serves every system: POSIX reads
131
+ // "C:\" as a relative name and drops it, and Windows reads "/" as the current drive's root, which
132
+ // is one of the letters already.
133
+ export function rootNames(path = { isAbsolute, resolve }) {
134
+ const names = ["/", ...Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ", (letter) => `${letter}:\\`)];
135
+ return [...new Set(names.filter((name) => path.isAbsolute(name)).map((name) => path.resolve(name)))];
136
+ }
137
+
138
+ // What is mounted under a mount folder: a folder on another device than the one holding it.
139
+ // One on the same device is a per-user folder, looked into while `depth` allows.
140
+ async function mountedUnder(dir, depth, skip = []) {
141
+ const here = await folder(dir);
142
+ if (!here) return [];
143
+ const names = (await look(`names:${dir}`, () => readdir(dir))) ?? [],
144
+ out = [];
145
+ for (const name of names.sort()) {
146
+ if (skip.includes(name)) continue;
147
+ const child = join(dir, name),
148
+ at = await folder(child);
149
+ if (!at) continue;
150
+ if (at.dev !== here.dev) out.push({ path: child, at });
151
+ else if (depth > 0) out.push(...(await mountedUnder(child, depth - 1)));
152
+ }
153
+ return out;
154
+ }
155
+
156
+ // Android is told by its own linker being there, which a Linux userland running on it still sees
157
+ // and which no platform name says. The name alone counts, wherever its link leads.
158
+ function onAndroid() {
159
+ try {
160
+ lstatSync("/system/bin/linker64");
161
+ return true;
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+
167
+ const tilde = (path, home) => (path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path);
168
+
169
+ // The app's shared media folder is made once per run when missing, so one a user removed stays
170
+ // removed until the next start.
171
+ const made = new Set();
172
+
173
+ // The answer itself: { places, volumes }, each row { path, label, glyph }, a volume with its
174
+ // `size` in bytes or null. `given` replaces what is read from the machine, for the tests.
175
+ export async function probePlaces(given = {}) {
176
+ const env = given.env ?? process.env,
177
+ home = resolve(given.home ?? (env.HOME || homedir())),
178
+ android = given.android ?? onAndroid(),
179
+ shared = given.shared ?? "/sdcard",
180
+ temps = given.temps ?? [tmpdir(), env.TMPDIR, "/tmp"],
181
+ places = [],
182
+ taken = new Set();
183
+ const add = (row, at) => {
184
+ const id = at ? at.id : `path:${row.path}`;
185
+ if (taken.has(id)) return;
186
+ taken.add(id);
187
+ places.push(row);
188
+ };
189
+ // Home is the rail's own first row, so a folder that is home again is no second one.
190
+ const homeAt = await folder(home);
191
+ if (homeAt) taken.add(homeAt.id);
192
+
193
+ const folders = async (root, dirs) => {
194
+ const found = await Promise.all(
195
+ FOLDERS.map(async (kind) => {
196
+ const named = dirs.get(kind.role);
197
+ for (const path of [...(named ? [named] : []), ...kind.names.map((name) => join(root, name))]) {
198
+ const at = await holdsFile(path);
199
+ if (at) return { row: { path, label: basename(path), glyph: kind.glyph }, at };
200
+ }
201
+ return null;
202
+ }),
203
+ );
204
+ for (const one of found) if (one) add(one.row, one.at);
205
+ };
206
+
207
+ let dirs = new Map();
208
+ try {
209
+ dirs = userDirs(await readFile(join(env.XDG_CONFIG_HOME || join(home, ".config"), "user-dirs.dirs"), "utf8"), home);
210
+ } catch {}
211
+ await folders(home, dirs);
212
+
213
+ if (android) {
214
+ // The floor: the shared storage and its Download are rows whatever they answer.
215
+ add({ path: shared, label: "Internal storage", glyph: "smartphone" }, await folder(shared));
216
+ const download = join(shared, "Download");
217
+ add({ path: download, label: "Download", glyph: "download" }, await folder(download));
218
+ await folders(shared, new Map());
219
+ const app = temps.map(appPackage).find(Boolean);
220
+ if (app) {
221
+ const media = join(shared, "Android", "media", app.name);
222
+ if (!made.has(media)) {
223
+ made.add(media);
224
+ await look(`make:${media}`, () => mkdir(media, { recursive: true }));
225
+ }
226
+ for (const [path, label] of [[media, "App media"], [app.dir, "App data"]]) {
227
+ const at = await folder(path);
228
+ if (at) add({ path, label, glyph: "folder" }, at);
229
+ }
230
+ }
231
+ }
232
+
233
+ for (const name of temps) {
234
+ if (!name || !isAbsolute(name)) continue;
235
+ const path = resolve(name),
236
+ at = await folder(path);
237
+ if (at) add({ path, label: "Temp", glyph: "folder" }, at);
238
+ }
239
+
240
+ // Two rows never share a label: the later one is named by its whole path, home written ~.
241
+ const used = new Set(["Home", "Recent", "Trash"]);
242
+ for (const row of places) {
243
+ if (used.has(row.label)) row.label = tilde(row.path, home);
244
+ used.add(row.label);
245
+ }
246
+
247
+ const volumes = [],
248
+ seen = new Set(),
249
+ named = new Set();
250
+ // A drive is one row however many names reach it, with its size when the filesystem tells it.
251
+ const volume = async (path, at, label) => {
252
+ if (seen.has(at.id)) return;
253
+ seen.add(at.id);
254
+ const size = await look(`size:${path}`, async () => {
255
+ const fs = await statfs(path);
256
+ return Number(fs.bsize) * Number(fs.blocks);
257
+ });
258
+ const name = named.has(label) ? path : label;
259
+ named.add(name);
260
+ volumes.push({ path, label: name, glyph: "drive", size: size > 0 ? size : null });
261
+ };
262
+ // A root is a row when it can be listed, which an empty card reader or a locked drive cannot;
263
+ // its label is itself without the trailing separator, so "C:" and "/".
264
+ const roots = await Promise.all(
265
+ (given.roots ?? rootNames()).map(async (path) => ((await look(`list:${path}`, () => readdir(path))) ? { path, at: await folder(path) } : null)),
266
+ );
267
+ for (const root of roots) if (root?.at) await volume(root.path, root.at, root.path.replace(/[\\/]+$/, "") || root.path);
268
+ const mounts = given.mounts ?? [...MOUNTS, ...(android ? [{ dir: given.storage ?? "/storage", depth: 0, skip: STORAGE_OWN }] : [])];
269
+ for (const mount of mounts)
270
+ for (const found of await mountedUnder(mount.dir, mount.depth, mount.skip)) await volume(found.path, found.at, basename(found.path));
271
+ // QML navigation uses forward slashes; keep native separators inside IO only.
272
+ if (process.platform === "win32")
273
+ for (const row of [...places, ...volumes]) row.path = row.path.replaceAll("\\", "/");
274
+ return { places, volumes };
275
+ }
276
+
277
+ let timer = null,
278
+ sent = null,
279
+ busy = false,
280
+ owed = false;
281
+
282
+ // Looks, and sends a `places` line when asked for one or when the answer changed.
283
+ async function report(send, asked, given) {
284
+ if (busy) {
285
+ owed ||= asked;
286
+ return;
287
+ }
288
+ busy = true;
289
+ try {
290
+ const found = await probePlaces(given),
291
+ text = JSON.stringify(found);
292
+ if (asked || owed || text !== sent) send({ t: "places", ...found });
293
+ sent = text;
294
+ } catch (error) {
295
+ console.error(`flea-qml backend: places: ${error?.message ?? error}`);
296
+ } finally {
297
+ busy = false;
298
+ owed = false;
299
+ }
300
+ }
301
+
302
+ // The `places` request: an answer now, and with `watch` true a fresh look every five seconds
303
+ // until `watch` false, which a rail sends while it is shown. Nothing looks while no rail asks.
304
+ export function places(request, send, given) {
305
+ if (request.watch === true && !timer) {
306
+ timer = setInterval(() => report(send, false, given), POLL_MS);
307
+ timer.unref?.();
308
+ } else if (request.watch === false && timer) {
309
+ clearInterval(timer);
310
+ timer = null;
311
+ }
312
+ return request.watch === false ? Promise.resolve() : report(send, true, given);
313
+ }
@@ -0,0 +1,10 @@
1
+ // Flea-side hints for ambiguous extensions. Leave jsmdcui's normal detection
2
+ // untouched unless the bounded source prefix contains a V function declaration.
3
+ export function previewLanguage(path, bytes) {
4
+ if (!/\.v$/i.test(path ?? '')) return null;
5
+ const source = bytes.subarray(0, 65536).toString('utf8')
6
+ .replace(/\/\*[\s\S]*?(?:\*\/|$)|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, ' ');
7
+ // Verilog/SystemVerilog keeps priority when its module terminator is present.
8
+ if (/\bendmodule\b/.test(source)) return null;
9
+ return /^\s*(?:pub\s+)?fn\s+(?:\([^\r\n)]*\)\s*)?[A-Za-z_]\w*\s*(?:\[[^\r\n\]]*\]\s*)?\(/m.test(source) ? 'v' : null;
10
+ }