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
package/src/uistate.js ADDED
@@ -0,0 +1,393 @@
1
+ // ui.json's shape, the rule each key is measured against and the merges, with no disk in them:
2
+ // read a file onto the defaults, apply one caller patch, carry 0.1.3's view.json across.
3
+ // Upstream: src/uischema.rs, src/uistate.rs, src/uimigrate.rs, src/uiplugins.rs.
4
+
5
+ const DEFAULTS = {
6
+ view: "list",
7
+ pickerView: "list",
8
+ density: "compact",
9
+ columns: ["name", "size", "date"],
10
+ columnsLimit: 3,
11
+ columnWidths: {},
12
+ addressBar: "breadcrumb",
13
+ sort: { key: "name", reverse: false },
14
+ rememberSort: true,
15
+ folderSorts: {},
16
+ rememberViewPerFolder: false,
17
+ folderViews: {},
18
+ dual: { paths: [], focus: 0, split: 5000 },
19
+ foldersFirst: true,
20
+ groupByKind: false,
21
+ hidden: false,
22
+ langMarks: true,
23
+ hiddenLast: false,
24
+ showGitStatus: false,
25
+ highlightToday: false,
26
+ wrapAtEnds: false,
27
+ escapeUp: false,
28
+ openMode: "double",
29
+ clickRename: true,
30
+ openFoldersWhileDragging: false,
31
+ // flea-qml: on by default, so menus show their keys and flyout letters; upstream starts it off.
32
+ keyHints: true,
33
+ startIn: "home",
34
+ startFolder: "",
35
+ lastPath: "",
36
+ lastTabs: { paths: [], index: 0 },
37
+ newTab: "current",
38
+ trashAutoEmpty: false,
39
+ trashSweptOn: 0,
40
+ places: {
41
+ favourites: [],
42
+ showHome: true, showRecent: false, showNetwork: true, showDevices: true, showTrash: true,
43
+ driveSize: false, trashCount: false, showUnmounted: true, showTags: false, rail: "shown", autoHide: false, sidebarWidth: 192,
44
+ },
45
+ shelf: { enabled: false, bar: true, rail: "off", screenshots: true, recordings: true, recent: 3 },
46
+ preview: {
47
+ column: true, loadOn: "automatic", thumbnails: "media", thumbSize: "medium",
48
+ thumbNetwork: false, thumbPhone: false, thumbUsb: true,
49
+ ctrlZoom: true, volume: 100, width: 0, tracks: [], wrap: false,
50
+ },
51
+ previewFacts: "compact",
52
+ keys: "default",
53
+ display: { textSize: { mode: "system" }, hyprlandIcons: false },
54
+ menu: {
55
+ hidden: ["delete", "openTerminal", "placeMenu", "runScript", "moveto", "copyto", "properties", "permissions",
56
+ "copyAs", "pasteAs", "invertSelection", "extThumbs", "run", "openAsText", "openAgent"],
57
+ },
58
+ updates: { autoCheck: true },
59
+ plugins: {},
60
+ stateVersion: 4,
61
+ };
62
+ export const defaults = () => structuredClone(DEFAULTS);
63
+
64
+ const OPTIONAL_COLUMNS = ["mode", "size", "date", "kind"];
65
+ const COLUMN_KEYS = ["name", "mode", "size", "date", "kind"];
66
+ const COLUMN_WIDTH_KEYS = ["mode", "size", "date", "kind"];
67
+ const COLUMN_WIDTH_MIN = 48, COLUMN_WIDTH_MAX = 480;
68
+ const SORT_KEYS = ["name", "size", "date", "kind"];
69
+ const TEXT_SIZE_STOPS = [9, 10, 11, 12, 14, 16, 20];
70
+ const SIDEBAR_STOPS = [160, 192, 224, 256];
71
+ const MAX_LAST_TABS = 9, MAX_FOLDER_SORTS = 500, MAX_FOLDER_VIEWS = 500;
72
+ const MAX_PLUGINS = 256, MAX_PLUGIN_SETTINGS = 64, MAX_PLUGIN_ID = 64;
73
+ const STATE_VERSION = "stateVersion";
74
+
75
+ // Rules: a kind, plus its words, range or sub-schema.
76
+ const bool = { kind: "bool" };
77
+ const word = (...words) => ({ kind: "word", words });
78
+ const count = (low, high) => ({ kind: "count", low, high });
79
+ const group = (schema) => ({ kind: "group", schema });
80
+ const r = (kind) => ({ kind });
81
+
82
+ const SCHEMA = {
83
+ view: word("list", "columns", "grid", "dual"),
84
+ pickerView: word("list", "grid"),
85
+ density: word("tight", "compact", "normal", "comfortable"),
86
+ columns: r("columns"),
87
+ columnsLimit: count(2, 5),
88
+ columnWidths: r("columnWidths"),
89
+ addressBar: word("path", "breadcrumb"),
90
+ sort: group({ key: word(...SORT_KEYS), reverse: bool }),
91
+ rememberSort: bool,
92
+ folderSorts: r("folderSorts"),
93
+ rememberViewPerFolder: bool,
94
+ folderViews: r("folderViews"),
95
+ dual: group({ paths: r("pair"), focus: count(0, 1), split: count(0, 10000) }),
96
+ foldersFirst: bool,
97
+ groupByKind: bool,
98
+ hidden: bool,
99
+ langMarks: bool,
100
+ hiddenLast: bool,
101
+ showGitStatus: bool,
102
+ highlightToday: bool,
103
+ wrapAtEnds: bool,
104
+ escapeUp: bool,
105
+ openMode: word("double", "single"),
106
+ clickRename: bool,
107
+ openFoldersWhileDragging: bool,
108
+ keyHints: bool,
109
+ startIn: word("home", "last", "folder"),
110
+ startFolder: r("place"),
111
+ lastPath: r("place"),
112
+ lastTabs: r("lastTabs"),
113
+ newTab: word("current", "home", "start"),
114
+ trashAutoEmpty: bool,
115
+ trashSweptOn: count(0, 4000000),
116
+ places: group({
117
+ favourites: r("favourites"), showHome: bool, showRecent: bool, showNetwork: bool, showDevices: bool,
118
+ showTrash: bool, driveSize: bool, trashCount: bool, showUnmounted: bool, showTags: bool,
119
+ rail: word("shown", "hidden"), autoHide: bool, sidebarWidth: r("sidebarWidth"),
120
+ }),
121
+ shelf: group({
122
+ enabled: bool, bar: bool, rail: word("off", "left", "right", "bottom"),
123
+ screenshots: bool, recordings: bool, recent: count(0, 6),
124
+ }),
125
+ preview: group({
126
+ column: bool, loadOn: word("automatic", "manual"), thumbnails: word("off", "images", "media"),
127
+ thumbSize: word("small", "medium", "large", "xlarge", "huge", "largest"),
128
+ thumbNetwork: bool, thumbPhone: bool, thumbUsb: bool, ctrlZoom: bool,
129
+ volume: count(0, 100), width: count(0, 100), tracks: r("tracks"), wrap: bool,
130
+ }),
131
+ previewFacts: word("compact", "full"),
132
+ keys: word("default", "vim", "mac", "windows"),
133
+ display: group({ textSize: group({ mode: r("textSize") }), hyprlandIcons: bool }),
134
+ menu: group({ hidden: r("ids") }),
135
+ updates: group({ autoCheck: bool }),
136
+ plugins: r("plugins"),
137
+ [STATE_VERSION]: r("version"),
138
+ };
139
+
140
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
141
+ const isWhole = (v) => typeof v === "number" && Number.isFinite(v) && Number.isInteger(v);
142
+ // A remembered place: an absolute path or a URI naming its root.
143
+ const isPlace = (s) => typeof s === "string" && (s.startsWith("/") || s.includes("://"));
144
+ const isActionId = (s) => typeof s === "string" && /^[A-Za-z0-9_-]+$/.test(s);
145
+ const isSortOrder = (v) =>
146
+ isObject(v) && Object.keys(v).length === 2 && SORT_KEYS.includes(v.key) && typeof v.reverse === "boolean";
147
+ const isFolderView = (v) => ["list", "columns", "grid"].includes(v);
148
+ const isPluginId = (id) =>
149
+ id.length <= MAX_PLUGIN_ID && id.split(".").length >= 2 && id.split(".").every((s) => /^[a-z0-9_-]+$/.test(s));
150
+ const isScalar = (v) => typeof v === "boolean" || typeof v === "number" || typeof v === "string";
151
+ const pluginEntryFits = (entry, patch) =>
152
+ isObject(entry) &&
153
+ Object.entries(entry).every(([key, inner]) =>
154
+ key === "enabled" ? typeof inner === "boolean"
155
+ : key === "settings" ? isObject(inner) && Object.keys(inner).length <= MAX_PLUGIN_SETTINGS
156
+ && Object.values(inner).every((v) => isScalar(v) || (patch && v === null))
157
+ : false);
158
+
159
+ function fits(rule, v) {
160
+ switch (rule.kind) {
161
+ case "bool": return typeof v === "boolean";
162
+ case "word": return rule.words.includes(v);
163
+ case "count": return isWhole(v) && v >= rule.low && v <= rule.high;
164
+ case "group": return isObject(v);
165
+ case "columns": {
166
+ if (!Array.isArray(v)) return false;
167
+ const seen = new Set();
168
+ for (const key of v) {
169
+ if (!COLUMN_KEYS.includes(key) || seen.has(key)) return false;
170
+ seen.add(key);
171
+ }
172
+ return seen.has("name");
173
+ }
174
+ case "favourites": return Array.isArray(v);
175
+ case "sidebarWidth": return typeof v === "number" && Number.isFinite(v);
176
+ // The two panes or nothing: a third path is a shape no restore can read.
177
+ case "pair": return Array.isArray(v) && (v.length === 0 || v.length === 2) && v.every(isPlace);
178
+ case "place": return typeof v === "string" && (v === "" || isPlace(v));
179
+ case "ids": return Array.isArray(v) && v.every(isActionId);
180
+ case "lastTabs": {
181
+ if (!isObject(v) || Object.keys(v).length !== 2 || !Array.isArray(v.paths)) return false;
182
+ if (v.paths.length > MAX_LAST_TABS || !v.paths.every(isPlace) || !isWhole(v.index) || v.index < 0) return false;
183
+ return v.paths.length === 0 ? v.index === 0 : v.index < v.paths.length;
184
+ }
185
+ case "folderSorts": return isObject(v) && Object.entries(v).every(([p, o]) => isPlace(p) && isSortOrder(o));
186
+ case "folderViews": return isObject(v) && Object.entries(v).every(([p, o]) => isPlace(p) && isFolderView(o));
187
+ case "columnWidths":
188
+ return isObject(v) && Object.entries(v).every(([k, w]) =>
189
+ COLUMN_WIDTH_KEYS.includes(k) && isWhole(w) && w >= COLUMN_WIDTH_MIN && w <= COLUMN_WIDTH_MAX);
190
+ case "tracks": return Array.isArray(v) && v.length <= 4 && v.every((t) => fits(count(1, 10000), t));
191
+ case "plugins":
192
+ return isObject(v) && Object.keys(v).length <= MAX_PLUGINS
193
+ && Object.entries(v).every(([id, e]) => isPluginId(id) && pluginEntryFits(e, false));
194
+ case "version": return isWhole(v) && v >= 0;
195
+ case "textSize": return v === "system" || TEXT_SIZE_STOPS.includes(v);
196
+ default: return false;
197
+ }
198
+ }
199
+
200
+ // A patch may forget a folder or a plugin with null; a stored map never holds one.
201
+ function fitsPatch(rule, v) {
202
+ if (rule.kind === "folderSorts")
203
+ return isObject(v) && Object.entries(v).every(([p, o]) => isPlace(p) && (o === null || isSortOrder(o)));
204
+ if (rule.kind === "folderViews")
205
+ return isObject(v) && Object.entries(v).every(([p, o]) => isPlace(p) && (o === null || isFolderView(o)));
206
+ if (rule.kind === "plugins")
207
+ return isObject(v) && Object.entries(v).every(([id, e]) => isPluginId(id) && (e === null || pluginEntryFits(e, true)));
208
+ return fits(rule, v);
209
+ }
210
+
211
+ // One elided line for an error sentence, never the whole document.
212
+ function oneLine(v) {
213
+ const flat = JSON.stringify(v) ?? String(v);
214
+ return [...flat].length > 40 ? `${[...flat].slice(0, 40).join("")}...` : flat;
215
+ }
216
+
217
+ function check(patch, schema, prefix) {
218
+ for (const [key, value] of Object.entries(patch)) {
219
+ const rule = schema[key];
220
+ if (!rule) throw new Error(`${prefix}${key} is not a ui.json key`);
221
+ if (rule.kind === "group") {
222
+ if (!isObject(value)) throw new Error(`${prefix}${key} takes an object, not ${oneLine(value)}`);
223
+ check(value, rule.schema, `${prefix}${key}.`);
224
+ } else if (rule.kind === "version") {
225
+ // A front end that could write the stamp could rewind a migration the file has already had.
226
+ throw new Error(`${prefix}${key} is kept by Flea and no patch sets it`);
227
+ } else if (!fitsPatch(rule, value)) throw new Error(`${prefix}${key} does not take ${oneLine(value)}`);
228
+ }
229
+ }
230
+
231
+ function normalized(rule, value) {
232
+ if (rule?.kind === "sidebarWidth") {
233
+ const wanted = typeof value === "number" ? value : 192;
234
+ return SIDEBAR_STOPS.reduce((best, stop) => (Math.abs(stop - wanted) < Math.abs(best - wanted) ? stop : best));
235
+ }
236
+ // The maps are oldest first, so a write past their cap drops the longest-ago entries.
237
+ if (rule?.kind === "folderSorts" || rule?.kind === "folderViews") {
238
+ const cap = rule.kind === "folderViews" ? MAX_FOLDER_VIEWS : MAX_FOLDER_SORTS;
239
+ const entries = Object.entries(value);
240
+ return entries.length > cap ? Object.fromEntries(entries.slice(entries.length - cap)) : value;
241
+ }
242
+ return value;
243
+ }
244
+
245
+ const cleanMap = (value, keep) =>
246
+ Object.fromEntries(Object.entries(isObject(value) ? value : {}).filter(([k, v]) => keep(k, v)));
247
+ const cleanPlugins = (value) =>
248
+ Object.fromEntries(Object.entries(isObject(value) ? value : {})
249
+ .filter(([id, e]) => isPluginId(id) && pluginEntryFits(e, false)).slice(0, MAX_PLUGINS));
250
+
251
+ // Known keys in the shipped order, then whatever a newer Flea left behind, kept as it was read.
252
+ function merge(fallback, found, schema) {
253
+ if (!isObject(found)) return structuredClone(fallback);
254
+ const out = {};
255
+ for (const [key, rule] of Object.entries(schema)) {
256
+ const base = fallback[key];
257
+ if (!(key in found)) out[key] = structuredClone(base);
258
+ else if (rule.kind === "group") out[key] = merge(base, found[key], rule.schema);
259
+ // A hand-edited map heals one bad entry rather than losing the key.
260
+ else if (rule.kind === "folderSorts") out[key] = normalized(rule, cleanMap(found[key], (p, o) => isPlace(p) && isSortOrder(o)));
261
+ else if (rule.kind === "folderViews") out[key] = normalized(rule, cleanMap(found[key], (p, o) => isPlace(p) && isFolderView(o)));
262
+ else if (rule.kind === "plugins") out[key] = cleanPlugins(found[key]);
263
+ else out[key] = fits(rule, found[key]) ? normalized(rule, structuredClone(found[key])) : structuredClone(base);
264
+ }
265
+ for (const [key, value] of Object.entries(found)) if (!(key in schema)) out[key] = value;
266
+ return out;
267
+ }
268
+
269
+ // Each path or edge named is forgotten, then re-pushed newest; null forgets it outright.
270
+ function mergeEntries(held, patch, allowNull) {
271
+ const out = { ...(isObject(held) ? held : {}) };
272
+ for (const [key, value] of Object.entries(patch)) {
273
+ delete out[key];
274
+ if (!(allowNull && value === null)) out[key] = value;
275
+ }
276
+ return out;
277
+ }
278
+
279
+ function mergePlugins(held, patch) {
280
+ const out = { ...(isObject(held) ? held : {}) };
281
+ for (const [id, entry] of Object.entries(patch)) {
282
+ const before = out[id];
283
+ delete out[id];
284
+ if (entry === null) continue;
285
+ const next = { ...(isObject(before) ? before : {}) };
286
+ for (const [key, value] of Object.entries(entry)) {
287
+ if (key === "settings") {
288
+ const settings = { ...(isObject(next.settings) ? next.settings : {}) };
289
+ for (const [name, setting] of Object.entries(value)) {
290
+ delete settings[name];
291
+ if (setting !== null) settings[name] = setting;
292
+ }
293
+ next.settings = settings;
294
+ } else next[key] = value;
295
+ }
296
+ out[id] = next;
297
+ }
298
+ return cleanPlugins(out);
299
+ }
300
+
301
+ function apply(current, patch, schema) {
302
+ const out = { ...current };
303
+ for (const [key, value] of Object.entries(patch)) {
304
+ const rule = schema[key];
305
+ if (rule?.kind === "group" && isObject(out[key])) out[key] = apply(out[key], value, rule.schema);
306
+ // A map patch merges per entry, so one window's write keeps what another wrote since its read.
307
+ else if (rule?.kind === "folderSorts" || rule?.kind === "folderViews") out[key] = normalized(rule, mergeEntries(out[key], value, true));
308
+ else if (rule?.kind === "columnWidths") out[key] = mergeEntries(out[key], value, false);
309
+ else if (rule?.kind === "plugins") out[key] = mergePlugins(out[key], value);
310
+ else out[key] = normalized(rule, structuredClone(value));
311
+ }
312
+ return out;
313
+ }
314
+
315
+ // The one-time changes a stored file is owed, each recorded by the stateVersion it arrived in.
316
+ const withHidden = (state, change) => {
317
+ const hidden = state.menu?.hidden;
318
+ if (!Array.isArray(hidden) || hidden.some((item) => typeof item !== "string")) return state;
319
+ return { ...state, menu: { ...state.menu, hidden: change([...hidden]) } };
320
+ };
321
+ const addMissing = (names) => (hidden) => [...hidden, ...names.filter((name) => !hidden.includes(name))];
322
+ const STEPS = [
323
+ [1, (state) => ({ ...state, places: { ...state.places, showUnmounted: true } })],
324
+ // copypath becomes copyAs where it stands; a file that held both keeps the first.
325
+ [2, (state) => withHidden(state, (hidden) => {
326
+ const kept = [];
327
+ for (const item of hidden) {
328
+ const name = item === "copypath" ? "copyAs" : item;
329
+ if (!(name === "copyAs" && kept.includes("copyAs"))) kept.push(name);
330
+ }
331
+ return addMissing(["pasteAs", "invertSelection"])(kept);
332
+ })],
333
+ [3, (state) => withHidden(state, addMissing(["run", "openAsText"]))],
334
+ [4, (state) => withHidden(state, (hidden) =>
335
+ addMissing(hidden.includes("runScript") ? ["openAgent", "actions"] : ["openAgent"])(hidden))],
336
+ ];
337
+ const storedVersion = (found) => (typeof found?.[STATE_VERSION] === "number" ? found[STATE_VERSION] : 0);
338
+ export const newerThanKnown = (found) => storedVersion(found) > STEPS.at(-1)[0];
339
+
340
+ function migrated(found, merged) {
341
+ const stored = storedVersion(found);
342
+ // Menu migrations apply to a saved list, never to the shipped default a missing one fell back to.
343
+ const storedMenu = Array.isArray(found?.menu?.hidden)
344
+ && JSON.stringify(found.menu.hidden) === JSON.stringify(merged.menu.hidden);
345
+ let state = merged;
346
+ for (const [version, step] of STEPS) {
347
+ if (stored >= version) continue;
348
+ if (version < 2 || storedMenu) state = step(state);
349
+ state = { ...state, [STATE_VERSION]: version };
350
+ }
351
+ return state;
352
+ }
353
+
354
+ /** Whether a parsed document is a ui.json this Flea may read and rewrite. */
355
+ export function isUiDocument(doc) {
356
+ if (!isObject(doc)) return false;
357
+ if (STATE_VERSION in doc && (!isWhole(doc[STATE_VERSION]) || newerThanKnown(doc))) return false;
358
+ const keys = Object.keys(doc);
359
+ return keys.length === 0 || keys.some((key) => key in DEFAULTS);
360
+ }
361
+
362
+ /** Never fails: a file that cannot be read is one whose every key is the shipped default. */
363
+ export function fromFile(text) {
364
+ let found;
365
+ try {
366
+ found = JSON.parse(text);
367
+ } catch {
368
+ return defaults();
369
+ }
370
+ return migrated(found, merge(DEFAULTS, found, SCHEMA));
371
+ }
372
+
373
+ /** 0.1.3's view.json named the hidden columns; uiScale is dropped on purpose. */
374
+ export function fromViewJson(text) {
375
+ const out = defaults();
376
+ let hidden;
377
+ try {
378
+ hidden = JSON.parse(text)?.hiddenCols;
379
+ } catch {}
380
+ if (!Array.isArray(hidden)) return out;
381
+ out.columns = ["name", ...OPTIONAL_COLUMNS.filter((key) => !hidden.includes(key))];
382
+ return out;
383
+ }
384
+
385
+ /** The whole patch is checked before any of it lands, so one bad key changes nothing. */
386
+ export function patched(current, patch) {
387
+ if (!isObject(patch)) throw new Error("a ui.json patch must be a JSON object");
388
+ check(patch, SCHEMA, "");
389
+ return apply(merge(DEFAULTS, current, SCHEMA), patch, SCHEMA);
390
+ }
391
+
392
+ /** The file's own text for a document, two-space indented with a final newline. */
393
+ export const render = (state) => `${JSON.stringify(state, null, 2)}\n`;
package/src/uistore.js ADDED
@@ -0,0 +1,168 @@
1
+ // Where ui.json lives and the one way it is rewritten: the lock, the re-read, the temp and the
2
+ // rename (docs/architecture "The state file" upstream). Upstream: src/uistore.rs.
3
+ //
4
+ // Upstream locks with flock(2), which neither Bun nor Windows offers. Here the lock is a file
5
+ // created exclusively that names its holder; one whose holder is gone, or that is older than any
6
+ // update takes, is stale and taken over. It has its own name, so it never meets the ui.json.lock
7
+ // upstream's binary leaves behind when the two share a state directory.
8
+ import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { defaults, fromFile, fromViewJson, isUiDocument, patched, render } from "./uistate.js";
12
+
13
+ const STATE_NOT_WRITTEN = "the state file was not written";
14
+ const LOCK_STALE_MS = 30_000,
15
+ LOCK_WAIT_MS = 10_000;
16
+
17
+ const envDir = (name) => (process.env[name] ? process.env[name] : null);
18
+
19
+ export function storePaths() {
20
+ const home = process.env.HOME || homedir();
21
+ const state = envDir("XDG_STATE_HOME") ?? join(home, ".local", "state");
22
+ const config = envDir("XDG_CONFIG_HOME") ?? join(home, ".config");
23
+ return {
24
+ file: join(state, "flea", "ui.json"),
25
+ lock: join(state, "flea", "ui.json.flea-qml.lock"),
26
+ legacy: join(config, "flea", "view.json"),
27
+ };
28
+ }
29
+
30
+ const kind = (path) => {
31
+ try {
32
+ return lstatSync(path);
33
+ } catch (error) {
34
+ if (error.code === "ENOENT") return null;
35
+ throw new Error(`${path} could not be inspected (${error.code}), so ${STATE_NOT_WRITTEN}`);
36
+ }
37
+ };
38
+
39
+ // A link, a device or an unreadable file is refused, never renamed over.
40
+ function readTarget(path) {
41
+ const st = kind(path);
42
+ if (!st) return null;
43
+ if (st.isSymbolicLink()) throw new Error(`${path} is a symbolic link, so ${STATE_NOT_WRITTEN}`);
44
+ if (!st.isFile()) throw new Error(`${path} is not a regular file, so ${STATE_NOT_WRITTEN}`);
45
+ try {
46
+ return readFileSync(path, "utf8");
47
+ } catch {
48
+ throw new Error(`${path} could not be read, so ${STATE_NOT_WRITTEN}`);
49
+ }
50
+ }
51
+
52
+ /** Never fails, so a front end can read it before first paint. */
53
+ export function readState(paths = storePaths()) {
54
+ let text;
55
+ try {
56
+ text = readFileSync(paths.file, "utf8");
57
+ } catch {}
58
+ if (text !== undefined) {
59
+ let doc;
60
+ try {
61
+ doc = JSON.parse(text);
62
+ } catch {}
63
+ return isUiDocument(doc) ? fromFile(text) : defaults();
64
+ }
65
+ // A ui.json that is there is the state file even unreadable; view.json only while there is none.
66
+ if (kind(paths.file)) return defaults();
67
+ try {
68
+ return fromViewJson(readFileSync(paths.legacy, "utf8"));
69
+ } catch {
70
+ return defaults();
71
+ }
72
+ }
73
+
74
+ const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
75
+ const alive = (pid) => {
76
+ try {
77
+ process.kill(pid, 0);
78
+ return true;
79
+ } catch (error) {
80
+ return error.code === "EPERM";
81
+ }
82
+ };
83
+
84
+ function takeLock(path) {
85
+ const deadline = Date.now() + LOCK_WAIT_MS;
86
+ for (;;) {
87
+ try {
88
+ const fd = openSync(path, "wx", 0o600);
89
+ writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
90
+ closeSync(fd);
91
+ return;
92
+ } catch (error) {
93
+ if (error.code !== "EEXIST") throw new Error(`${path} could not be opened as the ui.json lock (${error.code})`);
94
+ }
95
+ // A lock just created may not name its holder yet; until it does, only its age can make it stale.
96
+ let holder = null, age = 0;
97
+ try {
98
+ age = Date.now() - lstatSync(path).mtimeMs;
99
+ holder = JSON.parse(readFileSync(path, "utf8"));
100
+ } catch {}
101
+ const stale = age > LOCK_STALE_MS
102
+ || (Number.isInteger(holder?.pid) && !alive(holder.pid));
103
+ if (stale) {
104
+ rmSync(path, { force: true });
105
+ continue;
106
+ }
107
+ if (Date.now() > deadline) throw new Error(`${path} is held by process ${holder.pid}, so ${STATE_NOT_WRITTEN}`);
108
+ sleep(10);
109
+ }
110
+ }
111
+
112
+ // Create this process's own temp exclusively, sync it, and rename it over the file last.
113
+ function replace(path, text) {
114
+ const tmp = `${path}.${process.pid}.tmp`;
115
+ rmSync(tmp, { force: true });
116
+ try {
117
+ const fd = openSync(tmp, "wx", 0o600);
118
+ try {
119
+ writeSync(fd, text);
120
+ fsyncSync(fd);
121
+ } finally {
122
+ closeSync(fd);
123
+ }
124
+ renameSync(tmp, path);
125
+ } catch (error) {
126
+ rmSync(tmp, { force: true });
127
+ throw new Error(`${path} could not be written (${error.code ?? error.message})`);
128
+ }
129
+ }
130
+
131
+ // A file that is not a JSON object is kept as ui.json.broken and replaced; a foreign or newer one is refused.
132
+ function write(paths, state) {
133
+ const text = readTarget(paths.file);
134
+ if (text !== null) {
135
+ let doc;
136
+ try {
137
+ doc = JSON.parse(text);
138
+ } catch {}
139
+ if (doc !== null && typeof doc === "object" && !Array.isArray(doc)) {
140
+ if (!isUiDocument(doc))
141
+ throw new Error(`${paths.file}: foreign or newer-schema document; file was left unchanged, so ${STATE_NOT_WRITTEN}`);
142
+ } else {
143
+ const backup = join(dirname(paths.file), "ui.json.broken");
144
+ readTarget(backup);
145
+ replace(backup, text);
146
+ }
147
+ }
148
+ replace(paths.file, render(state));
149
+ }
150
+
151
+ /** Merges one caller patch under the lock and returns the document now on disk. */
152
+ export function updateState(patch, paths = storePaths()) {
153
+ try {
154
+ mkdirSync(dirname(paths.file), { recursive: true, mode: 0o700 });
155
+ } catch (error) {
156
+ throw new Error(`${dirname(paths.file)} could not be created (${error.code})`);
157
+ }
158
+ takeLock(paths.lock);
159
+ try {
160
+ const next = patched(readState(paths), patch);
161
+ write(paths, next);
162
+ return next;
163
+ } finally {
164
+ try {
165
+ unlinkSync(paths.lock);
166
+ } catch {}
167
+ }
168
+ }