buninu-browser 0.2.4 → 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.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +102 -2
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +123 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +116 -0
- package/frontends/wayland/input.js +56 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +158 -0
- package/index.js +28 -1
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +57 -0
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +78 -13
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/paint/frame.js +35 -3
- package/lib/paint/raw-frames.js +47 -0
- package/lib/qml/README.md +971 -0
- package/lib/qml/builtins.js +700 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/document.js +1176 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +324 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +517 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +159 -0
- package/lib/qml/runtime.js +3381 -0
- package/lib/qml/shapes.js +243 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +123 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +11 -0
- package/lib/renderer/canvas-2d.js +13 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +25 -33
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/host.js +128 -92
- package/lib/renderer/main.js +38 -4
- package/lib/renderer/page-realm.js +32 -1
- package/lib/renderer/page-renderer.js +250 -21
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +2 -2
- package/package.json +1 -1
- package/test/clipboard.test.js +54 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +73 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/raw-capture.test.js +152 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-urlbar.test.js +38 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -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
|
+
}
|