buninu-browser 0.3.0 → 0.5.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 (296) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +40 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +152 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +130 -0
  20. package/frontends/wayland/input.js +75 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +162 -0
  26. package/index.js +28 -1
  27. package/lib/audio/shared-connection.js +31 -0
  28. package/lib/cdp-server.js +15 -2
  29. package/lib/clipboard-command.js +10 -0
  30. package/lib/clipboard-powershell.js +22 -0
  31. package/lib/clipboard.js +37 -2
  32. package/lib/document-types.js +11 -0
  33. package/lib/headless-shell.js +54 -12
  34. package/lib/layout/text-layout.js +17 -9
  35. package/lib/paint/display-list.js +6 -1
  36. package/lib/qml/README.md +1427 -0
  37. package/lib/qml/builtins.js +771 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/canvas.js +76 -0
  40. package/lib/qml/document.js +1199 -0
  41. package/lib/qml/fonts.js +69 -0
  42. package/lib/qml/format.js +9 -0
  43. package/lib/qml/host.js +352 -0
  44. package/lib/qml/images.js +199 -0
  45. package/lib/qml/launcher.js +132 -0
  46. package/lib/qml/loader.js +521 -0
  47. package/lib/qml/media-renderer.js +49 -0
  48. package/lib/qml/media.js +86 -0
  49. package/lib/qml/models.js +622 -0
  50. package/lib/qml/parser.js +381 -0
  51. package/lib/qml/qs +5 -0
  52. package/lib/qml/qscasty +3 -0
  53. package/lib/qml/qsheadless +3 -0
  54. package/lib/qml/qswayland +3 -0
  55. package/lib/qml/reactive.js +182 -0
  56. package/lib/qml/runtime.js +3603 -0
  57. package/lib/qml/shader/fragment.js +83 -0
  58. package/lib/qml/shader/qsb.js +27 -0
  59. package/lib/qml/shader/renderer.js +39 -0
  60. package/lib/qml/shader/runtime.js +61 -0
  61. package/lib/qml/shapes.js +251 -0
  62. package/lib/qml/shortcuts.js +65 -0
  63. package/lib/qml/text.js +109 -0
  64. package/lib/qml/textures.js +126 -0
  65. package/lib/qml/wayland-clipboard.js +32 -0
  66. package/lib/qml/window-constraints.js +16 -0
  67. package/lib/qml/worker-thread.js +70 -0
  68. package/lib/render-tree/index.js +16 -1
  69. package/lib/renderer/bindings.js +16 -1
  70. package/lib/renderer/canvas-2d.js +29 -0
  71. package/lib/renderer/document-selection.js +19 -0
  72. package/lib/renderer/host.js +45 -17
  73. package/lib/renderer/main.js +21 -6
  74. package/lib/renderer/page-realm.js +20 -10
  75. package/lib/renderer/page-renderer.js +497 -42
  76. package/lib/renderer/pointer-move.js +5 -3
  77. package/lib/renderer/render-interval.js +9 -0
  78. package/lib/renderer/text-input-paint.js +54 -0
  79. package/lib/renderer/text-input.js +57 -0
  80. package/lib/style/computed-style.js +10 -4
  81. package/package.json +1 -1
  82. package/test/audio-connection-lifetime.test.js +46 -0
  83. package/test/canvas-paint.test.js +8 -0
  84. package/test/clipboard.test.js +19 -0
  85. package/test/computed-style.test.js +12 -0
  86. package/test/document-selection.test.js +27 -0
  87. package/test/double-click.test.js +85 -0
  88. package/test/fixtures/canvas-damage.js +40 -0
  89. package/test/fixtures/fake-renderer.js +1 -0
  90. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  91. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  92. package/test/fixtures/flea-helpers/bin/bat +3 -0
  93. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  94. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  95. package/test/fixtures/flea-helpers/bin/glow +3 -0
  96. package/test/fixtures/flea-helpers/bin/qs +3 -0
  97. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  98. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  99. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  100. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  101. package/test/fixtures/flea-helpers/check.mjs +36 -0
  102. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  103. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  104. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  105. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  106. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  107. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  108. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  109. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  110. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  111. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  112. package/test/fixtures/pointer-paint-batching.js +23 -0
  113. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  114. package/test/fixtures/qml/flea/LICENSE +21 -0
  115. package/test/fixtures/qml/flea/README.md +25 -0
  116. package/test/fixtures/qml/flea/Theme.qml +8 -0
  117. package/test/fixtures/qml/flea/demo.qml +24 -0
  118. package/test/fixtures/qml/flea/qmldir +3 -0
  119. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  120. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  121. package/test/fixtures/qml/flea-theme/README.md +70 -0
  122. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  123. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  124. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  125. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  126. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  127. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  128. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  129. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  130. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  131. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  132. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  133. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  134. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  135. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  136. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  137. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  138. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  139. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  140. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  141. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  142. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  143. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  144. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  145. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  146. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  147. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  148. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  173. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  174. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  175. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  176. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  177. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  178. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  179. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  180. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  181. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  182. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  183. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  184. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  185. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  186. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  187. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  188. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  189. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  190. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  191. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  192. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  193. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  194. package/test/fixtures/qml/hello.qml +26 -0
  195. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  196. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  197. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  198. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  199. package/test/fixtures/qml/imports/logic/base.js +2 -0
  200. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  201. package/test/fixtures/qml/imports/logic/math.js +3 -0
  202. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  203. package/test/fixtures/qml/imports/main.qml +21 -0
  204. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  205. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  206. package/test/fixtures/qml/multiply.qml +61 -0
  207. package/test/fixtures/qml-canvas-dom.js +90 -0
  208. package/test/fixtures/qml-fonts/main.qml +10 -0
  209. package/test/fixtures/qml-locale/check.mjs +6 -0
  210. package/test/fixtures/qml-locale/main.qml +8 -0
  211. package/test/fixtures/qml-media-audio.js +39 -0
  212. package/test/fixtures/qml-shader/grab.qml +14 -0
  213. package/test/fixtures/qml-shader/gradient.frag +5 -0
  214. package/test/fixtures/qml-shader/main.qml +14 -0
  215. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  216. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  217. package/test/fixtures/qml-shader-check.mjs +24 -0
  218. package/test/fixtures/title-no-paint.js +20 -0
  219. package/test/helpers/flea-sandbox.js +48 -0
  220. package/test/helpers/quickshell-fixture.js +125 -0
  221. package/test/html-clipboard-wayland.test.js +47 -0
  222. package/test/index.test.js +13 -0
  223. package/test/manual-casty-without-qml.js +87 -0
  224. package/test/manual-double-click.html +21 -0
  225. package/test/manual-flea-application.js +49 -0
  226. package/test/manual-flea-binary.js +62 -0
  227. package/test/manual-flea-qml.js +9 -0
  228. package/test/manual-flea-settings.js +40 -0
  229. package/test/manual-flea-theme.js +40 -0
  230. package/test/manual-mouse-buttons.html +18 -0
  231. package/test/manual-omastorm-engine.mjs +19 -0
  232. package/test/manual-omastorm-shaders.mjs +28 -0
  233. package/test/manual-omastorm-software.mjs +25 -0
  234. package/test/manual-pointer-scroll.html +43 -0
  235. package/test/manual-qml-removal.js +46 -0
  236. package/test/manual-without-qml.js +45 -0
  237. package/test/pointer-drag.test.js +104 -0
  238. package/test/qml-canvas-dom.test.js +37 -0
  239. package/test/qml-canvas.test.js +21 -0
  240. package/test/qml-clipboard-bridge.test.js +25 -0
  241. package/test/qml-clipboard-wayland.test.js +40 -0
  242. package/test/qml-connections-timing.test.js +95 -0
  243. package/test/qml-controls-window.test.js +182 -0
  244. package/test/qml-dynamic.test.js +401 -0
  245. package/test/qml-flea-app.test.js +146 -0
  246. package/test/qml-flea-backend.test.js +78 -0
  247. package/test/qml-flea-columns-backend.test.js +38 -0
  248. package/test/qml-flea-compile.test.js +78 -0
  249. package/test/qml-flea-launcher.test.js +88 -0
  250. package/test/qml-flea-markdown-backend.test.js +31 -0
  251. package/test/qml-flea-media.test.js +28 -0
  252. package/test/qml-flea-navigation-backend.test.js +35 -0
  253. package/test/qml-flea-navigation-timing.test.js +147 -0
  254. package/test/qml-flea-preview-backend.test.js +53 -0
  255. package/test/qml-flea-previews.test.js +163 -0
  256. package/test/qml-flea-rows.test.js +101 -0
  257. package/test/qml-flea-textures.test.js +59 -0
  258. package/test/qml-flea-view-switch.test.js +33 -0
  259. package/test/qml-flea-wayland.test.js +68 -0
  260. package/test/qml-flea.test.js +34 -0
  261. package/test/qml-fonts.test.js +31 -0
  262. package/test/qml-grid-multiline.test.js +48 -0
  263. package/test/qml-host.test.js +238 -0
  264. package/test/qml-imports.test.js +102 -0
  265. package/test/qml-inherited-handlers.test.js +51 -0
  266. package/test/qml-input-selection.test.js +66 -0
  267. package/test/qml-interactions.test.js +309 -0
  268. package/test/qml-language.test.js +167 -0
  269. package/test/qml-launcher.test.js +54 -0
  270. package/test/qml-layout-text.test.js +205 -0
  271. package/test/qml-locale.test.js +45 -0
  272. package/test/qml-media.test.js +63 -0
  273. package/test/qml-nonvisual-visible.test.js +19 -0
  274. package/test/qml-persisted-delegates.test.js +45 -0
  275. package/test/qml-quickshell-controls.test.js +65 -0
  276. package/test/qml-quickshell-settings.test.js +62 -0
  277. package/test/qml-repeater.test.js +72 -0
  278. package/test/qml-shader-runtime.test.js +6 -0
  279. package/test/qml-shortcuts.test.js +39 -0
  280. package/test/qml-socket.test.js +60 -0
  281. package/test/qml-software-shader.test.js +55 -0
  282. package/test/qml-style-updates.test.js +124 -0
  283. package/test/qml-textures.test.js +66 -0
  284. package/test/qml-views.test.js +332 -0
  285. package/test/qml-window-update.test.js +36 -0
  286. package/test/qml-worker.test.js +69 -0
  287. package/test/qml.test.js +83 -0
  288. package/test/render-interval.test.js +33 -0
  289. package/test/renderer-host.test.js +11 -0
  290. package/test/text-input-pointer.test.js +31 -0
  291. package/test/text-input-scroll.test.js +29 -0
  292. package/test/text-input.test.js +47 -0
  293. package/test/title-no-paint.test.js +6 -0
  294. package/test/wayland-frontend.test.js +83 -0
  295. package/test/wayland-scroll.test.js +40 -0
  296. package/test/wayland-urlbar.test.js +61 -0
@@ -0,0 +1,320 @@
1
+ .pragma library
2
+
3
+ .import "Format.js" as Format
4
+ .import "Protocols.js" as Protocols
5
+ .import "Menu.js" as Menu
6
+
7
+ // Whole sentences with "%1" for a value, filled by fill so each translates as one; replace would read "$$" in a label as a pattern.
8
+ var WORDS = {
9
+ nothingToRelease: "%1 has nothing to eject or unmount",
10
+ stillEjecting: "Still ejecting %1; wait for its result",
11
+ stillUnmounting: "Still unmounting %1; wait for its result",
12
+ anotherMounting: "Another drive is still mounting. Try again when it finishes",
13
+ stillUnmountingTryAgain: "%1 is still unmounting. Try again when it finishes",
14
+ ejecting: "Ejecting %1. Do not unplug it yet.",
15
+ noFolder: "%1 was mounted, but Flea could not find its folder.",
16
+ ejectUnfinished: "%1 did not finish ejecting and is still mounted.",
17
+ mountFailed: "%1 could not be mounted.",
18
+ damaged: "%1 could not be mounted: its file system is damaged or not supported.",
19
+ unmountFailed: "%1 could not be unmounted.",
20
+ ejectFailed: "%1 could not be ejected.",
21
+ busy: "%1 is busy; close what is using it and try again.",
22
+ mountRefused: "%1 was not mounted: you do not have permission.",
23
+ unmountRefused: "%1 was not unmounted: you do not have permission.",
24
+ ejectRefused: "%1 was not ejected: you do not have permission.",
25
+ noDisc: "%1 has no disc or card in it.",
26
+ notMountable: "%1 cannot be mounted on this computer.",
27
+ anotherOpening: "Another network location is still opening. Try again when it opens",
28
+ notSaved: "%1 is not saved, and stays in the sidebar until it is unmounted",
29
+ forgottenStays: "%1 is forgotten, and stays in the sidebar until it is unmounted",
30
+ forgotten: "%1 is forgotten",
31
+ unmountedLine: "Unmounted %1",
32
+ bridgeMissing: "Could not open %1 because part of Flea did not load. Reinstall Flea.",
33
+ transferUsing: "%1 could not be unmounted. A transfer may still be using it."
34
+ }
35
+
36
+ // Sample input, captured live on the box with one network share mounted (2026-08-31):
37
+ // Drive(0): KBG40ZNS256G NVMe KIOXIA 256GB
38
+ // Type: GProxyDrive (GProxyVolumeMonitorUDisks2)
39
+ // Mount(0): isos on 192.168.1.10 -> smb://192.168.1.10/isos/
40
+ // Type: GDaemonMount
41
+ // Only Mount() lines matter here; Drive() and every indented "Type:" line are noise for the rail.
42
+ function parseMounts(output) {
43
+ var out = []
44
+ var mounts = mountLines(output)
45
+ for (var i = 0; i < mounts.length; i++) {
46
+ var uri = mounts[i].uri
47
+ // corner: a local device mount (file://) is Favorites territory, not Network; Places.js skips the inverse.
48
+ if (uri.indexOf("file://") === 0)
49
+ continue
50
+ // A phone is DEVICES territory the same way: its row is ui/js/Phones.js's, built from the
51
+ // volume block, and this line is gio's shadow GDaemonMount printed beside it. afc is the
52
+ // iPhone's own, whose root mount prints here rather than inside its volume block.
53
+ if (/^(mtp|gphoto2|afc):\/\//i.test(uri))
54
+ continue
55
+ out.push({ label: Protocols.shareName(mounts[i].name, uri), uri: uri })
56
+ }
57
+ return out
58
+ }
59
+
60
+ // Sample input: "Mount(2): isos on 192.168.1.10 -> smb://192.168.1.10/isos/" answers { name: "isos on 192.168.1.10", uri: "smb://192.168.1.10/isos/" }.
61
+ // Every top-level "Mount(N): name -> uri" line of gio mount -l; the one owner of that line's grammar.
62
+ function mountLines(output) {
63
+ var out = []
64
+ var lines = String(output || "").split("\n")
65
+ for (var i = 0; i < lines.length; i++) {
66
+ var m = lines[i].match(/^Mount\(\d+\):\s*(.+?)\s*->\s*(\S+)\s*$/)
67
+ if (m)
68
+ out.push({ name: m[1], uri: m[2] })
69
+ }
70
+ return out
71
+ }
72
+
73
+ // Sample input: "Mount(0): photos -> file:///mnt/kmount-photos" answers { path: "/mnt/kmount-photos", label: "photos", uri: "file:///mnt/kmount-photos" }.
74
+ // Issue #257: what gio adds to a kernel row, matched by path; a non-file line adds nothing, an unlisted mount keeps its leaf label.
75
+ function gioKernelMounts(gioText) {
76
+ var out = []
77
+ var mounts = mountLines(gioText)
78
+ for (var i = 0; i < mounts.length; i++) {
79
+ var m = mounts[i]
80
+ if (m.uri.indexOf("file://") !== 0)
81
+ continue
82
+ var path = decodePath(m.uri.substring("file://".length)).replace(/\/+$/, "")
83
+ out.push({ path: path.length > 0 ? path : "/", label: Protocols.shareName(m.name, m.uri), uri: m.uri })
84
+ }
85
+ return out
86
+ }
87
+
88
+ // Sample input, "gio info" under the C locale ui/NetworkMounts.qml pins on every gio call:
89
+ // uri: smb://192.168.1.10/isos/
90
+ // local path: /run/user/1000/gvfs/smb-share:server=192.168.1.10,share=isos
91
+ // Only a location GVFS exposes through its FUSE mount prints that line, so "" means there is no
92
+ // browsable folder. One resolver, so the product and tests/js/network.js read the same wording.
93
+ function localPath(body) {
94
+ var lines = String(body || "").split("\n")
95
+ for (var i = 0; i < lines.length; i++) {
96
+ if (lines[i].indexOf("local path: ") === 0)
97
+ return lines[i].substring("local path: ".length).trim()
98
+ }
99
+ return ""
100
+ }
101
+
102
+ // Issue 133 (mfilm77): no GVFS backend implements trash, so gio trash refuses every path under its FUSE folder.
103
+ // Sample input: /run/user/1000/gvfs/smb-share:server=192.168.21.25,share=data/photos, where "smb-share:" names the mount.
104
+ // A read-only folder has no trash either: the listed line's own writability rides along, so iso9660
105
+ // and a root-owned stick root offer Delete permanently instead of a Trash row that only fails.
106
+ function trashable(path, writable) {
107
+ if (writable === false)
108
+ return false
109
+ return !underGvfs(path)
110
+ }
111
+
112
+ // A path inside the GVFS FUSE root /run/user/<uid>/gvfs, where the "scheme:" segment names the mount; a folder named gvfs elsewhere is plain.
113
+ function underGvfs(path) {
114
+ return /^\/run\/user\/[^/]+\/gvfs\/[A-Za-z0-9-]+:/.test(String(path || ""))
115
+ }
116
+
117
+ // Sample input: the operator's real bookmarks file, ui/js/Places.js "bookmarks" reads the same lines.
118
+ // file:///home/gm/Downloads Downloads
119
+ // smb://192.168.1.10/ NAS
120
+ // Places.bookmarks() keeps the file:// line and drops the smb:// one; this is the exact mirror.
121
+ function nonFileBookmarks(body) {
122
+ var out = []
123
+ var lines = String(body || "").split("\n")
124
+ for (var i = 0; i < lines.length; i++) {
125
+ var line = lines[i].trim()
126
+ if (line.length === 0 || line.indexOf("file://") === 0)
127
+ continue
128
+ var space = line.indexOf(" ")
129
+ var uri = space < 0 ? line : line.substring(0, space)
130
+ // corner: a line with no scheme at all is not a bookmark, not just not a file:// one.
131
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(uri))
132
+ continue
133
+ var label = space < 0 ? "" : line.substring(space + 1).trim()
134
+ out.push({ uri: uri, label: label.length > 0 ? label : leaf(uri) })
135
+ }
136
+ return out
137
+ }
138
+
139
+ // The bare-root form (smb://host/) has no path leaf, so this falls back to the host itself.
140
+ function leaf(uri) {
141
+ var stripped = String(uri).replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/+$/, "")
142
+ var slash = stripped.lastIndexOf("/")
143
+ var tail = slash < 0 ? stripped : stripped.substring(slash + 1)
144
+ return tail.length > 0 ? tail : stripped
145
+ }
146
+
147
+ // decodeURIComponent throws on a malformed escape, and a bookmarks line is arbitrary text, so a
148
+ // failure answers the raw path rather than taking the whole rail rebuild with it.
149
+ function decodePath(raw) {
150
+ try {
151
+ return decodeURIComponent(String(raw))
152
+ } catch (e) {
153
+ return String(raw)
154
+ }
155
+ }
156
+
157
+ // One canonical form: every trailing slash stripped except a bare host root, which keeps one, and
158
+ // a port the scheme would have used anyway dropped, because gio's own listing never reports one.
159
+ function normalize(uri) {
160
+ var stripped = Protocols.stripDefaultPort(String(uri || "").replace(/\/+$/, ""))
161
+ var bareRoot = /^[a-z][a-z0-9+.-]*:\/\/[^\/]+$/i.test(stripped)
162
+ return bareRoot ? stripped + "/" : stripped
163
+ }
164
+
165
+ // NETWORK shows on its discoveries, DEVICES on those plus its own; the deadline shows whatever each has.
166
+ function railGroupsReady(networkDone, devicesDone, elapsedMs, deadlineMs) {
167
+ if (elapsedMs >= deadlineMs)
168
+ return { showNetwork: true, showDevices: true }
169
+ var showNetwork = networkDone === true
170
+ return { showNetwork: showNetwork, showDevices: showNetwork && devicesDone === true }
171
+ }
172
+
173
+ // A password may be given to any scheme here whose authority names a user.
174
+ function credentialed(uri) {
175
+ return /^(smb|sftp|ftp|ftps|dav|davs):\/\/[^\/]*@/i.test(String(uri || ""))
176
+ }
177
+
178
+ // Try sftp keys before asking for a password.
179
+ function keyless(uri) {
180
+ return /^sftp:\/\//i.test(String(uri || ""))
181
+ }
182
+
183
+ // PR #21: a live mount wins the rail row, and the operator's own bookmark label wins its name, or a
184
+ // rename typed on a mounted share is written to the file and never drawn. Matched the way rebuild dedups.
185
+ function railLabel(mount, marks) {
186
+ for (var i = 0; i < marks.length; i++) {
187
+ if (normalize(marks[i].uri) === normalize(mount.uri)) return marks[i].label
188
+ }
189
+ return mount.label
190
+ }
191
+
192
+ // Sample input: two arrays of rail entries as ui/NetworkMounts.qml and ui/DeviceMounts.qml build
193
+ // them, [{path:"", label:"NAS", group:"network", kind:"share", uri:"smb://example.com/data",
194
+ // mounted:false, glyph:"server"}]. A poll that found no change must not assign a fresh array: the
195
+ // Repeater rebinds every rail row on the assignment, and a rebound row is a row whose editor lost
196
+ // what was typed into it. Both builders are covered, so every field either one writes is compared.
197
+ function sameEntries(a, b) {
198
+ if (!a || !b || a.length !== b.length)
199
+ return false
200
+ for (var i = 0; i < a.length; i++) {
201
+ if (!sameEntry(a[i], b[i]))
202
+ return false
203
+ }
204
+ return true
205
+ }
206
+
207
+ // The two shapes differ only in uri against device, and an absent field is undefined on both sides.
208
+ function sameEntry(x, y) {
209
+ return x.path === y.path && x.label === y.label && x.group === y.group && x.kind === y.kind
210
+ && x.uri === y.uri && x.device === y.device && x.mounted === y.mounted && x.glyph === y.glyph
211
+ && x.size === y.size && x.editable === y.editable && x.removable === y.removable
212
+ && x.fstype === y.fstype && x.fsver === y.fsver && x.mediaRemovable === y.mediaRemovable && x.readOnly === y.readOnly
213
+ && (x.locked || false) === (y.locked || false) && x.loop === y.loop
214
+ }
215
+
216
+ // Sample input: one rail entry as ui/DeviceMounts.qml and ui/NetworkMounts.qml build them,
217
+ // {label:"128GB", group:"device", kind:"volume", device:"/dev/sda1", mounted:true, removable:true}.
218
+ // Rows come from ui/js/Menu.js kind R; gio's -f is offered nowhere, since forcing an unmount over an open write loses data.
219
+ function railMenu(entry) {
220
+ if (!entry)
221
+ return []
222
+ return Menu.railEntries(entry)
223
+ }
224
+
225
+ // A root-only remote mount covers its saved addressable paths; SMB shares remain path-specific.
226
+ function addressMountCovers(liveUri, savedUri) {
227
+ var live = normalize(liveUri)
228
+ var saved = normalize(savedUri)
229
+ return /^(sftp|ftp|ftps|dav|davs):\/\/[^\/]+\/$/i.test(live)
230
+ && saved.length > live.length && saved.indexOf(live) === 0
231
+ }
232
+
233
+ // The rail's right click opens railMenu's rows; ui/js/Eject.js reads railMenu too, so Ctrl+E refuses an unmounted row.
234
+ function rowMenu(entry) {
235
+ return railMenu(entry)
236
+ }
237
+
238
+ // The handle a chosen menu row carries back: a volume's device node, a share's uri, "" for a row
239
+ // with no release. The rail rebuilds on a five second poll, so an index taken when the menu opened
240
+ // can name a different row by the time a row inside it is chosen; a key cannot.
241
+ function railKey(entry) {
242
+ if (!entry)
243
+ return ""
244
+ if (entry.group === "device" && entry.kind === "volume")
245
+ return String(entry.device || "")
246
+ if (entry.group === "device" && entry.kind === "phone")
247
+ return String(entry.uri || "")
248
+ if (entry.group === "network" && entry.kind === "share")
249
+ return String(entry.uri || "")
250
+ return ""
251
+ }
252
+
253
+ // Where that key resolves back to a position in the group's own list, or -1 when the row is gone.
254
+ // An empty key matches nothing: a row that carries no key must never resolve to whatever is at 0.
255
+ function rowByKey(entries, key) {
256
+ if (String(key || "").length === 0)
257
+ return -1
258
+ var list = entries || []
259
+ for (var i = 0; i < list.length; i++) {
260
+ if (railKey(list[i]) === key)
261
+ return i
262
+ }
263
+ return -1
264
+ }
265
+
266
+ // The device or network row with a release whose path holds the directory, or null; a favourite never names a volume.
267
+ function holding(entries, dir) {
268
+ var list = entries || []
269
+ var path = String(dir || "")
270
+ for (var i = 0; i < list.length; i++) {
271
+ var e = list[i]
272
+ if (!e.path || (e.group !== "device" && e.group !== "network") || railMenu(e).length === 0)
273
+ continue
274
+ if (inside(path, e.path))
275
+ return e
276
+ }
277
+ return null
278
+ }
279
+
280
+ // Whether dir is root itself or lies under it; ui/js/Places.js heldBy asks the rail the same question.
281
+ function inside(dir, root) {
282
+ return dir === root || dir.indexOf(root + "/") === 0
283
+ }
284
+
285
+ // The keyboard's route into the rail menu, and where a rail row without one is answered: the sheet
286
+ // advertises m, so a key that silently did nothing on a favourite would read as a broken one.
287
+ function raiseMenu(pane, sidebar) {
288
+ var entry = sidebar.entries[sidebar.cursorIndex]
289
+ if (!entry)
290
+ return
291
+ if (rowMenu(entry).length > 0)
292
+ sidebar.openCursorMenu()
293
+ else
294
+ pane.message(Format.fill(WORDS.nothingToRelease, entry.label), false)
295
+ }
296
+
297
+ // Sample input: (true, "a", "a", "b", "b") answers "skip", while (true, "a", "x", "b", "b") answers "rebuild".
298
+ function pollDecision(listedOnce, polledListing, lastListing, mountinfoText, lastMountinfo) {
299
+ if (!listedOnce) return "none"
300
+ if (polledListing === lastListing && mountinfoText === lastMountinfo) return "skip"
301
+ return "rebuild"
302
+ }
303
+
304
+ // Sample input: the operator's own bookmarks file, favourites and network places in one list.
305
+ // smb://192.168.1.10/isos NAS isos
306
+ // Read off the trimmed line the way nonFileBookmarks reads it, or an indented line is a rail row
307
+ // nothing removes, and normalized the way Places.relabel matches; a kept line is pushed back raw.
308
+ function removeBookmark(body, uri) {
309
+ var target = normalize(uri)
310
+ var lines = String(body || "").split("\n")
311
+ var out = []
312
+ for (var i = 0; i < lines.length; i++) {
313
+ var trimmed = lines[i].trim(), space = trimmed.indexOf(" ")
314
+ var one = space < 0 ? trimmed : trimmed.substring(0, space)
315
+ if (target.length > 0 && one.length > 0 && normalize(one) === target)
316
+ continue
317
+ out.push(lines[i])
318
+ }
319
+ return out.join("\n")
320
+ }
@@ -0,0 +1,13 @@
1
+ .pragma library
2
+
3
+ // The mouse's side buttons: Nav.js mouseBack takes refused, WindowBody.qml takes both after refusing its own overlays.
4
+
5
+ // No navigation closes the pane's context menu or the collision card, so a press behind one would act on another directory's rows.
6
+ function refused(pane) {
7
+ return pane.menuVisible || !!(pane.collide && pane.collide.opened)
8
+ }
9
+
10
+ // Forward takes pane.goForward, the key's own entry, so Trash and Recent refuse it as they refuse the key.
11
+ function forward(pane) {
12
+ if (!refused(pane)) pane.goForward()
13
+ }
@@ -0,0 +1,328 @@
1
+ .pragma library
2
+
3
+ .import "DirSizes.js" as DirSizes
4
+ .import "Filter.js" as Filter
5
+ .import "Kinds.js" as Kinds
6
+ .import "Thumbs.js" as Thumbs
7
+ .import "Search.js" as Search
8
+ .import "ColumnMenu.js" as ColumnMenu
9
+ .import "BackReturn.js" as BackReturn
10
+ .import "Anchor.js" as Anchor
11
+ .import "MouseNav.js" as MouseNav
12
+ .import "Swap.js" as Swap
13
+
14
+ // Where the pane has been and how it gets back, taking ui/Pane.qml's root the way Search.js and
15
+ // Ops.js do: the pane holds the state, this holds what the state does.
16
+
17
+ // A listing inside its deadline refuses a row action; a waiting one is let go by the navigation itself, see ui/Pane.qml openWithoutHistory.
18
+ function loading(pane) { return pane.listInFlight && pane.listingState !== "waiting" }
19
+
20
+ // While a listing is out, the folder it asked for is the pane's current place for every navigation, and the drawn path only until its answer lands.
21
+ function place(pane) { return pane.listInFlight && !!pane.listingPath ? pane.listingPath : pane.path }
22
+
23
+ // A new destination discards the forward branch and pushes the drawn place unless the listing out already did; a re-read keeps both.
24
+ function open(pane, newPath) {
25
+ if (place(pane).length > 0 && newPath !== place(pane)) {
26
+ if (!BackReturn.pending(pane))
27
+ pane.history = pane.history.concat([BackReturn.capture(pane)])
28
+ // A Back out pushed the drawn place onto the forward branch cleared below, so history takes it instead.
29
+ else if (BackReturn.pushedOnto(pane) === "forwardHistory")
30
+ pane.history = pane.history.concat([BackReturn.drawn(pane)])
31
+ pane.forwardHistory = []
32
+ }
33
+ pane.openWithoutHistory(newPath, { supersede: true })
34
+ }
35
+
36
+ function back(pane) {
37
+ if (pane.history.length === 0) {
38
+ return
39
+ }
40
+ BackReturn.travel(pane, "history", "forwardHistory")
41
+ }
42
+
43
+ function forward(pane) {
44
+ if (!pane.forwardHistory || pane.forwardHistory.length === 0) return
45
+ BackReturn.travel(pane, "forwardHistory", "history")
46
+ }
47
+
48
+ // The mouse back button follows history, or climbs when no history exists.
49
+ function mouseBack(pane) {
50
+ if (MouseNav.refused(pane)) {
51
+ return
52
+ }
53
+ if (pane.history.length > 0) {
54
+ back(pane)
55
+ return
56
+ }
57
+ parent(pane)
58
+ }
59
+
60
+ // Every listing the pane asks for; of options, ui/Pane.qml reads keepHidden and ui/js/Swap.js begin() the rest.
61
+ // A walk owns the rows until this lands, so leaving it is the shared step with the tabs:
62
+ // tab switches, Back, Up, rail clicks and jumps all funnel through here.
63
+ function openWithoutHistory(pane, newPath, options) {
64
+ Anchor.clearWaiting(pane)
65
+ var ask = options || {}
66
+ if (pane.listInFlight) {
67
+ // Only a navigation takes the place of a listing out; a re-list of the folder shown must not undo it.
68
+ if (ask.supersede !== true) {
69
+ pane.message("A directory is already loading.", false)
70
+ return
71
+ }
72
+ // The listing let go answers into nothing: its listed line names another folder and its rows follow it, see ui/js/Swap.js onListed.
73
+ pane.swap.stopWaiting()
74
+ BackReturn.dropOwed(pane)
75
+ }
76
+ // A hop elsewhere drops a waiting background menu, while the armed hop keeps its own request.
77
+ if (pane.pendingBackground && pane.pendingBackground.length > 0 && !ColumnMenu.samePath(newPath, pane.pendingBackground))
78
+ clearPendingBackground(pane)
79
+ Search.leaveWalk(pane)
80
+ pane.listInFlight = true
81
+ pane.listedSeen = false
82
+ // A hop into another view clears at once like a tab switch, so held rows never draw in a view they were not listed in.
83
+ var shown = pane.viewMode
84
+ if (pane.restoreView)
85
+ pane.restoreView(newPath)
86
+ // A manual reload's notice belongs to its own re-read: a navigation that lands first spends it.
87
+ pane.reloadFrom = -1
88
+ // The path is not written here. A refused listing never answers a listed line, so leaving the
89
+ // pane's own path alone is what keeps a refused hop from moving the breadcrumb onto a directory
90
+ // nobody could read; ui/PaneSwap.qml applyListed takes it from the answer instead. The directory
91
+ // asked for is recorded, because a drop landing while the reply is out means that one and not
92
+ // the directory being left; ui/Pane.qml dropPath reads it and only while this listing is out.
93
+ if (newPath !== pane.path) pane.linkTargetPendingId = 0 // A waiting Show original belongs to the listing being left, so leaving it drops the reveal.
94
+ pane.listingPath = newPath
95
+ // Verdicts stay the left directory's until fsinfo answers, so re-read keeps them.
96
+ if (newPath !== pane.path) { pane.storageClass = ""; pane.storageKnown = false; pane.fsAnswered = false; pane.systemVolume = false; pane.canTag = false }
97
+ if (pane.viewMode !== shown) ask.clearAtOnce = true
98
+ // A walk closed above keeps its rows and its strip until this listing lands.
99
+ if (pane.searchLeaving === true) ask.holdWalk = true
100
+ // A filter line held with its rows gives up the caret, so keys typed meanwhile meet the gate, not a query the swap forgets.
101
+ if (holdOrForget(pane, ask, ask.keptQuery) && pane.filterTyping)
102
+ Filter.commit(pane)
103
+ pane.appliedListingPreferences = pane.listingPreferences
104
+ // leaveWalk above has closed or is closing any search, so the armed row is this listing's to find; a path in another folder names none.
105
+ pane.backend.list(newPath, pane.windowSize, pane.showHidden, ask.wantChanged === true, pane.pendingSelect)
106
+ // One statfs per directory, not per row: the bar's right half only changes when the pane moves.
107
+ pane.backend.askFsInfo()
108
+ }
109
+
110
+ // A settled listing stays drawn until the new rows land, see AGENTS.md "The listing swap"; true when held, else forgotten at once, and a fallen-back loading mark stays through the newest request.
111
+ function holdOrForget(pane, ask, keptQuery) {
112
+ var fell = pane.swap.fellBack === true
113
+ var held = pane.swap.hold(ask)
114
+ if (!held) {
115
+ forget(pane, keptQuery)
116
+ if (fell) pane.swap.phase = Swap.expired(pane.swap.phase)
117
+ }
118
+ return held
119
+ }
120
+
121
+ // Everything a fresh listing forgets, written once: at the request, or by ui/PaneSwap.qml when held rows go.
122
+ function forget(pane, keptQuery) {
123
+ // An exit landing clears the drawn search state in the same turn its rows do.
124
+ if (pane.searchLeaving === true) Search.closeLeaving(pane)
125
+ // The editor row and the held sort belong to the replaced listing, so both go with it, before the count drops.
126
+ pane.pendingSort = null
127
+ pane.renamingIndex = -1
128
+ pane.total = 0
129
+ pane.held = 0
130
+ pane.rows = []
131
+ pane.kindNames = []
132
+ pane.thumbState = Thumbs.empty()
133
+ pane.dirSizeState = DirSizes.empty()
134
+ pane.cursorIndex = 0
135
+ pane.trashArmedAt = 0
136
+ // A re-list puts a different file at that index, so the tap record goes with the rename.
137
+ if (pane.cancelSlowClick) pane.cancelSlowClick()
138
+ // A filter narrows the rows already listed, so a new listing forgets it unless ui/js/Anchor.js hands it back.
139
+ Filter.close(pane)
140
+ if (keptQuery)
141
+ pane.filterQuery = keptQuery
142
+ pane.listingState = "loading"
143
+ pane.stateMessage = ""
144
+ pane.lockedMode = 0
145
+ pane.clearSelection()
146
+ pane.listArea.primeSettle()
147
+ }
148
+
149
+ // Which row the listing re-reveals after a rename. A rename the pointer committed keeps the row the
150
+ // pointer chose instead, because re-selecting the renamed one would undo the click a round trip
151
+ // after it landed. One shot: the next rename reveals again.
152
+ function renameRefreshTarget(pane, path) {
153
+ if (!pane.renameKeepsPointerRow) {
154
+ return path
155
+ }
156
+ pane.renameKeepsPointerRow = false
157
+ return ""
158
+ }
159
+
160
+ // An operation changed the directory under the listing, so it is read again. Passing the path the
161
+ // operation produced re-selects that row through pendingSelect instead of dropping the cursor to the
162
+ // top. It is not a navigation, so it never touches the history.
163
+ function refresh(pane, selectPath, keepSelection) {
164
+ pane.pendingSelect = selectPath ? selectPath : ""
165
+ pane.pendingMenu = false
166
+ clearPendingBackground(pane)
167
+ // A search keeps its rows and numbering, so one window ask re-stats the held rows; marks and cursor stay.
168
+ if (keepSelection === true && pane.searchMode === Search.RESULTS) {
169
+ pane.backend.window(pane.held, pane.windowSize)
170
+ return
171
+ }
172
+ if (keepSelection === true) {
173
+ // Metadata-only writes use the watcher's identity anchor and interaction debt.
174
+ pane.wire.stale = true
175
+ pane.wire.reread()
176
+ return
177
+ }
178
+ pane.openWithoutHistory(pane.path, { inPlace: true })
179
+ }
180
+
181
+ // The list request names the target, so its first rows answer alone looks for it at any depth, by listed name (NFC finds NFD).
182
+ function applyPendingSelect(pane) {
183
+ // A search arms its return while the query stands, so its own landings skip it until Esc.
184
+ if (pane.pendingSelect.length === 0 || (pane.searchMode || "").length > 0) {
185
+ return
186
+ }
187
+ var target = pane.pendingSelect
188
+ pane.pendingSelect = ""
189
+ var at = Anchor.selectMatch(pane.rows, target, pane.path)
190
+ if (at >= 0) {
191
+ var index = pane.held + at
192
+ pane.setCursor(index)
193
+ pane.selection.only(index, true)
194
+ pane.selectionAnchor = index
195
+ pane.selectionVersion++
196
+ if (pane.pendingMenu) {
197
+ pane.pendingMenu = false
198
+ pane.openCursorMenu()
199
+ }
200
+ return
201
+ }
202
+ // The row is not in this listing, so the intent behind it must not fire on some later match.
203
+ pane.pendingMenu = false
204
+ }
205
+
206
+ // A failed listing drops the deferred background menu, so no later rows open it.
207
+ function clearPendingBackground(pane) { ColumnMenu.clearPendingBackground(pane) }
208
+
209
+ // The deferred neighbour background, consumed on every landing by identity.
210
+ function applyPendingBackground(pane) { ColumnMenu.applyPendingBackground(pane) }
211
+
212
+ // The cursor row once Enter's three guards pass, else null with the reason shown; the in-flight guard stops a second Enter queueing a second listing.
213
+ function openableRow(pane) {
214
+ if (loading(pane)) {
215
+ pane.message("A directory is already loading.", false)
216
+ return null
217
+ }
218
+ var row = pane.rowFor(pane.cursorIndex)
219
+ if (!row) {
220
+ pane.message("That row has not loaded yet.", false)
221
+ return null
222
+ }
223
+ if (!Filter.cursorShown(pane)) {
224
+ pane.message("That row is hidden by the filter.", false)
225
+ return null
226
+ }
227
+ return row
228
+ }
229
+
230
+ // Enter on the cursor row: a directory navigates, an archive opens Flea's own view, anything else goes to the opener.
231
+ function openCursor(pane, opener) {
232
+ var row = openableRow(pane)
233
+ if (!row) return
234
+ var path = pane.join(Search.baseOf(pane), row.n)
235
+ if (row.d) {
236
+ pane.open(path)
237
+ return
238
+ }
239
+ // Handing an archive on opens another file manager, and this is ui/Preview.qml's own classifier; an AppImage is a program first, so it asks on the run card.
240
+ if (Kinds.quickLookKind(row.i, path) === Kinds.ARCHIVE && !/\.appimage$/i.test(row.n)) {
241
+ pane.quickLook().open(path, row.i, row.s, pane.kindNames[row.k] || "",
242
+ pane.thumbState && pane.thumbState.file ? Thumbs.fileFor(pane.thumbState, pane.cursorIndex) : "")
243
+ return
244
+ }
245
+ opener.open(path)
246
+ }
247
+
248
+ // The run card's entrances always reach the opener, so Run asks through flea --open like Enter does.
249
+ function runCursor(pane, opener, verb) {
250
+ var row = openableRow(pane)
251
+ if (!row) return
252
+ var path = pane.join(Search.baseOf(pane), row.n)
253
+ if (verb === "open") opener.open(path)
254
+ else pane.runText(path)
255
+ }
256
+
257
+ // The folder the Locked tile names, for its own menu and every row that menu offers: the
258
+ // directory asked for while its tile is up, which is the parent's own path when the folder
259
+ // failed on its own re-read. "" while no Locked tile is drawn, so the background menu stands.
260
+ function lockedTarget(pane) {
261
+ if (pane.listingState !== "locked") return ""
262
+ var asked = pane.listingPath || ""
263
+ return asked.length > 0 ? trimSlash(asked) : pane.path
264
+ }
265
+
266
+ // The path helpers the columns view needs. A root has no parent and no leaf of its own.
267
+ function trimSlash(path) {
268
+ // A bookmark can carry one trailing slash, which no row name ever has; "/" alone keeps its own.
269
+ var text = String(path)
270
+ return text.length > 1 && text.charAt(text.length - 1) === "/" ? text.substring(0, text.length - 1) : text
271
+ }
272
+
273
+ function parentOf(path) {
274
+ var text = trimSlash(path)
275
+ var cut = text.lastIndexOf("/")
276
+ return cut <= 0 ? "/" : text.substring(0, cut)
277
+ }
278
+
279
+ function leafOf(path) {
280
+ var text = String(path)
281
+ var cut = text.lastIndexOf("/")
282
+ return cut < 0 || cut === text.length - 1 ? text : text.substring(cut + 1)
283
+ }
284
+
285
+ // Issue 193: a hop that is out, refused or waiting climbs from the folder it asked for; ui/Pane.qml canGoUp reads it too.
286
+ function climbsFromListing(pane) {
287
+ return (pane.listInFlight || pane.listingState === "locked" || pane.listingState === "waiting") && !!pane.listingPath && pane.listingPath !== pane.path
288
+ }
289
+
290
+ // Backspace, h and the chrome's up arrow, stopping at the root; the parent lands on the directory left, at any depth.
291
+ function parent(pane) {
292
+ if (climbsFromListing(pane)) {
293
+ pane.pendingSelect = trimSlash(pane.listingPath)
294
+ pane.open(parentOf(pane.listingPath))
295
+ return
296
+ }
297
+ if (pane.path === "/") {
298
+ return
299
+ }
300
+ var here = pane.path
301
+ var cut = here.lastIndexOf("/")
302
+ pane.pendingSelect = here
303
+ pane.open(cut <= 0 ? "/" : here.substring(0, cut))
304
+ }
305
+
306
+ // A click on the folder already shown is a no-op, so the rail and columns routes land here.
307
+ function showing(pane, path) {
308
+ if (trimSlash(path) !== pane.path) return false
309
+ if (pane.listInFlight) return false
310
+ if ((pane.searchMode || "").length > 0) return false
311
+ if (!pane.trash || pane.trash.opened) return false
312
+ return pane.listingState === "ready" || pane.listingState === "empty"
313
+ }
314
+
315
+ // Same-path Enter re-lists only a failed or locked listing; Trash, search and in-flight stay a no-op.
316
+ function pathFailed(pane) {
317
+ if (!pane || pane.listInFlight) return false
318
+ if ((pane.searchMode || "").length > 0) return false
319
+ if (!pane.trash || pane.trash.opened) return false
320
+ return pane.listingState === "error" || pane.listingState === "locked"
321
+ }
322
+
323
+ // The rail's and the columns view's route in: the shown folder returns false and lists nothing.
324
+ function openPlace(pane, path) {
325
+ if (showing(pane, path)) return false
326
+ pane.open(path)
327
+ return true
328
+ }