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,438 @@
1
+ .pragma library
2
+
3
+ // ui/ViewState.qml's one-writer bookkeeping: saved/inflight/pending are patch bytes, pinned by tests/js/uistate.js.
4
+
5
+ // The window's own read of ui.json: an unreadable document stays as written and says so, while no file at all is a first launch.
6
+ function fromFile(text) {
7
+ try {
8
+ var found = JSON.parse(text)
9
+ if (found && typeof found === "object" && !Array.isArray(found)) {
10
+ return { state: found, unreadable: false }
11
+ }
12
+ } catch (e) {
13
+ // A hand edit this cannot parse, which is the ordinary way in and is not an error here.
14
+ }
15
+ return { state: {}, unreadable: text.length > 0 }
16
+ }
17
+
18
+ // The one sentence for a ui.json that could not be read when favorites were refreshed, said by this module and by ViewState.
19
+ var FAVOURITES_UNREAD = "Favorites could not be refreshed because ui.json could not be read. The old list stays."
20
+
21
+ // External favourites update independently of this window's settings drafts and pending patches.
22
+ function refreshedFavourites(state, text) {
23
+ var read = fromFile(text)
24
+ var places = read.state.places
25
+ if (text.length === 0 || read.unreadable || (places !== undefined && (!places || typeof places !== "object" || Array.isArray(places)))
26
+ || (places && places.favourites !== undefined && !Array.isArray(places.favourites)))
27
+ return { state: state, error: FAVOURITES_UNREAD }
28
+ var records = places && places.favourites || []
29
+ if (JSON.stringify((state.places || {}).favourites || []) === JSON.stringify(records))
30
+ return { state: state, error: "" }
31
+ return { state: withGroup(state, "places", { favourites: records }), error: "" }
32
+ }
33
+
34
+ // Compare a save with this window's intent, because the writer response can include concurrent edits.
35
+ function favouritesAfter(records, operation) {
36
+ var next = records.slice()
37
+ if (operation.op === "add") next.push(operation.record)
38
+ else if (operation.op === "remove") next.splice(operation.index, 1)
39
+ else if (operation.op === "move") next.splice(operation.to, 0, next.splice(operation.index, 1)[0])
40
+ else if (operation.op === "rename") next[operation.index] = Object.assign({}, next[operation.index], { label: operation.label })
41
+ return next
42
+ }
43
+
44
+ // Per-window keys name where a window is rather than how Flea behaves, so no window ever takes them from the file.
45
+ var WINDOW_KEYS = ["view", "pickerView", "columnWidths", "dual", "lastPath", "lastTabs",
46
+ "trashSweptOn", "stateVersion", "sort", "previewFacts"]
47
+
48
+ function isWindowKey(key) {
49
+ return WINDOW_KEYS.indexOf(key) >= 0
50
+ }
51
+
52
+ // Per-window leaves stay per window like the sidebar: a change still writes the file, but open windows keep their own.
53
+ var WINDOW_LEAF_KEYS = ["places.rail", "preview.column", "preview.thumbSize"]
54
+
55
+ function isWindowLeaf(key, leaf) {
56
+ return WINDOW_LEAF_KEYS.indexOf(key + "." + leaf) >= 0
57
+ }
58
+
59
+ // Sample patch: {"columns":["name","bogus"]} is refused whole while {"columns":["name","size"]} retries; only schema-invalid shapes prune.
60
+ function isPatchInvalid(patchText) {
61
+ return invalidKeys(patchText).length > 0
62
+ }
63
+
64
+ // Sample patch: {"columns":["name","bogus"],"density":"normal"} refuses ["columns"] alone, so a valid key beside it is never pruned.
65
+ function invalidKeys(patchText) {
66
+ var refused = []
67
+ var patch = null
68
+ try {
69
+ patch = JSON.parse(patchText)
70
+ } catch (e) {
71
+ return refused
72
+ }
73
+ if (!patch || typeof patch !== "object" || Array.isArray(patch))
74
+ return refused
75
+ if (patch.columns !== undefined) {
76
+ var cols = patch.columns
77
+ var bad = !Array.isArray(cols) || cols.indexOf("name") < 0
78
+ if (!bad) {
79
+ var seen = {}
80
+ var keys = ["name", "mode", "size", "date", "kind"]
81
+ for (var i = 0; i < cols.length; i++) {
82
+ if (keys.indexOf(cols[i]) < 0 || seen[cols[i]]) { bad = true; break }
83
+ }
84
+ }
85
+ if (bad)
86
+ refused.push("columns")
87
+ }
88
+ if (patch.places !== undefined && (patch.places === null || typeof patch.places !== "object" || Array.isArray(patch.places)))
89
+ refused.push("places")
90
+ return refused
91
+ }
92
+
93
+ // True when text parses as a JSON object; empty or garbage is ignored until the next valid write.
94
+ function parsesAsObject(text) {
95
+ try {
96
+ var found = JSON.parse(text)
97
+ return found && typeof found === "object" && !Array.isArray(found)
98
+ } catch (e) {
99
+ return false
100
+ }
101
+ }
102
+
103
+ // A settler's exit and its collected text land in either order, so only a whole answer is ever read.
104
+ function landed(answer, half) {
105
+ return Object.assign({}, answer, half)
106
+ }
107
+
108
+ // An empty text is an answer too: a run that says nothing has still finished saying it.
109
+ function whole(answer) {
110
+ return answer.code !== undefined && answer.text !== undefined
111
+ }
112
+
113
+ // The whole answer a settler that never started is given: no real exit status is negative.
114
+ var NEVER_RAN = { code: -1, text: "" }
115
+
116
+ // A settler that cannot start raises no exited, only running going false, and a real exit lands its status before that false.
117
+ function neverRan(answer, running) {
118
+ return !running && answer.code === undefined
119
+ }
120
+
121
+ // A refused write queues its prune behind a running settle instead of hijacking that settle's mode.
122
+ function pruneAsk(running, failedPatch) {
123
+ return running ? { queue: failedPatch, start: "" } : { queue: "", start: failedPatch }
124
+ }
125
+
126
+ // A settle stays pending until both halves land, so one half landed still counts as running.
127
+ function settleBusy(running, mode, answer) {
128
+ return running || (mode.length > 0 && !whole(answer))
129
+ }
130
+
131
+ // A settle's end spends a queued prune before a dirty re-read, keeping the dirty flag across the prune so the re-read still runs after it.
132
+ function settleNext(dirty, pruneQueued) {
133
+ if (pruneQueued.length > 0)
134
+ return "prune"
135
+ if (dirty)
136
+ return "apply"
137
+ return "idle"
138
+ }
139
+
140
+ // Drops only refused keys a retry can never land, keeping a valid key and a newer owed value the refused writer never carried.
141
+ function dropInvalid(unsaved, inflightText) {
142
+ var refused = invalidKeys(inflightText)
143
+ if (refused.length === 0)
144
+ return unsaved
145
+ var inflight = JSON.parse(inflightText)
146
+ var out = {}
147
+ for (var k in unsaved)
148
+ out[k] = unsaved[k]
149
+ for (var r = 0; r < refused.length; r++) {
150
+ var key = refused[r]
151
+ if (out[key] === undefined)
152
+ continue
153
+ if (isGroup(out[key]) && isGroup(inflight[key])) {
154
+ var kept = {}
155
+ var any = false
156
+ for (var leaf in out[key]) {
157
+ if (inflight[key][leaf] !== undefined
158
+ && JSON.stringify(out[key][leaf]) === JSON.stringify(inflight[key][leaf]))
159
+ continue
160
+ kept[leaf] = out[key][leaf]
161
+ any = true
162
+ }
163
+ if (any)
164
+ out[key] = kept
165
+ else
166
+ delete out[key]
167
+ } else if (JSON.stringify(out[key]) === JSON.stringify(inflight[key])) {
168
+ delete out[key]
169
+ }
170
+ }
171
+ return out
172
+ }
173
+
174
+ // Reverts only refused keys still showing the refused value, so a newer change made behind the refused writer survives.
175
+ function revertedState(state, inflightText, settled) {
176
+ var refused = invalidKeys(inflightText)
177
+ if (refused.length === 0)
178
+ return state
179
+ var inflight = null
180
+ try {
181
+ inflight = JSON.parse(inflightText)
182
+ } catch (e) {
183
+ return state
184
+ }
185
+ if (!inflight || typeof inflight !== "object" || !settled)
186
+ return state
187
+ var out = {}
188
+ for (var s in state)
189
+ out[s] = state[s]
190
+ for (var r = 0; r < refused.length; r++) {
191
+ var key = refused[r]
192
+ if (settled[key] === undefined || inflight[key] === undefined)
193
+ continue
194
+ if (isGroup(inflight[key]) && isGroup(settled[key]) && isGroup(out[key])) {
195
+ var group = {}
196
+ for (var h in out[key])
197
+ group[h] = out[key][h]
198
+ for (var leaf in inflight[key]) {
199
+ if (JSON.stringify(group[leaf]) !== JSON.stringify(inflight[key][leaf]))
200
+ continue
201
+ if (settled[key][leaf] !== undefined)
202
+ group[leaf] = settled[key][leaf]
203
+ else
204
+ delete group[leaf]
205
+ }
206
+ out[key] = group
207
+ } else if (JSON.stringify(out[key]) === JSON.stringify(inflight[key])) {
208
+ out[key] = settled[key]
209
+ }
210
+ }
211
+ return out
212
+ }
213
+
214
+ // A change another window saved: preference keys the file names and this window does not owe take the file's value, never clobbering owed leaves or in-flight writes.
215
+ function applyExternal(state, unsaved, text) {
216
+ var read = fromFile(text)
217
+ if (read.unreadable)
218
+ return { state: state, changed: false }
219
+ var file = read.state
220
+ var out = {}
221
+ for (var s in state)
222
+ out[s] = state[s]
223
+ var changed = false
224
+ for (var key in file) {
225
+ if (isWindowKey(key))
226
+ continue
227
+ var value = file[key]
228
+ if (key === "places" && isGroup(value)) {
229
+ var merged = {}
230
+ for (var f in value) {
231
+ if (f !== "favourites")
232
+ merged[f] = value[f]
233
+ }
234
+ var held = state.places || {}
235
+ if (held.favourites !== undefined)
236
+ merged.favourites = held.favourites
237
+ var owedPlaces = unsaved ? unsaved.places : undefined
238
+ if (isGroup(owedPlaces)) {
239
+ for (var leaf in owedPlaces) {
240
+ if (leaf === "favourites")
241
+ continue
242
+ if (held[leaf] !== undefined)
243
+ merged[leaf] = held[leaf]
244
+ else
245
+ delete merged[leaf]
246
+ }
247
+ }
248
+ if (held.rail !== undefined)
249
+ merged.rail = held.rail
250
+ if (JSON.stringify(merged) !== JSON.stringify(state.places)) {
251
+ out.places = merged
252
+ changed = true
253
+ }
254
+ continue
255
+ }
256
+ var owed = unsaved ? unsaved[key] : undefined
257
+ if (owed !== undefined) {
258
+ if (isGroup(owed) && isGroup(value) && !isWholeKey(key)) {
259
+ var group = {}
260
+ for (var g in value)
261
+ group[g] = value[g]
262
+ var current = state[key] || {}
263
+ for (var o in owed) {
264
+ if (current[o] !== undefined)
265
+ group[o] = current[o]
266
+ else
267
+ delete group[o]
268
+ }
269
+ for (var w in group) {
270
+ if (isWindowLeaf(key, w)) {
271
+ if (current[w] !== undefined)
272
+ group[w] = current[w]
273
+ else
274
+ delete group[w]
275
+ }
276
+ }
277
+ if (JSON.stringify(group) !== JSON.stringify(state[key])) {
278
+ out[key] = group
279
+ changed = true
280
+ }
281
+ }
282
+ continue
283
+ }
284
+ if (isGroup(value) && isGroup(state[key])) {
285
+ var anyLeafWindow = false
286
+ for (var vl in value) {
287
+ if (isWindowLeaf(key, vl)) {
288
+ anyLeafWindow = true
289
+ break
290
+ }
291
+ }
292
+ if (anyLeafWindow) {
293
+ var leafGroup = {}
294
+ for (var vg in value) {
295
+ if (!isWindowLeaf(key, vg))
296
+ leafGroup[vg] = value[vg]
297
+ }
298
+ var heldGroup = state[key] || {}
299
+ for (var hg in heldGroup) {
300
+ if (isWindowLeaf(key, hg))
301
+ leafGroup[hg] = heldGroup[hg]
302
+ }
303
+ if (JSON.stringify(leafGroup) !== JSON.stringify(state[key])) {
304
+ out[key] = leafGroup
305
+ changed = true
306
+ }
307
+ continue
308
+ }
309
+ }
310
+ if (JSON.stringify(value) !== JSON.stringify(state[key])) {
311
+ out[key] = value
312
+ changed = true
313
+ }
314
+ }
315
+ return { state: changed ? out : state, changed: changed }
316
+ }
317
+
318
+ // A copy of the document with one top-level key replaced, and the nested version of the same. QML
319
+ // notifies on assignment and not on a mutation, so every writer rebuilds rather than reaching in;
320
+ // the nested one merges into the group beside it, because a whole-group assignment would take the
321
+ // half a writer holds as the whole of it. ui/ViewState.qml runs both over two documents at once:
322
+ // the state it draws from, and the patch it owes the state file.
323
+ function withKey(state, key, value) {
324
+ var out = {}
325
+ for (var s in state)
326
+ out[s] = state[s]
327
+ out[key] = value
328
+ return out
329
+ }
330
+
331
+ function withGroup(state, key, next) {
332
+ var group = {}
333
+ var held = state[key] || {}
334
+ for (var h in held)
335
+ group[h] = held[h]
336
+ for (var n in next)
337
+ group[n] = next[n]
338
+ return withKey(state, key, group)
339
+ }
340
+
341
+ // The book a window starts with: nothing of its own written yet, and no writer running. Its own read
342
+ // of the file is not a patch it sent, so `saved` starts empty rather than holding what it read.
343
+ function book() {
344
+ return { saved: "", inflight: "", pending: "" }
345
+ }
346
+
347
+ // A change asks for a write. The answer is the next book plus `start`, the patch to launch now.
348
+ function asked(b, patch) {
349
+ // The newest patch this window has landed or has on its way, so asking for exactly those bytes
350
+ // again sends nothing and a refused one is never short-circuited.
351
+ if (patch === (b.pending || b.inflight || b.saved)) {
352
+ return { saved: b.saved, inflight: b.inflight, pending: b.pending, start: "" }
353
+ }
354
+ // One writer at a time, and the newest patch waits rather than being dropped on the floor.
355
+ if (b.inflight.length > 0) {
356
+ return { saved: b.saved, inflight: b.inflight, pending: patch, start: "" }
357
+ }
358
+ return { saved: b.saved, inflight: patch, pending: "", start: patch }
359
+ }
360
+
361
+ // The writer exited. The answer is the next book plus `start`, and `failed` for the pane to report.
362
+ // `owed` is what the window still owes NOW and is what a queued writer launches with, because the
363
+ // bytes waiting in `pending` were built before this writer landed: they still name the settings it
364
+ // just stored, and re-sending one writes this window's own copy of it over whatever another window
365
+ // or the CLI put there in between. A refusal changes nothing, so there `owed` is those same bytes.
366
+ function exited(b, code, owed) {
367
+ // Nothing waiting, or nothing left owed once this writer's own settings came out of it, which a
368
+ // value changed and changed back under one writer produces: an empty patch is a process and a
369
+ // rename spent on a document that would come out byte for byte the same.
370
+ var next = (b.pending.length > 0 && owed !== "{}") ? owed : ""
371
+ return {
372
+ // Only a zero status proves the patch reached the file: src/main.rs exits 2 on a refused
373
+ // patch and on a state directory it could not write, and the change is on screen either way.
374
+ saved: code === 0 ? b.inflight : b.saved,
375
+ inflight: next,
376
+ pending: "",
377
+ start: next,
378
+ failed: code !== 0
379
+ }
380
+ }
381
+
382
+ // What is still owed once the patch a writer landed is taken out of it. A setting is only cleared
383
+ // when what the window owes for it now is what that writer carried: a change made while the writer
384
+ // ran is a newer value for the same setting, and the file does not have that one yet.
385
+ function acknowledged(unsaved, patch) {
386
+ var landed
387
+ try {
388
+ landed = JSON.parse(patch)
389
+ } catch (e) {
390
+ // Bytes this file built itself, so a parse failure clears nothing rather than clearing wrong.
391
+ return unsaved
392
+ }
393
+ if (!landed || typeof landed !== "object" || Array.isArray(landed))
394
+ return unsaved
395
+ var out = {}
396
+ for (var key in unsaved) {
397
+ var still = stillOwed(unsaved[key], landed[key], key)
398
+ if (still !== undefined)
399
+ out[key] = still
400
+ }
401
+ return out
402
+ }
403
+
404
+ // One key of the owed patch against the same key of the landed one: `undefined` when the writer took
405
+ // all of it, and otherwise what is left. Two objects are a settings group and are walked leaf by
406
+ // leaf, because changeLeaf owes the leaf alone and clearing the group would drop a leaf beside it
407
+ // that no writer has taken yet. A map (changeMapEntries) owes entry by entry, which is the same
408
+ // walk. A whole-value object key below is the exception: it is compared and cleared whole, because
409
+ // src/uischema.rs Rule::LastTabs stands or falls together and a half value is refused whole.
410
+ function stillOwed(owed, landed, key) {
411
+ if (landed === undefined)
412
+ return owed
413
+ if (isWholeKey(key) && isGroup(owed) && isGroup(landed))
414
+ return JSON.stringify(owed) === JSON.stringify(landed) ? undefined : owed
415
+ if (isGroup(owed) && isGroup(landed)) {
416
+ var kept = {}
417
+ var any = false
418
+ for (var leaf in owed) {
419
+ if (JSON.stringify(owed[leaf]) === JSON.stringify(landed[leaf]))
420
+ continue
421
+ kept[leaf] = owed[leaf]
422
+ any = true
423
+ }
424
+ return any ? kept : undefined
425
+ }
426
+ return JSON.stringify(owed) === JSON.stringify(landed) ? undefined : owed
427
+ }
428
+
429
+ // A settings group, which is the only shape withGroup builds: an array is a whole key's value.
430
+ function isGroup(value) {
431
+ return value !== null && typeof value === "object" && !Array.isArray(value)
432
+ }
433
+
434
+ // Whole-value object keys: owed and cleared whole, never leaf by leaf. Today only lastTabs, whose
435
+ // src/uischema.rs Rule::LastTabs takes exactly paths plus index together.
436
+ function isWholeKey(key) {
437
+ return key === "lastTabs"
438
+ }
@@ -0,0 +1,132 @@
1
+ .pragma library
2
+
3
+ .import "Format.js" as Format
4
+
5
+ // The updater's row state and every word it shows, pure so tests/js/update.js drives it without a window; ui/UpdateCheck.qml runs the processes.
6
+
7
+ // The background poll's period, which is also how long a finished check stands before Settings > About asks again.
8
+ var INTERVAL_MS = 6 * 60 * 60 * 1000
9
+ // The first background look, a minute after the window opens: off the launch path, and early enough for the menu row.
10
+ var FIRST_CHECK_MS = 60 * 1000
11
+
12
+ // Every word the About row, its note line and the footer use, in one table so a restyle touches nothing else.
13
+ var WORDS = {
14
+ check: "Check",
15
+ checking: "Checking",
16
+ available: "%1 available",
17
+ current: "Up to date · %1",
18
+ failed: "Could not check",
19
+ launched: "Updating in terminal",
20
+ rolling: "flea-git · rolling",
21
+ source: "Built from source",
22
+ noteLaunched: "Restart Flea when the update finishes",
23
+ noteFailed: "Offline: Omarchy update still opens",
24
+ noteRolling: "Rolling build: yay -Sua --devel follows main",
25
+ noteSource: "Built locally: rebuild from the checkout",
26
+ opening: "Opening Omarchy update · restart Flea when it finishes",
27
+ launchFailed: "Omarchy's updater could not be started."
28
+ }
29
+
30
+ // The four words `flea --update check` prints first, see src/update.rs; anything else is a check that failed.
31
+ var ANSWERS = ["available", "current", "failed", "unchecked"]
32
+
33
+ function idle() {
34
+ return { state: "idle", kind: "", installed: "", latest: "", checkedAt: 0 }
35
+ }
36
+
37
+ function copy(status, changes) {
38
+ return Object.assign({}, status, changes)
39
+ }
40
+
41
+ function checking(status) {
42
+ return copy(status, { state: "checking" })
43
+ }
44
+
45
+ // Sample input: "available opr 0.3.2-1 0.3.3-1\n", the one line src/update.rs prints.
46
+ function answered(status, text, now) {
47
+ // A launch is final until Flea restarts: a check that was already running when it began changes nothing.
48
+ if (status.state === "launched")
49
+ return status
50
+ var words = String(text || "").trim().split(" ")
51
+ var known = words.length === 4 && ANSWERS.indexOf(words[0]) >= 0
52
+ return copy(status, { state: known ? words[0] : "failed", kind: known ? words[1] : "",
53
+ installed: known && words[2] !== "-" ? words[2] : "",
54
+ latest: known && words[3] !== "-" ? words[3] : "", checkedAt: now })
55
+ }
56
+
57
+ // A launch hands the machine to Omarchy until Flea restarts; one that failed leaves the row as it was.
58
+ function launchedFrom(status, started) {
59
+ return started ? copy(status, { state: "launched" }) : status
60
+ }
61
+
62
+ // What the About row's Enter does: check, launch Omarchy's updater, or nothing while a check or an update runs.
63
+ function onActivate(status) {
64
+ switch (status.state) {
65
+ case "idle": case "current": return "check"
66
+ case "available": case "failed": return "launch"
67
+ }
68
+ return ""
69
+ }
70
+
71
+ // The chevron: drawn exactly when Enter does something.
72
+ function acts(status) {
73
+ return onActivate(status) !== ""
74
+ }
75
+
76
+ // A rolling or local build has nothing to check or launch, so its row is a fact.
77
+ function isFact(status) {
78
+ return status.state === "unchecked"
79
+ }
80
+
81
+ // About opening asks again unless the switch is off, a check or an update is running, or an answer from the last period stands.
82
+ function due(status, now, automatic) {
83
+ if (!automatic || status.state === "checking" || status.state === "launched")
84
+ return false
85
+ var settled = status.state === "available" || status.state === "current" || status.state === "unchecked"
86
+ return !settled || now - status.checkedAt >= INTERVAL_MS
87
+ }
88
+
89
+ // pkgver alone, unless that is what is installed and only the package release moved.
90
+ function shownVersion(latest, installed) {
91
+ var version = latest.split("-")[0]
92
+ return version === installed.split("-")[0] ? latest : version
93
+ }
94
+
95
+ function value(status) {
96
+ switch (status.state) {
97
+ case "checking": return WORDS.checking
98
+ case "available": return Format.fill(WORDS.available, shownVersion(status.latest, status.installed))
99
+ case "current": return Format.fill(WORDS.current, status.installed.split("-")[0])
100
+ case "failed": return WORDS.failed
101
+ case "launched": return WORDS.launched
102
+ case "unchecked": return status.kind === "git" ? WORDS.rolling : WORDS.source
103
+ }
104
+ return WORDS.check
105
+ }
106
+
107
+ // The value's colour role in ui/SettingsRow.qml: accent for an update, muted while checking, foreground otherwise.
108
+ function role(status) {
109
+ if (status.state === "available")
110
+ return "accent"
111
+ return status.state === "checking" ? "" : "live"
112
+ }
113
+
114
+ // The line under the Updates group, as [words, role], or null in the states that need no explanation.
115
+ function note(status) {
116
+ switch (status.state) {
117
+ case "launched": return [WORDS.noteLaunched, "accent"]
118
+ case "failed": return [WORDS.noteFailed, "foreground"]
119
+ case "unchecked": return [status.kind === "git" ? WORDS.noteRolling : WORDS.noteSource, "foreground"]
120
+ }
121
+ return null
122
+ }
123
+
124
+ // The background menu's row exists only while a newer build is known, and its hint is that version.
125
+ function menuVersion(status) {
126
+ return status.state === "available" ? shownVersion(status.latest, status.installed) : ""
127
+ }
128
+
129
+ // What the footer says after a launch, as [sentence, isError].
130
+ function launchSentence(started) {
131
+ return started ? [WORDS.opening, false] : [WORDS.launchFailed, true]
132
+ }
@@ -0,0 +1,56 @@
1
+ .pragma library
2
+
3
+ // Sample input: XDG_DOWNLOAD_DIR="$HOME/Photo\"Inbox" # local inbox
4
+ function parseLine(line, home) {
5
+ var kv = String(line || "").match(/^[ \t]*XDG_([A-Z]+)_DIR[ \t]*=([\s\S]*)$/)
6
+ if (!kv)
7
+ return null
8
+ var value = kv[2].match(/^[ \t]*"(\$HOME(?=\/|"))?((?:\\[\s\S]|[^"\\])*)"/)
9
+ var path = null
10
+ if (value && (value[1] || value[2].charAt(0) === "/")) {
11
+ path = (value[1] ? home : "") + value[2].replace(/\\([\s\S])/g, "$1")
12
+ path = path.replace(/\/+$/, "")
13
+ if (path.charAt(0) !== "/")
14
+ path = null
15
+ }
16
+ return { role: kv[1], path: path }
17
+ }
18
+
19
+ // Sample input: XDG_DOWNLOAD_DIR="$HOME/Old" then XDG_DOWNLOAD_DIR="$HOME/Inbox"; the last valid path wins.
20
+ function readRoles(body, home) {
21
+ var roles = []
22
+ var paths = {}
23
+ var lines = String(body || "").split("\n")
24
+ for (var i = 0; i < lines.length; i++) {
25
+ var entry = parseLine(lines[i], home)
26
+ if (!entry)
27
+ continue
28
+ if (roles.indexOf(entry.role) < 0)
29
+ roles.push(entry.role)
30
+ if (entry.path)
31
+ paths[entry.role] = entry.path
32
+ }
33
+ var out = []
34
+ for (var r = 0; r < roles.length; r++) {
35
+ if (paths[roles[r]] !== undefined)
36
+ out.push({ role: roles[r], path: paths[roles[r]] })
37
+ }
38
+ return out
39
+ }
40
+
41
+ // Sample input: XDG_DOWNLOAD_DIR="$HOME/Downloads".
42
+ function userDirs(body, home) {
43
+ var out = []
44
+ var entries = readRoles(body, home)
45
+ for (var i = 0; i < entries.length; i++) {
46
+ // A disabled role points at Home, which is not a separate place or a default-sort folder.
47
+ if (entries[i].path !== home)
48
+ out.push({ path: entries[i].path, label: leaf(entries[i].path) })
49
+ }
50
+ return out
51
+ }
52
+
53
+ function leaf(path) {
54
+ var cut = path.lastIndexOf("/")
55
+ return cut < 0 ? path : path.substring(cut + 1)
56
+ }