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
package/ui/Pane.qml ADDED
@@ -0,0 +1,1296 @@
1
+ import QtQuick
2
+ import Quickshell
3
+ import "." as Flea
4
+ import "js/DirSizes.js" as DirSizes
5
+ import "js/Dropbox.js" as Dropbox
6
+ import "js/ExtThumbs.js" as ExtThumbs
7
+ import "js/Filter.js" as Filter
8
+ import "js/Format.js" as Format
9
+ import "js/Focus.js" as Focus
10
+ import "js/Marks.js" as Marks
11
+ import "js/Menu.js" as Menu
12
+ import "js/Mounts.js" as Mounts
13
+ import "js/Search.js" as Search
14
+ import "js/Archive.js" as Archive
15
+ import "js/DragOut.js" as DragOut
16
+ import "js/Startup.js" as Startup
17
+ import "js/Nav.js" as Nav
18
+ import "js/RecentMode.js" as RecentMode
19
+ import "js/Ops.js" as Ops
20
+ import "js/Clipboard.js" as Clipboard
21
+ import "js/Permissions.js" as Permissions
22
+ import "js/Selection.js" as Selection
23
+ import "js/Anchor.js" as Anchor
24
+ import "js/SlowClick.js" as SlowClick
25
+ import "js/Sort.js" as Sort
26
+ import "js/Tabs.js" as Tabs
27
+ import "js/Thumbs.js" as Thumbs
28
+ import "js/FolderViews.js" as FolderViews
29
+
30
+ FocusScope {
31
+ id: root
32
+ focus: true
33
+ // Unhandled child keys reach the pane, including keys from an actually focused rail.
34
+ Keys.onPressed: function (event) { DragOut.keys.track(event, true); event.accepted = Focus.handleKey(event, root.sidebar && root.sidebar.activeFocus ? root.railPane : root, root.sidebar) }
35
+ Keys.onReleased: function (event) { DragOut.keys.track(event, false) }
36
+ enabled: !root.settingsPanel || !root.settingsPanel.opened
37
+ property var backend: null
38
+ property string path: ""
39
+ // Set once by shell.qml from FLEA_SELECT; applied to the first `rows` this pane receives, then forgotten.
40
+ property string pendingSelect: ""
41
+ // Set with pendingSelect by a right click on a peeked column row: the menu opens on the row once it is the cursor.
42
+ property bool pendingMenu: false
43
+ // Set by a right click on a neighbour column's empty space: the drawn directory opens, then its background menu opens at the stored scene point once the rows land.
44
+ property string pendingBackground: ""
45
+ property var pendingBackgroundAt: null
46
+ // The directory the listing in flight asked for, which is not pane.path until the reply lands.
47
+ property string listingPath: ""
48
+ property int total: 0
49
+ property int cursorIndex: 0
50
+ // Filter and Marks bump this on every cursor move, ending a shift gesture.
51
+ property int cursorSeq: 0
52
+ // The wheel's last pull of the cursor, which ui/js/Filter.js pulledFrom reads for a Shift range's start.
53
+ property var cursorPulled: null
54
+ property string listingState: "loading"
55
+ property string stateMessage: ""
56
+ property int lockedMode: 0
57
+ // The folder the Locked tile names while one is drawn, "" otherwise; ui/js/Nav.js owns it,
58
+ // and the tile's right click and the m key open that folder's own menu through it.
59
+ readonly property string lockedTarget: Nav.lockedTarget(root)
60
+ // Off by default: dotfiles stay out of every listing until the context menu or "." turns them on.
61
+ property bool showHidden: ViewState.state.hidden === true
62
+ // A zip's one image member, which Flash to drive needs named before it shows on a zip.
63
+ property string flashZipMember: ""
64
+ // The asked zip with its size and mtime, so a late answer for an earlier file never arms a later one.
65
+ property string flashZipAsked: ""
66
+ onCursorIndexChanged: Ops.checkZip(root)
67
+ onSelectionVersionChanged: Ops.checkZip(root)
68
+ onPathChanged: Ops.checkZip(root)
69
+ onBackendChanged: Ops.checkZip(root)
70
+ // Issue 27's state-file key: with it on a cursor step past an end comes round; ui/js/Focus.js step is the only reader.
71
+ readonly property bool wrapAtEnds: ViewState.state.wrapAtEnds === true
72
+ // ClickAndRefresh under Settings, View: Escape climbs, one tap opens, and the slow click renames.
73
+ readonly property bool escapeUp: ViewState.state.escapeUp === true
74
+ readonly property bool singleClick: ViewState.state.openMode === "single"
75
+ readonly property bool clickRename: ViewState.state.clickRename !== false
76
+ // A manual reload's own count, or -1; ui/js/Reload.js sets it and ui/PaneSwap.qml spends it.
77
+ property int reloadFrom: -1
78
+ // Added plus removed rows on the reload's listed line, or -1; ui/PaneSwap.qml sets it from changed.
79
+ property int reloadChanged: -1
80
+ // The slow click's own window: the last tap's row and time, read by ui/js/SlowClick.js alone.
81
+ property double slowClickAt: 0
82
+ property int slowClickIndex: SlowClick.CLEARED
83
+ // One slow-click timer for the whole pane; a tap arms it and a second tap in time opens as a double click does, and its firing renames only while the row still holds the cursor.
84
+ Timer {
85
+ id: slowClickTimer
86
+ interval: Qt.styleHints.mouseDoubleClickInterval
87
+ repeat: false
88
+ onTriggered: SlowClick.fire(root)
89
+ }
90
+ function armSlowClick(index, modifiers, dragging, wasSole) {
91
+ if (SlowClick.arm(root, index, modifiers, Date.now(), Qt.styleHints.mouseDoubleClickInterval, dragging, wasSole))
92
+ slowClickTimer.restart()
93
+ else slowClickTimer.stop()
94
+ }
95
+ function slowClickWasSole(index) { return SlowClick.wasSoleSelection(root, index) }
96
+ // A press stops the timer without clearing the tap record, so a hold past the interval cannot fire while down;
97
+ // the release still arms through armSlowClick, and a drag clears fully at lift.
98
+ function pressSlowClick() { slowClickTimer.stop() }
99
+ function cancelSlowClick() { slowClickTimer.stop(); SlowClick.cancel(root) }
100
+ // ui/js/Tabs.js is a .pragma library and cannot reach a QML singleton, so the state it asks
101
+ // ui/js/Startup.js about rides in through the pane, the way every other setting it reads does.
102
+ readonly property var uiState: ViewState.state
103
+ // When the first d of the dd pair landed; ui/js/Focus.js reads it and Nav's reset clears it.
104
+ property double trashArmedAt: 0
105
+ // The first row a trash request went out with, or -1: what the block left, where the cursor lands.
106
+ property int trashedFirst: -1
107
+ property string keySequence: ""
108
+ property string keySequenceIdentity: ""
109
+ // "" off, "typing" while the query line has the keyboard, "results" once Enter keeps them; ui/js/Search.js owns every transition.
110
+ property string searchMode: ""
111
+ // An exit owed its landing: the backend closed at the request and the drawn state clears with the rows.
112
+ property bool searchLeaving: false
113
+ // Search origin survives a home-wide walk.
114
+ property string searchFrom: ""
115
+ property string searchQuery: ""
116
+ // flea-qml: the query Find last ran, and when Enter last ran one (for the Enter debounce).
117
+ property string searchSent: ""
118
+ property real searchEnterAt: 0
119
+ // Issue 30: which scope the next walk takes, flipped by tab on the query line; see ui/js/Search.js.
120
+ // flea-qml: the toolbar's two toggles. Off, each opens as its key does; on, Find stops its run and
121
+ // leaves, Filter clears and closes. Neither touches the other.
122
+ function toggleFind() {
123
+ if (root.searchMode.length > 0) Search.dismiss(root)
124
+ else root.act("search")
125
+ }
126
+ function toggleFilter() {
127
+ if (root.filterTyping || root.filterQuery.length > 0) Filter.close(root)
128
+ else root.act("filter")
129
+ }
130
+
131
+ // flea-qml: Find's scope, Search.HERE, HOME or ROOT; it starts in the folder and Tab turns it.
132
+ property int searchStep: 0
133
+ // flea-qml: Ctrl+G's content search: Find runs grep over the scope instead of find.
134
+ property bool searchContents: false
135
+ property bool searchRunning: false
136
+ property bool searchCancelled: false
137
+ // Newest sent query generation, rising across sessions; answers for an older one are dropped.
138
+ property int searchGen: 0
139
+ // The gen a session opened at; a line at or below it is from a closed session.
140
+ property int searchOpenedGen: 0
141
+ // Stopwatch for the newest gen: keyAt at the send, firstRowsAt at its first rows, settledAt at its landed searched; a stale gen sets none.
142
+ property real searchKeyAt: 0
143
+ property real searchFirstRowsAt: 0
144
+ property real searchSettledAt: 0
145
+ // The listing's own base: the scope for a live answer, the folder for an empty one.
146
+ property string searchBase: ""
147
+ // Recent place: "" off, "results" once answered; RecentMode.js owns transitions; pane path is history base.
148
+ property string recentMode: ""
149
+ // Where Recent was opened over, and the listing it hands back on leaving.
150
+ property string recentFrom: ""
151
+ // The history paths the listing stands on, so a refresh with the rail hidden re-asks them.
152
+ property var recentPaths: []
153
+ property var recentVisits: ({})
154
+ // The sort Recent replaced, handed back on leaving; ui/js/RecentMode.js run and restoreSort write it.
155
+ property string recentSortBy: ""
156
+ property bool recentSortDesc: false
157
+ // Filter.js owns the standing query and its typing state.
158
+ property string filterQuery: ""
159
+ property bool filterTyping: false
160
+ // The filter stopwatch Ipc's filterStamps reads: the newest keystroke and when it narrowed the shown model.
161
+ property real filterKeyAt: 0
162
+ property real filterNarrowedAt: 0
163
+ property int searchScanned: 0
164
+ property real searchMs: 0
165
+ property bool listInFlight: false
166
+ property bool listedSeen: false
167
+ readonly property bool menuVisible: menu.opened
168
+ // The keyboard-highlighted menu row, so a test can drive the menu without OCR.
169
+ readonly property int menuCursor: menu.cursor
170
+ // What the header case reads over IPC, the same alias idiom Row.qml uses for its icon.
171
+ readonly property alias header: header
172
+ property var sharedSidebar: null
173
+ property var railPane: root
174
+ // The window's chrome bar and tab strip, the two Tab stops after the rail; ui/js/ChromeKeys.js reads them.
175
+ property var chromeBar: null
176
+ property var tabStrip: null
177
+ // The window-long network host, injected by ui/WindowBody.qml (the primary pane's own,
178
+ // shared by the second pane): mounts, bridge waits and dialog answers outlive the rail.
179
+ property var sharedNetworkService: null
180
+ property var networkService: null
181
+ readonly property var pathAlias: root.networkService ? root.networkService.pathAlias(root.path) : null
182
+ // Answers the one network host ui/WindowBody.qml builds with the window.
183
+ function ensureNetworkService() {
184
+ if (root.networkService) return root.networkService
185
+ if (root.sharedNetworkService) { root.networkService = root.sharedNetworkService; return root.networkService }
186
+ if (root.overlayParent) root.networkService = root.overlayParent.ensureNetworkService()
187
+ return root.networkService
188
+ }
189
+ readonly property var sidebar: root.sharedSidebar || railHost.item
190
+ readonly property real sidebarWidth: railHost.railWidth
191
+ // RailAdditions rule 4 and directive 77, both answered in ui/PaneRail.qml.
192
+ readonly property bool railHidden: railHost.hidden
193
+ // What the rail takes from the pane, which an overlay never does; the sidebar case reads it.
194
+ readonly property real railInset: railHost.inset
195
+ readonly property bool railRevealed: railHost.revealed
196
+ readonly property real railX: railHost.railX
197
+ // Directive 77: a withdrawn rail is still a place Tab can go, because arriving there reveals it.
198
+ readonly property bool railAvailable: root.sidebar !== null || railHost.overlay
199
+ function toggleRail() { ViewState.toggleRail() }
200
+ // Ctrl+E with the rail hidden: the Sidebar is unloaded, so the key cannot read entries or
201
+ // release through it; PaneRail spins a transient DeviceMounts for one listing instead.
202
+ function ejectHidden() { railHost.ejectHidden() }
203
+ // Hiding a focused rail strands the keyboard on a view that no longer exists, so the listing takes it.
204
+ onRailHiddenChanged: if (root.railHidden) Focus.railHidden(root)
205
+ property bool paneFocused: true
206
+ property bool listOnly: false
207
+ signal focusRequested()
208
+ // A list-only pane draws no view of its own, so a view chosen on it goes to the window, which routes it as its chrome does.
209
+ signal viewRequested(string mode)
210
+ // Which view Tab last handed the keyboard to; the rail's own cursor is read the same way.
211
+ property string focusView: Focus.LIST
212
+ readonly property int railCursor: root.sidebar ? root.sidebar.cursorIndex : 0
213
+ // So a test can wait for the rail's async FileViews instead of sleeping and guessing.
214
+ readonly property int railCount: root.sidebar ? root.sidebar.entries.length : 0
215
+ property var preview: null
216
+ // WindowBody's Quick Look loader; quickLook() builds the overlay on the first open and answers it.
217
+ property var previewLoader: null
218
+ function quickLook() {
219
+ if (root.previewLoader)
220
+ root.previewLoader.active = true
221
+ return root.preview
222
+ }
223
+ property var statusBar: null
224
+ // shell.qml's ui/ShareBrowser.qml overlay, wired the same way as preview above.
225
+ property var shareBrowser: null
226
+ // shell.qml's ui/KeymapSheet.qml, which ? opens from either the list or the rail.
227
+ property var keymapSheet: null
228
+ // shell.qml's ui/SettingsPanel.qml, which the comma key opens from the list and the rail alike, see act() below.
229
+ property var settingsPanel: null
230
+ property Item overlayParent: null
231
+ readonly property alias trash: trashHost
232
+ // Set by the window's flash host while it covers this pane; every gate below reads it.
233
+ property bool flashActive: false
234
+ readonly property alias menuActions: menuActions
235
+ readonly property alias menuHost: menu
236
+ readonly property var emptyState: paneStates.emptyItem
237
+ readonly property var stateMessageItem: paneStates.messageItem
238
+ readonly property alias retrySelectionText: wire.retrySelectionText
239
+ // The listing swap, ui/PaneSwap.qml: ui/js/Nav.js starts a hold through it and the views read holding.
240
+ readonly property alias swap: wire.swap
241
+ readonly property alias wire: wire
242
+ // The rows serialize on the first read after they change, never inside the landing turn; rows are only replaced
243
+ // whole, so the array the text came from tells a stale text whatever order the handlers run in.
244
+ readonly property var rowsIdentity: ({ rows: null, text: "" })
245
+ onRowsChanged: { root.rowsIdentity.rows = null; root.rowsIdentity.text = ""; Ops.checkZip(root) }
246
+ // Notifies whenever an input of menuSelectionIdentity() does, and serializes nothing.
247
+ readonly property var menuSelectionInputs: [root.path, root.held, root.selectionVersion, root.cursorIndex,
248
+ root.total, root.listInFlight, root.rows]
249
+ function menuSelectionIdentity() {
250
+ if (root.rowsIdentity.rows !== root.rows) {
251
+ root.rowsIdentity.rows = root.rows
252
+ // A tag write changes only a row's tg, which names no file, so a menu opened before it still names the same rows.
253
+ root.rowsIdentity.text = JSON.stringify(root.rows, function (key, value) { return key === "tg" ? undefined : value })
254
+ }
255
+ return JSON.stringify([root.path, root.held, root.selectionVersion, root.cursorIndex, root.total,
256
+ root.listInFlight]) + "\n" + root.rowsIdentity.text
257
+ }
258
+
259
+ signal opened(string path)
260
+ signal message(string text, bool isError)
261
+ // The dd prompt of an arm, said apart from message so the status bar never reads a sentence to find it.
262
+ signal armPrompted(string text)
263
+ signal forgetMessage(string text)
264
+ signal operationResult(string headline, string detail, bool isError)
265
+ // The status bar's sticky slot, which unlike message does not time out; empty clears it.
266
+ signal sticky(string text)
267
+ // The same slot for the GVFS bridge's Starting line, so the status bar lights its busy mark from the call and not from the sentence.
268
+ signal stickyStarting(string text)
269
+ // The one popup, hosted in shell.qml beside the network dialog rather than inside the pane.
270
+ signal convertRequested(string name)
271
+ // Archives040: the password compress resolves through the cards host, which Focus reaches
272
+ // through this signal so Focus takes no new first-frame import.
273
+ signal compressPasswordRequested(var paths, int menuId)
274
+ signal extractPasswordRequested(string path, string dest, string err)
275
+ signal permissionsRequested(string path)
276
+ // MenuAdditions040: the whole selection's paths for the multi-row card.
277
+ signal permissionsBatchRequested(var paths)
278
+ // Bulk rename takes the whole selection's paths for its card.
279
+ signal bulkRenameRequested(var paths)
280
+ signal pathBarRequested() // ":" and Ctrl+L; the bar is chrome, so shell.qml opens it as it does the popup above
281
+ signal textSizeRequested(int direction) // issue 9's zoom pair, +1, -1 or 0 to follow Omarchy again; the size is the window's
282
+
283
+ // The window covers [held, held + rows.length) and nothing outside it is in memory.
284
+ property int held: 0
285
+ property var rows: []
286
+ // The current response's Kind dictionary; every row's own "k" is an index into this one array.
287
+ property var kindNames: []
288
+ // The listing rows the filter leaves standing, null when none is up: a view position goes in and
289
+ // a listing row comes out, which is what every row-indexed cache and the selection still hold.
290
+ readonly property var shown: Filter.shown(root.rows, root.held, root.filterQuery)
291
+ readonly property int shownTotal: root.shown === null ? root.total : root.shown.length
292
+
293
+ readonly property int minBuffer: 50
294
+ readonly property int maxBuffer: 1000
295
+ readonly property int defaultBuffer: 150
296
+ property int bufferRows: defaultBuffer
297
+ readonly property int buffer: Math.max(minBuffer, Math.min(maxBuffer, bufferRows))
298
+ readonly property int visibleRows: Math.max(1, Math.ceil(list.height / Theme.fileRowHeight))
299
+ readonly property int windowSize: visibleRows + 2 * buffer
300
+ // The leading result rows a live answer may carry folder sizes for: none when the shown view would ask for none.
301
+ readonly property int sizeWish: DirSizes.wantsSizes(root.viewMode, ViewState.hiddenCols, root.storageClass, root.storageKnown) ? root.visibleRows : 0
302
+ readonly property int refetchMargin: 25
303
+ readonly property int cacheRows: 4
304
+ readonly property int coalesceMs: 16
305
+ // A settle, not a stream: a fling must issue no request at all, see AGENTS.md "Thumbnail requests in the GUI".
306
+ readonly property int settleMs: 120
307
+ // The first screen's settle only has to outlast the compositor's resize, see AGENTS.md "Thumbnail requests in the GUI".
308
+ readonly property int firstSettleMs: 70
309
+ // Seven screens of history at this row height, so a policy bug costs memory slowly, not without limit.
310
+ readonly property int thumbCap: 240
311
+ // Sizes are only ever what the user asked for, so the map holds a whole listing's folders.
312
+ readonly property int dirSizeCap: 1000000
313
+ property var thumbState: Thumbs.empty()
314
+ // The class verdict the last refresh spent, so a preview change that moves nothing for
315
+ // this directory (zoom step, other classes) refreshes nothing; see ui/js/ExtThumbs.js.
316
+ property string extVerdict: ExtThumbs.verdict("", ViewState.preview)
317
+ // Same cap as thumbState, same reason; a directory row's size is only ever asked for by a viewport.
318
+ property var dirSizeState: DirSizes.empty()
319
+ // The filesystem answers user extended attributes here, probed once per directory beside the fsinfo figures.
320
+ property bool canTag: true
321
+ // File tags (ui/TagState.qml) are built when a listing first carries one or a tag key or menu first asks.
322
+ property var tagState: null
323
+ function tags() {
324
+ if (root.tagState === null) root.tagState = Qt.createComponent("TagState.qml").createObject(root, { pane: root })
325
+ return root.tagState
326
+ }
327
+ // Input to rows is stamped inside the UI, because a harness that polls IPC across the interval times itself; see AGENTS.md "Testing".
328
+ property real inputAt: 0
329
+ property real rowsAt: 0
330
+
331
+ // The cut or copied paths, absolute because a paste lands in a different directory; see ui/js/Ops.js.
332
+ property var clipboard: Ops.emptyClipboard()
333
+ // Shared system clipboard session, including outstanding backend replies and owner tokens.
334
+ property var clipboardState: Clipboard.state()
335
+ // True when this pane's clipboard watcher could not start; Paste then reads on demand.
336
+ property bool clipboardWatchFailed: false
337
+ // The outstanding ordered clipboard request, or null; late replies cannot publish an older choice.
338
+ property var clipPending: null
339
+ // Ordered choices waiting for the untagged paths reply or already resolved by a menu snapshot.
340
+ property var clipQueue: []
341
+ // Monotonic choice number; only the newest request can publish to the system clipboard.
342
+ property int clipSequence: 0
343
+ // Which asker a pending paths reply belongs to, null meaning the clipboard, which is what every
344
+ // reply meant before compress also had to resolve a selection wider than this pane holds.
345
+ property var pathsPending: null
346
+ // A file drag in progress holds the watched re-read, so rows never shift under a live pointer.
347
+ property bool dragHolding: false
348
+ property bool awaitingPaths: false
349
+ readonly property int anchorRowHeight: Theme.fileRowHeight
350
+ // What the status bar's sticky slot is reporting, or an idle transfer; see ui/js/Ops.js.
351
+ property var transfer: Ops.emptyTransfer()
352
+ // The row that is its own editor right now, or -1; ui/List.qml's delegate reads it per row.
353
+ property int renamingIndex: -1
354
+ // Set by a pointer-committed rename so the reply reveals nothing; ui/js/Nav.js clears it.
355
+ property bool renameKeepsPointerRow: false
356
+
357
+ // Read once for the window: the chrome's path and the search strip's scope both shorten with it.
358
+ readonly property string home: Quickshell.env("HOME") || ""
359
+
360
+ // "list", "columns" or "grid"; the chrome's own buttons write it and the views read it.
361
+ property string viewMode: "list"
362
+ Component.onCompleted: root.viewMode = root.desiredView(root.path)
363
+ // A view switch takes the filter with it; only chooseView() and adoptView() persist, so a restored view writes nothing.
364
+ onViewModeChanged: Filter.close(root)
365
+ // The folder a view belongs to; a search, Recent or Trash standing over it is not that folder.
366
+ readonly property string viewFolder: root.searchMode.length === 0 && root.recentMode.length === 0 && !trashHost.opened ? root.path : ""
367
+ // Dual draws lists in both panes, and a folder's own view applies only while it is remembered.
368
+ function desiredView(path) {
369
+ return root.listOnly || root.dualMode ? "list" : FolderViews.viewFor(ViewState.folderViews, path, ViewState.view,
370
+ ViewState.rememberViewPerFolder)
371
+ }
372
+ function restoreView(path) {
373
+ var desired = root.desiredView(path)
374
+ if (root.viewMode !== desired)
375
+ root.viewMode = desired
376
+ }
377
+ // A tab brings back the view it held: a remembered folder keeps its own, any other view becomes the standing one.
378
+ function adoptView(path, mode) {
379
+ var own = ViewState.rememberViewPerFolder && FolderViews.has(ViewState.folderViews, path)
380
+ if (!own && !root.dualMode && mode !== ViewState.view)
381
+ ViewState.changeKey("view", mode)
382
+ root.restoreView(path)
383
+ }
384
+ // Effective values, so a settled ui.json that only spells out the defaults re-lists nothing and leaves the view where it is.
385
+ readonly property string listingPreferences: Sort.preferences(ViewState.state)
386
+ property string appliedListingPreferences: ""
387
+ // A preference re-list keeps selection and cursor by name, Finder's rule; the watcher defers
388
+ // while a selection stands, this one preserves it instead. Cleared when its rows land.
389
+ property var preferenceAnchor: null
390
+ onListingPreferencesChanged: {
391
+ // A hidden dual pane retains its session sort when the single pane changes the saved default.
392
+ if (root.backend && root.backend.preserveSort && !root.visible && root.appliedListingPreferences.length > 0) {
393
+ var applied = JSON.parse(root.appliedListingPreferences)
394
+ applied[1] = ViewState.state.sort
395
+ root.appliedListingPreferences = JSON.stringify(applied)
396
+ }
397
+ preferences.restart()
398
+ }
399
+ onVisibleChanged: if (root.visible) preferences.restart()
400
+ // A restored folder that never answers leaves "loading" for the waiting state, with Home
401
+ // reachable through it; the late rows, if they ever land, are dropped by the swap as stale.
402
+ readonly property int listingWaitMs: Startup.LISTING_WAIT_MS
403
+ Timer {
404
+ id: listingWait
405
+ interval: root.listingWaitMs
406
+ onTriggered: {
407
+ if (root.listInFlight && root.listingState === "loading") {
408
+ root.listingState = "waiting"
409
+ root.stateMessage = "That folder is not responding."
410
+ }
411
+ }
412
+ }
413
+ onListInFlightChanged: if (!root.listInFlight) { preferences.restart(); Sort.applyPending(root); listingWait.stop() } else listingWait.restart()
414
+ onSearchModeChanged: { preferences.restart(); root.moveSearchFocus() }
415
+ // flea-qml: typing gives the Find field the keyboard; leaving it hands the keyboard back to the list.
416
+ function moveSearchFocus() {
417
+ if (root.searchMode === Search.TYPING) Qt.callLater(header.focusSearchField)
418
+ else if (header.searchFieldFocused && root.listArea) root.listArea.forceActiveFocus()
419
+ }
420
+ onRecentModeChanged: if (root.recentMode.length === 0) preferences.restart()
421
+ Timer {
422
+ id: preferences
423
+ interval: 0
424
+ onTriggered: {
425
+ root.restoreView(root.viewFolder)
426
+ if (!root.visible || !root.path || root.listInFlight || root.searchMode.length > 0
427
+ || root.recentMode.length > 0
428
+ || root.appliedListingPreferences === root.listingPreferences) return
429
+ root.preferenceAnchor = Anchor.preference(root)
430
+ root.openWithoutHistory(root.path, { inPlace: true })
431
+ if (root.preferenceAnchor && root.preferenceAnchor.start > 0)
432
+ root.backend.window(root.preferenceAnchor.start, root.windowSize)
433
+ }
434
+ }
435
+ Connections {
436
+ target: ViewState
437
+ function onStateChanged() { preferences.restart() }
438
+ // A class switched on in Settings decodes again the same way the menu row does.
439
+ function onPreviewChanged() { root.refreshExtThumbs() }
440
+ }
441
+ // The zip probe lands here; the open menu is never rebuilt for it, the next opening draws it.
442
+ Connections {
443
+ target: root.backend
444
+ function onFlashImageResult(message) { Ops.noteZip(root, message) }
445
+ }
446
+
447
+
448
+ // Each tab retains its own back and forward navigation.
449
+ property var history: []
450
+ property var forwardHistory: []
451
+ property var tabs: null
452
+ property int tabCountNotice: -1
453
+ readonly property bool canGoBack: trashHost.opened || root.recentMode.length > 0 || root.history.length > 0
454
+ readonly property bool canGoUp: root.path.length > 1 || root.recentMode.length > 0 || Nav.climbsFromListing(root)
455
+
456
+ // The filesystem line the status bar draws, refreshed once per directory rather than per row.
457
+ property string fsName: ""
458
+ property real fsFree: 0
459
+ // False from a request for another folder until the backend's fsinfo line for it lands, so the strip says nothing, not unknown, before then.
460
+ property bool fsAnswered: false
461
+ // ExtThumbs: src/backend/extclass.rs's own word for the directory being shown, network,
462
+ // phone, usb or "", carried beside the fsinfo line once per directory change, never per row.
463
+ // storageKnown is false until that line lands, so the first settle never spends unknown as local.
464
+ property string storageClass: ""
465
+ property bool storageKnown: false
466
+ // The backend's fact that a system mountpoint holds this directory, found by its device number, so a link or bind mount counts as its volume.
467
+ property bool systemVolume: false
468
+
469
+ // Back and up close Recent rather than travelling; forward stays put outside the history.
470
+ function goBack() { if (root.flashActive) root.overlayParent.closeFlash(); else if (trashHost.opened) trashHost.close(); else if (root.recentMode.length > 0) RecentMode.close(root); else Nav.back(root) }
471
+ function goForward() { if (!trashHost.opened && !root.flashActive && root.recentMode.length === 0) Nav.forward(root) }
472
+
473
+ // Rename lives in ui/js/Ops.js with the other write operations; ui/List.qml's editor commits through this.
474
+ function commitRename(newName) { Ops.commitRename(root, newName) }
475
+ property int renameMenuId: 0
476
+ property string renameSource: ""
477
+ property string renameError: ""
478
+ property var renameRequest: null
479
+ readonly property bool renamePending: root.renameRequest !== null
480
+ // A sort asked for while an edit was open or a rename pending, held by
481
+ // ui/js/Sort.js and applied when the edit ends; never dropped, see applyPending.
482
+ property var pendingSort: null
483
+ property var convertSource: null
484
+ onRenamingIndexChanged: if (root.renamingIndex < 0) {
485
+ root.renameMenuId = 0
486
+ root.renameSource = ""
487
+ root.renameError = ""
488
+ // A rename that settled and an Escape that abandoned both end here, so a
489
+ // held sort applies on either; a navigation holds listInFlight and drops it.
490
+ Sort.applyPending(root)
491
+ }
492
+ // A navigation during a slow rename leaves renamingIndex at -1 with the
493
+ // request still pending, so no index change fires when it settles.
494
+ onRenamePendingChanged: if (!root.renamePending) Sort.applyPending(root)
495
+
496
+ // A set of row indices over the current listing, mutated in place; selectionVersion tells a reactive binding (List.qml's delegate, StatusBar's count) to re-read it. Task 8 declined ScriptModel plus ItemSelectionModel on measured memory, see AGENTS.md "The list model".
497
+ property var selection: Selection.create()
498
+ property int selectionVersion: 0
499
+ property int selectionAnchor: 0
500
+ property var selectionBand: null
501
+ // A button is down over the header or the view; ui/PaneStates.qml writes it, and ui/js/Anchor.js busy holds a watched re-read while it is true.
502
+ property bool pointerHeld: false
503
+ function isSelected(index) { return root.selectionVersion >= 0 && root.selection.has(index) }
504
+ function selectionCount() { return root.selectionVersion >= 0 ? root.selection.count() : 0 }
505
+ function selectedIndices() { return root.selectionVersion >= 0 ? root.selection.indices() : [] }
506
+ function toggleSelect() { Marks.toggleSelect(root) }
507
+ function selectAll() { Marks.selectAll(root); root.selectionVersion++ }
508
+ function clearSelection() { root.selection.clear(); root.selectionVersion++ }
509
+ function selectOnly(index, context) {
510
+ root.setCursor(index, context)
511
+ root.selection.only(root.cursorIndex)
512
+ root.selectionAnchor = root.cursorIndex
513
+ root.selectionVersion++
514
+ }
515
+ function extendSelection(delta) { Marks.extend(root, delta) }
516
+ // Ctrl+click and shift+click, the mouse's twins of v and shift+j/k; see keys.toml's [[pointer]].
517
+ function toggleSelectAt(index) { Marks.toggleRow(root, index) }
518
+ function extendSelectionTo(index) { Marks.extendToRow(root, index) }
519
+ // Named escapePressed, not escape, which collides with the JS global URI function; clears an active selection first, see keys.toml.
520
+ function escapePressed() { if (root.selection.count() > 0) { root.clearSelection(); return }; root.message("", false) }
521
+
522
+ function applyPendingSelect() { Nav.applyPendingSelect(root); Nav.applyPendingBackground(root) }
523
+ function openBackgroundMenu(at) { menu.openBackground(at) }
524
+ // flea-qml: measure folders only when the user asks; see src/dirsize.js and ui/js/DirSizes.js.
525
+ function measureDirs(rows) {
526
+ var dirs = rows.filter(function (i) { var r = root.rowFor(i); return r && r.d })
527
+ if (dirs.length === 0) return
528
+ root.dirSizeState = DirSizes.asking(root.dirSizeState, dirs)
529
+ root.backend.dirsize(dirs)
530
+ }
531
+ function measureAllDirs() {
532
+ root.dirSizeState = DirSizes.askingAll(root.dirSizeState)
533
+ root.backend.dirsizeAll()
534
+ }
535
+
536
+ function refresh(selectPath, keepSelection) {
537
+ // Recent re-reads the history rather than re-listing its base, which is the root: see
538
+ // ui/js/RecentMode.js refresh, and Nav.refresh for every other listing.
539
+ if (root.recentMode === "trash" || (root.recentMode.length > 0 && keepSelection !== true)) RecentMode.refresh(root, selectPath)
540
+ else Nav.refresh(root, selectPath, keepSelection)
541
+ }
542
+
543
+ function open(newPath) {
544
+ if (trashHost.confirming) return
545
+ if (root.flashActive) root.overlayParent.closeFlash()
546
+ Nav.open(root, newPath)
547
+ }
548
+
549
+ // An eject releases the mount under path: queued reads stop first, the preview lets go,
550
+ // and a pane showing that volume moves Home rather than standing on a vanished folder.
551
+ function quiesceVolume(path) {
552
+ if (root.backend)
553
+ root.backend.quiesce()
554
+ if (root.preview)
555
+ root.preview.close()
556
+ var home = root.home
557
+ if (home.length > 0 && String(path).length > 0
558
+ && (root.path === path || root.path.indexOf(path + "/") === 0))
559
+ Nav.openPlace(root, home)
560
+ }
561
+
562
+ // A refused hop clears nothing: only a navigation takes the place of a listing out, and Recent's rows must survive a re-list that does not.
563
+ function openWithoutHistory(newPath, options) {
564
+ // Only a navigation lets a waiting listing go; an in-place re-list of the folder being left keeps it.
565
+ if (!options || options.inPlace !== true) Anchor.clearWaiting(root)
566
+ // Only a navigation takes the place of a listing out, and the folder asked for last has its own wait, not the first one's clock.
567
+ if (root.listInFlight && (!options || options.supersede !== true)) {
568
+ root.message("A directory is already loading.", false)
569
+ return
570
+ }
571
+ if (root.listInFlight) listingWait.restart()
572
+ // Every real navigation leaves Recent, Trash and Flash; options.inPlace, a re-list of this folder, leaves Trash up.
573
+ if (!options || options.inPlace !== true) { trashHost.close(); if (root.flashActive) root.overlayParent.closeFlash() }
574
+ RecentMode.leave(root)
575
+ var applied = root.appliedListingPreferences ? JSON.parse(root.appliedListingPreferences) : []
576
+ // Search exit can enter here before the preferences timer consumes a deferred Settings change.
577
+ if (JSON.stringify(applied[1]) !== JSON.stringify(ViewState.state.sort)) root.backend.resetSort(newPath)
578
+ // options.keepHidden is the tab restore's alone: it just put back this tab's own dotfile answer, which the standing preference would overwrite.
579
+ if (!options || options.keepHidden !== true) root.showHidden = ViewState.state.hidden === true
580
+ Nav.openWithoutHistory(root, newPath, options)
581
+ }
582
+
583
+ // The toggle re-lists rather than filtering client-side: the model is a row count over the
584
+ // backend's own listing, which never held the dotfiles to begin with when they were off.
585
+ // Re-listing also clears the cursor and selection, the same as opening any other directory.
586
+ // Recent lists what the history names, dotfiles among them, so the toggle has nothing to hide there.
587
+ function toggleHidden() {
588
+ if (root.recentMode.length > 0) { root.message("This collection includes hidden items", false); return }
589
+ root.showHidden = !root.showHidden
590
+ ViewState.changeKey("hidden", root.showHidden)
591
+ // flea-qml: a standing Find takes the new setting and stays, rather than leaving for the folder.
592
+ if (root.searchMode.length > 0) { Search.rehide(root); return }
593
+ root.open(root.path)
594
+ }
595
+
596
+ // Where a drop on this pane lands. While a listing is out the pane's own path is still the
597
+ // directory it is leaving, so a drop taken in that window landed in the wrong one: measured by
598
+ // tests/drag.sh R7, where a drop on a tab whose listing was still out copied into the source.
599
+ readonly property string dropPath: root.listInFlight && root.listingPath.length > 0
600
+ ? root.listingPath : root.path
601
+
602
+ function rowFor(index) {
603
+ var offset = index - root.held
604
+ if (offset < 0 || offset >= root.rows.length)
605
+ return null
606
+ return root.rows[offset]
607
+ }
608
+
609
+ // The list's own delegate, which is what every reader of a rendered row cell needs; only the
610
+ // list draws those cells at all.
611
+ function itemFor(index) {
612
+ return list.itemAtIndex(index)
613
+ }
614
+
615
+ // The delegate actually on screen, whichever view is showing. A coordinate taken off a hidden
616
+ // view is a coordinate nothing can be clicked at, which is what the columns view used to return.
617
+ // index is a listing row and every view's itemAtIndex wants a view position, which is a different
618
+ // number under a filter: passing the listing row answered null for a row plainly on screen, and
619
+ // once the match count passed it, the delegate at that position under an unrelated row.
620
+ function visibleItemFor(index) {
621
+ return root.listArea.itemAtIndex(Filter.viewOf(root.shown, index))
622
+ }
623
+
624
+ // shell.qml's IPC thumbFile reader calls this; the lookup lives with the thumbnail machinery in ui/List.qml.
625
+ function thumbFor(index) { return list.thumbFor(index) }
626
+
627
+ // Lifted to Focus.act, see ui/js/Focus.js, which routes "settings" here from the list and the rail alike.
628
+ function act(action, menuId, paths, context) {
629
+ // Every dispatched action ends a pending slow click; its own rename runs after the timer cleared the record.
630
+ cancelSlowClick()
631
+ if (trashHost.confirming) return
632
+ if (action === "openTrash" || action === "emptyTrash" || action === "restoreAll") { trashHost.action(action); return }
633
+ if (trashHost.browsing && ["restoreTrashSelection", "deletePermanently", "trash"].indexOf(action) >= 0) { trashHost.action(action, menuId); return }
634
+ if (action === "settings") { root.settingsPanel.open(root); return }
635
+ if (action === "toggleLangMarks") { ViewState.toggleLangMarks(); return }
636
+ if (action === "permissions") { root.openPermissions(); return }
637
+ if (action === "rename" && root.openBulkRename(paths)) return // several rows open the bulk card
638
+ // Recent is a history, not a directory: a new file there would land in the root it stands on.
639
+ if (action === "newFile" && root.recentMode.length > 0) { root.message((root.recentMode === "trash" ? "Trash" : "Recent") + " is a collection. Open a folder first", false); return }
640
+ if (["newFile", "rename", "openWith", "moveTo", "copyTo", "properties", "deletePermanently"].indexOf(action) >= 0) {
641
+ menuActions.open(action, menuId || 0, context)
642
+ return
643
+ }
644
+ Focus.act(action, root, menuId, paths)
645
+ }
646
+ // The menu's own dispatch lives with the rest of the menu machinery; this is the one seam the
647
+ // rail's place menu and the dialogs still call through.
648
+ function performMenu(action, menuId, paths) { menuActions.perform(action, menuId, paths) }
649
+ // The sheet runs a row with no menu opened, so stand the menu on the cursor row as openAt does, or validateChoice finds nothing it opened.
650
+ function sheetMenuAction(action) {
651
+ menu.clearRail()
652
+ menu.forHeader = false
653
+ menu.hasRow = true
654
+ menu.openedIdentity = root.menuSelectionIdentity()
655
+ menuActions.snapshot()
656
+ menuActions.sheetRequest = menuActions.requestId
657
+ menuActions.activate(action, true)
658
+ }
659
+
660
+ function permissionSelection() {
661
+ var indices = Ops.targetIndices(root)
662
+ return indices.length === 1 ? root.rowFor(indices[0]) : null
663
+ }
664
+ // MenuAdditions040 callout 10: Make executable acts on exactly one target and that target is
665
+ // the cursor row; anything else hides the row and the action refuses.
666
+ function isSingleCursorTarget() {
667
+ var indices = Ops.targetIndices(root)
668
+ return indices.length === 1 && indices[0] === root.cursorIndex
669
+ }
670
+ // The cursor row's two-byte shebang probe, asked of the backend once per menu open for that one file.
671
+ property bool rowHasShebang: false
672
+ property string shebangAsked: ""
673
+ property int shebangId: 0
674
+ property int makeExecPendingId: 0
675
+ // Show original's own pending id, dropped by any navigation before its reply lands.
676
+ property int linkTargetPendingId: 0
677
+ // The one path a shebang answer may land for, "" unless the cursor row is a regular file without any execute bit.
678
+ function shebangTarget() {
679
+ var row = root.cursorRow
680
+ if (!row || row.d) return ""
681
+ var bits = Number(row.p) || 0
682
+ if ((bits & Format.S_IFMT) !== Format.S_IFREG || (bits & Format.ANY_EXECUTE_BIT) !== 0) return ""
683
+ return root.join(Search.baseOf(root), row.n)
684
+ }
685
+ function checkShebang() {
686
+ var path = root.shebangTarget()
687
+ if (path.length === 0) {
688
+ root.shebangAsked = ""
689
+ root.rowHasShebang = false
690
+ return
691
+ }
692
+ root.shebangId += 1
693
+ root.shebangAsked = path
694
+ root.rowHasShebang = false
695
+ root.backend.send({ c: "shebang", path: path, id: root.shebangId })
696
+ }
697
+ // Only the newest id on the asked path lands, so a late answer for an earlier file never arms a later one.
698
+ function noteShebang(path, hasShebang, id) {
699
+ if (!Permissions.landsShebang(path, id, root.shebangAsked, root.shebangId)) return
700
+ root.rowHasShebang = hasShebang
701
+ if (menu.opened && menu.hasRow) menu.refreshProviderRows()
702
+ }
703
+ // MenuAdditions040 callout 10: adds the owner execute bit through the permissions batch, so one undo restores it.
704
+ // The context menu is its one caller, and it acts on the single cursor target.
705
+ function makeExecutable() {
706
+ if (!root.isSingleCursorTarget()) {
707
+ root.message("Make executable works on a script that cannot run yet", false)
708
+ return
709
+ }
710
+ var row = root.rowFor(root.cursorIndex)
711
+ if (!row || !Menu.canMakeExecutable(row.p, 1, root.rowHasShebang, true)) {
712
+ root.message("Make executable works on a script that cannot run yet", false)
713
+ return
714
+ }
715
+ var taken = [root.join(Search.baseOf(root), row.n)]
716
+ var oldBits = (Number(row.p) || 0) & 0o7777
717
+ var octal = Permissions.octal((oldBits | Format.S_IXUSR) & 0o7777)
718
+ root.makeExecPendingId += 1
719
+ root.backend.send({ c: "permissionsBatch", paths: taken, modes: [octal], id: Permissions.MAKE_EXEC_ID + root.makeExecPendingId })
720
+ }
721
+ // MenuAdditions040: Permissions takes the whole selection, so the menu
722
+ // row carries every target's mode and the dialog inspects each in turn.
723
+ function permissionModes() {
724
+ var out = []
725
+ var indices = Ops.targetIndices(root)
726
+ for (var i = 0; i < indices.length; i++) {
727
+ var row = root.rowFor(indices[i])
728
+ if (row) out.push(row.p)
729
+ }
730
+ return out
731
+ }
732
+ function openPermissions() {
733
+ var idx = Ops.targetIndices(root)
734
+ if (idx.length === 0) { Ops.sayNoTarget(root); return }
735
+ if (root.pathsPending || root.clipPending !== null) { Ops.pathsBusy(root); return }
736
+ // The selection can reach past the held window, so the backend
737
+ // resolves the indices while it still can, the same rule clip follows.
738
+ root.pathsPending = { kind: "permissions" }
739
+ root.backend.askPaths(idx)
740
+ }
741
+ function openPermissionsWith(paths) {
742
+ if (paths.length === 0) { Ops.sayNoTarget(root); return }
743
+ if (paths.length === 1) {
744
+ var target = root.permissionSelection()
745
+ var single = target ? target.p : 0
746
+ if (Menu.permissionsEntry(single, 1).disabled) {
747
+ root.message("Permissions takes one item, not a link.", true)
748
+ return
749
+ }
750
+ root.permissionsRequested(paths[0])
751
+ return
752
+ }
753
+ root.permissionsBatchRequested(paths)
754
+ }
755
+ // Several rows open the bulk card; one row answers false so the caller keeps its single-row route.
756
+ function openBulkRename(paths) {
757
+ if (paths && paths.length > 1) { root.bulkRenameRequested(paths); return true }
758
+ var idx = paths && paths.length ? [] : Ops.targetIndices(root)
759
+ if (idx.length <= 1) return false
760
+ if (root.pathsPending || root.clipPending !== null) { Ops.pathsBusy(root); return true }
761
+ root.pathsPending = { kind: "bulkrename" }
762
+ root.backend.askPaths(idx)
763
+ return true
764
+ }
765
+ // The backend's answer to openBulkRename's askPaths.
766
+ function openBulkRenameWith(paths) {
767
+ if (paths.length <= 1) { Ops.sayNoTarget(root); return }
768
+ root.bulkRenameRequested(paths)
769
+ }
770
+ // One line for the whole batch, and one undo reverses every rename it made.
771
+ function bulkRenamed(ok) {
772
+ root.message(Ops.bulkDoneLine(ok), false)
773
+ root.clearSelection()
774
+ root.refresh("")
775
+ }
776
+ // c and P open Copy as and Paste as on their first flyout row.
777
+ function openCopyAs() {
778
+ if (!root.openCursorMenu()) {
779
+ root.message("No item under the cursor to open a menu on", false)
780
+ return
781
+ }
782
+ menu.openSubmenuFor("copyAs")
783
+ }
784
+ function openPasteAs() {
785
+ if (!root.openCursorMenu())
786
+ menu[root.cursorRow ? "openAt" : "openBackground"](root.listSlot.mapToItem(null, root.listSlot.width / 2, root.listSlot.height / 2))
787
+ if (!menu.clipboardAvailable) {
788
+ menu.close()
789
+ root.message(Menu.EMPTY_CLIPBOARD, false)
790
+ return
791
+ }
792
+ menu.openSubmenuFor("pasteAs")
793
+ }
794
+ // MenuAdditions040: V flips the marks over the rows the listing draws;
795
+ // the filter applies, and a close match is never selected.
796
+ function invertSelection() { Marks.invert(root) }
797
+ // MenuAdditions040: Show original reveals a symlink's target in its own
798
+ // folder, the same path Show in folder uses.
799
+ function showOriginal() {
800
+ var row = root.rowFor(root.cursorIndex)
801
+ if (!row || !Format.isSymlink(row.p)) {
802
+ root.message("Show original needs the cursor on a link", false)
803
+ return
804
+ }
805
+ root.requestLinkTarget(root.join(Search.baseOf(root), row.n))
806
+ }
807
+ // Both entrances send through here, so one pending id covers the cursor and the menu.
808
+ function requestLinkTarget(path) {
809
+ root.linkTargetPendingId = root.backend.nextLinkTargetId()
810
+ root.backend.send({ c: "linktarget", path: path, id: root.linkTargetPendingId })
811
+ }
812
+ // Paste as links reads the file clipboard and asks through the collision card.
813
+ function pasteLink(kind) {
814
+ if (RecentMode.refusePaste(root)) return
815
+ Ops.pasteLink(root, kind)
816
+ }
817
+
818
+ // index is a listing row, which is what every caller outside ui/js/Filter.js holds; the clamp
819
+ // and the scroll both happen in view space, because a filter can be narrowing what is drawn.
820
+ // A click carries context 0; a keyboard move keeps the default 3.
821
+ function setCursor(index, context) { Filter.setCursor(root, index, context) }
822
+ // The cursor keeps three rows of context: the list and the columns view hold it through their own showCursor, the grid keeps Contain after its landing window.
823
+ function showRow(view, context) {
824
+ if (root.viewMode === "columns" && root.columnsArea) root.columnsArea.activeColumn().showCursor(view, context)
825
+ else if (root.viewMode === "list") list.showCursor(view, context)
826
+ else root.listArea.showCursor(view, context)
827
+ root.listArea.restartCoalesce()
828
+ }
829
+
830
+ // A successful pointer commit preserves the newly selected row; a refusal returns to its editor.
831
+ function commitOpenRename() {
832
+ var item = root.renameEditor()
833
+ if (!item || root.renamePending) return
834
+ root.renameKeepsPointerRow = true
835
+ if (!item.commitEditor() || root.renameError.length > 0) root.renameKeepsPointerRow = false
836
+ }
837
+
838
+ // The live editor or null: a set renamingIndex is not evidence one exists, see ui/RenameField.qml.
839
+ function renameEditor() {
840
+ if (root.renamingIndex < 0) return null
841
+ // The columns view draws one editor over its active column rather than one inside each row.
842
+ if (root.viewMode === "columns")
843
+ return root.columnsArea && root.columnsArea.activeColumn().renaming ? root.columnsArea.activeColumn() : null
844
+ var item = root.viewMode === "grid" ? (root.listArea ? root.listArea.currentRenameTile(root) : null) : root.visibleItemFor(root.renamingIndex)
845
+ return item && item.renaming ? item : null
846
+ }
847
+
848
+ function openCursor() { Nav.openCursor(root, wire.opener) }
849
+
850
+ // A path the caller already resolved, for the columns view's neighbour rows, which have no cursor.
851
+ function openFile(path) { wire.opener.open(path) }
852
+
853
+ // A terminal in the caller's place or the directory being shown, through flea --terminal.
854
+ function openTerminal(path) { wire.opener.openTerminal(path || root.path) }
855
+
856
+ function newWindow() { Quickshell.execDetached(Tabs.freshLaunch([Quickshell.env("FLEA_BIN") || "flea", root.path])) }
857
+
858
+ // Quoted when it holds whitespace, because this one is pasted into a shell: see ui/js/Format.js.
859
+ function copyDirPath() { wire.opener.copyText(Format.shellQuoted(root.path)) }
860
+
861
+ function openParent() { if (root.flashActive) root.overlayParent.closeFlash(); else if (trashHost.opened) trashHost.close(); else if (root.recentMode.length > 0) RecentMode.close(root); else Nav.parent(root) }
862
+
863
+ // The sort Recent replaced, handed back on leaving; ui/js/Search.js reaches it through here.
864
+ function restoreRecentSort() { RecentMode.restoreSort(root) }
865
+ // The rail's Recent row answers with the history's paths, read bounded the way the
866
+ // path jump reads them; a second open while one lands replaces it, the way a navigation does.
867
+ function openRecent(paths, visits) { RecentMode.run(root, paths, visits) }
868
+ function closeRecent() { RecentMode.close(root) }
869
+
870
+ function join(base, name) {
871
+ if (base === "/" && (/^[A-Za-z]:[\\/]/.test(name) || name.indexOf("//") === 0)) return name
872
+ return base === "/" ? "/" + name : base + "/" + name
873
+ }
874
+
875
+ Flea.PaneWire {
876
+ id: wire
877
+ pane: root
878
+ }
879
+
880
+ Flea.PaneRail {
881
+ id: railHost
882
+ anchors.left: parent.left
883
+ // Over the listing, because with auto-hide on the rail is an overlay and not a column.
884
+ z: 3
885
+ pane: root
886
+ service: root.networkService
887
+ }
888
+
889
+ // On glass the listing's ground, beside the rail and below the path and search strips, never under them; see ui/Glass.qml.
890
+ Rectangle {
891
+ anchors { top: root.searchMode.length > 0 ? header.bottom : panePath.bottom; bottom: parent.bottom; left: railHost.right; right: parent.right }
892
+ visible: Glass.on
893
+ color: Glass.listBackground
894
+ }
895
+
896
+ Flea.PanePath {
897
+ id: panePath
898
+ anchors { left: railHost.right; right: parent.right; top: parent.top }
899
+ height: root.dualMode && !trashHost.opened ? Theme.chromeHeight : 0
900
+ visible: height > 0
901
+ // A history is a location and not a directory, so the strip says its own name.
902
+ path: root.recentMode === "trash" ? "trash:///" : root.recentMode.length > 0 ? "Recent" : root.path
903
+ home: root.home
904
+ pathAlias: root.pathAlias
905
+ focused: root.paneFocused
906
+ inputLive: !(root.preview && root.preview.active)
907
+ pane: root
908
+ onChosen: function (path) { if (path === "trash:///") root.trash.open(); else root.open(path) }
909
+ onEditRequested: root.pathBarRequested()
910
+ }
911
+
912
+ PointHandler {
913
+ id: focusPointer
914
+ acceptedButtons: Qt.AllButtons
915
+ onActiveChanged: if (active) railHost.focusPress(focusPointer.point.position)
916
+ }
917
+
918
+ Loader {
919
+ id: trashStrip
920
+ anchors { top: panePath.bottom; left: railHost.right; right: parent.right }
921
+ active: trashHost.browsing
922
+ height: active ? Theme.chromeHeight : 0
923
+ visible: active
924
+ source: "TrashToolbar.qml"
925
+ onLoaded: item.trash = trashHost
926
+ }
927
+
928
+ Flea.Header {
929
+ id: header
930
+ // Only the list view has columns to head, and neither the grid board nor the columns board
931
+ // draws one; the strip collapses rather than hiding, so the view below starts at the top of
932
+ // the pane instead of a gap. A search takes the strip whole, in any view.
933
+ visible: !trashHost.opened && !root.flashActive && (root.viewMode === "list" || root.searchMode.length > 0)
934
+ height: visible ? implicitHeight : 0
935
+ anchors.top: trashStrip.bottom
936
+ anchors.left: railHost.right
937
+ anchors.right: parent.right
938
+ // The held rows a double click fits and F4 fits whole; ListColumns040 never scans the directory.
939
+ pane: root
940
+ viewMode: root.viewMode
941
+ sortBy: root.backend.sortBy
942
+ sortDesc: root.backend.sortDesc
943
+ dualMode: root.dualMode
944
+ recent: root.recentMode.length > 0
945
+ hiddenCols: root.dualMode ? ["mode", "kind"].concat(ViewState.hiddenCols) : ViewState.hiddenCols
946
+ onSortRequested: function (key) { Sort.column(root, key) }
947
+ onMenuRequested: function (pos) { menu.openForHeader(pos) }
948
+ searchMode: root.searchMode
949
+ searchQuery: root.searchQuery
950
+ searchScope: Search.scope(Search.scopeRoot(root.path, root.home, root.searchStep), root.home)
951
+ // flea-qml: the count is the last run's; an edited line waiting for Enter says so instead.
952
+ searchNote: Search.note(root.total, root.searchRunning, root.searchSent.length === 0)
953
+ // flea-qml: the keys the strip answers, named plainly.
954
+ searchWayOut: "Enter/Tab/Esc/Ctrl+G"
955
+ searchPlaceholder: Search.placeholder(root.path, root.home, root.searchStep, root.searchContents)
956
+ searchTabHint: ""
957
+ searchRunning: root.searchRunning
958
+ onSearchCloseRequested: Search.dismiss(root)
959
+ // flea-qml: the field edits the query; Enter, Esc, Tab and the arrows take the pane's own route.
960
+ onSearchEdited: function (text) { root.searchQuery = text }
961
+ onSearchKeyOut: function (event) { Focus.handleKey(event, root, root.sidebar, "find") }
962
+ onSearchFieldEntered: if (root.searchMode === Search.RESULTS) root.searchMode = Search.TYPING
963
+ }
964
+
965
+ // The two views share the same slot, the same rows and the same cursor; only one is ever up, and
966
+ // listArea points at whichever it is, so every caller of restartSettle stays view-agnostic and
967
+ // shell.qml lays the empty-state overlay over the right one.
968
+ // The list stands in for a view still being built, so no binding on listArea ever reads null.
969
+ readonly property var listArea: root.viewMode === "grid" && gridLoader.item ? gridLoader.item
970
+ : root.viewMode === "columns" && columnsLoader.item ? columnsLoader.item : list
971
+ // How far a cursor step down moves: one row in the list, one row of tiles in the grid.
972
+ // The columns view's own preview, exposed so a test can assert its facts without OCR; null until built.
973
+ readonly property var columnsArea: columnsLoader.item
974
+ readonly property bool dragActive: root.dragHolding || !!(root.listArea && root.listArea.fileDragActive === true)
975
+ // Where the view sits, for an anchor: a Loader's item is no sibling of anything here, its Loader is.
976
+ readonly property Item listSlot: root.viewMode === "grid" ? gridLoader : root.viewMode === "columns" ? columnsLoader : list
977
+ readonly property int cursorStride: root.viewMode === "grid" && gridLoader.item ? gridLoader.item.columns : 1
978
+
979
+ Flea.FilterStrip {
980
+ id: filterStrip
981
+ anchors.top: header.bottom
982
+ anchors.left: railHost.right
983
+ anchors.right: parent.right
984
+ pane: root
985
+ }
986
+
987
+ // A hidden view is not a free view, see AGENTS.md rule 6: each of these two is built by its first
988
+ // switch and kept, so a launch in the list view pays for one view's rows and marks, not three.
989
+ property bool columnsBuilt: false
990
+ property bool gridBuilt: false
991
+ // setSource, not source: the view reads pane in its own bindings, so it has to hold one from birth.
992
+ Loader {
993
+ id: columnsLoader
994
+ active: root.viewMode === "columns" || root.columnsBuilt
995
+ visible: !trashHost.opened && !root.flashActive
996
+ focus: visible && root.viewMode === "columns"
997
+ anchors { top: filterStrip.bottom; left: railHost.right; right: parent.right; bottom: parent.bottom }
998
+ Component.onCompleted: setSource("ColumnsArea.qml", { pane: root, menu: menu, focus: true })
999
+ onLoaded: { root.columnsBuilt = true; item.visible = Qt.binding(function () { return root.viewMode === "columns" }) }
1000
+ }
1001
+
1002
+ Loader {
1003
+ id: gridLoader
1004
+ active: root.viewMode === "grid" || root.gridBuilt
1005
+ visible: !trashHost.opened && !root.flashActive
1006
+ focus: visible && root.viewMode === "grid"
1007
+ anchors { top: filterStrip.bottom; left: railHost.right; right: parent.right; bottom: parent.bottom }
1008
+ Component.onCompleted: setSource("GridArea.qml", { pane: root, menu: menu })
1009
+ onLoaded: { root.gridBuilt = true; item.visible = Qt.binding(function () { return root.viewMode === "grid" }) }
1010
+ }
1011
+
1012
+ // The grid's plans land the way the list's do below: it computes, and only the pane writes the two states.
1013
+ Connections {
1014
+ target: columnsLoader.item
1015
+ function onThumbsApplied(work) { root.thumbState = Thumbs.applied(root.thumbState, work) }
1016
+ function onDirSizesApplied(ask) { root.dirSizeState = DirSizes.applied(root.dirSizeState, ask) }
1017
+ }
1018
+
1019
+ Connections {
1020
+ target: gridLoader.item
1021
+ function onThumbsApplied(work) { root.thumbState = Thumbs.applied(root.thumbState, work) }
1022
+ function onDirSizesApplied(ask) { root.dirSizeState = DirSizes.applied(root.dirSizeState, ask) }
1023
+ function onDirSizesCancelled() { root.dirSizeState = DirSizes.cancelled(root.dirSizeState) }
1024
+ }
1025
+
1026
+ readonly property int previewIndex: root.viewMode === "columns" && columnsLoader.item ? columnsLoader.item.previewIndex : -1
1027
+ readonly property var previewColumnItem: root.viewMode === "columns" && columnsLoader.item ? columnsLoader.item.previewColumn : null
1028
+ function loadSelectionPreview() {
1029
+ if (!ViewState.previewColumn || root.dualMode) return
1030
+ if (root.viewMode === "columns" && columnsLoader.item) columnsLoader.item.loadSelection()
1031
+ }
1032
+ function togglePreviewColumn() { ViewState.changeLeaf("preview", { column: !ViewState.previewColumn }) }
1033
+ // ExtThumbs: the background menu's class row writes the class setting, never "this drive".
1034
+ // A class switched on decodes again: its cache-only misses leave and the viewport re-asks.
1035
+ function toggleExtThumbs() {
1036
+ var key = ExtThumbs.keyForClass(root.storageClass)
1037
+ if (key === "") return
1038
+ var on = !ExtThumbs.classOn(root.storageClass, ViewState.preview)
1039
+ // No direct refresh: the setting change below routes through onPreviewChanged,
1040
+ // which refreshes once on the verdict edge instead of twice.
1041
+ ViewState.changeSetting("preview." + key, on)
1042
+ root.message(ExtThumbs.statusLine(root.storageClass, on), false)
1043
+ }
1044
+ function refreshExtThumbs() {
1045
+ var now = ExtThumbs.verdict(root.storageClass, ViewState.preview)
1046
+ if (now === root.extVerdict) return
1047
+ root.extVerdict = now
1048
+ root.thumbState = ExtThumbs.forgetMisses(root.thumbState)
1049
+ if (root.listArea) root.listArea.restartSettle()
1050
+ // The preview follows rather than loads, so a class just switched off holds instead of decoding.
1051
+ if (root.viewMode === "columns" && columnsLoader.item) columnsLoader.item.askMeta()
1052
+ }
1053
+ function chooseView(mode) {
1054
+ if (FolderViews.MODES.indexOf(mode) < 0 && mode !== "dual")
1055
+ return
1056
+ if (root.listOnly) {
1057
+ root.viewRequested(mode)
1058
+ return
1059
+ }
1060
+ // Dual is a window layout and no folder's view: choosing it, or any view while in it, sets the standing view first.
1061
+ if (mode === "dual" || root.dualMode)
1062
+ ViewState.changeKey("view", mode)
1063
+ if (mode === "dual")
1064
+ return
1065
+ root.viewMode = mode
1066
+ if (FolderViews.shouldRemember(ViewState.rememberViewPerFolder, root.viewFolder)) {
1067
+ var leaf = {}
1068
+ leaf[root.viewFolder] = mode
1069
+ ViewState.changeMapEntries("folderViews", leaf, FolderViews.set(ViewState.folderViews, root.viewFolder, mode))
1070
+ } else if (ViewState.state.view !== mode) {
1071
+ ViewState.changeKey("view", mode)
1072
+ }
1073
+ }
1074
+ function focusPreviewColumn() {
1075
+ if (!ViewState.previewColumn || root.dualMode) return
1076
+ if (root.viewMode === "columns" && columnsLoader.item) columnsLoader.item.focusPreview()
1077
+ }
1078
+ property bool dualMode: false
1079
+ readonly property alias pathCrumbs: panePath.crumbItems
1080
+ readonly property alias pathSlot: panePath.crumbSlot
1081
+ readonly property alias pathStrip: panePath
1082
+ signal switchPane()
1083
+ signal switchToRail()
1084
+
1085
+ Flea.List {
1086
+ id: list
1087
+ visible: !trashHost.opened && !root.flashActive && root.viewMode === "list"
1088
+ focus: visible
1089
+ anchors.top: filterStrip.bottom
1090
+ anchors.left: railHost.right
1091
+ anchors.right: parent.right
1092
+ anchors.bottom: parent.bottom
1093
+ pane: root
1094
+ menu: menu
1095
+
1096
+ // List only computes the clamp and the thumbnail plan; cursorIndex and thumbState are Pane's own to write.
1097
+ onCursorClamped: function (first, last) { Filter.clampCursor(root, first, last) }
1098
+ onThumbsApplied: function (work) { root.thumbState = Thumbs.applied(root.thumbState, work) }
1099
+ onDirSizesApplied: function (ask) { root.dirSizeState = DirSizes.applied(root.dirSizeState, ask) }
1100
+ onDirSizesCancelled: root.dirSizeState = DirSizes.cancelled(root.dirSizeState)
1101
+ }
1102
+
1103
+ Rectangle {
1104
+ anchors { top: panePath.bottom; bottom: parent.bottom; left: railHost.right }
1105
+ visible: root.dualMode && root.paneFocused
1106
+ width: Theme.spacing.hairline * 2
1107
+ color: Theme.color.accent
1108
+ }
1109
+
1110
+ Flea.TrashHost {
1111
+ id: trashHost
1112
+ pane: root
1113
+ overlayParent: root.overlayParent
1114
+ anchors { top: parent.top; left: railHost.right; right: parent.right; bottom: parent.bottom }
1115
+ }
1116
+
1117
+ // The two menu rows open the pane through the window's host with the half they came from chosen.
1118
+ function openFlashImage(path) { root.overlayParent.openFlash(root, path, "") }
1119
+ function openFlashDrive(device) { root.overlayParent.openFlash(root, "", device) }
1120
+
1121
+ // Directory cursors retain the installed provider with its explicit file-only reason.
1122
+ readonly property var cursorRow: root.rowFor(root.cursorIndex)
1123
+ readonly property alias taildropService: wire.taildrop
1124
+ readonly property alias opener: wire.opener
1125
+ readonly property var dropboxService: root.networkService
1126
+ // The keymap sheet's query asks the menu's own rows, so the values stay live for as long as the sheet is open.
1127
+ readonly property bool sheetReadsMenu: root.keymapSheet !== null && root.keymapSheet.opened === true
1128
+ readonly property bool menuValuesLive: menu.preparing || menu.opened || root.sheetReadsMenu
1129
+ readonly property var menuPermissionRow: root.menuValuesLive ? root.permissionSelection() : null
1130
+
1131
+ Flea.MenuHost {
1132
+ id: menu
1133
+ pane: root
1134
+ parent: root.overlayParent || root
1135
+ focusOwner: root.listArea
1136
+ showHidden: root.showHidden
1137
+ providersRefreshing: menuActions.providersRefreshing
1138
+ taildropPeers: wire.taildrop.peers
1139
+ rowIsFile: root.cursorRow !== null && !root.cursorRow.d
1140
+ taildropInstalled: !root.backend.providers.taildrop || root.backend.providers.taildrop.installed !== false
1141
+ taildropReason: root.cursorRow && root.cursorRow.d ? "Taildrop sends files only" : wire.taildrop.reason
1142
+ taildropCode: wire.taildrop.code
1143
+ archiveFormats: root.backend.archiveFormats
1144
+ canConvert: root.backend.canConvert
1145
+ canExtract: root.menuValuesLive && root.cursorRow !== null
1146
+ && Archive.canExtract(root.cursorRow.n, root.backend.extraction)
1147
+ archiveName: root.cursorRow ? root.cursorRow.n : ""
1148
+ rowMode: root.menuPermissionRow ? root.menuPermissionRow.p : 0
1149
+ selectionCount: root.menuValuesLive ? Ops.targetIndices(root).length : 0
1150
+ // Held kinds answer at once; the menu snapshot carries kinds for marks outside the window.
1151
+ compareKinds: root.menuValuesLive ? Ops.menuKinds(root) : []
1152
+ cursorIsTarget: root.menuValuesLive && root.isSingleCursorTarget()
1153
+ openWithApps: menuActions.openWithApps
1154
+ openWithLoaded: menuActions.openWithLoaded
1155
+ identitySource: root
1156
+ clipboardAvailable: root.clipboard.paths.length > 0
1157
+ clipboardWatchFailed: root.clipboardWatchFailed
1158
+ // Scripts, send peers and the shebang are requested only when a row menu opens.
1159
+ onSnapshotRequested: { menuActions.snapshot(); Flea.Scripts.refresh(); menuActions.localSend.refresh(menu.localSend.installed); root.checkShebang(); if (root.canTag) root.tags().ask() }
1160
+ onRefused: function(reason) { root.message(reason, true) }
1161
+ rowIsArchive: root.menuValuesLive && root.cursorRow !== null && !root.cursorRow.d && Archive.isArchive(root.cursorRow.n)
1162
+ rowIsImage: root.cursorRow !== null && root.cursorRow.i === "image-x-generic"
1163
+ // Flash to drive reads the name, not the icon: only the name carries the ledger's suffixes.
1164
+ rowName: root.cursorRow !== null && !root.cursorRow.d ? root.cursorRow.n : ""
1165
+ flashZipMember: root.flashZipMember
1166
+ // MenuAdditions040: Show original is visible but only on a symlink.
1167
+ rowIsSymlink: root.cursorRow !== null && Format.isSymlink(root.cursorRow.p)
1168
+ rowHasShebang: root.rowHasShebang
1169
+ // Permissions carries every target's mode only while the menu needs it.
1170
+ selectionModes: root.menuValuesLive ? root.permissionModes() : []
1171
+ dropboxInstalled: !root.backend.providers.dropbox || root.backend.providers.dropbox.installed !== false
1172
+ localSend: ({ installed: (root.backend.providers.localsend || {}).installed === true, checking: menuActions.localSend.checking,
1173
+ peers: (root.cursorRow && !root.cursorRow.d) ? menuActions.localSend.peers : [], answeredOnce: menuActions.localSend.answeredOnce })
1174
+ dropboxPath: root.dropboxService && root.dropboxService.dropboxReady ? root.dropboxService.dropboxPath : ""
1175
+ dropboxReason: root.dropboxService ? root.dropboxService.dropboxReason : "Dropbox is not available"
1176
+ dropboxCode: root.dropboxService ? root.dropboxService.dropboxCode : ""
1177
+ rowInDropbox: root.menuValuesLive && root.dropboxService && root.cursorRow
1178
+ && Dropbox.contains(root.dropboxService.dropboxPath, root.join(Search.baseOf(root), root.cursorRow.n))
1179
+ // Issue 133: no GVFS mount, share or phone, has a trash of its own, so the row is not offered there.
1180
+ // A read-only folder has none either: the listed line's own writability rides along.
1181
+ canTrash: Mounts.trashable(root.path, root.backend ? root.backend.dirWritable !== false : true)
1182
+ // The listing's own w flag turns the menu's write rows off in a read-only folder.
1183
+ dirWritable: root.backend ? root.backend.dirWritable !== false : true
1184
+ // vfat and exfat hold no links, so the Paste as rows are not offered there either.
1185
+ canLink: root.fsName !== "vfat" && root.fsName !== "exfat"
1186
+ // The Tags flyout stands only while a menu is live; null drops the row where tags cannot be stored.
1187
+ tagSubmenu: root.menuValuesLive && root.canTag && root.tagState !== null ? root.tagState.flyout() : null
1188
+ // ExtThumbs: the class the background menu's thumbnail row is present for, "" locally.
1189
+ storageClass: root.storageClass
1190
+ // Issue 179: the Sort by flyout offers its forget row only where this folder has its own sort.
1191
+ hasFolderSort: root.menuValuesLive && root.searchMode.length === 0 && root.backend ? root.backend.folderHasSort(root.path) : false
1192
+ // The Locked tile's folder and mode while one is drawn; ui/ContextMenu.qml routes a
1193
+ // background right click to that folder's own menu through them.
1194
+ tileTarget: root.lockedTarget
1195
+ tileMode: root.lockedMode
1196
+ onChosen: function (action) {
1197
+ // Issue #170: Rename opens the editor at once by the route F2 takes
1198
+ // rather than queueing an activate behind the cold Open-with catalogue.
1199
+ if (action === "rename") { if (!root.openBulkRename(null)) menuActions.openRenameFromMenu(); return }
1200
+ menuActions.activate(action, menu.hasRow && !menu.forHeader)
1201
+ }
1202
+ // The Locked tile's folder arrives with the row: close() cleared the menu's own copy
1203
+ // before this runs, so reading menu.lockedPath here would always see "" and every row
1204
+ // would fall into the background dispatch above.
1205
+ onLockedChosen: function (action, path) { root.performLocked(path, action) }
1206
+ }
1207
+
1208
+ Flea.PaneMenuActions {
1209
+ id: menuActions
1210
+ parent: root.overlayParent || root
1211
+ pane: root
1212
+ }
1213
+
1214
+ // Every paste and drop asks through here first, see ui/js/Collide.js; built by the first ask and owned by the pane, so a closed tab takes its card.
1215
+ property var collide: null
1216
+ function asks() {
1217
+ var made = root.collide ? null : Qt.createComponent("CollideHost.qml")
1218
+ if (made && made.status !== Component.Ready) throw new Error("CollideHost.qml did not load: " + made.errorString())
1219
+ return root.collide || (root.collide = made.createObject(root, { pane: root, parent: root.overlayParent || root }))
1220
+ }
1221
+
1222
+ // Opening an executable file asks once, in the window's one card; see ui/WindowBody.qml.
1223
+ // A null host is a loud refusal, never a call into nothing; every product pane names one.
1224
+ readonly property string noWindowSentence: "This pane has no window, so nothing was done."
1225
+ function askRunCard(info) {
1226
+ if (!root.overlayParent) { root.message(root.noWindowSentence, true); return }
1227
+ root.overlayParent.askRunCard(root, info)
1228
+ }
1229
+ function runText(path) {
1230
+ if (!root.overlayParent) { root.message(root.noWindowSentence, true); return }
1231
+ root.overlayParent.runLane().openText(path, root)
1232
+ }
1233
+ // Without a path Open with opens over the cursor; with one the card snapshots the row that path still names.
1234
+ function showOpenWith(path) {
1235
+ if (path === undefined) { menuActions.open("openWith", 0); return }
1236
+ var at = Anchor.matchListed(root, path)
1237
+ if (at < 0) { root.message("That file is no longer listed.", true); return }
1238
+ menuActions.snapshot([at])
1239
+ menuActions.open("openWith", menuActions.requestId)
1240
+ }
1241
+
1242
+ // The one ui/ContextMenu.qml this pane owns, for ui/Ipc.qml: entries, flyout and row geometry
1243
+ // are read off it directly, so a new reader costs the seam a line and this file none.
1244
+ function contextMenu() { return menu.item || menu }
1245
+
1246
+ Flea.PaneStates {
1247
+ id: paneStates
1248
+ pane: root
1249
+ trashOpen: trashHost.opened
1250
+ }
1251
+
1252
+ function openConvert(menuId) { Ops.openConvert(root, menuId) }
1253
+ function moveToDropbox(menuId) { Ops.moveToDropbox(root, root.dropboxService && root.dropboxService.dropboxReady ? root.dropboxService.dropboxPath : "", menuId) }
1254
+ // The three foreign programs live in ui/PaneWire.qml with the backend's replies; these only name the row.
1255
+ function copyShareLink(path) {
1256
+ if (!path) { root.message("That menu is out of date. Reopen it.", true); return }
1257
+ wire.shareLink.copy(path)
1258
+ }
1259
+ function sendTaildrop(peerId, path) { Ops.sendTaildrop(root, wire.taildrop, peerId, path) }
1260
+
1261
+ // The Locked tile's own dispatch: every row its menu offers acts on the locked folder
1262
+ // alone, by path, never on the cursor, the selection or the parent standing behind it.
1263
+ // The menu row is already disabled where it cannot act, so no row here can fail refused.
1264
+ function performLocked(path, action) {
1265
+ if (path.length === 0) return
1266
+ if (action === "openTerminal") { wire.opener.openTerminal(path); return }
1267
+ if (action === "copypath") { wire.opener.copyText(path); return }
1268
+ // MenuAdditions040: the Locked tile offers Copy as over the one folder
1269
+ // it names, each leaf copying that path in its own form.
1270
+ if (action.indexOf("copyAs:") === 0) {
1271
+ var leaf = action.substring("copyAs:".length)
1272
+ var kinds = { copyPath: "path", copyName: "name", copyStem: "stem",
1273
+ copydirpath: "dirpath", copyUri: "uri", copyQuoted: "quoted" }
1274
+ if (kinds[leaf] !== undefined) { Ops.copyAs(root, kinds[leaf], [path]); return }
1275
+ }
1276
+ if (action === "copyAs") { Ops.copyAs(root, "path", [path]); return }
1277
+ if (action === "permissions") { root.permissionsRequested(path); return }
1278
+ root.message("That action is not available yet", false)
1279
+ }
1280
+
1281
+ // The keyboard's own entrance to the row menu; the placement itself is ui/js/Menu.js's.
1282
+ function openCursorMenu() {
1283
+ if (root.lockedTarget.length > 0) {
1284
+ var at = root.listSlot.mapToItem(null, root.listSlot.width / 2, root.listSlot.height / 2)
1285
+ menu.openLocked(root.lockedTarget, root.lockedMode, at)
1286
+ return true
1287
+ }
1288
+ return Menu.openAtCursor(root, menu, Theme.spacing.rowPaddingX)
1289
+ }
1290
+
1291
+ // The query lines' paste source (issue 287), read only when Ctrl+V or Shift+Insert reaches one.
1292
+ // flea-qml: buninu-browser reads its clipboard asynchronously and offers readClipboardText's
1293
+ // Promise for it; Quickshell itself answers the text at once.
1294
+ function clipboardText() { return Quickshell.readClipboardText ? Quickshell.readClipboardText() : Quickshell.clipboardText }
1295
+
1296
+ }