buninu-browser 0.3.0 → 0.4.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 (239) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +19 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -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 +116 -0
  20. package/frontends/wayland/input.js +56 -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 +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +37 -2
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +47 -8
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/qml/README.md +971 -0
  35. package/lib/qml/builtins.js +700 -0
  36. package/lib/qml/canvas-dom.js +115 -0
  37. package/lib/qml/document.js +1176 -0
  38. package/lib/qml/format.js +9 -0
  39. package/lib/qml/host.js +324 -0
  40. package/lib/qml/images.js +199 -0
  41. package/lib/qml/launcher.js +132 -0
  42. package/lib/qml/loader.js +517 -0
  43. package/lib/qml/media-renderer.js +49 -0
  44. package/lib/qml/media.js +86 -0
  45. package/lib/qml/models.js +622 -0
  46. package/lib/qml/parser.js +381 -0
  47. package/lib/qml/qs +5 -0
  48. package/lib/qml/qscasty +3 -0
  49. package/lib/qml/qsheadless +3 -0
  50. package/lib/qml/qswayland +3 -0
  51. package/lib/qml/reactive.js +159 -0
  52. package/lib/qml/runtime.js +3381 -0
  53. package/lib/qml/shapes.js +243 -0
  54. package/lib/qml/text.js +109 -0
  55. package/lib/qml/textures.js +123 -0
  56. package/lib/qml/wayland-clipboard.js +32 -0
  57. package/lib/qml/worker-thread.js +70 -0
  58. package/lib/render-tree/index.js +16 -1
  59. package/lib/renderer/bindings.js +11 -0
  60. package/lib/renderer/canvas-2d.js +13 -0
  61. package/lib/renderer/host.js +31 -6
  62. package/lib/renderer/main.js +19 -6
  63. package/lib/renderer/page-realm.js +16 -1
  64. package/lib/renderer/page-renderer.js +230 -13
  65. package/lib/renderer/text-input-paint.js +54 -0
  66. package/lib/renderer/text-input.js +57 -0
  67. package/lib/style/computed-style.js +2 -2
  68. package/package.json +1 -1
  69. package/test/clipboard.test.js +19 -0
  70. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  71. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  72. package/test/fixtures/flea-helpers/bin/bat +3 -0
  73. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  74. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  75. package/test/fixtures/flea-helpers/bin/glow +3 -0
  76. package/test/fixtures/flea-helpers/bin/qs +3 -0
  77. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  78. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  79. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  80. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  81. package/test/fixtures/flea-helpers/check.mjs +36 -0
  82. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  83. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  84. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  85. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  86. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  87. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  88. package/test/fixtures/qml/flea/LICENSE +21 -0
  89. package/test/fixtures/qml/flea/README.md +25 -0
  90. package/test/fixtures/qml/flea/Theme.qml +8 -0
  91. package/test/fixtures/qml/flea/demo.qml +24 -0
  92. package/test/fixtures/qml/flea/qmldir +3 -0
  93. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  94. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  95. package/test/fixtures/qml/flea-theme/README.md +70 -0
  96. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  97. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  98. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  99. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  100. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  101. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  102. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  103. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  104. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  107. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  108. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  109. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  110. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  111. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  112. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  113. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  114. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  115. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  116. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  117. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  118. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  119. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  120. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  121. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  122. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  123. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  124. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  125. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  126. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  127. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  128. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  167. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  168. package/test/fixtures/qml/hello.qml +26 -0
  169. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  170. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  171. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  172. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  173. package/test/fixtures/qml/imports/logic/base.js +2 -0
  174. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  175. package/test/fixtures/qml/imports/logic/math.js +3 -0
  176. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  177. package/test/fixtures/qml/imports/main.qml +21 -0
  178. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  179. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  180. package/test/fixtures/qml/multiply.qml +61 -0
  181. package/test/fixtures/qml-canvas-dom.js +90 -0
  182. package/test/fixtures/qml-media-audio.js +39 -0
  183. package/test/helpers/flea-sandbox.js +48 -0
  184. package/test/helpers/quickshell-fixture.js +125 -0
  185. package/test/html-clipboard-wayland.test.js +47 -0
  186. package/test/index.test.js +13 -0
  187. package/test/manual-casty-without-qml.js +87 -0
  188. package/test/manual-flea-application.js +49 -0
  189. package/test/manual-flea-binary.js +62 -0
  190. package/test/manual-flea-qml.js +9 -0
  191. package/test/manual-flea-settings.js +40 -0
  192. package/test/manual-flea-theme.js +40 -0
  193. package/test/manual-qml-removal.js +46 -0
  194. package/test/manual-without-qml.js +45 -0
  195. package/test/qml-canvas-dom.test.js +37 -0
  196. package/test/qml-clipboard-bridge.test.js +25 -0
  197. package/test/qml-clipboard-wayland.test.js +40 -0
  198. package/test/qml-connections-timing.test.js +73 -0
  199. package/test/qml-dynamic.test.js +401 -0
  200. package/test/qml-flea-app.test.js +146 -0
  201. package/test/qml-flea-backend.test.js +78 -0
  202. package/test/qml-flea-columns-backend.test.js +38 -0
  203. package/test/qml-flea-compile.test.js +78 -0
  204. package/test/qml-flea-launcher.test.js +88 -0
  205. package/test/qml-flea-markdown-backend.test.js +31 -0
  206. package/test/qml-flea-media.test.js +28 -0
  207. package/test/qml-flea-navigation-backend.test.js +35 -0
  208. package/test/qml-flea-navigation-timing.test.js +147 -0
  209. package/test/qml-flea-preview-backend.test.js +53 -0
  210. package/test/qml-flea-previews.test.js +163 -0
  211. package/test/qml-flea-rows.test.js +101 -0
  212. package/test/qml-flea-textures.test.js +59 -0
  213. package/test/qml-flea-view-switch.test.js +33 -0
  214. package/test/qml-flea-wayland.test.js +68 -0
  215. package/test/qml-flea.test.js +34 -0
  216. package/test/qml-host.test.js +238 -0
  217. package/test/qml-imports.test.js +102 -0
  218. package/test/qml-inherited-handlers.test.js +51 -0
  219. package/test/qml-input-selection.test.js +66 -0
  220. package/test/qml-interactions.test.js +309 -0
  221. package/test/qml-language.test.js +167 -0
  222. package/test/qml-launcher.test.js +54 -0
  223. package/test/qml-layout-text.test.js +205 -0
  224. package/test/qml-media.test.js +63 -0
  225. package/test/qml-persisted-delegates.test.js +45 -0
  226. package/test/qml-quickshell-controls.test.js +65 -0
  227. package/test/qml-quickshell-settings.test.js +62 -0
  228. package/test/qml-repeater.test.js +72 -0
  229. package/test/qml-style-updates.test.js +124 -0
  230. package/test/qml-textures.test.js +66 -0
  231. package/test/qml-views.test.js +332 -0
  232. package/test/qml-worker.test.js +69 -0
  233. package/test/qml.test.js +83 -0
  234. package/test/renderer-host.test.js +11 -0
  235. package/test/text-input-pointer.test.js +31 -0
  236. package/test/text-input-scroll.test.js +29 -0
  237. package/test/text-input.test.js +47 -0
  238. package/test/wayland-frontend.test.js +83 -0
  239. package/test/wayland-urlbar.test.js +38 -0
@@ -0,0 +1,513 @@
1
+ .pragma library
2
+
3
+ .import "Lists.js" as Lists
4
+ .import "Archive.js" as Archive
5
+ .import "ExtThumbs.js" as ExtThumbs
6
+ .import "Format.js" as Format
7
+ .import "Sort.js" as Sort
8
+ .import "StateWords.js" as StateWords
9
+
10
+ // Submenus carry an entry array, including an empty array while a provider is unavailable.
11
+ function hasSubmenu(entry) {
12
+ return entry !== undefined && entry !== null && entry.submenu !== undefined
13
+ }
14
+
15
+ // A separator or a disabled row is never the cursor, so a step skips over one and an end keeps it where it is.
16
+ function stepRow(rows, from, delta) {
17
+ for (var i = from + delta; i >= 0 && i < rows.length; i += delta)
18
+ if (rows[i].separator !== true && rows[i].disabled !== true) return i
19
+ return from
20
+ }
21
+
22
+ // Flip at the far edge first, then shift only when neither side fits.
23
+ function clamp(point, size, bounds) {
24
+ var start = point + size <= bounds ? point : point - size
25
+ return Math.max(0, Math.min(Math.max(0, bounds - size), start))
26
+ }
27
+
28
+ // The flyout's tail row, which is the only way into the dialog; ui/PaneMenuActions.qml reads it.
29
+ var OPEN_WITH_OTHER = "__another__"
30
+
31
+ // The file menu's rename row over two or more selected rows, one whole template.
32
+ var RENAME_MANY = "Rename %1 items\u2026"
33
+
34
+ // One order for every menu: F file, B background, T Trash, P Places, R rail rows (never hideable, never in Settings > Menus).
35
+ var INVENTORY = [
36
+ ["open", "Open", "folder-open", "FTPR", "open"],
37
+ // Show original reveals a symlink's target in its own folder.
38
+ ["showOriginal", "Show original", "symlink", "F", "open", "showOriginal"],
39
+ // MenuAdditions rule 3: a Places or Favorites row opens this menu for its own path, so the rows
40
+ // it carries are the ones that take a path and not the clipboard, archive, send or destroy ones.
41
+ ["openTab", "New tab", "plus", "P", "open"],
42
+ ["openwith", "Open with", "app-window", "F", "open", "openWith"],
43
+ // The run card's own entrances: Run asks through flea --open like Enter does, so the card stays the one place anything executes.
44
+ ["run", "Run", "play", "F", "open"],
45
+ ["openAsText", "Open as text", "file-text", "F", "open"],
46
+ // Mount and Unmount are one toggle; a locked container offers Unlock alone (ui/UnlockDialog.qml).
47
+ ["unlockVolume", "Unlock", "lock", "R", "open"],
48
+ ["mountVolume", "Mount", "drive", "R", "open"],
49
+ ["mountPhone", "Mount", "drive", "R", "open"],
50
+ ["unmountVolume", "Unmount", "drive", "R", "rrelease"],
51
+ ["unmountPhone", "Unmount", "drive", "R", "rrelease"],
52
+ ["unmount", "Unmount", "drive", "R", "rrelease"],
53
+ ["eject", "Eject", "eject", "R", "rrelease"],
54
+ // Flash image opens the flasher with this drive chosen: last, in a group of its own, no switch.
55
+ ["flashImage", "Flash image", "file", "R", "rflash"],
56
+ ["newFolder", "New folder", "folder-plus", "B", "open"],
57
+ ["newFile", "New file", "file-plus", "B", "open"],
58
+ ["cut", "Cut", "scissors", "F", "basic"],
59
+ ["copy", "Copy", "copy", "F", "basic"],
60
+ ["paste", "Paste", "clipboard", "FB", "basic"],
61
+ // MenuAdditions040: Paste as links, undoable, through the collision
62
+ // card; hidden, and holding only the three link rows.
63
+ ["pasteAs", "Paste as", "symlink", "FB", "basic", "pasteAs"],
64
+ ["duplicate", "Duplicate", "file-plus", "F", "basic"],
65
+ ["rename", "Rename", "rename", "FR", "basic"],
66
+ // A saved place's own rows beside Rename; Edit address and Remove from Network are rail-only.
67
+ ["editPlace", "Edit address", "sliders", "R", "basic"],
68
+ ["remove", "Remove from Network", "minus", "R", "rremove"],
69
+ ["selectAll", "Select all", "check", "B", "basic"],
70
+ // MenuAdditions040: Invert selection flips the marks over the rows the
71
+ // listing draws; hidden, present only while something is selected.
72
+ ["invertSelection", "Invert selection", "contrast", "B", "basic", "invertSelection"],
73
+ ["compress", "Compress", "archive", "F", "archive"],
74
+ ["extract", "Extract", "archive-out", "F", "archive"],
75
+ ["convert", "Convert", "sliders", "F", "archive"],
76
+ // Flash to drive opens the flasher with this image chosen, after Convert.
77
+ ["flashDrive", "Flash to drive", "drive", "F", "archive"],
78
+ // Flea's own spiral drawn static: FleaMark paints in over two seconds, which a menu row never waits for.
79
+ ["shelf", "Add to shelf", "shelf", "F", "share", "addToShelf"],
80
+ ["taildrop", "Send with Taildrop", "tailscale", "F", "share"],
81
+ // MenuAdditions rule 1: between Taildrop and Dropbox, present only while a localsend binary is
82
+ // on PATH, and carrying its own reproduced mark rather than a cut glyph.
83
+ ["localsend", "Send with LocalSend", "localsend", "F", "share"],
84
+ ["dropbox", "Move to Dropbox", "dropbox", "F", "share"],
85
+ ["sharelink", "Copy share link", "network", "F", "share"],
86
+ ["trash", "Move to Trash", "trash", "F", "trash"],
87
+ ["delete", "Delete permanently", "trash", "F", "trash", "deletePermanently"],
88
+ ["openTerminal", "Open in terminal", "terminal", "FBP", "inspect"],
89
+ ["moveto", "Move to", "folder-plus", "F", "inspect", "moveTo"],
90
+ ["copyto", "Copy to", "copy", "F", "inspect", "copyTo"],
91
+ ["properties", "Properties", "info", "F", "inspect"],
92
+ ["permissions", "Permissions", "lock", "F", "inspect"],
93
+ // Both boards place Make executable in inspect, without a key, only on scripts missing every execute bit.
94
+ ["makeExecutable", "Make executable", "play", "F", "inspect", "makeExecutable"],
95
+ // MenuAdditions040: Copy as replaces the file menu's hidden Copy path row; a place keeps the flat Copy path behind the same switch.
96
+ ["copyAs", "Copy as", "file-text", "FP", "inspect", "copyAs"],
97
+ // MenuAdditions rule 2: after Copy as, one row per executable in ~/.config/flea/scripts, and
98
+ // absent rather than greyed when that directory is missing or holds none.
99
+ ["runScript", "Run script", "terminal", "F", "inspect"],
100
+ // MenuAdditions040 specimen e: Compare sits before Add to Favorites, with no key, and is absent unless two regular files are marked.
101
+ ["compare", "Compare", "columns", "F", "inspect"],
102
+ ["addFavourite", "Add to Favorites", "star", "FBP", "inspect"],
103
+ ["tags", "Tags", "tag", "F", "inspect"],
104
+ ["removeFavourite", "Remove from Favorites", "minus", "PR", "inspect"],
105
+ ["sort", "Sort by", "sort", "B", "view"],
106
+ ["toggleHidden", "Show hidden files", "eye", "FB", "view"],
107
+ ["toggleLangMarks", "Show code icons", "code", "FB", "view"],
108
+ ["extThumbs", "Show thumbnails", "image", "B", "view"],
109
+ ["settings", "Settings", "sliders", "B", "settings"],
110
+ ["updateFlea", "Update Flea", "download", "B", "settings"],
111
+ ["restoreAll", "Restore all", "undo", "T", "restore"],
112
+ ["emptyTrash", "Empty Trash", "trash", "T", "empty"]
113
+ ]
114
+
115
+ function listingEntries(p) { return buildEntries(p.hasRow ? "F" : "B", p) }
116
+ // The Places rail's own, behind its Extras toggle: off, the rail keeps the menu it has today.
117
+ function placeEntries(p) { return isHidden(p.hiddenActions, "placeMenu") ? [] : buildEntries("P", p) }
118
+ function backgroundEntries(p) { return buildEntries("B", p) }
119
+ function trashEntries(total, busy) { return buildEntries("T", { trashTotal: total, busy: busy }) }
120
+
121
+ function buildEntries(kind, p) {
122
+ var out = [], group = ""
123
+ for (var i = 0; i < INVENTORY.length; i++) {
124
+ var spec = INVENTORY[i]
125
+ // Rail rows are never user-hideable: no switch governs them, so the stored set is not read here.
126
+ if (spec[3].indexOf(kind) < 0) continue
127
+ // The background menu shows Open in terminal without reading the hidden set.
128
+ if (kind !== "R" && isHidden(p.hiddenActions, spec[0])) {
129
+ if (!(kind === "B" && spec[0] === "openTerminal")) continue
130
+ }
131
+ var entry = { id: spec[0], action: spec[5] || spec[0], label: spec[1], glyph: spec[2] }
132
+ if (!availableEntry(entry, p, kind)) continue
133
+ if (out.length && group !== spec[4]) out.push({ separator: true })
134
+ group = spec[4]
135
+ out.push(entry)
136
+ }
137
+ return out
138
+ }
139
+
140
+ // The rail menu: the rows above with kind R, built with the listing menu's separator rule.
141
+ function railEntries(entry) { return buildEntries("R", { entry: entry }) }
142
+
143
+ function availableEntry(e, p, kind) {
144
+ // Rail availability reads the rail entry alone; nothing here is disabled, red or hideable.
145
+ if (kind === "R") return availableRail(e, p.entry)
146
+ var count = p.selectionCount === undefined ? 1 : p.selectionCount
147
+ // Rule 3: the last row adds the favourite or removes it, and the duplicate case is absent rather
148
+ // than grey, which is what keeps issue 138's second row impossible from the rail as well.
149
+ if (kind === "P" && (e.id === "addFavourite" || e.id === "removeFavourite"))
150
+ return (e.id === "removeFavourite") === (p.placeFavourite === true)
151
+ if (e.action === "addFavourite" && kind === "F")
152
+ e.disabled = count !== 1 || ((Number(p.rowMode) || 0) & 0o170000) !== 0o040000
153
+ if (e.action === "paste") e.disabled = p.clipboardAvailable !== true && p.clipboardWatchFailed !== true
154
+ // MenuAdditions040: Show original is visible but only on a symlink.
155
+ if (e.action === "showOriginal" && p.rowIsSymlink !== true) return false
156
+ // Paste as keeps its disabled row without a chevron until files are known.
157
+ if (e.action === "pasteAs") {
158
+ // A filesystem that holds no links offers no link rows at all.
159
+ if (p.canLink === false) return false
160
+ e.disabled = p.clipboardAvailable !== true
161
+ if (p.clipboardAvailable === true)
162
+ e.submenu = pasteAsEntries()
163
+ }
164
+ // Every Copy as variant covers the whole selection, one path per line; no board draws a place menu with it, so a place keeps 0.3.7's flat Copy path.
165
+ if (e.action === "copyAs") {
166
+ if (kind === "P") { e.id = e.action = "copypath"; e.label = "Copy path" }
167
+ else e.submenu = copyAsEntries()
168
+ }
169
+ // MenuAdditions040: Invert selection flips over the rows the listing
170
+ // draws, so with nothing selected there is nothing to flip from.
171
+ if (e.action === "invertSelection" && count === 0) return false
172
+ // Run names a file to execute, so a directory or a wider selection is not offered it.
173
+ if ((e.action === "run" || e.action === "openAsText")
174
+ && (count !== 1 || (Number(p.rowMode) & Format.S_IFMT) === Format.S_IFDIR)) return false
175
+ if (["duplicate", "properties"].indexOf(e.action) >= 0)
176
+ e.disabled = count !== 1
177
+ // Bulk rename: several selected rows open the card instead of refusing, and the row says how many.
178
+ if (e.action === "rename") {
179
+ e.disabled = count < 1
180
+ if (count > 1) e.label = RENAME_MANY.arg(Format.count(count))
181
+ }
182
+ if (e.action === "permissions") {
183
+ var permission = permissionsEntry(p.rowMode, count, p.selectionModes)
184
+ e.disabled = permission.disabled
185
+ if (permission.errored) { e.errored = true; if (permission.hint) e.hint = permission.hint }
186
+ }
187
+ // Only a cursor-row script without any execute bit qualifies; its two-byte probe runs once at menu open.
188
+ if (e.action === "makeExecutable" && !canMakeExecutable(p.rowMode, count, p.hasShebang, p.cursorIsTarget)) return false
189
+ // The held rows or menu snapshot supply every selected item's own kind.
190
+ if (e.action === "compare" && !canCompare(p.compareKinds)) return false
191
+ if (e.action === "runScript") {
192
+ if (!(p.scripts || []).length) return false
193
+ e.submenu = p.scripts.map(function (script) { return { id: script.id, label: script.label } })
194
+ }
195
+ if (e.action === "compress") {
196
+ if (!(p.archiveFormats || []).length) return false
197
+ e.submenu = Archive.formatEntries(p.archiveFormats).concat(Archive.passwordEntry(p.archiveFormats))
198
+ }
199
+ // Issue 133: a mount with no trash directory never offers the row, rather than offering one that
200
+ // fails; ui/js/Mounts.js trashable is the one reader of what the path says about that.
201
+ if (e.action === "trash" && p.canTrash === false) return false
202
+ // A read-only folder turns its write rows off; the listing's own w flag decides, never the mode.
203
+ if (p.dirWritable === false && ["newFolder", "newFile", "paste", "pasteAs", "duplicate",
204
+ "rename", "trash", "deletePermanently"].indexOf(e.action) >= 0) e.disabled = true
205
+ if (e.action === "tags" && !p.tagSubmenu) return false
206
+ if (e.action === "tags") e.submenu = p.tagSubmenu
207
+ if (e.action === "extract" && !Archive.extractEntry(e, p, count)) return false
208
+ if (e.action === "convert" && !(p.rowIsImage && p.canConvert && count === 1)) return false
209
+ // Flash to drive shows only when one disk image is selected, read off the name table.
210
+ if (e.action === "flashDrive" && (count !== 1 || !Archive.isDiskImage(p.rowName, p.flashZipMember))) return false
211
+ // OpenWith.html: the desktop's current default is first and carries the muted caption "default"
212
+ // in the hint slot, the registry order follows it, and the tail row sits under its own
213
+ // separator with the app-window glyph. The row self-hides when the registry names nothing.
214
+ if (e.action === "openWith") {
215
+ if (p.openWithLoaded === true && !(p.openWithApps || []).length) return false
216
+ var apps = p.openWithApps || []
217
+ var rows = apps.map(function (app) {
218
+ return { id: app.id, label: app.label, icon: app.icon || "",
219
+ hint: app.default === true ? "default" : "" }
220
+ })
221
+ if (rows.length) rows.push({ separator: true })
222
+ rows.push({ id: OPEN_WITH_OTHER, label: "Another application\u2026", glyph: "app-window" })
223
+ e.submenu = rows
224
+ e.disabled = count !== 1
225
+ }
226
+ if (e.action === "taildrop") {
227
+ if (!p.taildropInstalled) return false
228
+ e.mark = "tailscale"
229
+ delete e.glyph
230
+ e.submenu = p.taildropPeers || []
231
+ e.disabled = p.taildropRefreshing === true || !e.submenu.length
232
+ // The row says it cannot answer by red and one state word, not the reason: a sentence widened the menu past its frame mid-read.
233
+ if (e.disabled && p.taildropRefreshing !== true) { e.errored = true; e.hint = StateWords.taildrop(p) }
234
+ }
235
+ // Absent rather than greyed when nothing is installed, the Menu board's rule for a row whose
236
+ // whole destination is missing; the row is a plain send, so it has no submenu and no reason.
237
+ // Directive 71: the row is Taildrop's twin, so it opens the same flyout and reads the same way.
238
+ if (e.action === "localsend") {
239
+ if (!p.localSendInstalled) return false
240
+ e.mark = "localsend"
241
+ delete e.glyph
242
+ e.submenu = p.localSendPeers || []
243
+ e.disabled = p.localSendChecking === true || !e.submenu.length
244
+ if (e.disabled && p.localSendChecking !== true) { e.errored = true; e.hint = StateWords.localsend(p) }
245
+ }
246
+ if (e.action === "dropbox" || e.action === "sharelink") {
247
+ if (!p.dropboxInstalled || (e.action === "dropbox" ? p.rowInDropbox : !p.rowInDropbox)) return false
248
+ e.disabled = p.dropboxRefreshing === true || !p.dropboxPath
249
+ if (e.action === "dropbox") { e.mark = "dropbox"; delete e.glyph }
250
+ if (e.disabled && p.dropboxRefreshing !== true) { e.errored = true; e.hint = StateWords.dropbox(p) }
251
+ }
252
+ if (e.action === "sort") e.submenu = sortEntries(p.hasFolderSort === true)
253
+ // Present only while a check has found a newer build, whose version rides the hint slot beside the status square.
254
+ if (e.action === "updateFlea") {
255
+ if (!p.updateVersion) return false
256
+ e.hint = p.updateVersion
257
+ e.hintSquare = true
258
+ }
259
+ if (e.action === "toggleHidden") {
260
+ var hidden = hiddenRow(p.showHidden)
261
+ e.label = hidden.label
262
+ e.glyph = hidden.glyph
263
+ }
264
+ if (e.action === "toggleLangMarks") {
265
+ var code = codeIconsRow(p.langMarks)
266
+ e.label = code.label
267
+ e.glyph = code.glyph
268
+ }
269
+ // The class switch ships hidden; switched on, it is present only on network,
270
+ // phone or USB storage and reads Show while that class is off, Hide while on.
271
+ if (e.action === "extThumbs") return ExtThumbs.entry(e, p)
272
+ if (e.action === "restoreAll" || e.action === "emptyTrash") e.disabled = !(p.trashTotal > 0) || p.busy === true
273
+ if (["trash", "deletePermanently", "emptyTrash"].indexOf(e.action) >= 0) e.danger = true
274
+ return true
275
+ }
276
+
277
+ // Rail rows read the rail entry alone; Mount and Unmount never meet, and Open is the row's own click.
278
+ function availableRail(e, entry) {
279
+ if (!entry) return false
280
+ var device = entry.group === "device", network = entry.group === "network"
281
+ var volume = device && entry.kind === "volume", phone = device && entry.kind === "phone"
282
+ var share = network && entry.kind === "share"
283
+ var mounted = entry.mounted === true
284
+ switch (e.id) {
285
+ case "unlockVolume": return volume && entry.locked === true
286
+ case "mountVolume": return volume && !mounted && entry.locked !== true
287
+ case "mountPhone": return phone && !mounted
288
+ case "open": return (volume && mounted) || (phone && mounted) || (share && mounted)
289
+ case "unmountVolume": return volume && mounted
290
+ case "unmountPhone": return phone && mounted
291
+ case "unmount": return share && mounted
292
+ case "eject": return volume && mounted && (entry.removable === true || entry.loop === true)
293
+ // Flash image shows on a writable removable drive, mounted or not, never on an open crypt, whose mapper names no drive.
294
+ case "flashImage": return volume && entry.removable === true && entry.readOnly !== true && !/^\/dev\/(mapper\/|dm-)/.test(entry.device || "")
295
+ case "rename": return share && entry.editable !== false
296
+ case "editPlace": return share && entry.editable !== false
297
+ case "remove": return share && entry.editable !== false
298
+ case "removeFavourite": return entry.kind === "favourite"
299
+ default: return false
300
+ }
301
+ }
302
+
303
+ // The mode describes the selected object itself, so a symlink never grants access to its unseen target.
304
+ // Issue 193, decided 2026-09-24: a row that cannot act reads red with one state word, as a provider does.
305
+ // MenuAdditions040: Permissions takes the whole selection, so one bad row
306
+ // among modes refuses the row; the dialog inspects each file in turn.
307
+ function permissionsEntry(mode, count, modes) {
308
+ var word
309
+ if (modes !== undefined && modes !== null && modes.length > 0) {
310
+ for (var i = 0; i < modes.length; i++) {
311
+ var kind = (Number(modes[i]) || 0) & Format.S_IFMT
312
+ if (!(kind === Format.S_IFREG || kind === 0o040000)) {
313
+ var bad = { label: "Permissions", action: "permissions", glyph: "lock", disabled: true, errored: true }
314
+ word = StateWords.permissions(modes[i])
315
+ if (word) bad.hint = word
316
+ return bad
317
+ }
318
+ }
319
+ return { label: "Permissions", action: "permissions", glyph: "lock", disabled: false, errored: false }
320
+ }
321
+ var one = (Number(mode) || 0) & Format.S_IFMT
322
+ var allowed = count >= 1 && (one === Format.S_IFREG || one === 0o040000)
323
+ var entry = { label: "Permissions", action: "permissions", glyph: "lock", disabled: !allowed, errored: !allowed }
324
+ // No selection refuses the row for want of an item, not for a kind, so it names no kind; an unread mode names none either.
325
+ if (!allowed && count >= 1) {
326
+ word = StateWords.permissions(mode)
327
+ if (word) entry.hint = word
328
+ }
329
+ return entry
330
+ }
331
+
332
+ // Only a single cursor-row regular file with a shebang and no execute bit.
333
+ function canMakeExecutable(mode, count, hasShebang, cursorIsTarget) {
334
+ if (count !== 1 || hasShebang !== true || cursorIsTarget !== true) return false
335
+ var bits = Number(mode) || 0
336
+ if ((bits & Format.S_IFMT) !== Format.S_IFREG) return false
337
+ return (bits & Format.ANY_EXECUTE_BIT) === 0
338
+ }
339
+
340
+ // Exactly two marked regular files; kinds holds each item's own type bits, so links stay links.
341
+ // Sample input: [0o100000, 0o100000] is true; [0o100000, 0o040000], [0o100000] and a link left at 0o120000 are false.
342
+ function canCompare(kinds) {
343
+ if (!Lists.isList(kinds) || kinds.length !== 2) return false
344
+ return kinds.every(function (kind) { return ((Number(kind) || 0) & Format.S_IFMT) === Format.S_IFREG })
345
+ }
346
+
347
+ // The Copy as flyout: six leaves in board order, letters on keyHint.
348
+ function copyAsEntries() {
349
+ return [
350
+ { id: "copyPath", label: "Path", glyph: "file-text", keyHint: "p" },
351
+ { id: "copyName", label: "Name", glyph: "type", keyHint: "n" },
352
+ { id: "copyStem", label: "Name without extension", glyph: "type", keyHint: "e" },
353
+ { id: "copydirpath", label: "Folder path", glyph: "folder", keyHint: "f" },
354
+ { id: "copyUri", label: "File URI", glyph: "globe", keyHint: "u" },
355
+ { id: "copyQuoted", label: "Shell-quoted", glyph: "terminal", keyHint: "s" }
356
+ ]
357
+ }
358
+
359
+ // MenuAdditions040: the Paste as flyout. Link is relative from the
360
+ // destination, Absolute link stores the full path; l and h open and close a
361
+ // flyout, so the letters are L, a and H.
362
+ function pasteAsEntries() {
363
+ return [
364
+ { id: "pasteLink", label: "Link", glyph: "symlink", keyHint: "L" },
365
+ { id: "pasteAbsoluteLink", label: "Absolute link", glyph: "symlink", keyHint: "a" },
366
+ { id: "pasteHardLink", label: "Hard link", glyph: "copy", keyHint: "H" }
367
+ ]
368
+ }
369
+
370
+ // A hidden row still opens its flyout, built from the action.
371
+ function flyoutEntries(action) {
372
+ if (action === "copyAs")
373
+ return copyAsEntries()
374
+ if (action === "pasteAs")
375
+ return pasteAsEntries()
376
+ return []
377
+ }
378
+ // The sentence an empty Paste as refuses with, the same one ui/Pane.qml shows.
379
+ var EMPTY_CLIPBOARD = "There is nothing to paste; y copies and x cuts."
380
+ // Sample input: submenuFor("pasteAs", [], false) answers refuse.
381
+ function submenuFor(action, entries, clipboardAvailable) {
382
+ if (flyoutEntries(action).length === 0)
383
+ return { kind: "none" }
384
+ for (var i = 0; i < entries.length; i++) {
385
+ if (entries[i].action === action) {
386
+ if (entries[i].disabled === true || !hasSubmenu(entries[i]))
387
+ return { kind: "refuse" }
388
+ return { kind: "row", index: i }
389
+ }
390
+ }
391
+ if (action === "pasteAs" && clipboardAvailable !== true)
392
+ return { kind: "refuse" }
393
+ return { kind: "lone" }
394
+ }
395
+ // Sample input: loneChoice("copyAs", "copyPath", false, false, true, "a", "a") fires "copyAs:copyPath".
396
+ function loneChoice(action, id, forRail, forHeader, hasRow, openedIdentity, selectionIdentity) {
397
+ var leaves = flyoutEntries(action)
398
+ var known = false
399
+ for (var i = 0; i < leaves.length; i++)
400
+ if (leaves[i].separator !== true && leaves[i].id === id) known = true
401
+ var moved = !forRail && !forHeader && hasRow && openedIdentity !== selectionIdentity
402
+ if (moved) return { kind: "moved" }
403
+ if (!known) return { kind: "unknown" }
404
+ return { kind: "fire", fired: action + ":" + id }
405
+ }
406
+
407
+
408
+ // The Sort by flyout, built from ui/js/Sort.js's own ORDERS so it can only ever offer an order the
409
+ // backend really produces; a fourth key would earn a refusal instead of a listing.
410
+ var SORT_LABELS = { name: "Name", size: "Size", mtime: "Modified", kind: "Kind" }
411
+
412
+ function sortEntries(hasOwn) {
413
+ var out = []
414
+ for (var i = 0; i < Sort.ORDERS.length; i++)
415
+ out.push({ id: Sort.ORDERS[i], label: SORT_LABELS[Sort.ORDERS[i]] })
416
+ // The id is the spelling ui/js/Sort.js's forget branch reads, so the two must agree.
417
+ if (hasOwn === true) { out.push({ separator: true }); out.push({ id: "__default__", label: "Use the default sort" }) }
418
+ return out
419
+ }
420
+
421
+ // The one mark every row of an open flyout draws, keyed on the row that opened it: a Taildrop peer
422
+ // is a machine, a sort order is the row above it, and an archive format is a file about to exist.
423
+ function submenuGlyph(action) {
424
+ if (action === "taildrop")
425
+ return "server"
426
+ if (action === "runScript")
427
+ return "terminal"
428
+ if (action === "sort")
429
+ return "sort"
430
+ return "archive"
431
+ }
432
+
433
+ // The visibility consumer SettingsMenus.html specifies, and the only reader of the stored hidden
434
+ // set: a switched-off action's row leaves, and a rule that has lost every row it divided leaves
435
+ // with it, so a menu never grows a doubled, leading or trailing separator. Order never changes,
436
+ // because the board offers visibility and deliberately not ordering.
437
+ function applyHidden(entries, hiddenActions) {
438
+ var kept = []
439
+ for (var i = 0; i < entries.length; i++) {
440
+ var entry = entries[i]
441
+ if (entry.separator === true) {
442
+ if (kept.length > 0 && kept[kept.length - 1].separator !== true)
443
+ kept.push(entry)
444
+ continue
445
+ }
446
+ if (!isHidden(hiddenActions, entry.id || entry.action))
447
+ kept.push(entry)
448
+ }
449
+ while (kept.length > 0 && kept[kept.length - 1].separator === true)
450
+ kept.pop()
451
+ return kept
452
+ }
453
+
454
+ // Open and the hidden toggle are never in the stored set: ui/js/Settings.js draws them locked, and
455
+ // this is the second half of that lock, so a hand-edited state file cannot empty the menu either.
456
+ function isHidden(hiddenActions, action) {
457
+ if (action === "open" || action === "toggleHidden")
458
+ return false
459
+ for (var i = 0; hiddenActions && i < hiddenActions.length; i++) {
460
+ if (hiddenActions[i] === action)
461
+ return true
462
+ }
463
+ return false
464
+ }
465
+
466
+ // Hidden-files labels and glyphs flip with state; MenuRow draws no checkmark.
467
+ function hiddenRow(showHidden) {
468
+ return {
469
+ label: showHidden ? "Hide hidden files" : "Show hidden files",
470
+ action: "toggleHidden",
471
+ glyph: showHidden ? "eye-off" : "eye"
472
+ }
473
+ }
474
+
475
+ // Code icons use the hidden-files row's label flip and the existing code glyph.
476
+ function codeIconsRow(langMarks) {
477
+ return { label: langMarks !== false ? "Hide code icons" : "Show code icons", action: "toggleLangMarks", glyph: "code" }
478
+ }
479
+
480
+ // ui/Header.qml's own rows, on a right click over the column titles. Name is not among them: it is
481
+ // the one column a file manager cannot do without (see ui/js/Columns.js), so it is never hidden and
482
+ // never offered. A hidden column reads "Show", a drawn one "Hide", the flip the state rows use.
483
+ function headerEntries(hiddenCols, showHidden) {
484
+ var out = []
485
+ var hidden = {}
486
+ for (var h = 0; h < hiddenCols.length; h++)
487
+ hidden[hiddenCols[h]] = true
488
+ var columns = [["mode", "Mode"], ["size", "Size"], ["date", "Modified"], ["kind", "Kind"]]
489
+ var glyphs = { mode: "lock", size: "drive", date: "download", kind: "type" }
490
+ for (var i = 0; i < columns.length; i++) {
491
+ var key = columns[i][0]
492
+ var shown = !hidden[key]
493
+ out.push({
494
+ label: shown ? "Hide " + columns[i][1] : "Show " + columns[i][1],
495
+ action: "col:" + key,
496
+ glyph: glyphs[key]
497
+ })
498
+ }
499
+ out.push({ separator: true })
500
+ out.push(hiddenRow(showHidden))
501
+ return out
502
+ }
503
+
504
+ // The keyboard's own entrance to the row menu, under the cursor row the way the rail's opens under
505
+ // its own; setCursor first, because a wheel scroll in the grid can leave the cursor off screen.
506
+ // paddingX is ui/Theme.qml's rowPaddingX, passed in because a singleton has no name in a library.
507
+ function openAtCursor(root, menu, paddingX) {
508
+ root.setCursor(root.cursorIndex)
509
+ var row = root.visibleItemFor(root.cursorIndex)
510
+ if (row)
511
+ menu.openAt(row.mapToItem(null, paddingX, row.height))
512
+ return row !== null
513
+ }
@@ -0,0 +1,16 @@
1
+ .pragma library
2
+
3
+ // The structural transitions use OutCubic, 180 ms to reveal and 140 ms to hide.
4
+
5
+ var durMs = {
6
+ // A reveal reads slower than a hide.
7
+ open: 180,
8
+ close: 140
9
+ }
10
+
11
+ // Open rises into place from this far below its resting position; close does not translate,
12
+ // opacity only (see the Preview/ShareBrowser/NetworkDialog verticalCenterOffset/y bindings).
13
+ var translateUpPx = 10
14
+
15
+ // How long the pointer rests at the left edge before the auto-hidden rail opens; the OEM bar holds its centre peek 120 ms (Bar.qml:834).
16
+ var dwellMs = 120