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,33 @@
1
+ // The Omarchy theme palette in colors.toml, parsed and queried. Pure, with no QML imports, so
2
+ // tests/js/palette.js can run it under qml6 with no window.
3
+
4
+ // Sample input: cyan = "#27a6a2" # the ANSI ring
5
+ function parse(body) {
6
+ var found = {};
7
+ var lines = String(body || "").split("\n");
8
+ for (var i = 0; i < lines.length; i++) {
9
+ var kv = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/);
10
+ if (kv)
11
+ found[kv[1]] = kv[2];
12
+ }
13
+ return found;
14
+ }
15
+
16
+ // ThemeRoles.html's surface ladder: the chrome plane, then background as the neutral fallback when
17
+ // it is absent, then selection for the alacritty-derived file that emits neither ladder key at all.
18
+ var SURFACE_KEYS = ["dark_background", "background", "selection"];
19
+
20
+ // The first key the theme actually set wins, and a role no theme models keeps Flea's own colour.
21
+ function pick(found, keys, fallback) {
22
+ for (var i = 0; i < keys.length; i++) {
23
+ if (found[keys[i]])
24
+ return found[keys[i]];
25
+ }
26
+ return fallback;
27
+ }
28
+
29
+ // A theme need not set every role and pick() already falls back per role, so the only question a
30
+ // readiness flag can answer honestly is whether the file yielded any colour at all.
31
+ function isPalette(found) {
32
+ return Object.keys(found).length > 0;
33
+ }
@@ -0,0 +1,236 @@
1
+ .pragma library
2
+
3
+ .import "Mounts.js" as Mounts
4
+ .import "UserDirs.js" as UserDirs
5
+
6
+ // Sample input: XDG_DOWNLOAD_DIR="$HOME/Downloads"
7
+ function userDirs(body, home) {
8
+ return UserDirs.userDirs(body, home)
9
+ }
10
+
11
+ // Sample input: file:///home/gm/Downloads Downloads
12
+ function bookmarks(body) {
13
+ var out = []
14
+ var lines = String(body || "").split("\n")
15
+ for (var i = 0; i < lines.length; i++) {
16
+ var line = lines[i].trim()
17
+ // corner: a bookmark may be smb:// or sftp://, which is a Location and not a Favorite.
18
+ if (line.indexOf("file://") !== 0)
19
+ continue
20
+ var space = line.indexOf(" ")
21
+ var uri = space < 0 ? line : line.substring(0, space)
22
+ var label = space < 0 ? "" : line.substring(space + 1).trim()
23
+ var path = Mounts.decodePath(uri.substring("file://".length))
24
+ out.push({ path: path, label: label.length > 0 ? label : leaf(path) })
25
+ }
26
+ return out
27
+ }
28
+
29
+ // The FAVORITES rail's rows, Home first and always, then XDG dirs and bookmarked places in file
30
+ // order, a path seen twice keeping its first position. glyphFor resolves each row's mark so the
31
+ // rail's Icons import stays the rail's own business; see ui/Sidebar.qml's rebuild.
32
+ function favorites(home, dirsText, marksText, glyphFor) {
33
+ var favs = [{ path: home, label: "Home", group: "favorite", kind: "favorite", glyph: glyphFor("Home") }]
34
+ var seen = {}
35
+ seen[home] = true
36
+ var groups = [userDirs(dirsText, home), bookmarks(marksText)]
37
+ for (var g = 0; g < groups.length; g++) {
38
+ for (var i = 0; i < groups[g].length; i++) {
39
+ if (seen[groups[g][i].path])
40
+ continue
41
+ var e = groups[g][i]
42
+ seen[e.path] = true
43
+ favs.push({ path: e.path, label: e.label, group: "favorite", kind: "favorite", glyph: glyphFor(e.label) })
44
+ }
45
+ }
46
+ return favs
47
+ }
48
+
49
+ function leaf(path) {
50
+ var cut = path.lastIndexOf("/")
51
+ return cut < 0 ? path : path.substring(cut + 1)
52
+ }
53
+
54
+ // Sample input: "smb://192.168.1.10/data NAS"; see AGENTS.md "Places.relabel" for the matching, duplicate and control-character rules.
55
+ function relabel(body, path, name) {
56
+ // A trust boundary: an embedded newline could otherwise split one bookmark into two lines.
57
+ var trimmed = String(name || "").replace(/[\r\n]/g, "").trim()
58
+ if (trimmed.length === 0)
59
+ return String(body || "")
60
+ var target = Mounts.normalize(path)
61
+ var lines = String(body || "").split("\n")
62
+ var found = false
63
+ for (var i = 0; i < lines.length; i++) {
64
+ // Read off the trimmed line the way Mounts.removeBookmark reads it, or an indented bookmark
65
+ // is one Remove can drop and this could only ever duplicate.
66
+ var line = lines[i].trim()
67
+ if (line.length === 0)
68
+ continue
69
+ var space = line.indexOf(" ")
70
+ var uri = space < 0 ? line : line.substring(0, space)
71
+ if (Mounts.normalize(uri) === target) {
72
+ lines[i] = uri + " " + trimmed
73
+ found = true
74
+ }
75
+ }
76
+ if (found)
77
+ return lines.join("\n")
78
+ var out = String(body || "")
79
+ if (out.length > 0 && out.charAt(out.length - 1) !== "\n")
80
+ out += "\n"
81
+ return out + target + " " + trimmed + "\n"
82
+ }
83
+
84
+ // Sample input: body as relabel's, oldUri the saved line being rewritten, newUri the address the
85
+ // form's Mounts-as line showed. Matched the way relabel matches, so a live mount's trailing slash
86
+ // still finds the saved line, and an empty oldUri matches nothing and appends instead. PR 21.
87
+ function replace(body, oldUri, newUri, label) {
88
+ var next = Mounts.normalize(newUri)
89
+ if (next.length === 0)
90
+ return String(body || "")
91
+ // The same trust boundary relabel names: an embedded newline would split one bookmark into two.
92
+ var name = String(label || "").replace(/[\r\n]/g, "").trim()
93
+ if (name.length === 0)
94
+ name = leaf(next)
95
+ var target = Mounts.normalize(oldUri)
96
+ var lines = String(body || "").split("\n")
97
+ var found = false
98
+ for (var i = 0; i < lines.length; i++) {
99
+ var line = lines[i].trim()
100
+ if (line.length === 0)
101
+ continue
102
+ var space = line.indexOf(" ")
103
+ var uri = space < 0 ? line : line.substring(0, space)
104
+ if (target.length > 0 && Mounts.normalize(uri) === target) {
105
+ lines[i] = next + " " + name
106
+ found = true
107
+ }
108
+ }
109
+ if (found)
110
+ return lines.join("\n")
111
+ var out = String(body || "")
112
+ if (out.length > 0 && out.charAt(out.length - 1) !== "\n")
113
+ out += "\n"
114
+ return out + next + " " + name + "\n"
115
+ }
116
+
117
+ var WIDTH_STOPS = [160, 192, 224, 256]
118
+ function sidebarWidth(value) {
119
+ if (typeof value !== "number" || !isFinite(value)) return 192
120
+ var nearest = WIDTH_STOPS[0]
121
+ for (var i = 1; i < WIDTH_STOPS.length; i++) {
122
+ if (Math.abs(WIDTH_STOPS[i] - value) < Math.abs(nearest - value)) nearest = WIDTH_STOPS[i]
123
+ }
124
+ return nearest
125
+ }
126
+
127
+ function recordError(record) {
128
+ if (!record || typeof record !== "object" || Array.isArray(record)) return "invalid favorite record"
129
+ if (typeof record.label !== "string" || record.label.trim().length === 0) return "label needs visible text"
130
+ if (typeof record.path !== "string" || /[\u0000-\u001f\u007f]/.test(record.path)) return "invalid path"
131
+ if (record.path.charAt(0) === "/" || record.path === "~" || record.path.indexOf("~/") === 0) return ""
132
+ var schemes = ["smb://", "sftp://", "ftp://", "dav://", "davs://", "afp://", "nfs://", "file://"]
133
+ for (var i = 0; i < schemes.length; i++) {
134
+ if (record.path.indexOf(schemes[i]) === 0 && record.path.length > schemes[i].length) return ""
135
+ }
136
+ return "use a full path, a path that starts with ~/, or a network address"
137
+ }
138
+
139
+ function storedEntries(records, home) {
140
+ var out = []
141
+ for (var i = 0; Array.isArray(records) && i < records.length; i++) {
142
+ var record = records[i]
143
+ var error = recordError(record)
144
+ var label = record && typeof record.label === "string" ? record.label : JSON.stringify(record)
145
+ var path = record && typeof record.path === "string" ? record.path : ""
146
+ var resolved = path === "~" ? home : path.indexOf("~/") === 0 ? home + path.substring(1) : path
147
+ out.push({ label: label || "Invalid favorite", path: resolved, storedPath: path,
148
+ error: error, original: record, favouriteIndex: i, group: "favourite", kind: "favourite",
149
+ glyph: path.indexOf("://") >= 0 ? "network" : "folder" })
150
+ }
151
+ return out
152
+ }
153
+
154
+ function homeEntries(home, dirsText, glyphFor) {
155
+ var entries = [{ label: "Home", path: home }].concat(userDirs(dirsText, home))
156
+ return entries.map(function (entry) {
157
+ return { label: entry.label, path: entry.path, group: "home", kind: "home", glyph: glyphFor(entry.label) }
158
+ })
159
+ }
160
+
161
+ // The rail and query use the same validation, URI decoding and mount-first open.
162
+ function openEntry(entry, ports) {
163
+ if (!entry) return
164
+ if (entry.kind === "trash") { ports.trash(); return }
165
+ var error = entry.kind === "favourite" ? recordError(entry.original) : ""
166
+ if (error) { ports.message("Could not open %1 · %2".replace(/%([12])/g, function (m, n) { return n === "1" ? entry.label : error }), true); return }
167
+ if (entry.path.indexOf("://") >= 0 && entry.path.indexOf("file://") !== 0) {
168
+ var host = ports.networkHost()
169
+ if (host) host.openChildShare(entry.path, entry.label, ports.pane)
170
+ } else {
171
+ ports.opened(entry.path.indexOf("file://") === 0 ? Mounts.decodePath(entry.path.substring(7)) : entry.path)
172
+ }
173
+ }
174
+
175
+ function railIdentity(entry) {
176
+ if (!entry) return ""
177
+ if (entry.kind === "favourite") return JSON.stringify([entry.kind, entry.original])
178
+ return JSON.stringify([entry.group, entry.kind, Mounts.railKey(entry) || entry.device || entry.uri || entry.path])
179
+ }
180
+
181
+ // Duplicate originals have no stored ID; a changed multiplicity cannot safely identify the selected occurrence.
182
+ function railCursorAfter(before, after, index) {
183
+ if (before.length === 0 && index === 0) return 0
184
+ if (index < 0 || index >= before.length) return -1
185
+ var key = railIdentity(before[index]), oldCount = 0, occurrence = 0, matches = []
186
+ for (var i = 0; i < before.length; i++) {
187
+ if (railIdentity(before[i]) !== key) continue
188
+ oldCount++
189
+ if (i < index) occurrence++
190
+ }
191
+ for (var j = 0; j < after.length; j++) {
192
+ if (railIdentity(after[j]) === key) matches.push(j)
193
+ }
194
+ return matches.length === oldCount ? matches[occurrence] : -1
195
+ }
196
+
197
+ // Under gvfs only the mounted row opened under view.shareKey lights, never a folder prefix or the disk.
198
+ function heldBy(entries, dir, view) {
199
+ var list = entries || []
200
+ var at = view || ({ kind: "dir" })
201
+ var path = String(dir || "")
202
+ var gvfs = Mounts.underGvfs(path)
203
+ var best = -1, depth = -1
204
+ for (var i = 0; i < list.length; i++) {
205
+ var e = list[i]
206
+ if (at.kind !== "dir") {
207
+ if (e.kind === at.kind && (at.kind !== "tag" || e.tag === at.tag))
208
+ return i
209
+ continue
210
+ }
211
+ if (e.kind === "recent" || e.kind === "trash" || e.kind === "tag" || e.error || e.mounted === false)
212
+ continue
213
+ if (gvfs) {
214
+ var key = String(at.shareKey || "")
215
+ if (key.length > 0 && !e.path && (Mounts.normalize(e.uri) === key || Mounts.normalize(e.mountUri) === key))
216
+ return i
217
+ continue
218
+ }
219
+ if (typeof e.path !== "string" || e.path.length === 0)
220
+ continue
221
+ if ((e.path === "/" ? path.charAt(0) === "/" : Mounts.inside(path, e.path)) && e.path.length > depth) {
222
+ best = i
223
+ depth = e.path.length
224
+ }
225
+ }
226
+ return best
227
+ }
228
+
229
+ // pathAlias is the pane's remembered gvfs share (or null); its key is the share's normalized address.
230
+ function litView(trashOpened, recentMode, searchMode, answeredQuery, pathAlias) {
231
+ if (trashOpened) return { kind: "trash" }
232
+ if (String(recentMode || "").length > 0) return { kind: "recent" }
233
+ var query = String(answeredQuery || "")
234
+ if (String(searchMode || "").length > 0 && query.indexOf("tag:") === 0) return { kind: "tag", tag: query.substring(4) }
235
+ return { kind: "dir", shareKey: pathAlias ? String(pathAlias.key || "") : "" }
236
+ }
@@ -0,0 +1,296 @@
1
+ .pragma library
2
+
3
+ // The Add-network-place form is a form over a gvfs URI, never a second mount system: a protocol
4
+ // picks the scheme, prefills the port and swaps the field set, and the Mounts-as line always shows
5
+ // the exact URI the dialog will hand to gio mount.
6
+
7
+ // The five the canvas draws, in the order it draws them.
8
+ var PROTOCOLS = ["SMB", "SFTP", "FTPS", "WebDAV", "NFS"]
9
+
10
+ // Which fields each protocol asks for. "path" is the label the canvas gives that row, which differs
11
+ // per protocol because the thing it names differs: a share, a remote path, an export.
12
+ var FIELDS = {
13
+ "SMB": { pathLabel: "Share", credentials: true, domain: true, tls: false },
14
+ "SFTP": { pathLabel: "Path", credentials: true, domain: false, tls: false },
15
+ "FTPS": { pathLabel: "Path", credentials: true, domain: false, tls: true },
16
+ "WebDAV": { pathLabel: "Path", credentials: true, domain: false, tls: true },
17
+ // NFS has no credentials at all: the export is authorised by the server, not by a password.
18
+ "NFS": { pathLabel: "Export", credentials: false, domain: false, tls: false }
19
+ }
20
+
21
+ // The TLS box flips two schemes and defaults on, which is what the canvas draws ticked.
22
+ function scheme(protocol, tls) {
23
+ switch (protocol) {
24
+ case "SMB": return "smb"
25
+ case "SFTP": return "sftp"
26
+ case "FTPS": return tls ? "ftps" : "ftp"
27
+ case "WebDAV": return tls ? "davs" : "dav"
28
+ case "NFS": return "nfs"
29
+ }
30
+ return ""
31
+ }
32
+
33
+ function fieldsFor(protocol) {
34
+ return FIELDS[protocol] || FIELDS["SMB"]
35
+ }
36
+
37
+ // The port the form prefills. It belongs to the scheme, never to the protocol: WebDAV builds two
38
+ // schemes and SCHEME_PORTS below is the one table both this and the rail's dedup read it out of.
39
+ function defaultPort(protocol, tls) {
40
+ return defaultPortFor(scheme(protocol, tls))
41
+ }
42
+
43
+ // The exact URI gio mount will be handed. Empty when there is not enough to build one, so the
44
+ // Mounts-as line shows nothing rather than a half-formed address.
45
+ function uri(form) {
46
+ var host = String(form.host || "").trim()
47
+ var port = String(form.port || "").trim()
48
+ if (!validHost(host) || !validPort(port)) {
49
+ return ""
50
+ }
51
+ var spec = fieldsFor(form.protocol)
52
+ var head = scheme(form.protocol, form.tls) + "://"
53
+ if (spec.credentials) {
54
+ head += userPart(form, spec)
55
+ }
56
+ head += host
57
+ // Keep canvas defaults in the form, but omit them because GVFS NFS rejects an explicit :2049.
58
+ if (port.length > 0 && Number(port) !== defaultPort(form.protocol, form.tls)) {
59
+ head += ":" + port
60
+ }
61
+ return head + pathPart(form.path)
62
+ }
63
+
64
+ function validHost(host) {
65
+ return /^\[[0-9A-Fa-f:.]+\]$/.test(host) || /^[A-Za-z0-9._-]+$/.test(host)
66
+ }
67
+
68
+ function validPort(port) {
69
+ if (port.length === 0) {
70
+ return true
71
+ }
72
+ if (!/^[0-9]+$/.test(port)) {
73
+ return false
74
+ }
75
+ var number = Number(port)
76
+ return number >= 1 && number <= 65535
77
+ }
78
+
79
+ // smb://DOMAIN;user@host/share is the form a domain takes; without one it is just user@host.
80
+ function userPart(form, spec) {
81
+ var user = String(form.user || "").trim()
82
+ if (user.length === 0) {
83
+ return ""
84
+ }
85
+ var domain = spec.domain ? String(form.domain || "").trim() : ""
86
+ return (domain.length > 0 ? encodeURIComponent(domain) + ";" : "")
87
+ + encodeURIComponent(user) + "@"
88
+ }
89
+
90
+ function pathPart(path) {
91
+ var text = String(path || "").trim()
92
+ if (text.length === 0) {
93
+ return "/"
94
+ }
95
+ var parts = text.split("/")
96
+ for (var i = 0; i < parts.length; i++) {
97
+ parts[i] = encodeURIComponent(parts[i])
98
+ }
99
+ var encoded = parts.join("/")
100
+ return encoded.charAt(0) === "/" ? encoded : "/" + encoded
101
+ }
102
+
103
+ // The label the sidebar row will carry: what the operator typed, or the last part of the path.
104
+ function label(form) {
105
+ var given = String(form.label || "").trim()
106
+ if (given.length > 0) {
107
+ return given
108
+ }
109
+ var path = String(form.path || "").replace(/\/+$/, "")
110
+ var cut = path.lastIndexOf("/")
111
+ var leaf = cut >= 0 ? path.substring(cut + 1) : path
112
+ return leaf.length > 0 ? leaf : String(form.host || "").trim()
113
+ }
114
+
115
+ // A form with nothing to mount cannot be saved, which is what greys the Save row out.
116
+ function complete(form) {
117
+ var host = String(form.host || "").trim()
118
+ var port = String(form.port || "").trim()
119
+ return validHost(host) && validPort(port)
120
+ }
121
+
122
+ // The port each scheme drops, which is a property of the scheme and not of the protocol the form
123
+ // picked: WebDAV builds two schemes whose ports differ, and one number for both stripped a real
124
+ // ":443" off "dav://" and left its real ":80" on, the duplicate rail row this exists to remove.
125
+ // Measured on this box against gvfs 1.60.2 and glib2 2.88.3 by round-tripping each spelling through
126
+ // Gio.File, the uri mapper "gio mount -l" prints through: gio drops smb 445, sftp 22, ftp 21,
127
+ // ftps 21 (not IANA's 990), dav 80 and davs 443, and keeps every other port it is given, ftps 990
128
+ // and dav 443 included. It drops no nfs port at all, so 2049 is here for the other half of the job:
129
+ // the port an nfs client uses when the line omits one, so two spellings of one export make one row.
130
+ var SCHEME_PORTS = {
131
+ "smb": 445, "sftp": 22, "ftp": 21, "ftps": 21, "dav": 80, "davs": 443, "nfs": 2049
132
+ }
133
+
134
+ // A bookmarks line is arbitrary text, so an inherited Object member must never answer as a port.
135
+ function defaultPortFor(uriScheme) {
136
+ var want = String(uriScheme || "").toLowerCase()
137
+ return SCHEME_PORTS.hasOwnProperty(want) ? SCHEME_PORTS[want] : 0
138
+ }
139
+
140
+ // "gio mount -l" never reports a scheme's default port and the add form spells out the one it
141
+ // prefilled, so the same share arrives spelled two ways and must resolve to one rail row. The port
142
+ // is found in the authority alone: a ":" or an "@" further along belongs to the path.
143
+ function stripDefaultPort(uri) {
144
+ var text = String(uri || "")
145
+ var auth = authority(text)
146
+ var tail = ":" + defaultPortFor(schemeOf(text))
147
+ // A bracketed IPv6 literal ends in "]", so its own trailing digits can never match this tail.
148
+ if (auth.length === 0 || tail === ":0" || auth.substring(auth.length - tail.length) !== tail)
149
+ return text
150
+ var mark = text.indexOf("://") + 3
151
+ return text.substring(0, mark) + auth.substring(0, auth.length - tail.length)
152
+ + text.substring(mark + auth.length)
153
+ }
154
+
155
+ // The scheme a built or bookmarked URI carries, lowercased; "" when the text is not a URI at all.
156
+ function schemeOf(uri) {
157
+ var text = String(uri || "")
158
+ var mark = text.indexOf("://")
159
+ return mark < 0 ? "" : text.substring(0, mark).toLowerCase()
160
+ }
161
+
162
+ // Everything between "://" and the next "/", which is the only place a userinfo, a host or a port
163
+ // can live: a scan over the whole URI would read a "@" or a ":" inside the path as one of those.
164
+ function authority(uri) {
165
+ var text = String(uri || "")
166
+ var mark = text.indexOf("://")
167
+ if (mark < 0)
168
+ return ""
169
+ var rest = text.substring(mark + 3)
170
+ var slash = rest.indexOf("/")
171
+ return slash < 0 ? rest : rest.substring(0, slash)
172
+ }
173
+
174
+ // The host alone: userinfo dropped at the authority's own last "@", a bracketed IPv6 literal kept
175
+ // whole because its colons are not a port separator, and any real port dropped.
176
+ function hostOf(uri) {
177
+ var auth = authority(uri)
178
+ var at = auth.lastIndexOf("@")
179
+ var hostPort = at < 0 ? auth : auth.substring(at + 1)
180
+ if (hostPort.charAt(0) === "[") {
181
+ var close = hostPort.indexOf("]")
182
+ return close < 0 ? hostPort : hostPort.substring(0, close + 1)
183
+ }
184
+ var colon = hostPort.indexOf(":")
185
+ return colon < 0 ? hostPort : hostPort.substring(0, colon)
186
+ }
187
+
188
+ // gvfsd renders a network mount as "<share> <word> <host>" and translates that word, so issue #36's
189
+ // rule reads the URI instead: the tail that gets cut is this URI's own host, in any language, and a
190
+ // label that does not end in it is left exactly as gio gave it.
191
+ function shareName(rawLabel, uri) {
192
+ var text = String(rawLabel || "")
193
+ var host = hostOf(uri)
194
+ var head = text.substring(0, text.length - host.length)
195
+ if (host.length === 0 || head.length === 0 || text.substring(head.length) !== host)
196
+ return text
197
+ // The one assumption left after the host check: gvfsd's connector is a single whitespace-delimited
198
+ // word in whatever language it renders. A translation using two would leave one of them on the
199
+ // name, a wrong label and never a wrong destination; a head this does not shorten is left whole.
200
+ var name = head.replace(/\s+\S+\s+$/, "")
201
+ return name.length > 0 && name !== head ? name : text
202
+ }
203
+
204
+ // Sample input: smb://dan@192.168.1.75/test, the reporter's share, and its FUSE folder
205
+ // /run/user/1000/gvfs/smb-share:server=192.168.1.75,share=test,user=dan. A FUSE path for
206
+ // another user, another share or another server must never open as this share's listing:
207
+ // a mount problem answers as one instead of reading like an unreadable directory.
208
+ function decodeSegment(raw) {
209
+ try { return decodeURIComponent(String(raw)) } catch (e) { return String(raw) }
210
+ }
211
+
212
+ // The smb:// request as gio sees it: the user past any DOMAIN; prefix, the host without
213
+ // a port, and the share as the first path segment. Null when not an smb:// URI at all.
214
+ function smbParts(uri) {
215
+ if (schemeOf(uri) !== "smb") return null
216
+ var auth = authority(uri)
217
+ if (auth.length === 0) return null
218
+ var at = auth.lastIndexOf("@")
219
+ var info = at < 0 ? "" : auth.substring(0, at)
220
+ var semi = info.indexOf(";")
221
+ var user = semi < 0 ? info : info.substring(semi + 1)
222
+ var mark = String(uri).indexOf("://") + 3
223
+ var rest = String(uri).substring(mark + auth.length)
224
+ if (rest.charAt(0) === "/") rest = rest.substring(1)
225
+ var cut = rest.indexOf("/")
226
+ var share = cut < 0 ? rest : rest.substring(0, cut)
227
+ return { user: decodeSegment(user), host: hostOf(uri).toLowerCase(), share: decodeSegment(share) }
228
+ }
229
+
230
+ // The smb-share: FUSE directory gio info prints a local path for, or null when the path
231
+ // is no smb FUSE directory at all. Null is undecided, never a mismatch: an unknown shape
232
+ // opens exactly as before rather than refusing a mount over a format this never learned.
233
+ function fuseSmbParts(path) {
234
+ var m = String(path || "").match(/smb-share:server=([^,\/]+),share=([^,\/]+)(?:,user=([^,\/]+))?/)
235
+ if (!m) return null
236
+ return { host: String(m[1]).toLowerCase(), share: m[2], user: m[3] || "" }
237
+ }
238
+
239
+ // Whether gio info's answer belongs to the smb:// request that asked for it. True for any
240
+ // non-smb URI and whenever either side will not parse; false only on a parsed mismatch, a
241
+ // user the answer drops or changes above all, so that answer is refused as a mount failure.
242
+ function smbFuseMatches(uri, fusePath) {
243
+ if (schemeOf(uri) !== "smb") return true
244
+ var want = smbParts(uri)
245
+ var got = fuseSmbParts(fusePath)
246
+ if (!want || !got) return true
247
+ var wantUser = String(want.user || "")
248
+ var gotUser = String(got.user || "")
249
+ var semi = gotUser.indexOf(";")
250
+ if (semi >= 0) gotUser = gotUser.substring(semi + 1)
251
+ return want.host === got.host
252
+ && String(want.share).toLowerCase() === String(got.share).toLowerCase()
253
+ && wantUser.toLowerCase() === gotUser.toLowerCase()
254
+ }
255
+
256
+ // Sample input: uri smb://dan@192.168.1.75/test with gio info printing the path without
257
+ // ,user=dan while only the user=dan folder exists. Answers the entry whose user the URI
258
+ // names; with no user the single share match wins, several prefer the desktop login.
259
+ function smbResolve(uri, fusePaths, login) {
260
+ var found = { path: "", users: [] }
261
+ var want = smbParts(uri)
262
+ if (!want) return found
263
+ var hits = []
264
+ var list = fusePaths || []
265
+ for (var i = 0; i < list.length; i++) {
266
+ var got = fuseSmbParts(list[i])
267
+ if (!got || got.host !== want.host) continue
268
+ if (String(got.share).toLowerCase() !== String(want.share).toLowerCase()) continue
269
+ hits.push({ path: list[i], user: String(got.user || "") })
270
+ }
271
+ if (String(want.user || "").length > 0) {
272
+ for (var j = 0; j < hits.length; j++) {
273
+ if (hits[j].user.toLowerCase() === String(want.user).toLowerCase()) {
274
+ found.path = hits[j].path
275
+ return found
276
+ }
277
+ }
278
+ return found
279
+ }
280
+ if (hits.length === 1) {
281
+ found.path = hits[0].path
282
+ return found
283
+ }
284
+ if (hits.length > 1) {
285
+ for (var k = 0; k < hits.length; k++) {
286
+ if (hits[k].user.toLowerCase() === String(login || "").toLowerCase()) {
287
+ found.path = hits[k].path
288
+ return found
289
+ }
290
+ }
291
+ for (var m = 0; m < hits.length; m++) {
292
+ if (found.users.indexOf(hits[m].user) < 0) found.users.push(hits[m].user)
293
+ }
294
+ }
295
+ return found
296
+ }