Stop the menu from opening on the previous evaluation's answers (#6601)
* Evaluate menu guards one run at a time A second evaluation starting while one was in flight could not replace it. Process ignores a command change until the next run and `running = true` is a no-op while running, so setting them did nothing -- but clearing `collected` first threw away the lines the running script had already emitted. Its tail then landed as the entire result, and every id missing from it went back to showing, since `when:` only hides a row on an explicit false. That is how Setup > Defaults > Browser ends up listing browsers that are not installed. Queue the evaluation instead and run it once the one in flight lands, the way provider enumeration already waits its turn. * Answer repeated menu guard questions once per evaluation The menu opens on the last evaluation's answers, so however long the guard batch takes is how long a row can contradict the state it describes: stop a recording and Screenrecord still offers to stop it, because the `pgrep` that would hide it is queued behind fifty package lookups. Almost none of that time is the questions, it is asking them one process at a time. The shipped menu runs `omarchy-pkg-present` 54 times and `omarchy-cmd-present` 23, and reads `omarchy-default-browser` once per row in Defaults > Browser. Prepend a prelude that answers all of it inside the one guard process, off a single package listing, bash's own PATH lookup, and one capture per reader command. The captures are eager because `checked:` reads them inside `$()`, where a lazy memo would not outlive the subshell. Takes the shipped batch from 1.49s to 0.25s with identical answers for all 175 guards. * Make the guard prelude answer exactly as the commands it stands in for The prelude only helps if it is indistinguishable from the commands it shadows, and it was not: - `pacman -Q` resolves a name through what installed packages provide, so with gvim installed it reports `vim` as present. A set built from `pacman -Qq` sees only names, so `install.editor.vim` came back and offered to install what was already there. Build the set from provides too, and send version constraints, which no set can answer, to pacman. - `omarchy-cmd-present` uses `command -v`, which finds builtins; `type -P` searches PATH alone and disagreed on every one of them. - Shadowing a reader with a function caught far more than the plain `$(reader)` the rows use: `command -v omarchy-dns` got the function name, and `VAR=x omarchy-channel-current` got an answer captured without the variable. Substitute the captured value into the expression instead and leave every other form to run the real command. - A reader that exits nonzero could take the batch down under a login shell with errexit set. Also keep the results of a batch that was killed rather than finished, since a row whose `when:` went unanswered shows, which is the failure this set of changes exists to remove. Costs 0.25s -> 0.33s against 1.49s before any of this, still with answers identical to evaluating each guard on its own. * Read every provide pacman reports, wrapped or not `pacman -Qi` wraps a long list onto indented continuation lines whenever COLUMNS is set in the environment, which the login shell the batch runs under may well have done. Reading only the line that starts with `Provides` dropped the rest: at COLUMNS=80 that is 537 of 856 provides on this machine, which puts back exactly the "offers to install what is already there" failure the provides lookup was added to prevent. Follow the continuation lines instead. The version-constraint case was also not testing what it claimed. Interpolating the argument into the shadow's script text let `bash>=1` parse as a redirection, so the shadow was handed `bash` and quietly agreed for the wrong reason -- and left an `=1` file behind, which got committed. Pass arguments as argv to both sides, drop the file, and wrap gvim's provides in the stub so the parser is held to the format pacman actually emits.
This commit is contained in:
@@ -372,8 +372,115 @@ function displayRow(items, itemOrder, checkedResults, entry, detail, score, sect
|
||||
}
|
||||
}
|
||||
|
||||
// Commands a `checked:` expression reads a value out of. Every sibling row
|
||||
// asks the same one -- Defaults > Browser has seven rows all comparing
|
||||
// against `omarchy-default-browser` -- so the batch runs it once and the rows
|
||||
// read the captured answer.
|
||||
//
|
||||
// The capture has to be eager. These are read inside `$(...)`, and a value
|
||||
// cached while one expression runs lives in that subshell only, so a lazy
|
||||
// memo never survives to the expression after it.
|
||||
var GUARD_READERS = [
|
||||
"omarchy-channel-current",
|
||||
"omarchy-default-agent",
|
||||
"omarchy-default-browser",
|
||||
"omarchy-default-editor",
|
||||
"omarchy-default-terminal",
|
||||
"omarchy-dns"
|
||||
]
|
||||
|
||||
// Package and command presence account for most of what the guards ask, and
|
||||
// asked one at a time they are almost all fork: the shipped menu spends over
|
||||
// a second on them. Answer them inside the guard process instead. These
|
||||
// shadow the real commands for the batch only, so they have to agree with
|
||||
// them everywhere, including for no arguments at all (present is true of
|
||||
// nothing, missing is not).
|
||||
//
|
||||
// `pacman -Q` resolves a name through what installed packages provide, not
|
||||
// just what they are called -- with gvim installed it reports `vim` as
|
||||
// present -- so the set has to carry provides too, or `install.editor.vim`
|
||||
// comes back and offers to install what is already there. A version
|
||||
// constraint (`bash>=1`) is not a name any set can answer, so it goes to
|
||||
// pacman itself; no shipped guard writes one.
|
||||
//
|
||||
// `pacman -Qi` wraps a long list across continuation lines whenever COLUMNS
|
||||
// is set in the environment, which a login shell may well have done, so the
|
||||
// parser follows the indented lines rather than reading the first one and
|
||||
// dropping half of what is installed.
|
||||
function guardHelpers() {
|
||||
return 'declare -A __omarchy_pkgs=()\n'
|
||||
+ 'mapfile -t __omarchy_pkg_names < <({ pacman -Qq; LC_ALL=C pacman -Qi'
|
||||
+ " | awk '/^[A-Za-z]/ { provides = ($0 ~ /^Provides/); sub(/^[^:]*: /, \"\") }"
|
||||
+ ' provides && $0 != "None" { n = split($0, p, " ");'
|
||||
+ ' for (i = 1; i <= n; i++) { sub(/[<>=].*/, "", p[i]); print p[i] } }\'; } 2>/dev/null)\n'
|
||||
+ 'for __omarchy_pkg in "${__omarchy_pkg_names[@]}"; do __omarchy_pkgs[$__omarchy_pkg]=1; done\n'
|
||||
+ '__omarchy_pkg_has() { [[ -n ${__omarchy_pkgs[$1]-} ]] && return 0; '
|
||||
+ '[[ $1 == *[\\<\\>=]* ]] && { pacman -Q "$1" &>/dev/null; return; }; return 1; }\n'
|
||||
+ 'omarchy-pkg-present() { local p; for p in "$@"; do __omarchy_pkg_has "$p" || return 1; done; return 0; }\n'
|
||||
+ 'omarchy-pkg-missing() { local p; for p in "$@"; do __omarchy_pkg_has "$p" || return 0; done; return 1; }\n'
|
||||
+ 'omarchy-cmd-present() { local c; for c in "$@"; do command -v "$c" &>/dev/null || return 1; done; return 0; }\n'
|
||||
+ 'omarchy-cmd-missing() { local c; for c in "$@"; do command -v "$c" &>/dev/null || return 0; done; return 1; }\n'
|
||||
}
|
||||
|
||||
// Substitute the captured answer into the expression rather than shadowing
|
||||
// the reader with a function. `$(reader)` and the variable holding what it
|
||||
// printed are interchangeable -- both strip trailing newlines, both split the
|
||||
// same way unquoted -- while a function would also catch `command -v reader`,
|
||||
// `VAR=x reader`, and every other form, and answer those wrong. Anything but
|
||||
// the plain substitution is left alone to run the real command.
|
||||
function guardPrelude(guards) {
|
||||
var prelude = guardHelpers()
|
||||
|
||||
for (var i = 0; i < GUARD_READERS.length; i++) {
|
||||
// The guards arrive already substituted, so what marks a reader as wanted
|
||||
// is the slot standing in for it, not the call it replaced.
|
||||
if (guards.indexOf(guardReaderSlot(i)) < 0) continue
|
||||
// `|| :` so a reader that exits nonzero cannot take the batch down with
|
||||
// it under a login shell that turned on errexit.
|
||||
prelude += "__omarchy_read_" + i + "=$(" + GUARD_READERS[i] + " 2>/dev/null) || :\n"
|
||||
}
|
||||
|
||||
return prelude
|
||||
}
|
||||
|
||||
function guardReaderSlot(index) {
|
||||
return "${__omarchy_read_" + index + "}"
|
||||
}
|
||||
|
||||
function substituteGuardReaders(expression) {
|
||||
for (var i = 0; i < GUARD_READERS.length; i++)
|
||||
expression = expression.split("$(" + GUARD_READERS[i] + ")").join(guardReaderSlot(i))
|
||||
|
||||
return expression
|
||||
}
|
||||
|
||||
function guardLine(id, tag, expression) {
|
||||
return "if { " + substituteGuardReaders(expression) + "; } >/dev/null 2>&1; then echo "
|
||||
+ id + ":" + tag + ":1; else echo " + id + ":" + tag + ":0; fi\n"
|
||||
}
|
||||
|
||||
// One bash script for every `when:` and `checked:` in the menu, reporting
|
||||
// `<id>:<w|c>:<0|1>` per line. Speed is the whole point: the menu opens on
|
||||
// the last evaluation's answers, so however long this takes is how long a row
|
||||
// can contradict the state it describes.
|
||||
function guardScript(items) {
|
||||
var guards = ""
|
||||
var ids = Object.keys(items || {})
|
||||
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var entry = items[ids[i]]
|
||||
if (!entry) continue
|
||||
if (entry.when) guards += guardLine(ids[i], "w", entry.when)
|
||||
if (entry.checked) guards += guardLine(ids[i], "c", entry.checked)
|
||||
}
|
||||
|
||||
return guards ? guardPrelude(guards) + guards : ""
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
guardReaders: GUARD_READERS,
|
||||
guardScript: guardScript,
|
||||
stripJsonc: stripJsonc,
|
||||
normalizeAliases: normalizeAliases,
|
||||
normalizeItem: normalizeItem,
|
||||
|
||||
Reference in New Issue
Block a user