flea-qml 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (637) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +511 -2
  3. package/THIRD-PARTY-NOTICES.md +29 -0
  4. package/demos/App.java +5 -0
  5. package/demos/Dockerfile +4 -0
  6. package/demos/Main.kt +3 -0
  7. package/demos/Program.cs +1 -0
  8. package/demos/README.md +12 -0
  9. package/demos/app.dart +3 -0
  10. package/demos/app.deb +0 -0
  11. package/demos/app.js +1 -0
  12. package/demos/backup.tar +0 -0
  13. package/demos/budget.xlsx +0 -0
  14. package/demos/build.sh +2 -0
  15. package/demos/bundle.zip +0 -0
  16. package/demos/clip.mp4 +0 -0
  17. package/demos/data.sqlite +0 -0
  18. package/demos/disk.iso +0 -0
  19. package/demos/engine.cpp +5 -0
  20. package/demos/firmware.bin +0 -0
  21. package/demos/flake.nix +4 -0
  22. package/demos/folder/inside.txt +1 -0
  23. package/demos/font.ttf +0 -0
  24. package/demos/index.html +5 -0
  25. package/demos/index.php +2 -0
  26. package/demos/init.lua +1 -0
  27. package/demos/letter.docx +0 -0
  28. package/demos/lib.rs +3 -0
  29. package/demos/main.c +6 -0
  30. package/demos/main.h +3 -0
  31. package/demos/math-and-diagrams.md +200 -0
  32. package/demos/notes.tar.gz +0 -0
  33. package/demos/notes.txt +3 -0
  34. package/demos/photo.png +0 -0
  35. package/demos/program +2 -0
  36. package/demos/report.pdf +32 -0
  37. package/demos/script.py +1 -0
  38. package/demos/server.go +7 -0
  39. package/demos/settings.json +4 -0
  40. package/demos/slides.pptx +0 -0
  41. package/demos/song.mp3 +0 -0
  42. package/demos/style.css +4 -0
  43. package/demos/tool.rb +1 -0
  44. package/demos/types.ts +2 -0
  45. package/docs/pele.md +23 -0
  46. package/docs/protocol.md +3420 -0
  47. package/docs/temml.md +63 -0
  48. package/index.js +107 -0
  49. package/package.json +34 -4
  50. package/src/archive.js +407 -0
  51. package/src/archiveops.js +728 -0
  52. package/src/backend.js +369 -0
  53. package/src/bulkrename.js +95 -0
  54. package/src/clip-host.js +76 -0
  55. package/src/clip.js +462 -0
  56. package/src/code.js +168 -0
  57. package/src/compare.js +74 -0
  58. package/src/create.js +128 -0
  59. package/src/dirsize.js +147 -0
  60. package/src/figure-svg.js +530 -0
  61. package/src/figure-thread.js +23 -0
  62. package/src/figure.js +132 -0
  63. package/src/find.js +55 -0
  64. package/src/flea.js +39 -0
  65. package/src/fsinfo.js +39 -0
  66. package/src/highlight.js +141 -0
  67. package/src/html.js +52 -0
  68. package/src/kinds.js +89 -0
  69. package/src/listing.js +130 -0
  70. package/src/markdown-html.js +317 -0
  71. package/src/mathml.js +186 -0
  72. package/src/menu.js +180 -0
  73. package/src/meta.js +135 -0
  74. package/src/office.js +810 -0
  75. package/src/order.js +100 -0
  76. package/src/peek.js +49 -0
  77. package/src/pele-wrapper.js +695 -0
  78. package/src/permissions.js +180 -0
  79. package/src/places.js +313 -0
  80. package/src/preview-language.js +10 -0
  81. package/src/terminal-server.js +95 -0
  82. package/src/terminal-shell.js +26 -0
  83. package/src/transfer.js +669 -0
  84. package/src/trash-record.js +41 -0
  85. package/src/trash.js +210 -0
  86. package/src/trashbrowse.js +327 -0
  87. package/src/uistate.js +393 -0
  88. package/src/uistore.js +168 -0
  89. package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
  90. package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
  91. package/src/vendor/LICENSES/MIT-katex.txt +21 -0
  92. package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
  93. package/src/vendor/LICENSES/MIT-pele.txt +40 -0
  94. package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
  95. package/src/vendor/LICENSES/MIT-temml.txt +21 -0
  96. package/src/vendor/pele.js +145 -0
  97. package/src/vendor/temml.mjs +14708 -0
  98. package/src/watch.js +98 -0
  99. package/src/wayland/client.js +109 -0
  100. package/src/wayland/protocol.js +55 -0
  101. package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
  102. package/src/wayland/protocols/wayland.xml +3248 -0
  103. package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
  104. package/src/wayland/resource-io.js +6 -0
  105. package/src/wayland/socket-discovery.js +26 -0
  106. package/src/wayland/transport-posix.js +120 -0
  107. package/src/wayland/transport-win32.js +11 -0
  108. package/src/wayland/transport.js +3 -0
  109. package/src/wayland/win32-resources.js +80 -0
  110. package/src/wayland/win32-wire.js +62 -0
  111. package/test/archive.test.js +80 -0
  112. package/test/bench-markdown-scroll.mjs +190 -0
  113. package/test/clip-host.test.js +113 -0
  114. package/test/code-preview.test.js +35 -0
  115. package/test/compare-ui-paths.test.js +24 -0
  116. package/test/compare.test.js +54 -0
  117. package/test/delete-trash.test.js +67 -0
  118. package/test/dirsize.test.js +138 -0
  119. package/test/file-ops.test.js +307 -0
  120. package/test/file-url.test.js +24 -0
  121. package/test/find-contents.test.js +161 -0
  122. package/test/find-input.test.js +66 -0
  123. package/test/fixtures/pele-parity.json +1006 -0
  124. package/test/highlight-launcher.test.js +35 -0
  125. package/test/html-preview.test.js +22 -0
  126. package/test/input.test.js +15 -0
  127. package/test/keymap.test.js +26 -0
  128. package/test/language-icons.test.js +36 -0
  129. package/test/launcher.test.js +51 -0
  130. package/test/manual-archive-here-ui.mjs +47 -0
  131. package/test/manual-code-scroll.mjs +28 -0
  132. package/test/manual-column-scroll.mjs +24 -0
  133. package/test/manual-compare-ui.mjs +20 -0
  134. package/test/manual-find-status.mjs +37 -0
  135. package/test/manual-folder-focus.mjs +63 -0
  136. package/test/manual-html-security.mjs +23 -0
  137. package/test/manual-html-ui.mjs +30 -0
  138. package/test/manual-syntax-preview.mjs +26 -0
  139. package/test/manual-terminal-lifecycle.mjs +42 -0
  140. package/test/manual-terminal-navigation.mjs +48 -0
  141. package/test/manual-terminal-ui.mjs +47 -0
  142. package/test/manual-trash-ui.mjs +86 -0
  143. package/test/markdown-diagrams.test.js +93 -0
  144. package/test/markdown-html.test.js +105 -0
  145. package/test/mathml.test.js +92 -0
  146. package/test/menu-transfer.test.js +57 -0
  147. package/test/office-preview.test.js +154 -0
  148. package/test/pele-parity.test.js +28 -0
  149. package/test/places.test.js +134 -0
  150. package/test/preview-keys.test.js +60 -0
  151. package/test/preview-language.test.js +22 -0
  152. package/test/status-filename.test.js +20 -0
  153. package/test/transfer-replace-merge.test.js +187 -0
  154. package/test/trash-address.test.js +23 -0
  155. package/test/trash-view.test.js +37 -0
  156. package/test/trashbrowse.test.js +135 -0
  157. package/ui/AboutFacts.qml +78 -0
  158. package/ui/ArchiveCards.qml +95 -0
  159. package/ui/ArchiveCompressDialog.qml +378 -0
  160. package/ui/ArchiveHereConfirm.qml +127 -0
  161. package/ui/ArchivePasswordDialog.qml +257 -0
  162. package/ui/Backend.qml +775 -0
  163. package/ui/BulkOldNames.qml +47 -0
  164. package/ui/BulkRenameDialog.qml +380 -0
  165. package/ui/CardFrame.qml +67 -0
  166. package/ui/CardScroll.qml +79 -0
  167. package/ui/CheckBox.qml +73 -0
  168. package/ui/ChromeBar.qml +360 -0
  169. package/ui/ChromeButton.qml +131 -0
  170. package/ui/ChromeModes.qml +43 -0
  171. package/ui/CodeLines.qml +256 -0
  172. package/ui/CodeRefusal.qml +52 -0
  173. package/ui/CollideConfirm.qml +147 -0
  174. package/ui/CollideHost.qml +167 -0
  175. package/ui/ColumnPane.qml +525 -0
  176. package/ui/ColumnRow.qml +380 -0
  177. package/ui/ColumnsArea.qml +594 -0
  178. package/ui/ColumnsLanding.qml +147 -0
  179. package/ui/CompareView.qml +321 -0
  180. package/ui/ContextMenu.qml +743 -0
  181. package/ui/ConvertDialog.qml +456 -0
  182. package/ui/CountedWorker.qml +7 -0
  183. package/ui/Crumb.qml +64 -0
  184. package/ui/CrumbDrop.qml +25 -0
  185. package/ui/CsvTable.qml +394 -0
  186. package/ui/CursorMark.qml +41 -0
  187. package/ui/DefaultClaim.qml +130 -0
  188. package/ui/DeviceLock.qml +47 -0
  189. package/ui/DeviceMounts.qml +540 -0
  190. package/ui/DialogButton.qml +150 -0
  191. package/ui/DialogField.qml +147 -0
  192. package/ui/DialogHeader.qml +31 -0
  193. package/ui/DialogScrim.qml +20 -0
  194. package/ui/DialogTitle.qml +37 -0
  195. package/ui/Divider.qml +9 -0
  196. package/ui/DropInto.qml +126 -0
  197. package/ui/DropTarget.qml +35 -0
  198. package/ui/DropboxMark.qml +60 -0
  199. package/ui/EmptyState.qml +150 -0
  200. package/ui/EscapeHint.qml +11 -0
  201. package/ui/FactsTable.qml +54 -0
  202. package/ui/FastScrollHandler.qml +476 -0
  203. package/ui/FatNamesConfirm.qml +157 -0
  204. package/ui/Favourites.qml +143 -0
  205. package/ui/FigureService.qml +120 -0
  206. package/ui/FileDrag.qml +267 -0
  207. package/ui/FilterStrip.qml +133 -0
  208. package/ui/FitMetrics.qml +22 -0
  209. package/ui/FlashDriveRows.qml +137 -0
  210. package/ui/FlashHost.qml +72 -0
  211. package/ui/FlashImageRow.qml +104 -0
  212. package/ui/FlashRunner.qml +119 -0
  213. package/ui/FlashStepRow.qml +172 -0
  214. package/ui/FlashStrip.qml +38 -0
  215. package/ui/FlashView.qml +397 -0
  216. package/ui/FleaMark.qml +88 -0
  217. package/ui/FlyoutFade.qml +23 -0
  218. package/ui/FontPane.qml +170 -0
  219. package/ui/Glass.qml +114 -0
  220. package/ui/GlassTagger.qml +59 -0
  221. package/ui/Glyph.qml +64 -0
  222. package/ui/GlyphSolid.qml +22 -0
  223. package/ui/GridArea.qml +664 -0
  224. package/ui/GridTile.qml +325 -0
  225. package/ui/GvfsBridge.qml +164 -0
  226. package/ui/Header.qml +433 -0
  227. package/ui/HoverTimers.qml +39 -0
  228. package/ui/HtmlClip.qml +30 -0
  229. package/ui/HtmlPane.qml +63 -0
  230. package/ui/HyprlandMark.qml +45 -0
  231. package/ui/Ipc.qml +1005 -0
  232. package/ui/IpcControls.qml +7 -0
  233. package/ui/IpcMenuState.qml +48 -0
  234. package/ui/IpcPreviewState.qml +185 -0
  235. package/ui/JumpPath.qml +51 -0
  236. package/ui/KeymapSheet.qml +561 -0
  237. package/ui/KeymapSheetResult.qml +89 -0
  238. package/ui/KindStack.qml +40 -0
  239. package/ui/LangMark.qml +51 -0
  240. package/ui/LangMarkLoader.qml +26 -0
  241. package/ui/List.qml +480 -0
  242. package/ui/LoadingState.qml +54 -0
  243. package/ui/LocalSend.qml +56 -0
  244. package/ui/LocalSendMark.qml +73 -0
  245. package/ui/MarkdownBlockView.qml +311 -0
  246. package/ui/MarkdownBlocks.qml +35 -0
  247. package/ui/MarkdownFigure.qml +254 -0
  248. package/ui/MarkdownImages.qml +72 -0
  249. package/ui/MarkdownList.qml +81 -0
  250. package/ui/MarkdownMathsText.qml +97 -0
  251. package/ui/MarkdownPane.qml +83 -0
  252. package/ui/MarkdownPictures.qml +41 -0
  253. package/ui/MarkdownQuote.qml +52 -0
  254. package/ui/MarkdownRemote.qml +82 -0
  255. package/ui/MarkdownSource.qml +155 -0
  256. package/ui/MarkdownTable.qml +222 -0
  257. package/ui/MarkdownTableScroll.qml +64 -0
  258. package/ui/MarkdownTableWheel.qml +126 -0
  259. package/ui/MarkdownText.qml +87 -0
  260. package/ui/MarkdownWorker.js +132 -0
  261. package/ui/MatchText.qml +99 -0
  262. package/ui/MediaSession.qml +38 -0
  263. package/ui/MediaSound.qml +26 -0
  264. package/ui/MediaStrip.qml +191 -0
  265. package/ui/MediaTrack.qml +36 -0
  266. package/ui/MediaVolume.qml +54 -0
  267. package/ui/MenuActionDialog.qml +396 -0
  268. package/ui/MenuEdgeFade.qml +24 -0
  269. package/ui/MenuHost.qml +227 -0
  270. package/ui/MenuRow.qml +351 -0
  271. package/ui/MountListing.qml +100 -0
  272. package/ui/NetworkDialog.qml +435 -0
  273. package/ui/NetworkForm.qml +426 -0
  274. package/ui/NetworkMounts.qml +913 -0
  275. package/ui/NetworkPlaces.qml +127 -0
  276. package/ui/OpenWithDialog.qml +613 -0
  277. package/ui/Opener.qml +95 -0
  278. package/ui/Pane.qml +1320 -0
  279. package/ui/PaneMenuActions.qml +527 -0
  280. package/ui/PanePath.qml +90 -0
  281. package/ui/PaneRail.qml +268 -0
  282. package/ui/PaneStates.qml +88 -0
  283. package/ui/PaneSwap.qml +339 -0
  284. package/ui/PaneWire.qml +601 -0
  285. package/ui/ParkedWait.qml +9 -0
  286. package/ui/PathEntry.qml +186 -0
  287. package/ui/PathJump.qml +400 -0
  288. package/ui/PdfViewer.qml +297 -0
  289. package/ui/PermissionsDialog.qml +658 -0
  290. package/ui/PermissionsHeadings.qml +31 -0
  291. package/ui/PhoneMounts.qml +67 -0
  292. package/ui/PickerAnswers.qml +75 -0
  293. package/ui/PickerBox.qml +26 -0
  294. package/ui/PickerChrome.qml +295 -0
  295. package/ui/PickerFilter.qml +97 -0
  296. package/ui/PickerGrid.qml +308 -0
  297. package/ui/PickerHeader.qml +28 -0
  298. package/ui/PickerLifecycle.qml +27 -0
  299. package/ui/PickerList.qml +218 -0
  300. package/ui/PickerListing.qml +149 -0
  301. package/ui/PickerPathBar.qml +127 -0
  302. package/ui/PickerPlaces.qml +189 -0
  303. package/ui/PickerRecent.qml +92 -0
  304. package/ui/PickerSave.qml +101 -0
  305. package/ui/PickerSelection.qml +155 -0
  306. package/ui/PickerWindow.qml +846 -0
  307. package/ui/PowerSectorsReader.qml +10 -0
  308. package/ui/PrebuildDriver.qml +146 -0
  309. package/ui/Preview.qml +1005 -0
  310. package/ui/PreviewAnimation.qml +24 -0
  311. package/ui/PreviewArchive.qml +91 -0
  312. package/ui/PreviewColumn.qml +553 -0
  313. package/ui/PreviewEdge.qml +63 -0
  314. package/ui/PreviewFacts.qml +30 -0
  315. package/ui/PreviewFactsLine.qml +34 -0
  316. package/ui/PreviewHtml.qml +233 -0
  317. package/ui/PreviewImage.qml +253 -0
  318. package/ui/PreviewImagePan.qml +13 -0
  319. package/ui/PreviewImageSharp.qml +32 -0
  320. package/ui/PreviewLines.qml +106 -0
  321. package/ui/PreviewMarkdown.qml +706 -0
  322. package/ui/PreviewMedia.qml +250 -0
  323. package/ui/PreviewPdf.qml +482 -0
  324. package/ui/PreviewScroll.qml +201 -0
  325. package/ui/PreviewSwap.qml +335 -0
  326. package/ui/PreviewText.qml +149 -0
  327. package/ui/ProtocolChip.qml +18 -0
  328. package/ui/QuickLookArchive.qml +41 -0
  329. package/ui/QuickLookBar.qml +103 -0
  330. package/ui/QuickLookPrepare.qml +355 -0
  331. package/ui/QuickLookSwap.qml +10 -0
  332. package/ui/QuickLookWarm.qml +65 -0
  333. package/ui/RailDrop.qml +155 -0
  334. package/ui/RailHeading.qml +19 -0
  335. package/ui/RailPlaces.qml +38 -0
  336. package/ui/RailTrashRow.qml +67 -0
  337. package/ui/RenameField.qml +353 -0
  338. package/ui/RendererRetry.qml +11 -0
  339. package/ui/ResizeHandle.qml +52 -0
  340. package/ui/Reveal.qml +54 -0
  341. package/ui/Row.qml +476 -0
  342. package/ui/RowDrag.qml +213 -0
  343. package/ui/RowDropTarget.qml +27 -0
  344. package/ui/RunCard.qml +286 -0
  345. package/ui/Scripts.qml +65 -0
  346. package/ui/SearchStrip.qml +232 -0
  347. package/ui/SelectionBand.qml +140 -0
  348. package/ui/SelectionPreview.qml +370 -0
  349. package/ui/SettingsFavourite.qml +122 -0
  350. package/ui/SettingsGroup.qml +96 -0
  351. package/ui/SettingsHint.qml +12 -0
  352. package/ui/SettingsMetrics.qml +29 -0
  353. package/ui/SettingsPane.qml +178 -0
  354. package/ui/SettingsPanel.qml +547 -0
  355. package/ui/SettingsRail.qml +128 -0
  356. package/ui/SettingsRow.qml +409 -0
  357. package/ui/SettingsRuler.qml +85 -0
  358. package/ui/SettingsSegment.qml +85 -0
  359. package/ui/ShareBrowser.qml +113 -0
  360. package/ui/ShareLink.qml +67 -0
  361. package/ui/ShelfPins.qml +101 -0
  362. package/ui/Sidebar.qml +728 -0
  363. package/ui/SidebarRow.qml +358 -0
  364. package/ui/Spinner.qml +60 -0
  365. package/ui/StableModel.qml +89 -0
  366. package/ui/StateMessage.qml +84 -0
  367. package/ui/StatusBar.qml +512 -0
  368. package/ui/StatusFileName.qml +27 -0
  369. package/ui/TabBar.qml +673 -0
  370. package/ui/TabDragGeometry.qml +60 -0
  371. package/ui/TagMenuMark.qml +52 -0
  372. package/ui/TagRailSection.qml +53 -0
  373. package/ui/TagSlot.qml +40 -0
  374. package/ui/TagState.qml +150 -0
  375. package/ui/TagSwatch.qml +28 -0
  376. package/ui/Taildrop.qml +86 -0
  377. package/ui/TailscaleMark.qml +50 -0
  378. package/ui/TerminalService.qml +69 -0
  379. package/ui/TerminalTab.qml +48 -0
  380. package/ui/TerminalTabs.qml +27 -0
  381. package/ui/Theme.qml +460 -0
  382. package/ui/ThemeColors.qml +43 -0
  383. package/ui/TransferCard.qml +239 -0
  384. package/ui/TrashConfirm.qml +129 -0
  385. package/ui/TrashDropHandler.qml +11 -0
  386. package/ui/TrashHost.qml +202 -0
  387. package/ui/TrashMonitor.qml +30 -0
  388. package/ui/TrashToolbar.qml +19 -0
  389. package/ui/TrashView.qml +492 -0
  390. package/ui/UnlockDialog.qml +450 -0
  391. package/ui/UnlockHost.qml +206 -0
  392. package/ui/UpdateCheck.qml +97 -0
  393. package/ui/UserDirs.qml +25 -0
  394. package/ui/ViewState.qml +565 -0
  395. package/ui/ViewportScrollBar.qml +195 -0
  396. package/ui/ViewportScrollBars.qml +25 -0
  397. package/ui/WebPreview.qml +33 -0
  398. package/ui/WebPreviewView.qml +26 -0
  399. package/ui/WindowBody.qml +829 -0
  400. package/ui/WindowDrag.qml +35 -0
  401. package/ui/boot/Commons/AudioNodes.qml +50 -0
  402. package/ui/boot/Commons/AudioNodesModel.js +96 -0
  403. package/ui/boot/Commons/Border.qml +243 -0
  404. package/ui/boot/Commons/BorderGeometry.js +373 -0
  405. package/ui/boot/Commons/Color.qml +258 -0
  406. package/ui/boot/Commons/I18n.qml +118 -0
  407. package/ui/boot/Commons/InputDevices.qml +112 -0
  408. package/ui/boot/Commons/InputMethodState.qml +38 -0
  409. package/ui/boot/Commons/IpcRegistry.qml +77 -0
  410. package/ui/boot/Commons/Numbers.js +19 -0
  411. package/ui/boot/Commons/Plural.js +91 -0
  412. package/ui/boot/Commons/ShellIpc.qml +12 -0
  413. package/ui/boot/Commons/Style.qml +561 -0
  414. package/ui/boot/Commons/Translations.qml +100 -0
  415. package/ui/boot/Commons/UntypedInput.qml +152 -0
  416. package/ui/boot/Commons/Util.qml +188 -0
  417. package/ui/boot/Commons/qmldir +13 -0
  418. package/ui/boot/LICENSE-omarchy +20 -0
  419. package/ui/boot/Ui/BackgroundMedia.qml +49 -0
  420. package/ui/boot/Ui/BarIconButton.qml +106 -0
  421. package/ui/boot/Ui/BarIconContent.qml +38 -0
  422. package/ui/boot/Ui/BarIndicator.qml +53 -0
  423. package/ui/boot/Ui/BarWidget.qml +50 -0
  424. package/ui/boot/Ui/BorderOverlay.qml +54 -0
  425. package/ui/boot/Ui/BorderSurface.qml +40 -0
  426. package/ui/boot/Ui/Button.qml +224 -0
  427. package/ui/boot/Ui/ButtonGroup.qml +131 -0
  428. package/ui/boot/Ui/ConfirmDialog.qml +134 -0
  429. package/ui/boot/Ui/CursorSurface.qml +42 -0
  430. package/ui/boot/Ui/Dropdown.qml +245 -0
  431. package/ui/boot/Ui/FilterInput.qml +26 -0
  432. package/ui/boot/Ui/IconContentBounds.qml +141 -0
  433. package/ui/boot/Ui/KeyboardPanel.qml +453 -0
  434. package/ui/boot/Ui/MultiSelect.qml +624 -0
  435. package/ui/boot/Ui/NumberField.qml +86 -0
  436. package/ui/boot/Ui/OpticalGlyph.qml +68 -0
  437. package/ui/boot/Ui/OverlayWindow.qml +102 -0
  438. package/ui/boot/Ui/Panel.qml +60 -0
  439. package/ui/boot/Ui/PanelActionButton.qml +105 -0
  440. package/ui/boot/Ui/PanelController.qml +16 -0
  441. package/ui/boot/Ui/PanelHero.qml +112 -0
  442. package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
  443. package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
  444. package/ui/boot/Ui/PanelSeparator.qml +19 -0
  445. package/ui/boot/Ui/PanelSlider.qml +156 -0
  446. package/ui/boot/Ui/PanelToolTip.qml +50 -0
  447. package/ui/boot/Ui/PluginBarApi.qml +89 -0
  448. package/ui/boot/Ui/PointerMoveGate.qml +54 -0
  449. package/ui/boot/Ui/PopupCard.qml +181 -0
  450. package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
  451. package/ui/boot/Ui/SearchableDropdown.qml +354 -0
  452. package/ui/boot/Ui/SizedImage.qml +125 -0
  453. package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
  454. package/ui/boot/Ui/TextField.qml +58 -0
  455. package/ui/boot/Ui/Toggle.qml +118 -0
  456. package/ui/boot/Ui/ToggleSwitch.qml +115 -0
  457. package/ui/boot/Ui/WidgetButton.qml +172 -0
  458. package/ui/boot/Ui/qmldir +41 -0
  459. package/ui/boot/fleatab.qml +50 -0
  460. package/ui/boot/picker.qml +39 -0
  461. package/ui/boot/shell.qml +131 -0
  462. package/ui/boot/tabtearoff.qml +51 -0
  463. package/ui/js/Anchor.js +379 -0
  464. package/ui/js/AnchorHold.js +162 -0
  465. package/ui/js/Archive.js +157 -0
  466. package/ui/js/ArchiveCard.js +44 -0
  467. package/ui/js/ArchiveOps.js +44 -0
  468. package/ui/js/BackReturn.js +118 -0
  469. package/ui/js/BulkRename.js +100 -0
  470. package/ui/js/Buttons.js +54 -0
  471. package/ui/js/ChromeKeys.js +116 -0
  472. package/ui/js/ClipMarks.js +73 -0
  473. package/ui/js/ClipSelection.js +67 -0
  474. package/ui/js/Clipboard.js +179 -0
  475. package/ui/js/Cloud.js +97 -0
  476. package/ui/js/CodeRuns.js +72 -0
  477. package/ui/js/Collide.js +212 -0
  478. package/ui/js/ColumnFit.js +28 -0
  479. package/ui/js/ColumnMenu.js +111 -0
  480. package/ui/js/Columns.js +298 -0
  481. package/ui/js/ColumnsCarry.js +103 -0
  482. package/ui/js/Compare.js +74 -0
  483. package/ui/js/Contrast.js +157 -0
  484. package/ui/js/ControlReader.js +42 -0
  485. package/ui/js/Convert.js +52 -0
  486. package/ui/js/CopyAs.js +52 -0
  487. package/ui/js/Crumbs.js +178 -0
  488. package/ui/js/Csv.js +231 -0
  489. package/ui/js/Density.js +44 -0
  490. package/ui/js/Devices.js +440 -0
  491. package/ui/js/DirSizes.js +180 -0
  492. package/ui/js/Drag.js +360 -0
  493. package/ui/js/DragOut.js +166 -0
  494. package/ui/js/DropDev.js +52 -0
  495. package/ui/js/Dropbox.js +41 -0
  496. package/ui/js/Eject.js +217 -0
  497. package/ui/js/Errors.js +188 -0
  498. package/ui/js/ExtThumbs.js +115 -0
  499. package/ui/js/Facts.js +402 -0
  500. package/ui/js/FigureDate.js +14 -0
  501. package/ui/js/Filter.js +285 -0
  502. package/ui/js/Find.js +46 -0
  503. package/ui/js/Flash.js +184 -0
  504. package/ui/js/FlashCard.js +79 -0
  505. package/ui/js/FlashEndings.js +125 -0
  506. package/ui/js/FlashPane.js +116 -0
  507. package/ui/js/FleaBin.js +12 -0
  508. package/ui/js/Focus.js +504 -0
  509. package/ui/js/FolderSorts.js +108 -0
  510. package/ui/js/FolderViews.js +44 -0
  511. package/ui/js/Format.js +314 -0
  512. package/ui/js/Fuzzy.js +141 -0
  513. package/ui/js/GlassTag.js +72 -0
  514. package/ui/js/Grid.js +33 -0
  515. package/ui/js/GridGeometry.js +90 -0
  516. package/ui/js/GridNames.js +153 -0
  517. package/ui/js/GvfsBridge.js +200 -0
  518. package/ui/js/HostClipboard.js +29 -0
  519. package/ui/js/Icons.js +218 -0
  520. package/ui/js/Input.js +50 -0
  521. package/ui/js/Jump.js +200 -0
  522. package/ui/js/KeyName.js +64 -0
  523. package/ui/js/Keymap.js +401 -0
  524. package/ui/js/Kinds.js +164 -0
  525. package/ui/js/LICENSES/ISC-lucide.txt +43 -0
  526. package/ui/js/LangMarkPaths.js +47 -0
  527. package/ui/js/LangMarks.js +120 -0
  528. package/ui/js/Lists.js +11 -0
  529. package/ui/js/LocalSend.js +39 -0
  530. package/ui/js/LockedMenu.js +63 -0
  531. package/ui/js/MakeDefault.js +186 -0
  532. package/ui/js/MarkBox.js +7 -0
  533. package/ui/js/Markdown.js +119 -0
  534. package/ui/js/MarkdownLists.js +86 -0
  535. package/ui/js/MarkdownMaths.js +58 -0
  536. package/ui/js/MarkdownPictures.js +32 -0
  537. package/ui/js/MarkdownPrepared.js +139 -0
  538. package/ui/js/MarkdownTableFit.js +34 -0
  539. package/ui/js/Marks.js +148 -0
  540. package/ui/js/Marquee.js +96 -0
  541. package/ui/js/Match.js +93 -0
  542. package/ui/js/Material.js +109 -0
  543. package/ui/js/MdBlocks.js +437 -0
  544. package/ui/js/MdBreak.js +71 -0
  545. package/ui/js/MdChunks.js +70 -0
  546. package/ui/js/MdContainer.js +137 -0
  547. package/ui/js/MdDocument.js +337 -0
  548. package/ui/js/MdEmph.js +97 -0
  549. package/ui/js/MdEntity.js +108 -0
  550. package/ui/js/MdEntityTable.js +223 -0
  551. package/ui/js/MdEscape.js +59 -0
  552. package/ui/js/MdFront.js +37 -0
  553. package/ui/js/MdHold.js +32 -0
  554. package/ui/js/MdHtml.js +307 -0
  555. package/ui/js/MdHtmlBlock.js +290 -0
  556. package/ui/js/MdHtmlHeading.js +53 -0
  557. package/ui/js/MdHtmlImage.js +179 -0
  558. package/ui/js/MdInline.js +154 -0
  559. package/ui/js/MdItems.js +212 -0
  560. package/ui/js/MdLeaf.js +309 -0
  561. package/ui/js/MdLink.js +196 -0
  562. package/ui/js/MdMath.js +67 -0
  563. package/ui/js/MdParagraphs.js +138 -0
  564. package/ui/js/MdRefs.js +232 -0
  565. package/ui/js/MdResolve.js +226 -0
  566. package/ui/js/MdRun.js +300 -0
  567. package/ui/js/MdUrl.js +226 -0
  568. package/ui/js/Menu.js +514 -0
  569. package/ui/js/MenuContext.js +85 -0
  570. package/ui/js/MenuFit.js +13 -0
  571. package/ui/js/MenuRefresh.js +74 -0
  572. package/ui/js/MenuWheel.js +35 -0
  573. package/ui/js/Messages.js +186 -0
  574. package/ui/js/Motion.js +16 -0
  575. package/ui/js/Mounts.js +320 -0
  576. package/ui/js/MouseNav.js +13 -0
  577. package/ui/js/Nav.js +329 -0
  578. package/ui/js/NetFs.js +26 -0
  579. package/ui/js/OpenWith.js +83 -0
  580. package/ui/js/Ops.js +585 -0
  581. package/ui/js/Palette.js +33 -0
  582. package/ui/js/PathBar.js +260 -0
  583. package/ui/js/Permissions.js +179 -0
  584. package/ui/js/Phones.js +112 -0
  585. package/ui/js/Picker.js +353 -0
  586. package/ui/js/PlaceMenu.js +96 -0
  587. package/ui/js/Places.js +265 -0
  588. package/ui/js/PreviewAsk.js +21 -0
  589. package/ui/js/PreviewKeys.js +155 -0
  590. package/ui/js/PreviewSettle.js +29 -0
  591. package/ui/js/PreviewSwap.js +101 -0
  592. package/ui/js/PropertyFacts.js +144 -0
  593. package/ui/js/Protocols.js +296 -0
  594. package/ui/js/QuickLookKinds.js +27 -0
  595. package/ui/js/QuickLookRect.js +24 -0
  596. package/ui/js/RailDrop.js +51 -0
  597. package/ui/js/RailKeys.js +87 -0
  598. package/ui/js/RailMenu.js +133 -0
  599. package/ui/js/Recent.js +135 -0
  600. package/ui/js/RecentMode.js +156 -0
  601. package/ui/js/Reload.js +60 -0
  602. package/ui/js/Renderer.js +9 -0
  603. package/ui/js/RunFile.js +143 -0
  604. package/ui/js/Scripts.js +28 -0
  605. package/ui/js/Scroll.js +304 -0
  606. package/ui/js/ScrollOff.js +69 -0
  607. package/ui/js/Search.js +478 -0
  608. package/ui/js/Selection.js +97 -0
  609. package/ui/js/Settings.js +476 -0
  610. package/ui/js/SettingsAbout.js +97 -0
  611. package/ui/js/SettingsShelf.js +52 -0
  612. package/ui/js/SheetKeys.js +42 -0
  613. package/ui/js/SheetQuery.js +281 -0
  614. package/ui/js/ShelfPile.js +27 -0
  615. package/ui/js/SlowClick.js +53 -0
  616. package/ui/js/SlowOp.js +6 -0
  617. package/ui/js/Sort.js +134 -0
  618. package/ui/js/Startup.js +63 -0
  619. package/ui/js/StateWords.js +17 -0
  620. package/ui/js/Status.js +238 -0
  621. package/ui/js/Swap.js +166 -0
  622. package/ui/js/TabMove.js +39 -0
  623. package/ui/js/Tabs.js +751 -0
  624. package/ui/js/Tags.js +134 -0
  625. package/ui/js/Taildrop.js +128 -0
  626. package/ui/js/Tap.js +180 -0
  627. package/ui/js/TextSize.js +107 -0
  628. package/ui/js/Thumbs.js +158 -0
  629. package/ui/js/Transfer.js +172 -0
  630. package/ui/js/Trash.js +84 -0
  631. package/ui/js/TrashDates.js +47 -0
  632. package/ui/js/TrashDrop.js +64 -0
  633. package/ui/js/UiState.js +438 -0
  634. package/ui/js/Unlock.js +152 -0
  635. package/ui/js/Update.js +130 -0
  636. package/ui/js/UserDirs.js +56 -0
  637. package/ui/qmldir +230 -0
package/ui/Pane.qml ADDED
@@ -0,0 +1,1320 @@
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 extractHereRequested(string path, int entries)
275
+ signal extractPasswordRequested(string path, string dest, string err)
276
+ signal permissionsRequested(string path)
277
+ // MenuAdditions040: the whole selection's paths for the multi-row card.
278
+ signal permissionsBatchRequested(var paths)
279
+ // Bulk rename takes the whole selection's paths for its card.
280
+ signal bulkRenameRequested(var paths)
281
+ signal pathBarRequested() // ":" and Ctrl+L; the bar is chrome, so shell.qml opens it as it does the popup above
282
+ signal textSizeRequested(int direction) // issue 9's zoom pair, +1, -1 or 0 to follow Omarchy again; the size is the window's
283
+
284
+ // The window covers [held, held + rows.length) and nothing outside it is in memory.
285
+ property int held: 0
286
+ property var rows: []
287
+ // The current response's Kind dictionary; every row's own "k" is an index into this one array.
288
+ property var kindNames: []
289
+ // The listing rows the filter leaves standing, null when none is up: a view position goes in and
290
+ // a listing row comes out, which is what every row-indexed cache and the selection still hold.
291
+ readonly property var shown: Filter.shown(root.rows, root.held, root.filterQuery)
292
+ readonly property int shownTotal: root.shown === null ? root.total : root.shown.length
293
+
294
+ readonly property int minBuffer: 50
295
+ readonly property int maxBuffer: 1000
296
+ readonly property int defaultBuffer: 150
297
+ property int bufferRows: defaultBuffer
298
+ readonly property int buffer: Math.max(minBuffer, Math.min(maxBuffer, bufferRows))
299
+ readonly property int visibleRows: Math.max(1, Math.ceil(list.height / Theme.fileRowHeight))
300
+ readonly property int windowSize: visibleRows + 2 * buffer
301
+ // The leading result rows a live answer may carry folder sizes for: none when the shown view would ask for none.
302
+ readonly property int sizeWish: DirSizes.wantsSizes(root.viewMode, ViewState.hiddenCols, root.storageClass, root.storageKnown) ? root.visibleRows : 0
303
+ readonly property int refetchMargin: 25
304
+ readonly property int cacheRows: 4
305
+ readonly property int coalesceMs: 16
306
+ // A settle, not a stream: a fling must issue no request at all, see AGENTS.md "Thumbnail requests in the GUI".
307
+ readonly property int settleMs: 120
308
+ // The first screen's settle only has to outlast the compositor's resize, see AGENTS.md "Thumbnail requests in the GUI".
309
+ readonly property int firstSettleMs: 70
310
+ // Seven screens of history at this row height, so a policy bug costs memory slowly, not without limit.
311
+ readonly property int thumbCap: 240
312
+ // Sizes are only ever what the user asked for, so the map holds a whole listing's folders.
313
+ readonly property int dirSizeCap: 1000000
314
+ property var thumbState: Thumbs.empty()
315
+ // The class verdict the last refresh spent, so a preview change that moves nothing for
316
+ // this directory (zoom step, other classes) refreshes nothing; see ui/js/ExtThumbs.js.
317
+ property string extVerdict: ExtThumbs.verdict("", ViewState.preview)
318
+ // Same cap as thumbState, same reason; a directory row's size is only ever asked for by a viewport.
319
+ property var dirSizeState: DirSizes.empty()
320
+ // The filesystem answers user extended attributes here, probed once per directory beside the fsinfo figures.
321
+ property bool canTag: true
322
+ // File tags (ui/TagState.qml) are built when a listing first carries one or a tag key or menu first asks.
323
+ property var tagState: null
324
+ function tags() {
325
+ if (root.tagState === null) root.tagState = Qt.createComponent("TagState.qml").createObject(root, { pane: root })
326
+ return root.tagState
327
+ }
328
+ // Input to rows is stamped inside the UI, because a harness that polls IPC across the interval times itself; see AGENTS.md "Testing".
329
+ property real inputAt: 0
330
+ property real rowsAt: 0
331
+
332
+ // The cut or copied paths, absolute because a paste lands in a different directory; see ui/js/Ops.js.
333
+ property var clipboard: Ops.emptyClipboard()
334
+ // Shared system clipboard session, including outstanding backend replies and owner tokens.
335
+ property var clipboardState: Clipboard.state()
336
+ // True when this pane's clipboard watcher could not start; Paste then reads on demand.
337
+ property bool clipboardWatchFailed: false
338
+ // The outstanding ordered clipboard request, or null; late replies cannot publish an older choice.
339
+ property var clipPending: null
340
+ // Ordered choices waiting for the untagged paths reply or already resolved by a menu snapshot.
341
+ property var clipQueue: []
342
+ // Monotonic choice number; only the newest request can publish to the system clipboard.
343
+ property int clipSequence: 0
344
+ // Which asker a pending paths reply belongs to, null meaning the clipboard, which is what every
345
+ // reply meant before compress also had to resolve a selection wider than this pane holds.
346
+ property var pathsPending: null
347
+ // A file drag in progress holds the watched re-read, so rows never shift under a live pointer.
348
+ property bool dragHolding: false
349
+ property bool awaitingPaths: false
350
+ readonly property int anchorRowHeight: Theme.fileRowHeight
351
+ // What the status bar's sticky slot is reporting, or an idle transfer; see ui/js/Ops.js.
352
+ property var transfer: Ops.emptyTransfer()
353
+ // The row that is its own editor right now, or -1; ui/List.qml's delegate reads it per row.
354
+ property int renamingIndex: -1
355
+ // Set by a pointer-committed rename so the reply reveals nothing; ui/js/Nav.js clears it.
356
+ property bool renameKeepsPointerRow: false
357
+
358
+ // Read once for the window: the chrome's path and the search strip's scope both shorten with it.
359
+ readonly property string home: Quickshell.env("HOME") || ""
360
+
361
+ // "list", "columns" or "grid"; the chrome's own buttons write it and the views read it.
362
+ property string viewMode: "list"
363
+ Component.onCompleted: root.viewMode = root.desiredView(root.path)
364
+ // A view switch takes the filter with it; only chooseView() and adoptView() persist, so a restored view writes nothing.
365
+ onViewModeChanged: Filter.close(root)
366
+ // The folder a view belongs to; a search, Recent or Trash standing over it is not that folder.
367
+ readonly property string viewFolder: root.searchMode.length === 0 && root.recentMode.length === 0 && !trashHost.opened ? root.path : ""
368
+ // Dual draws lists in both panes, and a folder's own view applies only while it is remembered.
369
+ function desiredView(path) {
370
+ return root.listOnly || root.dualMode ? "list" : FolderViews.viewFor(ViewState.folderViews, path, ViewState.view,
371
+ ViewState.rememberViewPerFolder)
372
+ }
373
+ function restoreView(path) {
374
+ var desired = root.desiredView(path)
375
+ if (root.viewMode !== desired)
376
+ root.viewMode = desired
377
+ }
378
+ // A tab brings back the view it held: a remembered folder keeps its own, any other view becomes the standing one.
379
+ function adoptView(path, mode) {
380
+ var own = ViewState.rememberViewPerFolder && FolderViews.has(ViewState.folderViews, path)
381
+ if (!own && !root.dualMode && mode !== ViewState.view)
382
+ ViewState.changeKey("view", mode)
383
+ root.restoreView(path)
384
+ }
385
+ // Effective values, so a settled ui.json that only spells out the defaults re-lists nothing and leaves the view where it is.
386
+ readonly property string listingPreferences: Sort.preferences(ViewState.state)
387
+ property string appliedListingPreferences: ""
388
+ // A preference re-list keeps selection and cursor by name, Finder's rule; the watcher defers
389
+ // while a selection stands, this one preserves it instead. Cleared when its rows land.
390
+ property var preferenceAnchor: null
391
+ onListingPreferencesChanged: {
392
+ // A hidden dual pane retains its session sort when the single pane changes the saved default.
393
+ if (root.backend && root.backend.preserveSort && !root.visible && root.appliedListingPreferences.length > 0) {
394
+ var applied = JSON.parse(root.appliedListingPreferences)
395
+ applied[1] = ViewState.state.sort
396
+ root.appliedListingPreferences = JSON.stringify(applied)
397
+ }
398
+ preferences.restart()
399
+ }
400
+ onVisibleChanged: if (root.visible) preferences.restart()
401
+ // A restored folder that never answers leaves "loading" for the waiting state, with Home
402
+ // reachable through it; the late rows, if they ever land, are dropped by the swap as stale.
403
+ readonly property int listingWaitMs: Startup.LISTING_WAIT_MS
404
+ Timer {
405
+ id: listingWait
406
+ interval: root.listingWaitMs
407
+ onTriggered: {
408
+ if (root.listInFlight && root.listingState === "loading") {
409
+ root.listingState = "waiting"
410
+ root.stateMessage = "That folder is not responding."
411
+ }
412
+ }
413
+ }
414
+ onListInFlightChanged: if (!root.listInFlight) { preferences.restart(); Sort.applyPending(root); listingWait.stop() } else listingWait.restart()
415
+ onSearchModeChanged: { preferences.restart(); root.moveSearchFocus() }
416
+ // flea-qml: typing gives the Find field the keyboard; leaving it hands the keyboard back to the list.
417
+ function moveSearchFocus() {
418
+ if (root.searchMode === Search.TYPING) Qt.callLater(header.focusSearchField)
419
+ else if (header.searchFieldFocused && root.listArea) root.listArea.forceActiveFocus()
420
+ }
421
+ onRecentModeChanged: if (root.recentMode.length === 0) preferences.restart()
422
+ Timer {
423
+ id: preferences
424
+ interval: 0
425
+ onTriggered: {
426
+ root.restoreView(root.viewFolder)
427
+ if (!root.visible || !root.path || root.listInFlight || root.searchMode.length > 0
428
+ || root.recentMode.length > 0
429
+ || root.appliedListingPreferences === root.listingPreferences) return
430
+ root.preferenceAnchor = Anchor.preference(root)
431
+ root.openWithoutHistory(root.path, { inPlace: true })
432
+ if (root.preferenceAnchor && root.preferenceAnchor.start > 0)
433
+ root.backend.window(root.preferenceAnchor.start, root.windowSize)
434
+ }
435
+ }
436
+ Connections {
437
+ target: ViewState
438
+ function onStateChanged() { preferences.restart() }
439
+ // A class switched on in Settings decodes again the same way the menu row does.
440
+ function onPreviewChanged() { root.refreshExtThumbs() }
441
+ }
442
+ // The zip probe lands here; the open menu is never rebuilt for it, the next opening draws it.
443
+ Connections {
444
+ target: root.backend
445
+ function onFlashImageResult(message) { Ops.noteZip(root, message) }
446
+ }
447
+
448
+
449
+ // Each tab retains its own back and forward navigation.
450
+ property var history: []
451
+ property var forwardHistory: []
452
+ property var tabs: null
453
+ property string terminalId: ""
454
+ onTerminalIdChanged: {
455
+ if (!terminalId) Qt.callLater(function() {
456
+ // A hidden WebEngineView no longer owns input. Wait until tab visibility
457
+ // settles, then return focus to the file list (not the old scope child).
458
+ if (!root.terminalId) {
459
+ root.focusView = Focus.LIST
460
+ root.listArea.forceActiveFocus()
461
+ }
462
+ })
463
+ }
464
+ readonly property var terminalHost: terminalLoader.item
465
+ property int tabCountNotice: -1
466
+ readonly property bool canGoBack: trashHost.opened || root.recentMode.length > 0 || root.history.length > 0
467
+ readonly property bool canGoUp: root.path.length > 1 || root.recentMode.length > 0 || Nav.climbsFromListing(root)
468
+
469
+ // The filesystem line the status bar draws, refreshed once per directory rather than per row.
470
+ property string fsName: ""
471
+ property real fsFree: 0
472
+ // 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.
473
+ property bool fsAnswered: false
474
+ // ExtThumbs: src/backend/extclass.rs's own word for the directory being shown, network,
475
+ // phone, usb or "", carried beside the fsinfo line once per directory change, never per row.
476
+ // storageKnown is false until that line lands, so the first settle never spends unknown as local.
477
+ property string storageClass: ""
478
+ property bool storageKnown: false
479
+ // 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.
480
+ property bool systemVolume: false
481
+
482
+ // Back and up close Recent rather than travelling; forward stays put outside the history.
483
+ 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) }
484
+ function goForward() { if (!trashHost.opened && !root.flashActive && root.recentMode.length === 0) Nav.forward(root) }
485
+
486
+ // Rename lives in ui/js/Ops.js with the other write operations; ui/List.qml's editor commits through this.
487
+ function commitRename(newName) { Ops.commitRename(root, newName) }
488
+ property int renameMenuId: 0
489
+ property string renameSource: ""
490
+ property string renameError: ""
491
+ property var renameRequest: null
492
+ readonly property bool renamePending: root.renameRequest !== null
493
+ // A sort asked for while an edit was open or a rename pending, held by
494
+ // ui/js/Sort.js and applied when the edit ends; never dropped, see applyPending.
495
+ property var pendingSort: null
496
+ property var convertSource: null
497
+ onRenamingIndexChanged: if (root.renamingIndex < 0) {
498
+ root.renameMenuId = 0
499
+ root.renameSource = ""
500
+ root.renameError = ""
501
+ // A rename that settled and an Escape that abandoned both end here, so a
502
+ // held sort applies on either; a navigation holds listInFlight and drops it.
503
+ Sort.applyPending(root)
504
+ }
505
+ // A navigation during a slow rename leaves renamingIndex at -1 with the
506
+ // request still pending, so no index change fires when it settles.
507
+ onRenamePendingChanged: if (!root.renamePending) Sort.applyPending(root)
508
+
509
+ // 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".
510
+ property var selection: Selection.create()
511
+ property int selectionVersion: 0
512
+ property int selectionAnchor: 0
513
+ property var selectionBand: null
514
+ // 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.
515
+ property bool pointerHeld: false
516
+ function isSelected(index) { return root.selectionVersion >= 0 && root.selection.has(index) }
517
+ function selectionCount() { return root.selectionVersion >= 0 ? root.selection.count() : 0 }
518
+ function selectedIndices() { return root.selectionVersion >= 0 ? root.selection.indices() : [] }
519
+ function toggleSelect() { Marks.toggleSelect(root) }
520
+ function selectAll() { Marks.selectAll(root); root.selectionVersion++ }
521
+ function clearSelection() { root.selection.clear(); root.selectionVersion++ }
522
+ function selectOnly(index, context) {
523
+ root.setCursor(index, context)
524
+ root.selection.only(root.cursorIndex)
525
+ root.selectionAnchor = root.cursorIndex
526
+ root.selectionVersion++
527
+ }
528
+ function extendSelection(delta) { Marks.extend(root, delta) }
529
+ // Ctrl+click and shift+click, the mouse's twins of v and shift+j/k; see keys.toml's [[pointer]].
530
+ function toggleSelectAt(index) { Marks.toggleRow(root, index) }
531
+ function extendSelectionTo(index) { Marks.extendToRow(root, index) }
532
+ // Named escapePressed, not escape, which collides with the JS global URI function; clears an active selection first, see keys.toml.
533
+ function escapePressed() { if (root.selection.count() > 0) { root.clearSelection(); return }; root.message("", false) }
534
+
535
+ function applyPendingSelect() { Nav.applyPendingSelect(root); Nav.applyPendingBackground(root) }
536
+ function openBackgroundMenu(at) { menu.openBackground(at) }
537
+ // flea-qml: measure folders only when the user asks; see src/dirsize.js and ui/js/DirSizes.js.
538
+ function measureDirs(rows) {
539
+ var dirs = rows.filter(function (i) { var r = root.rowFor(i); return r && r.d })
540
+ if (dirs.length === 0) return
541
+ root.dirSizeState = DirSizes.asking(root.dirSizeState, dirs)
542
+ root.backend.dirsize(dirs)
543
+ }
544
+ function measureAllDirs() {
545
+ root.dirSizeState = DirSizes.askingAll(root.dirSizeState)
546
+ root.backend.dirsizeAll()
547
+ }
548
+
549
+ function refresh(selectPath, keepSelection) {
550
+ // Recent re-reads the history rather than re-listing its base, which is the root: see
551
+ // ui/js/RecentMode.js refresh, and Nav.refresh for every other listing.
552
+ if (root.recentMode === "trash" || (root.recentMode.length > 0 && keepSelection !== true)) RecentMode.refresh(root, selectPath)
553
+ else Nav.refresh(root, selectPath, keepSelection)
554
+ }
555
+
556
+ function open(newPath) {
557
+ if (trashHost.confirming) return
558
+ if (root.flashActive) root.overlayParent.closeFlash()
559
+ Nav.open(root, newPath)
560
+ }
561
+
562
+ // An eject releases the mount under path: queued reads stop first, the preview lets go,
563
+ // and a pane showing that volume moves Home rather than standing on a vanished folder.
564
+ function quiesceVolume(path) {
565
+ if (root.backend)
566
+ root.backend.quiesce()
567
+ if (root.preview)
568
+ root.preview.close()
569
+ var home = root.home
570
+ if (home.length > 0 && String(path).length > 0
571
+ && (root.path === path || root.path.indexOf(path + "/") === 0))
572
+ Nav.openPlace(root, home)
573
+ }
574
+
575
+ // 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.
576
+ function openWithoutHistory(newPath, options) {
577
+ // Only a navigation lets a waiting listing go; an in-place re-list of the folder being left keeps it.
578
+ if (!options || options.inPlace !== true) Anchor.clearWaiting(root)
579
+ // 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.
580
+ if (root.listInFlight && (!options || options.supersede !== true)) {
581
+ root.message("A directory is already loading.", false)
582
+ return
583
+ }
584
+ if (root.listInFlight) listingWait.restart()
585
+ // Every real navigation leaves Recent, Trash and Flash; options.inPlace, a re-list of this folder, leaves Trash up.
586
+ if (!options || options.inPlace !== true) { trashHost.close(); if (root.flashActive) root.overlayParent.closeFlash() }
587
+ RecentMode.leave(root)
588
+ var applied = root.appliedListingPreferences ? JSON.parse(root.appliedListingPreferences) : []
589
+ // Search exit can enter here before the preferences timer consumes a deferred Settings change.
590
+ if (JSON.stringify(applied[1]) !== JSON.stringify(ViewState.state.sort)) root.backend.resetSort(newPath)
591
+ // options.keepHidden is the tab restore's alone: it just put back this tab's own dotfile answer, which the standing preference would overwrite.
592
+ if (!options || options.keepHidden !== true) root.showHidden = ViewState.state.hidden === true
593
+ Nav.openWithoutHistory(root, newPath, options)
594
+ }
595
+
596
+ // The toggle re-lists rather than filtering client-side: the model is a row count over the
597
+ // backend's own listing, which never held the dotfiles to begin with when they were off.
598
+ // Re-listing also clears the cursor and selection, the same as opening any other directory.
599
+ // Recent lists what the history names, dotfiles among them, so the toggle has nothing to hide there.
600
+ function toggleHidden() {
601
+ if (root.recentMode.length > 0) { root.message("This collection includes hidden items", false); return }
602
+ root.showHidden = !root.showHidden
603
+ ViewState.changeKey("hidden", root.showHidden)
604
+ // flea-qml: a standing Find takes the new setting and stays, rather than leaving for the folder.
605
+ if (root.searchMode.length > 0) { Search.rehide(root); return }
606
+ root.open(root.path)
607
+ }
608
+
609
+ // Where a drop on this pane lands. While a listing is out the pane's own path is still the
610
+ // directory it is leaving, so a drop taken in that window landed in the wrong one: measured by
611
+ // tests/drag.sh R7, where a drop on a tab whose listing was still out copied into the source.
612
+ readonly property string dropPath: root.listInFlight && root.listingPath.length > 0
613
+ ? root.listingPath : root.path
614
+
615
+ function rowFor(index) {
616
+ var offset = index - root.held
617
+ if (offset < 0 || offset >= root.rows.length)
618
+ return null
619
+ return root.rows[offset]
620
+ }
621
+
622
+ // The list's own delegate, which is what every reader of a rendered row cell needs; only the
623
+ // list draws those cells at all.
624
+ function itemFor(index) {
625
+ return list.itemAtIndex(index)
626
+ }
627
+
628
+ // The delegate actually on screen, whichever view is showing. A coordinate taken off a hidden
629
+ // view is a coordinate nothing can be clicked at, which is what the columns view used to return.
630
+ // index is a listing row and every view's itemAtIndex wants a view position, which is a different
631
+ // number under a filter: passing the listing row answered null for a row plainly on screen, and
632
+ // once the match count passed it, the delegate at that position under an unrelated row.
633
+ function visibleItemFor(index) {
634
+ return root.listArea.itemAtIndex(Filter.viewOf(root.shown, index))
635
+ }
636
+
637
+ // shell.qml's IPC thumbFile reader calls this; the lookup lives with the thumbnail machinery in ui/List.qml.
638
+ function thumbFor(index) { return list.thumbFor(index) }
639
+
640
+ // Lifted to Focus.act, see ui/js/Focus.js, which routes "settings" here from the list and the rail alike.
641
+ function act(action, menuId, paths, context) {
642
+ // Every dispatched action ends a pending slow click; its own rename runs after the timer cleared the record.
643
+ cancelSlowClick()
644
+ if (trashHost.confirming) return
645
+ if (action === "openTrash" || action === "emptyTrash" || action === "restoreAll") { trashHost.action(action); return }
646
+ if (trashHost.browsing && ["restoreTrashSelection", "deletePermanently", "trash"].indexOf(action) >= 0) { trashHost.action(action, menuId); return }
647
+ if (action === "settings") { root.settingsPanel.open(root); return }
648
+ if (action === "toggleLangMarks") { ViewState.toggleLangMarks(); return }
649
+ if (action === "permissions") { root.openPermissions(); return }
650
+ if (action === "rename" && root.openBulkRename(paths)) return // several rows open the bulk card
651
+ // Recent is a history, not a directory: a new file there would land in the root it stands on.
652
+ if (action === "newFile" && root.recentMode.length > 0) { root.message((root.recentMode === "trash" ? "Trash" : "Recent") + " is a collection. Open a folder first", false); return }
653
+ if (["newFile", "rename", "openWith", "moveTo", "copyTo", "properties", "deletePermanently"].indexOf(action) >= 0) {
654
+ menuActions.open(action, menuId || 0, context)
655
+ return
656
+ }
657
+ Focus.act(action, root, menuId, paths)
658
+ }
659
+ // The menu's own dispatch lives with the rest of the menu machinery; this is the one seam the
660
+ // rail's place menu and the dialogs still call through.
661
+ function performMenu(action, menuId, paths) { menuActions.perform(action, menuId, paths) }
662
+ // 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.
663
+ function sheetMenuAction(action) {
664
+ menu.clearRail()
665
+ menu.forHeader = false
666
+ menu.hasRow = true
667
+ menu.openedIdentity = root.menuSelectionIdentity()
668
+ menuActions.snapshot()
669
+ menuActions.sheetRequest = menuActions.requestId
670
+ menuActions.activate(action, true)
671
+ }
672
+
673
+ function permissionSelection() {
674
+ var indices = Ops.targetIndices(root)
675
+ return indices.length === 1 ? root.rowFor(indices[0]) : null
676
+ }
677
+ // MenuAdditions040 callout 10: Make executable acts on exactly one target and that target is
678
+ // the cursor row; anything else hides the row and the action refuses.
679
+ function isSingleCursorTarget() {
680
+ var indices = Ops.targetIndices(root)
681
+ return indices.length === 1 && indices[0] === root.cursorIndex
682
+ }
683
+ // The cursor row's two-byte shebang probe, asked of the backend once per menu open for that one file.
684
+ property bool rowHasShebang: false
685
+ property string shebangAsked: ""
686
+ property int shebangId: 0
687
+ property int makeExecPendingId: 0
688
+ // Show original's own pending id, dropped by any navigation before its reply lands.
689
+ property int linkTargetPendingId: 0
690
+ // The one path a shebang answer may land for, "" unless the cursor row is a regular file without any execute bit.
691
+ function shebangTarget() {
692
+ var row = root.cursorRow
693
+ if (!row || row.d) return ""
694
+ var bits = Number(row.p) || 0
695
+ if ((bits & Format.S_IFMT) !== Format.S_IFREG || (bits & Format.ANY_EXECUTE_BIT) !== 0) return ""
696
+ return root.join(Search.baseOf(root), row.n)
697
+ }
698
+ function checkShebang() {
699
+ var path = root.shebangTarget()
700
+ if (path.length === 0) {
701
+ root.shebangAsked = ""
702
+ root.rowHasShebang = false
703
+ return
704
+ }
705
+ root.shebangId += 1
706
+ root.shebangAsked = path
707
+ root.rowHasShebang = false
708
+ root.backend.send({ c: "shebang", path: path, id: root.shebangId })
709
+ }
710
+ // Only the newest id on the asked path lands, so a late answer for an earlier file never arms a later one.
711
+ function noteShebang(path, hasShebang, id) {
712
+ if (!Permissions.landsShebang(path, id, root.shebangAsked, root.shebangId)) return
713
+ root.rowHasShebang = hasShebang
714
+ if (menu.opened && menu.hasRow) menu.refreshProviderRows()
715
+ }
716
+ // MenuAdditions040 callout 10: adds the owner execute bit through the permissions batch, so one undo restores it.
717
+ // The context menu is its one caller, and it acts on the single cursor target.
718
+ function makeExecutable() {
719
+ if (!root.isSingleCursorTarget()) {
720
+ root.message("Make executable works on a script that cannot run yet", false)
721
+ return
722
+ }
723
+ var row = root.rowFor(root.cursorIndex)
724
+ if (!row || !Menu.canMakeExecutable(row.p, 1, root.rowHasShebang, true)) {
725
+ root.message("Make executable works on a script that cannot run yet", false)
726
+ return
727
+ }
728
+ var taken = [root.join(Search.baseOf(root), row.n)]
729
+ var oldBits = (Number(row.p) || 0) & 0o7777
730
+ var octal = Permissions.octal((oldBits | Format.S_IXUSR) & 0o7777)
731
+ root.makeExecPendingId += 1
732
+ root.backend.send({ c: "permissionsBatch", paths: taken, modes: [octal], id: Permissions.MAKE_EXEC_ID + root.makeExecPendingId })
733
+ }
734
+ // MenuAdditions040: Permissions takes the whole selection, so the menu
735
+ // row carries every target's mode and the dialog inspects each in turn.
736
+ function permissionModes() {
737
+ var out = []
738
+ var indices = Ops.targetIndices(root)
739
+ for (var i = 0; i < indices.length; i++) {
740
+ var row = root.rowFor(indices[i])
741
+ if (row) out.push(row.p)
742
+ }
743
+ return out
744
+ }
745
+ function openPermissions() {
746
+ var idx = Ops.targetIndices(root)
747
+ if (idx.length === 0) { Ops.sayNoTarget(root); return }
748
+ if (root.pathsPending || root.clipPending !== null) { Ops.pathsBusy(root); return }
749
+ // The selection can reach past the held window, so the backend
750
+ // resolves the indices while it still can, the same rule clip follows.
751
+ root.pathsPending = { kind: "permissions" }
752
+ root.backend.askPaths(idx)
753
+ }
754
+ function openPermissionsWith(paths) {
755
+ if (paths.length === 0) { Ops.sayNoTarget(root); return }
756
+ if (paths.length === 1) {
757
+ var target = root.permissionSelection()
758
+ var single = target ? target.p : 0
759
+ if (Menu.permissionsEntry(single, 1).disabled) {
760
+ root.message("Permissions takes one item, not a link.", true)
761
+ return
762
+ }
763
+ root.permissionsRequested(paths[0])
764
+ return
765
+ }
766
+ root.permissionsBatchRequested(paths)
767
+ }
768
+ // Several rows open the bulk card; one row answers false so the caller keeps its single-row route.
769
+ function openBulkRename(paths) {
770
+ if (paths && paths.length > 1) { root.bulkRenameRequested(paths); return true }
771
+ var idx = paths && paths.length ? [] : Ops.targetIndices(root)
772
+ if (idx.length <= 1) return false
773
+ if (root.pathsPending || root.clipPending !== null) { Ops.pathsBusy(root); return true }
774
+ root.pathsPending = { kind: "bulkrename" }
775
+ root.backend.askPaths(idx)
776
+ return true
777
+ }
778
+ // The backend's answer to openBulkRename's askPaths.
779
+ function openBulkRenameWith(paths) {
780
+ if (paths.length <= 1) { Ops.sayNoTarget(root); return }
781
+ root.bulkRenameRequested(paths)
782
+ }
783
+ // One line for the whole batch, and one undo reverses every rename it made.
784
+ function bulkRenamed(ok) {
785
+ root.message(Ops.bulkDoneLine(ok), false)
786
+ root.clearSelection()
787
+ root.refresh("")
788
+ }
789
+ // c and P open Copy as and Paste as on their first flyout row.
790
+ function openCopyAs() {
791
+ if (!root.openCursorMenu()) {
792
+ root.message("No item under the cursor to open a menu on", false)
793
+ return
794
+ }
795
+ menu.openSubmenuFor("copyAs")
796
+ }
797
+ function openPasteAs() {
798
+ if (!root.openCursorMenu())
799
+ menu[root.cursorRow ? "openAt" : "openBackground"](root.listSlot.mapToItem(null, root.listSlot.width / 2, root.listSlot.height / 2))
800
+ if (!menu.clipboardAvailable) {
801
+ menu.close()
802
+ root.message(Menu.EMPTY_CLIPBOARD, false)
803
+ return
804
+ }
805
+ menu.openSubmenuFor("pasteAs")
806
+ }
807
+ // MenuAdditions040: V flips the marks over the rows the listing draws;
808
+ // the filter applies, and a close match is never selected.
809
+ function invertSelection() { Marks.invert(root) }
810
+ // MenuAdditions040: Show original reveals a symlink's target in its own
811
+ // folder, the same path Show in folder uses.
812
+ function showOriginal() {
813
+ var row = root.rowFor(root.cursorIndex)
814
+ if (!row || !Format.isSymlink(row.p)) {
815
+ root.message("Show original needs the cursor on a link", false)
816
+ return
817
+ }
818
+ root.requestLinkTarget(root.join(Search.baseOf(root), row.n))
819
+ }
820
+ // Both entrances send through here, so one pending id covers the cursor and the menu.
821
+ function requestLinkTarget(path) {
822
+ root.linkTargetPendingId = root.backend.nextLinkTargetId()
823
+ root.backend.send({ c: "linktarget", path: path, id: root.linkTargetPendingId })
824
+ }
825
+ // Paste as links reads the file clipboard and asks through the collision card.
826
+ function pasteLink(kind) {
827
+ if (RecentMode.refusePaste(root)) return
828
+ Ops.pasteLink(root, kind)
829
+ }
830
+
831
+ // index is a listing row, which is what every caller outside ui/js/Filter.js holds; the clamp
832
+ // and the scroll both happen in view space, because a filter can be narrowing what is drawn.
833
+ // A click carries context 0; a keyboard move keeps the default 3.
834
+ function setCursor(index, context) { Filter.setCursor(root, index, context) }
835
+ // 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.
836
+ function showRow(view, context) {
837
+ if (root.viewMode === "columns" && root.columnsArea) root.columnsArea.activeColumn().showCursor(view, context)
838
+ else if (root.viewMode === "list") list.showCursor(view, context)
839
+ else root.listArea.showCursor(view, context)
840
+ root.listArea.restartCoalesce()
841
+ }
842
+
843
+ // A successful pointer commit preserves the newly selected row; a refusal returns to its editor.
844
+ function commitOpenRename() {
845
+ var item = root.renameEditor()
846
+ if (!item || root.renamePending) return
847
+ root.renameKeepsPointerRow = true
848
+ if (!item.commitEditor() || root.renameError.length > 0) root.renameKeepsPointerRow = false
849
+ }
850
+
851
+ // The live editor or null: a set renamingIndex is not evidence one exists, see ui/RenameField.qml.
852
+ function renameEditor() {
853
+ if (root.renamingIndex < 0) return null
854
+ // The columns view draws one editor over its active column rather than one inside each row.
855
+ if (root.viewMode === "columns")
856
+ return root.columnsArea && root.columnsArea.activeColumn().renaming ? root.columnsArea.activeColumn() : null
857
+ var item = root.viewMode === "grid" ? (root.listArea ? root.listArea.currentRenameTile(root) : null) : root.visibleItemFor(root.renamingIndex)
858
+ return item && item.renaming ? item : null
859
+ }
860
+
861
+ function openCursor() { Nav.openCursor(root, wire.opener) }
862
+
863
+ // A path the caller already resolved, for the columns view's neighbour rows, which have no cursor.
864
+ function openFile(path) { wire.opener.open(path) }
865
+
866
+ // A terminal in the caller's place or the directory being shown, through flea --terminal.
867
+ function openTerminal(path) {
868
+ terminalLoader.active = true
869
+ Tabs.openTerminal(root, path || root.path)
870
+ }
871
+ Loader {
872
+ id: terminalLoader
873
+ active: false
874
+ anchors { top: parent.top; left: railHost.right; right: parent.right; bottom: parent.bottom }
875
+ z: 1
876
+ source: "TerminalTabs.qml"
877
+ onLoaded: item.pane = root
878
+ }
879
+
880
+ function newWindow() { Quickshell.execDetached(Tabs.freshLaunch([Quickshell.env("FLEA_BIN") || "flea", root.path])) }
881
+
882
+ // Quoted when it holds whitespace, because this one is pasted into a shell: see ui/js/Format.js.
883
+ function copyDirPath() { wire.opener.copyText(Format.shellQuoted(root.path)) }
884
+
885
+ 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) }
886
+
887
+ // The sort Recent replaced, handed back on leaving; ui/js/Search.js reaches it through here.
888
+ function restoreRecentSort() { RecentMode.restoreSort(root) }
889
+ // The rail's Recent row answers with the history's paths, read bounded the way the
890
+ // path jump reads them; a second open while one lands replaces it, the way a navigation does.
891
+ function openRecent(paths, visits) { RecentMode.run(root, paths, visits) }
892
+ function closeRecent() { RecentMode.close(root) }
893
+
894
+ function join(base, name) {
895
+ if (base === "/" && (/^[A-Za-z]:[\\/]/.test(name) || name.indexOf("//") === 0)) return name
896
+ return base === "/" ? "/" + name : base + "/" + name
897
+ }
898
+
899
+ Flea.PaneWire {
900
+ id: wire
901
+ pane: root
902
+ }
903
+
904
+ Flea.PaneRail {
905
+ id: railHost
906
+ anchors.left: parent.left
907
+ // Over the listing, because with auto-hide on the rail is an overlay and not a column.
908
+ z: 3
909
+ pane: root
910
+ service: root.networkService
911
+ }
912
+
913
+ // On glass the listing's ground, beside the rail and below the path and search strips, never under them; see ui/Glass.qml.
914
+ Rectangle {
915
+ anchors { top: root.searchMode.length > 0 ? header.bottom : panePath.bottom; bottom: parent.bottom; left: railHost.right; right: parent.right }
916
+ visible: Glass.on
917
+ color: Glass.listBackground
918
+ }
919
+
920
+ Flea.PanePath {
921
+ id: panePath
922
+ anchors { left: railHost.right; right: parent.right; top: parent.top }
923
+ height: root.dualMode && !trashHost.opened ? Theme.chromeHeight : 0
924
+ visible: height > 0
925
+ // A history is a location and not a directory, so the strip says its own name.
926
+ path: root.recentMode === "trash" ? "trash:///" : root.recentMode.length > 0 ? "Recent" : root.path
927
+ home: root.home
928
+ pathAlias: root.pathAlias
929
+ focused: root.paneFocused
930
+ inputLive: !(root.preview && root.preview.active)
931
+ pane: root
932
+ onChosen: function (path) { if (path === "trash:///") root.trash.open(); else root.open(path) }
933
+ onEditRequested: root.pathBarRequested()
934
+ }
935
+
936
+ PointHandler {
937
+ id: focusPointer
938
+ acceptedButtons: Qt.AllButtons
939
+ onActiveChanged: if (active) railHost.focusPress(focusPointer.point.position)
940
+ }
941
+
942
+ Loader {
943
+ id: trashStrip
944
+ anchors { top: panePath.bottom; left: railHost.right; right: parent.right }
945
+ active: trashHost.browsing
946
+ height: active ? Theme.chromeHeight : 0
947
+ visible: active
948
+ source: "TrashToolbar.qml"
949
+ onLoaded: item.trash = trashHost
950
+ }
951
+
952
+ Flea.Header {
953
+ id: header
954
+ // Only the list view has columns to head, and neither the grid board nor the columns board
955
+ // draws one; the strip collapses rather than hiding, so the view below starts at the top of
956
+ // the pane instead of a gap. A search takes the strip whole, in any view.
957
+ visible: !trashHost.opened && !root.flashActive && (root.viewMode === "list" || root.searchMode.length > 0)
958
+ height: visible ? implicitHeight : 0
959
+ anchors.top: trashStrip.bottom
960
+ anchors.left: railHost.right
961
+ anchors.right: parent.right
962
+ // The held rows a double click fits and F4 fits whole; ListColumns040 never scans the directory.
963
+ pane: root
964
+ viewMode: root.viewMode
965
+ sortBy: root.backend.sortBy
966
+ sortDesc: root.backend.sortDesc
967
+ dualMode: root.dualMode
968
+ recent: root.recentMode.length > 0
969
+ hiddenCols: root.dualMode ? ["mode", "kind"].concat(ViewState.hiddenCols) : ViewState.hiddenCols
970
+ onSortRequested: function (key) { Sort.column(root, key) }
971
+ onMenuRequested: function (pos) { menu.openForHeader(pos) }
972
+ searchMode: root.searchMode
973
+ searchQuery: root.searchQuery
974
+ searchScope: Search.scope(Search.scopeRoot(root.path, root.home, root.searchStep), root.home)
975
+ // flea-qml: the count is the last run's; an edited line waiting for Enter says so instead.
976
+ searchNote: Search.note(root.total, root.searchRunning, root.searchSent.length === 0)
977
+ // flea-qml: the keys the strip answers, named plainly.
978
+ searchWayOut: "Enter/Tab/Esc/Ctrl+G"
979
+ searchPlaceholder: Search.placeholder(root.path, root.home, root.searchStep, root.searchContents)
980
+ searchTabHint: ""
981
+ searchRunning: root.searchRunning
982
+ onSearchCloseRequested: Search.dismiss(root)
983
+ // flea-qml: the field edits the query; Enter, Esc, Tab and the arrows take the pane's own route.
984
+ onSearchEdited: function (text) { root.searchQuery = text }
985
+ onSearchKeyOut: function (event) { Focus.handleKey(event, root, root.sidebar, "find") }
986
+ onSearchFieldEntered: if (root.searchMode === Search.RESULTS) root.searchMode = Search.TYPING
987
+ }
988
+
989
+ // The two views share the same slot, the same rows and the same cursor; only one is ever up, and
990
+ // listArea points at whichever it is, so every caller of restartSettle stays view-agnostic and
991
+ // shell.qml lays the empty-state overlay over the right one.
992
+ // The list stands in for a view still being built, so no binding on listArea ever reads null.
993
+ readonly property var listArea: root.viewMode === "grid" && gridLoader.item ? gridLoader.item
994
+ : root.viewMode === "columns" && columnsLoader.item ? columnsLoader.item : list
995
+ // How far a cursor step down moves: one row in the list, one row of tiles in the grid.
996
+ // The columns view's own preview, exposed so a test can assert its facts without OCR; null until built.
997
+ readonly property var columnsArea: columnsLoader.item
998
+ readonly property bool dragActive: root.dragHolding || !!(root.listArea && root.listArea.fileDragActive === true)
999
+ // Where the view sits, for an anchor: a Loader's item is no sibling of anything here, its Loader is.
1000
+ readonly property Item listSlot: root.viewMode === "grid" ? gridLoader : root.viewMode === "columns" ? columnsLoader : list
1001
+ readonly property int cursorStride: root.viewMode === "grid" && gridLoader.item ? gridLoader.item.columns : 1
1002
+
1003
+ Flea.FilterStrip {
1004
+ id: filterStrip
1005
+ anchors.top: header.bottom
1006
+ anchors.left: railHost.right
1007
+ anchors.right: parent.right
1008
+ pane: root
1009
+ }
1010
+
1011
+ // A hidden view is not a free view, see AGENTS.md rule 6: each of these two is built by its first
1012
+ // switch and kept, so a launch in the list view pays for one view's rows and marks, not three.
1013
+ property bool columnsBuilt: false
1014
+ property bool gridBuilt: false
1015
+ // setSource, not source: the view reads pane in its own bindings, so it has to hold one from birth.
1016
+ Loader {
1017
+ id: columnsLoader
1018
+ active: root.viewMode === "columns" || root.columnsBuilt
1019
+ visible: !trashHost.opened && !root.flashActive
1020
+ focus: visible && root.viewMode === "columns"
1021
+ anchors { top: filterStrip.bottom; left: railHost.right; right: parent.right; bottom: parent.bottom }
1022
+ Component.onCompleted: setSource("ColumnsArea.qml", { pane: root, menu: menu, focus: true })
1023
+ onLoaded: { root.columnsBuilt = true; item.visible = Qt.binding(function () { return root.viewMode === "columns" }) }
1024
+ }
1025
+
1026
+ Loader {
1027
+ id: gridLoader
1028
+ active: root.viewMode === "grid" || root.gridBuilt
1029
+ visible: !trashHost.opened && !root.flashActive
1030
+ focus: visible && root.viewMode === "grid"
1031
+ anchors { top: filterStrip.bottom; left: railHost.right; right: parent.right; bottom: parent.bottom }
1032
+ Component.onCompleted: setSource("GridArea.qml", { pane: root, menu: menu })
1033
+ onLoaded: { root.gridBuilt = true; item.visible = Qt.binding(function () { return root.viewMode === "grid" }) }
1034
+ }
1035
+
1036
+ // The grid's plans land the way the list's do below: it computes, and only the pane writes the two states.
1037
+ Connections {
1038
+ target: columnsLoader.item
1039
+ function onThumbsApplied(work) { root.thumbState = Thumbs.applied(root.thumbState, work) }
1040
+ function onDirSizesApplied(ask) { root.dirSizeState = DirSizes.applied(root.dirSizeState, ask) }
1041
+ }
1042
+
1043
+ Connections {
1044
+ target: gridLoader.item
1045
+ function onThumbsApplied(work) { root.thumbState = Thumbs.applied(root.thumbState, work) }
1046
+ function onDirSizesApplied(ask) { root.dirSizeState = DirSizes.applied(root.dirSizeState, ask) }
1047
+ function onDirSizesCancelled() { root.dirSizeState = DirSizes.cancelled(root.dirSizeState) }
1048
+ }
1049
+
1050
+ readonly property int previewIndex: root.viewMode === "columns" && columnsLoader.item ? columnsLoader.item.previewIndex : -1
1051
+ readonly property var previewColumnItem: root.viewMode === "columns" && columnsLoader.item ? columnsLoader.item.previewColumn : null
1052
+ function loadSelectionPreview() {
1053
+ if (!ViewState.previewColumn || root.dualMode) return
1054
+ if (root.viewMode === "columns" && columnsLoader.item) columnsLoader.item.loadSelection()
1055
+ }
1056
+ function togglePreviewColumn() { ViewState.changeLeaf("preview", { column: !ViewState.previewColumn }) }
1057
+ // ExtThumbs: the background menu's class row writes the class setting, never "this drive".
1058
+ // A class switched on decodes again: its cache-only misses leave and the viewport re-asks.
1059
+ function toggleExtThumbs() {
1060
+ var key = ExtThumbs.keyForClass(root.storageClass)
1061
+ if (key === "") return
1062
+ var on = !ExtThumbs.classOn(root.storageClass, ViewState.preview)
1063
+ // No direct refresh: the setting change below routes through onPreviewChanged,
1064
+ // which refreshes once on the verdict edge instead of twice.
1065
+ ViewState.changeSetting("preview." + key, on)
1066
+ root.message(ExtThumbs.statusLine(root.storageClass, on), false)
1067
+ }
1068
+ function refreshExtThumbs() {
1069
+ var now = ExtThumbs.verdict(root.storageClass, ViewState.preview)
1070
+ if (now === root.extVerdict) return
1071
+ root.extVerdict = now
1072
+ root.thumbState = ExtThumbs.forgetMisses(root.thumbState)
1073
+ if (root.listArea) root.listArea.restartSettle()
1074
+ // The preview follows rather than loads, so a class just switched off holds instead of decoding.
1075
+ if (root.viewMode === "columns" && columnsLoader.item) columnsLoader.item.askMeta()
1076
+ }
1077
+ function chooseView(mode) {
1078
+ if (FolderViews.MODES.indexOf(mode) < 0 && mode !== "dual")
1079
+ return
1080
+ if (root.listOnly) {
1081
+ root.viewRequested(mode)
1082
+ return
1083
+ }
1084
+ // Dual is a window layout and no folder's view: choosing it, or any view while in it, sets the standing view first.
1085
+ if (mode === "dual" || root.dualMode)
1086
+ ViewState.changeKey("view", mode)
1087
+ if (mode === "dual")
1088
+ return
1089
+ root.viewMode = mode
1090
+ if (FolderViews.shouldRemember(ViewState.rememberViewPerFolder, root.viewFolder)) {
1091
+ var leaf = {}
1092
+ leaf[root.viewFolder] = mode
1093
+ ViewState.changeMapEntries("folderViews", leaf, FolderViews.set(ViewState.folderViews, root.viewFolder, mode))
1094
+ } else if (ViewState.state.view !== mode) {
1095
+ ViewState.changeKey("view", mode)
1096
+ }
1097
+ }
1098
+ function focusPreviewColumn() {
1099
+ if (!ViewState.previewColumn || root.dualMode) return
1100
+ if (root.viewMode === "columns" && columnsLoader.item) columnsLoader.item.focusPreview()
1101
+ }
1102
+ property bool dualMode: false
1103
+ readonly property alias pathCrumbs: panePath.crumbItems
1104
+ readonly property alias pathSlot: panePath.crumbSlot
1105
+ readonly property alias pathStrip: panePath
1106
+ signal switchPane()
1107
+ signal switchToRail()
1108
+
1109
+ Flea.List {
1110
+ id: list
1111
+ visible: !trashHost.opened && !root.flashActive && root.viewMode === "list"
1112
+ focus: visible
1113
+ anchors.top: filterStrip.bottom
1114
+ anchors.left: railHost.right
1115
+ anchors.right: parent.right
1116
+ anchors.bottom: parent.bottom
1117
+ pane: root
1118
+ menu: menu
1119
+
1120
+ // List only computes the clamp and the thumbnail plan; cursorIndex and thumbState are Pane's own to write.
1121
+ onCursorClamped: function (first, last) { Filter.clampCursor(root, first, last) }
1122
+ onThumbsApplied: function (work) { root.thumbState = Thumbs.applied(root.thumbState, work) }
1123
+ onDirSizesApplied: function (ask) { root.dirSizeState = DirSizes.applied(root.dirSizeState, ask) }
1124
+ onDirSizesCancelled: root.dirSizeState = DirSizes.cancelled(root.dirSizeState)
1125
+ }
1126
+
1127
+ Rectangle {
1128
+ anchors { top: panePath.bottom; bottom: parent.bottom; left: railHost.right }
1129
+ visible: root.dualMode && root.paneFocused
1130
+ width: Theme.spacing.hairline * 2
1131
+ color: Theme.color.accent
1132
+ }
1133
+
1134
+ Flea.TrashHost {
1135
+ id: trashHost
1136
+ pane: root
1137
+ overlayParent: root.overlayParent
1138
+ anchors { top: parent.top; left: railHost.right; right: parent.right; bottom: parent.bottom }
1139
+ }
1140
+
1141
+ // The two menu rows open the pane through the window's host with the half they came from chosen.
1142
+ function openFlashImage(path) { root.overlayParent.openFlash(root, path, "") }
1143
+ function openFlashDrive(device) { root.overlayParent.openFlash(root, "", device) }
1144
+
1145
+ // Directory cursors retain the installed provider with its explicit file-only reason.
1146
+ readonly property var cursorRow: root.rowFor(root.cursorIndex)
1147
+ readonly property alias taildropService: wire.taildrop
1148
+ readonly property alias opener: wire.opener
1149
+ readonly property var dropboxService: root.networkService
1150
+ // The keymap sheet's query asks the menu's own rows, so the values stay live for as long as the sheet is open.
1151
+ readonly property bool sheetReadsMenu: root.keymapSheet !== null && root.keymapSheet.opened === true
1152
+ readonly property bool menuValuesLive: menu.preparing || menu.opened || root.sheetReadsMenu
1153
+ readonly property var menuPermissionRow: root.menuValuesLive ? root.permissionSelection() : null
1154
+
1155
+ Flea.MenuHost {
1156
+ id: menu
1157
+ pane: root
1158
+ parent: root.overlayParent || root
1159
+ focusOwner: root.listArea
1160
+ showHidden: root.showHidden
1161
+ providersRefreshing: menuActions.providersRefreshing
1162
+ taildropPeers: wire.taildrop.peers
1163
+ rowIsFile: root.cursorRow !== null && !root.cursorRow.d
1164
+ taildropInstalled: !root.backend.providers.taildrop || root.backend.providers.taildrop.installed !== false
1165
+ taildropReason: root.cursorRow && root.cursorRow.d ? "Taildrop sends files only" : wire.taildrop.reason
1166
+ taildropCode: wire.taildrop.code
1167
+ archiveFormats: root.backend.archiveFormats
1168
+ canConvert: root.backend.canConvert
1169
+ canExtract: root.menuValuesLive && root.cursorRow !== null
1170
+ && Archive.canExtract(root.cursorRow.n, root.backend.extraction)
1171
+ archiveName: root.cursorRow ? root.cursorRow.n : ""
1172
+ rowMode: root.menuPermissionRow ? root.menuPermissionRow.p : 0
1173
+ selectionCount: root.menuValuesLive ? Ops.targetIndices(root).length : 0
1174
+ // Held kinds answer at once; the menu snapshot carries kinds for marks outside the window.
1175
+ compareKinds: root.menuValuesLive ? Ops.menuKinds(root) : []
1176
+ cursorIsTarget: root.menuValuesLive && root.isSingleCursorTarget()
1177
+ openWithApps: menuActions.openWithApps
1178
+ openWithLoaded: menuActions.openWithLoaded
1179
+ identitySource: root
1180
+ clipboardAvailable: root.clipboard.paths.length > 0
1181
+ clipboardWatchFailed: root.clipboardWatchFailed
1182
+ // Scripts, send peers and the shebang are requested only when a row menu opens.
1183
+ onSnapshotRequested: { menuActions.snapshot(); Flea.Scripts.refresh(); menuActions.localSend.refresh(menu.localSend.installed); root.checkShebang(); if (root.canTag) root.tags().ask() }
1184
+ onRefused: function(reason) { root.message(reason, true) }
1185
+ rowIsArchive: root.menuValuesLive && root.cursorRow !== null && !root.cursorRow.d && Archive.isArchive(root.cursorRow.n)
1186
+ rowIsImage: root.cursorRow !== null && root.cursorRow.i === "image-x-generic"
1187
+ // Flash to drive reads the name, not the icon: only the name carries the ledger's suffixes.
1188
+ rowName: root.cursorRow !== null && !root.cursorRow.d ? root.cursorRow.n : ""
1189
+ flashZipMember: root.flashZipMember
1190
+ // MenuAdditions040: Show original is visible but only on a symlink.
1191
+ rowIsSymlink: root.cursorRow !== null && Format.isSymlink(root.cursorRow.p)
1192
+ rowHasShebang: root.rowHasShebang
1193
+ // Permissions carries every target's mode only while the menu needs it.
1194
+ selectionModes: root.menuValuesLive ? root.permissionModes() : []
1195
+ dropboxInstalled: !root.backend.providers.dropbox || root.backend.providers.dropbox.installed !== false
1196
+ localSend: ({ installed: (root.backend.providers.localsend || {}).installed === true, checking: menuActions.localSend.checking,
1197
+ peers: (root.cursorRow && !root.cursorRow.d) ? menuActions.localSend.peers : [], answeredOnce: menuActions.localSend.answeredOnce })
1198
+ dropboxPath: root.dropboxService && root.dropboxService.dropboxReady ? root.dropboxService.dropboxPath : ""
1199
+ dropboxReason: root.dropboxService ? root.dropboxService.dropboxReason : "Dropbox is not available"
1200
+ dropboxCode: root.dropboxService ? root.dropboxService.dropboxCode : ""
1201
+ rowInDropbox: root.menuValuesLive && root.dropboxService && root.cursorRow
1202
+ && Dropbox.contains(root.dropboxService.dropboxPath, root.join(Search.baseOf(root), root.cursorRow.n))
1203
+ // Issue 133: no GVFS mount, share or phone, has a trash of its own, so the row is not offered there.
1204
+ // A read-only folder has none either: the listed line's own writability rides along.
1205
+ canTrash: Mounts.trashable(root.path, root.backend ? root.backend.dirWritable !== false : true)
1206
+ // The listing's own w flag turns the menu's write rows off in a read-only folder.
1207
+ dirWritable: root.backend ? root.backend.dirWritable !== false : true
1208
+ // vfat and exfat hold no links, so the Paste as rows are not offered there either.
1209
+ canLink: root.fsName !== "vfat" && root.fsName !== "exfat"
1210
+ // The Tags flyout stands only while a menu is live; null drops the row where tags cannot be stored.
1211
+ tagSubmenu: root.menuValuesLive && root.canTag && root.tagState !== null ? root.tagState.flyout() : null
1212
+ // ExtThumbs: the class the background menu's thumbnail row is present for, "" locally.
1213
+ storageClass: root.storageClass
1214
+ // Issue 179: the Sort by flyout offers its forget row only where this folder has its own sort.
1215
+ hasFolderSort: root.menuValuesLive && root.searchMode.length === 0 && root.backend ? root.backend.folderHasSort(root.path) : false
1216
+ // The Locked tile's folder and mode while one is drawn; ui/ContextMenu.qml routes a
1217
+ // background right click to that folder's own menu through them.
1218
+ tileTarget: root.lockedTarget
1219
+ tileMode: root.lockedMode
1220
+ onChosen: function (action) {
1221
+ // Issue #170: Rename opens the editor at once by the route F2 takes
1222
+ // rather than queueing an activate behind the cold Open-with catalogue.
1223
+ if (action === "rename") { if (!root.openBulkRename(null)) menuActions.openRenameFromMenu(); return }
1224
+ menuActions.activate(action, menu.hasRow && !menu.forHeader)
1225
+ }
1226
+ // The Locked tile's folder arrives with the row: close() cleared the menu's own copy
1227
+ // before this runs, so reading menu.lockedPath here would always see "" and every row
1228
+ // would fall into the background dispatch above.
1229
+ onLockedChosen: function (action, path) { root.performLocked(path, action) }
1230
+ }
1231
+
1232
+ Flea.PaneMenuActions {
1233
+ id: menuActions
1234
+ parent: root.overlayParent || root
1235
+ pane: root
1236
+ }
1237
+
1238
+ // 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.
1239
+ property var collide: null
1240
+ function asks() {
1241
+ var made = root.collide ? null : Qt.createComponent("CollideHost.qml")
1242
+ if (made && made.status !== Component.Ready) throw new Error("CollideHost.qml did not load: " + made.errorString())
1243
+ return root.collide || (root.collide = made.createObject(root, { pane: root, parent: root.overlayParent || root }))
1244
+ }
1245
+
1246
+ // Opening an executable file asks once, in the window's one card; see ui/WindowBody.qml.
1247
+ // A null host is a loud refusal, never a call into nothing; every product pane names one.
1248
+ readonly property string noWindowSentence: "This pane has no window, so nothing was done."
1249
+ function askRunCard(info) {
1250
+ if (!root.overlayParent) { root.message(root.noWindowSentence, true); return }
1251
+ root.overlayParent.askRunCard(root, info)
1252
+ }
1253
+ function runText(path) {
1254
+ if (!root.overlayParent) { root.message(root.noWindowSentence, true); return }
1255
+ root.overlayParent.runLane().openText(path, root)
1256
+ }
1257
+ // Without a path Open with opens over the cursor; with one the card snapshots the row that path still names.
1258
+ function showOpenWith(path) {
1259
+ if (path === undefined) { menuActions.open("openWith", 0); return }
1260
+ var at = Anchor.matchListed(root, path)
1261
+ if (at < 0) { root.message("That file is no longer listed.", true); return }
1262
+ menuActions.snapshot([at])
1263
+ menuActions.open("openWith", menuActions.requestId)
1264
+ }
1265
+
1266
+ // The one ui/ContextMenu.qml this pane owns, for ui/Ipc.qml: entries, flyout and row geometry
1267
+ // are read off it directly, so a new reader costs the seam a line and this file none.
1268
+ function contextMenu() { return menu.item || menu }
1269
+
1270
+ Flea.PaneStates {
1271
+ id: paneStates
1272
+ pane: root
1273
+ trashOpen: trashHost.opened
1274
+ }
1275
+
1276
+ function openConvert(menuId) { Ops.openConvert(root, menuId) }
1277
+ function moveToDropbox(menuId) { Ops.moveToDropbox(root, root.dropboxService && root.dropboxService.dropboxReady ? root.dropboxService.dropboxPath : "", menuId) }
1278
+ // The three foreign programs live in ui/PaneWire.qml with the backend's replies; these only name the row.
1279
+ function copyShareLink(path) {
1280
+ if (!path) { root.message("That menu is out of date. Reopen it.", true); return }
1281
+ wire.shareLink.copy(path)
1282
+ }
1283
+ function sendTaildrop(peerId, path) { Ops.sendTaildrop(root, wire.taildrop, peerId, path) }
1284
+
1285
+ // The Locked tile's own dispatch: every row its menu offers acts on the locked folder
1286
+ // alone, by path, never on the cursor, the selection or the parent standing behind it.
1287
+ // The menu row is already disabled where it cannot act, so no row here can fail refused.
1288
+ function performLocked(path, action) {
1289
+ if (path.length === 0) return
1290
+ if (action === "openTerminal") { wire.opener.openTerminal(path); return }
1291
+ if (action === "copypath") { wire.opener.copyText(path); return }
1292
+ // MenuAdditions040: the Locked tile offers Copy as over the one folder
1293
+ // it names, each leaf copying that path in its own form.
1294
+ if (action.indexOf("copyAs:") === 0) {
1295
+ var leaf = action.substring("copyAs:".length)
1296
+ var kinds = { copyPath: "path", copyName: "name", copyStem: "stem",
1297
+ copydirpath: "dirpath", copyUri: "uri", copyQuoted: "quoted" }
1298
+ if (kinds[leaf] !== undefined) { Ops.copyAs(root, kinds[leaf], [path]); return }
1299
+ }
1300
+ if (action === "copyAs") { Ops.copyAs(root, "path", [path]); return }
1301
+ if (action === "permissions") { root.permissionsRequested(path); return }
1302
+ root.message("That action is not available yet", false)
1303
+ }
1304
+
1305
+ // The keyboard's own entrance to the row menu; the placement itself is ui/js/Menu.js's.
1306
+ function openCursorMenu() {
1307
+ if (root.lockedTarget.length > 0) {
1308
+ var at = root.listSlot.mapToItem(null, root.listSlot.width / 2, root.listSlot.height / 2)
1309
+ menu.openLocked(root.lockedTarget, root.lockedMode, at)
1310
+ return true
1311
+ }
1312
+ return Menu.openAtCursor(root, menu, Theme.spacing.rowPaddingX)
1313
+ }
1314
+
1315
+ // The query lines' paste source (issue 287), read only when Ctrl+V or Shift+Insert reaches one.
1316
+ // flea-qml: buninu-browser reads its clipboard asynchronously and offers readClipboardText's
1317
+ // Promise for it; Quickshell itself answers the text at once.
1318
+ function clipboardText() { return Quickshell.readClipboardText ? Quickshell.readClipboardText() : Quickshell.clipboardText }
1319
+
1320
+ }