Search from the launcher, and give the touchpad something to do
Four things a Hyprland desktop can do that this one was not. Searching from the launcher needed no launcher work at all: Vicinae already models it, so this is a script command with one percent-encoded argument. Make it the fallback command and anything typed that matches nothing else offers to search it. Bangs come free -- they are a property of where the query is sent, not of the launcher -- so !yt reaches YouTube without a line of bang parsing. Suggestions could not be a script command. They need a view that reacts as you type, which is an extension: TypeScript, compiled, querying the same endpoint Firefox's address bar uses. It debounces, and aborts the request in flight on every keystroke -- typing is faster than the network, and an older answer landing after a newer one leaves the list describing a query that is no longer on screen. A bang skips suggestions entirely, because Google has no useful guesses about "!yt". The engine is now written down twice, once in each. The contract pins that they agree, since searching from the fallback and searching from the suggestions reaching different places is the kind of wrong that looks fine. Gestures mirror GNOME: three fingers sideways for workspaces, up for the overview, down to dismiss it. Open and close rather than toggle both ways -- toggling means swiping up from an open overview closes it, which is not what the fingers meant. Hyprland reads gesture registrations at startup so they cannot be a setting, but distance and direction can be, and are. Window swallowing is off by default and a preference like every other misc setting here. A terminal that vanishes when you did not ask for it is confusing rather than broken, which is worse. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
This commit is contained in:
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The touchpad gestures, and the window swallowing beside them.
|
||||
#
|
||||
# Both are behaviour that only exists on hardware this machine may not have, so
|
||||
# neither can be checked by running it. What can be pinned is the shape:
|
||||
#
|
||||
# * Three gestures, mirroring GNOME. Sideways moves workspaces, up opens the
|
||||
# overview, down closes it.
|
||||
# * Up and down do not both toggle. That is the obvious way to write it and it
|
||||
# is wrong: swiping up from an open overview would close it, and swiping
|
||||
# down would reopen it, which is the opposite of what the fingers mean.
|
||||
# * Swallowing is off by default and driven by a preference. Turning it on for
|
||||
# everybody would make terminals appear to vanish on a machine nobody asked.
|
||||
# * The swallow regex names only terminals this desktop ships. Anything the
|
||||
# pattern matches can swallow, so a broad pattern is windows disappearing in
|
||||
# cases nobody intended.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
input="$repo_dir/config/dot/hypr/input.lua"
|
||||
looks="$repo_dir/config/dot/hypr/looks.lua"
|
||||
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
# ── Gestures ─────────────────────────────────────────────────────────────────
|
||||
|
||||
gestures="$(grep -c 'hl\.gesture({' "$input" || true)"
|
||||
(( gestures == 3 )) || note "input.lua registers $gestures gestures, not the 3 that mirror GNOME"
|
||||
|
||||
grep -qE 'fingers = 3, direction = "horizontal",[[:space:]]*action = "workspace"' "$input" \
|
||||
|| note 'three fingers sideways does not switch workspaces'
|
||||
grep -qE 'fingers = 3, direction = "up",[[:space:]]*action = overview\("open"\)' "$input" \
|
||||
|| note 'three fingers up does not open the overview'
|
||||
grep -qE 'fingers = 3, direction = "down",[[:space:]]*action = overview\("close"\)' "$input" \
|
||||
|| note 'three fingers down does not close the overview'
|
||||
|
||||
# The specific mistake worth a test of its own.
|
||||
if grep -qE 'direction = "(up|down)",[[:space:]]*action = overview\("toggle"\)' "$input"; then
|
||||
note 'a vertical gesture toggles the overview, so swiping the same way twice undoes itself'
|
||||
fi
|
||||
|
||||
# The tuning that Hyprland does accept at runtime has to be reachable, or the
|
||||
# gestures are unadjustable without editing this file -- which is the thing
|
||||
# Settings exists to avoid.
|
||||
for key in swipeDistance swipeInvert; do
|
||||
grep -q "key: \"$key\"" "$schema" \
|
||||
|| note "$key is not a preference, so the gestures cannot be tuned from Settings"
|
||||
done
|
||||
|
||||
# ── Swallowing ───────────────────────────────────────────────────────────────
|
||||
|
||||
grep -qE 'enable_swallow = prefs\.get\("windowSwallow", false\)' "$looks" \
|
||||
|| note 'window swallowing is not preference-driven and off by default'
|
||||
|
||||
regex_line="$(grep -E '^\s*swallow_regex' "$looks" || true)"
|
||||
[[ -n "$regex_line" ]] || note 'swallowing is enabled with no regex, so nothing can ever swallow'
|
||||
|
||||
# Anchored at both ends: an unanchored pattern matches any class containing the
|
||||
# name, which is a much larger set than the one intended.
|
||||
grep -qE 'swallow_regex\s*=\s*"\^\(.*\)\$"' "$looks" \
|
||||
|| note 'the swallow regex is not anchored, so it matches more window classes than it names'
|
||||
|
||||
for terminal in kitty ghostty; do
|
||||
grep -q "$terminal" <<<"$regex_line" \
|
||||
|| note "the swallow regex does not cover $terminal, which this desktop ships"
|
||||
done
|
||||
|
||||
# Whatever the regex names has to be something the machine will actually have.
|
||||
declared="$(cat "$repo_dir"/setup/packages/* 2>/dev/null | sed 's/#.*//' | tr -d ' ' | grep -v '^$')"
|
||||
for terminal in kitty ghostty; do
|
||||
grep -qix "$terminal" <<<"$declared" \
|
||||
|| note "the swallow regex names $terminal, which no package list installs"
|
||||
done
|
||||
|
||||
# ── Report ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
||||
printf 'gestures contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'gestures contract: PASS\n'
|
||||
@@ -51,6 +51,19 @@ done < <(grep -oE '\{ page: "[a-z-]+"' "$sidebar" | sed 's/.*"\([a-z-]*\)"/\1/')
|
||||
|
||||
(( ${#expected[@]} > 18 )) || fail 'no generated per-page commands were found; run scripts/panama-settings-commands'
|
||||
|
||||
# Commands that do not go through panama-action, and should not.
|
||||
#
|
||||
# Every command above asks the shell to do something, so routing them through
|
||||
# one dispatcher is what keeps that surface small. search-web is a different
|
||||
# animal: it takes a query and opens a browser, and neither half needs the
|
||||
# shell. Sending it through panama-action would mean a web search stops working
|
||||
# when Quickshell is down -- which is exactly when somebody is reaching for the
|
||||
# launcher to look up what went wrong.
|
||||
#
|
||||
# They are still commands, so everything else below applies to them: a title,
|
||||
# a description, search vocabulary, the Panama icon, and closing quietly.
|
||||
declare -a standalone=(search-web)
|
||||
|
||||
# Generated commands must match their source. A stale command dispatches to a
|
||||
# page that has been renamed or removed, and the launcher reports nothing wrong.
|
||||
"$repo_dir/config/dot/quickshell/scripts/panama-settings-commands" --check >/dev/null \
|
||||
@@ -69,11 +82,12 @@ chmod +x "$work/home/.config/quickshell/scripts/panama-action"
|
||||
# none: a shebang and the executable bit already select the interpreter, and an
|
||||
# extension is one more thing that has to stay in sync -- which it did not.
|
||||
mapfile -t actual_files < <(find "$commands_dir" -maxdepth 1 -type f -printf '%f\n' | sort)
|
||||
[[ ${#actual_files[@]} -eq ${#expected[@]} ]] \
|
||||
|| fail "expected ${#expected[@]} commands, found ${#actual_files[@]}"
|
||||
declared=$(( ${#expected[@]} + ${#standalone[@]} ))
|
||||
[[ ${#actual_files[@]} -eq $declared ]] \
|
||||
|| fail "expected $declared commands, found ${#actual_files[@]}"
|
||||
|
||||
declare -A seen_titles=()
|
||||
for script_name in "${!expected[@]}"; do
|
||||
for script_name in "${!expected[@]}" "${standalone[@]}"; do
|
||||
script="$commands_dir/$script_name"
|
||||
[[ -x "$script" ]] || fail "$script_name is missing or not executable"
|
||||
|
||||
@@ -109,6 +123,15 @@ for script_name in "${!expected[@]}"; do
|
||||
|| fail 'health command bypasses the stable dispatcher path'
|
||||
fi
|
||||
|
||||
# A standalone command has nothing to dispatch, and running it would open a
|
||||
# browser at whoever is running the tests.
|
||||
if [[ -z ${expected[$script_name]+x} ]]; then
|
||||
if grep -Fq 'panama-action' "$script"; then
|
||||
fail "$script_name is listed as standalone but goes through panama-action"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
: >"$dispatch_log"
|
||||
HOME="$work/home" PANAMA_COMMAND_TEST_LOG="$dispatch_log" "$script"
|
||||
dispatched="$(cat "$dispatch_log")"
|
||||
@@ -116,4 +139,4 @@ for script_name in "${!expected[@]}"; do
|
||||
|| fail "$script_name dispatched [$dispatched], expected [${expected[$script_name]}]"
|
||||
done
|
||||
|
||||
printf 'Panama commands contract: PASS (%d commands)\n' "${#expected[@]}"
|
||||
printf 'Panama commands contract: PASS (%d commands)\n' "$declared"
|
||||
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Searching the web from the launcher, in both the forms Panama ships it.
|
||||
#
|
||||
# There are two, deliberately: a script command that needs nothing but bash, and
|
||||
# an extension that adds live suggestions but has to be compiled. That is also
|
||||
# the risk this pins -- the search engine is written down twice, and two copies
|
||||
# of a URL drift.
|
||||
#
|
||||
# `vicinae script check` is the validator for the first, and it exits 0 even when
|
||||
# it rejects a file. Checking its exit status would pass on a script Vicinae
|
||||
# refuses to load, so its output is what counts.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
script="$repo_dir/config/local/share/vicinae/scripts/search-web"
|
||||
extension="$repo_dir/config/local/share/vicinae/extensions/panama-search"
|
||||
stage="$repo_dir/setup/scripts/link-vicinae-scripts"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
# ── The script command ───────────────────────────────────────────────────────
|
||||
|
||||
if [[ ! -x "$script" ]]; then
|
||||
note 'search-web is missing or not executable, so Vicinae will not load it'
|
||||
else
|
||||
if command -v vicinae >/dev/null 2>&1; then
|
||||
output="$(vicinae script check "$script" 2>&1)"
|
||||
[[ "$output" == *Error* ]] && note "Vicinae rejects search-web: $output"
|
||||
fi
|
||||
|
||||
# Exactly one argument. Vicinae only offers a no-view command as a fallback
|
||||
# -- type anything, get "Search" at the bottom -- when it takes a single
|
||||
# text argument, which is the whole point of shipping this one.
|
||||
arguments="$(grep -c '@vicinae.argument' "$script" || true)"
|
||||
(( arguments == 1 )) \
|
||||
|| note "search-web declares $arguments arguments; a fallback command takes exactly 1"
|
||||
|
||||
grep -q '"percentEncoded": true' "$script" \
|
||||
|| note 'the argument is not percent-encoded, so a query containing & or # is truncated'
|
||||
|
||||
grep -qE '@vicinae\.mode silent' "$script" \
|
||||
|| note 'search-web is not a silent command, so it would render a view it has nothing to put in'
|
||||
|
||||
# The default browser is a setting this desktop already owns. Naming a
|
||||
# browser here would quietly outrank the Applications page.
|
||||
grep -q 'xdg-open' "$script" \
|
||||
|| note 'search-web does not open through xdg-open, so it ignores the default browser'
|
||||
grep -qE '\b(helium|firefox|chromium|google-chrome)\b' "$script" \
|
||||
&& note 'search-web names a specific browser instead of following the default'
|
||||
fi
|
||||
|
||||
# ── The extension ────────────────────────────────────────────────────────────
|
||||
|
||||
manifest="$extension/package.json"
|
||||
if [[ ! -f "$manifest" ]]; then
|
||||
note 'the panama-search extension has no manifest'
|
||||
else
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq -e . "$manifest" >/dev/null 2>&1 || note 'the extension manifest is not valid JSON'
|
||||
|
||||
for field in name title description commands; do
|
||||
jq -e "has(\"$field\")" "$manifest" >/dev/null 2>&1 \
|
||||
|| note "the extension manifest has no \"$field\", which Vicinae requires"
|
||||
done
|
||||
|
||||
# A command's name is its source file. Getting this wrong builds an
|
||||
# extension with a command that cannot be opened.
|
||||
while read -r command_name; do
|
||||
[[ -n "$command_name" ]] || continue
|
||||
[[ -f "$extension/src/$command_name.tsx" || -f "$extension/src/$command_name.ts" ]] \
|
||||
|| note "the manifest declares command \"$command_name\" with no matching file in src/"
|
||||
done < <(jq -r '.commands[]?.name // empty' "$manifest" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── One engine, written twice ────────────────────────────────────────────────
|
||||
#
|
||||
# The script command and the extension both have to know where a search goes.
|
||||
# Nothing makes them agree, so this does: searching from the fallback and
|
||||
# searching from the suggestions list must not reach different places.
|
||||
|
||||
engine_in_script="$(grep -oE 'https://[^"]+\?q=' "$script" 2>/dev/null | head -1)"
|
||||
engine_in_extension="$(grep -oE 'https://[^"]+\?q=' "$extension/src/search.tsx" 2>/dev/null | head -1)"
|
||||
|
||||
if [[ -z "$engine_in_script" ]]; then
|
||||
note 'no search engine URL found in search-web'
|
||||
elif [[ -z "$engine_in_extension" ]]; then
|
||||
note 'no search engine URL found in the extension'
|
||||
elif [[ "$engine_in_script" != "$engine_in_extension" ]]; then
|
||||
note "the script searches $engine_in_script but the extension searches $engine_in_extension"
|
||||
fi
|
||||
|
||||
# ── Provisioning ─────────────────────────────────────────────────────────────
|
||||
#
|
||||
# An extension is compiled, so unlike a script command it cannot simply be
|
||||
# linked. If nothing builds it, it ships as source nobody can run.
|
||||
|
||||
grep -q 'npm run build' "$stage" \
|
||||
|| note 'no stage builds the Vicinae extensions, so they never reach the launcher'
|
||||
grep -q 'command -v npm' "$stage" \
|
||||
|| note 'the extension build does not check for npm, so a machine without it fails the stage'
|
||||
|
||||
# node_modules is a dependency tree, not configuration.
|
||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
||||
|| note 'the extension node_modules is not gitignored'
|
||||
|
||||
# ── Report ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
||||
printf 'launcher search contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'launcher search contract: PASS\n'
|
||||
Reference in New Issue
Block a user