Merge quattro into update security foundation
This commit is contained in:
commit
56ca654dc8
87 files changed
+923
-1071
No files matched your search
@@ -2,3 +2,5 @@
|
||||
# Python bytecode (orchestrator)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
.claude/
|
||||
@@ -15,10 +15,7 @@ Take a full-screen screenshot without opening the editor:
|
||||
omarchy capture screenshot fullscreen save
|
||||
```
|
||||
|
||||
The command prints the saved path and writes to the configured Pictures
|
||||
directory. Use `omarchy screenshot` for the interactive smart-region flow.
|
||||
Capture reference and candidate states as separate images when changing a
|
||||
layer-shell surface or layout, then compare both.
|
||||
The command writes to Omasnap's configured screenshot directory. Use `omarchy screenshot` for the interactive capture flow, which copies the capture and shows a timed preview. Use `omarchy screenshot --editor=overlay` to test annotation before output. Capture reference and candidate states as separate images when changing a layer-shell surface or layout, then compare both.
|
||||
|
||||
Record a short full-screen video for animation, transition, timing, capture, or
|
||||
screen-recording changes:
|
||||
|
||||
@@ -2,81 +2,22 @@
|
||||
|
||||
# omarchy:summary=Take a screenshot
|
||||
# omarchy:group=capture
|
||||
# omarchy:args=[smart|region|windows|fullscreen] [slurp|copy|save] [--editor=<name>]
|
||||
# omarchy:args=[smart|region|windows|fullscreen|scroll] [copy|save]
|
||||
# omarchy:examples=omarchy screenshot | omarchy capture screenshot region
|
||||
# omarchy:aliases=omarchy screenshot
|
||||
|
||||
[[ -f ~/.config/user-dirs.dirs ]] && source ~/.config/user-dirs.dirs
|
||||
OUTPUT_DIR="${OMARCHY_SCREENSHOT_DIR:-${XDG_PICTURES_DIR:-$HOME/Pictures}}"
|
||||
|
||||
if [[ ! -d $OUTPUT_DIR ]]; then
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
omarchy-notification-send "Created screenshot directory: $OUTPUT_DIR" -t 2000
|
||||
if [[ -n ${OMARCHY_SCREENSHOT_DIR:-} && -z ${OMASNAP_SCREENSHOT_DIR:-} ]]; then
|
||||
export OMASNAP_SCREENSHOT_DIR="$OMARCHY_SCREENSHOT_DIR"
|
||||
fi
|
||||
|
||||
pkill slurp && exit 0
|
||||
|
||||
SCREENSHOT_EDITOR="${OMARCHY_SCREENSHOT_EDITOR:-tensaku-edit}"
|
||||
|
||||
# Parse --editor flag from any position
|
||||
ARGS=()
|
||||
omasnap_args=()
|
||||
for arg in "$@"; do
|
||||
if [[ $arg == --editor=* ]]; then
|
||||
SCREENSHOT_EDITOR="${arg#--editor=}"
|
||||
else
|
||||
ARGS+=("$arg")
|
||||
fi
|
||||
case "$arg" in
|
||||
copy) omasnap_args+=(--copy) ;;
|
||||
save) omasnap_args+=(--save) ;;
|
||||
slurp) ;;
|
||||
*) omasnap_args+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
set -- "${ARGS[@]}"
|
||||
|
||||
MODE="${1:-smart}"
|
||||
PROCESSING="${2:-slurp}"
|
||||
|
||||
# The picker leaves the screen freeze running (PID on its first output line)
|
||||
# so grim captures the frozen overlay rather than live content shifting
|
||||
# during teardown.
|
||||
#
|
||||
# Software-composited cursors (Hyprland's fallback on GPUs without working
|
||||
# hardware cursors) are baked into the frames grim captures, so force
|
||||
# hardware cursors until after grim runs and restore the setting on exit.
|
||||
NO_HW_CURSORS=$(hyprctl getoption cursor:no_hardware_cursors -j | jq '.int')
|
||||
|
||||
set_no_hw_cursors() {
|
||||
hyprctl eval "hl.config({ cursor = { no_hardware_cursors = $1 } })" &>/dev/null ||
|
||||
hyprctl keyword cursor:no_hardware_cursors "$1" &>/dev/null
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
[[ -n $FREEZE_PID ]] && kill $FREEZE_PID 2>/dev/null
|
||||
set_no_hw_cursors "$NO_HW_CURSORS"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
set_no_hw_cursors 0
|
||||
{ read -r FREEZE_PID; read -r SELECTION; } < <(omarchy-capture-region "$MODE" --keep-freeze)
|
||||
|
||||
[[ -z $SELECTION ]] && exit 0
|
||||
|
||||
FILENAME="screenshot-$(date +'%Y-%m-%d_%H-%M-%S').png"
|
||||
FILEPATH="$OUTPUT_DIR/$FILENAME"
|
||||
|
||||
case "$PROCESSING" in
|
||||
slurp)
|
||||
grim -g "$SELECTION" "$FILEPATH" || exit 1
|
||||
echo "$FILEPATH"
|
||||
wl-copy --type image/png <"$FILEPATH"
|
||||
|
||||
# Best-effort: the screenshot is already saved and on the clipboard, so a
|
||||
# notification outage must not report the capture itself as failed.
|
||||
omarchy-notification-send "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" \
|
||||
--image "$FILEPATH" \
|
||||
--exec "$SCREENSHOT_EDITOR" "$FILEPATH" || true
|
||||
;;
|
||||
copy)
|
||||
grim -g "$SELECTION" - | wl-copy --type image/png
|
||||
;;
|
||||
save)
|
||||
grim -g "$SELECTION" "$FILEPATH" || exit 1
|
||||
echo "$FILEPATH"
|
||||
;;
|
||||
esac
|
||||
exec omasnap "${omasnap_args[@]}"
|
||||
@@ -30,7 +30,7 @@ open_image() {
|
||||
local path="$1"
|
||||
|
||||
[[ -r $path ]] || exit 1
|
||||
exec tensaku-edit "$path"
|
||||
exec omasnap "$path"
|
||||
}
|
||||
|
||||
open_text() {
|
||||
|
||||
@@ -12,37 +12,10 @@ if [[ -z $1 ]]; then
|
||||
fi
|
||||
|
||||
install_php() {
|
||||
omarchy-pkg-add php composer php-sqlite xdebug
|
||||
mise tool-alias set php github:nunomaduro/static-php-builds
|
||||
mise use --global php@latest
|
||||
|
||||
# Install Path for Composer
|
||||
if [[ :$PATH: != *:$HOME/.config/composer/vendor/bin:* ]]; then
|
||||
echo 'export PATH="$HOME/.config/composer/vendor/bin:$PATH"' >>"$HOME/.bashrc"
|
||||
source "$HOME/.bashrc"
|
||||
echo "Added Composer global bin directory to PATH."
|
||||
else
|
||||
echo "Composer global bin directory already in PATH."
|
||||
fi
|
||||
|
||||
# Enable some extensions
|
||||
local php_ini_path="/etc/php/php.ini"
|
||||
local extensions_to_enable=(
|
||||
"bcmath"
|
||||
"intl"
|
||||
"iconv"
|
||||
"openssl"
|
||||
"pdo_sqlite"
|
||||
"pdo_mysql"
|
||||
)
|
||||
|
||||
# Enable Xdebug
|
||||
sudo sed -i \
|
||||
-e 's/^;zend_extension=xdebug.so/zend_extension=xdebug.so/' \
|
||||
-e 's/^;xdebug.mode=debug/xdebug.mode=debug/' \
|
||||
/etc/php/conf.d/xdebug.ini
|
||||
|
||||
for ext in "${extensions_to_enable[@]}"; do
|
||||
sudo sed -i "s/^;extension=${ext}/extension=${ext}/" "$php_ini_path"
|
||||
done
|
||||
mise x php -- composer global config bin-dir "$HOME/.local/bin"
|
||||
}
|
||||
|
||||
install_node() {
|
||||
@@ -84,7 +57,7 @@ laravel)
|
||||
echo -e "Installing PHP and Laravel...\n"
|
||||
install_php
|
||||
install_node
|
||||
composer global require laravel/installer
|
||||
mise x php -- composer global require laravel/installer
|
||||
echo -e "\nYou can now run: laravel new myproject"
|
||||
;;
|
||||
symfony)
|
||||
|
||||
@@ -28,7 +28,7 @@ echo "Installing 1Password extension for Chromium..."
|
||||
install_chromium_extension
|
||||
|
||||
echo "Opening 1Password..."
|
||||
uwsm-app -- 1password >/dev/null 2>&1 &
|
||||
setsid uwsm-app -- 1password >/dev/null 2>&1 &
|
||||
|
||||
echo ""
|
||||
echo "1Password has been installed. Restart Chromium to load the browser extension."
|
||||
@@ -22,7 +22,9 @@ if [[ -z $interface ]]; then
|
||||
# menu's visibility gate describe. Fall back to the first connected Wi-Fi
|
||||
# device. nmcli localizes state names, so pin the locale, and the prefix
|
||||
# match accepts states like "connected (externally)".
|
||||
route_device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }')
|
||||
if ! route_device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }'); then
|
||||
route_device=""
|
||||
fi
|
||||
if [[ -n $route_device && -d /sys/class/net/$route_device/wireless ]]; then
|
||||
interface=$route_device
|
||||
else
|
||||
|
||||
@@ -10,6 +10,11 @@ if [[ -z $1 ]]; then
|
||||
fi
|
||||
|
||||
remove_php() {
|
||||
mise uninstall php --all
|
||||
mise rm -g php
|
||||
mise tool-alias unset php
|
||||
|
||||
# PHP from before it moved to mise
|
||||
omarchy-pkg-drop php composer php-sqlite xdebug
|
||||
}
|
||||
|
||||
@@ -46,7 +51,9 @@ php)
|
||||
;;
|
||||
laravel)
|
||||
echo -e "Removing Laravel...\n"
|
||||
composer global remove laravel/installer 2>/dev/null || true
|
||||
mise x php -- composer global remove laravel/installer 2>/dev/null || true
|
||||
|
||||
rm -f "$HOME/.local/bin/laravel" "$HOME/.config/composer/vendor/bin/laravel"
|
||||
;;
|
||||
symfony)
|
||||
echo -e "Removing Symfony CLI...\n"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash -p
|
||||
|
||||
# omarchy:summary=Update Omarchy and system packages
|
||||
# omarchy:alias=omarchy up
|
||||
# omarchy:args=[-y]
|
||||
# omarchy:examples=omarchy update | omarchy update -y
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
+2
-2
@@ -12,5 +12,5 @@
|
||||
# Rotate the currently open image by 90 degrees
|
||||
<Ctrl+r> = exec mogrify -rotate 90 "$imv_current_file"
|
||||
|
||||
# Edit the current image in Tensaku and quit the viewer
|
||||
<Ctrl+e> = exec tensaku-edit "$imv_current_file" & ; quit
|
||||
# Edit the current image in Omasnap and quit the viewer
|
||||
<Ctrl+e> = exec omasnap "$imv_current_file" & ; quit
|
||||
@@ -21,6 +21,9 @@
|
||||
{
|
||||
"id": "omarchy.indicators"
|
||||
},
|
||||
{
|
||||
"id": "omacom.elsewhen"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.clock",
|
||||
"format": "dddd HH:mm",
|
||||
|
||||
@@ -10,12 +10,11 @@ omarchy screenshot # Interactive smart-region flow
|
||||
omarchy capture screenshot region # Select a region
|
||||
omarchy capture screenshot windows # Pick a window
|
||||
omarchy capture screenshot fullscreen save # Full screen, straight to disk (no editor)
|
||||
omarchy capture screenshot scroll # Capture and stitch a scrolling region
|
||||
omarchy screenshot --editor=overlay # Opt into annotation before output
|
||||
```
|
||||
|
||||
The first argument picks the mode (`smart|region|windows|fullscreen`), the
|
||||
second what happens with it (`slurp|copy|save`). `save` skips the annotation
|
||||
editor and prints the saved path. Screenshots land in the configured Pictures
|
||||
directory (override with `OMARCHY_SCREENSHOT_DIR`).
|
||||
The first argument picks the Omasnap mode (`smart|region|windows|fullscreen|scroll`). By default, a capture copies to the clipboard and shows a preview for 10 seconds. Use the preview's Edit action to annotate, or pass `--editor=overlay` or `--editor=window` to edit before output. A second argument of `copy` or `save` skips the preview and sends the screenshot straight to that destination. Saved screenshots land in `~/Pictures/Screenshots` by default (override with `OMASNAP_SCREENSHOT_DIR`; the legacy `OMARCHY_SCREENSHOT_DIR` is also honored by the Omarchy command).
|
||||
|
||||
## Screen Recording
|
||||
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
-- Remove the 1px border around the slurp region selection used by screenshots.
|
||||
-- Remove the 1px border around the slurp region selection used by recordings, OCR, and QR capture.
|
||||
hl.layer_rule({ match = { namespace = "selection" }, no_anim = true, animation = "none" })
|
||||
|
||||
-- Keep the Omasnap overlay immediate and out of screen shares.
|
||||
hl.layer_rule({ match = { namespace = "^omasnap$" }, no_anim = true, animation = "none", no_screen_share = true })
|
||||
@@ -27,8 +27,6 @@ o.window("org.omarchy.about", { float = true })
|
||||
o.window("org.omarchy.about", { center = true })
|
||||
o.window("org.omarchy.about", { size = { 920, 480 } })
|
||||
|
||||
o.window("dev.tensaku.Tensaku", { float = true })
|
||||
o.window("dev.tensaku.Tensaku", { center = true })
|
||||
o.window("omacalc", { float = true })
|
||||
|
||||
-- Fullscreen screensaver.
|
||||
|
||||
@@ -277,8 +277,8 @@
|
||||
"install.development.javascript.node": {"icon":"","label":"Node.js","disabled":"[[ -d $HOME/.local/share/mise/installs/node ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env node'"},
|
||||
"install.development.javascript.bun": {"icon":"","label":"Bun","disabled":"[[ -d $HOME/.local/share/mise/installs/bun ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env bun'"},
|
||||
"install.development.javascript.deno": {"icon":"","label":"Deno","disabled":"[[ -d $HOME/.local/share/mise/installs/deno ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env deno'"},
|
||||
"install.development.php.php": {"icon":"","label":"PHP","disabled":"omarchy-pkg-present php","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env php'"},
|
||||
"install.development.php.laravel": {"icon":"","label":"Laravel","disabled":"[[ -x $HOME/.config/composer/vendor/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env laravel'"},
|
||||
"install.development.php.php": {"icon":"","label":"PHP","disabled":"[[ -d $HOME/.local/share/mise/installs/php ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env php'"},
|
||||
"install.development.php.laravel": {"icon":"","label":"Laravel","disabled":"[[ -x $HOME/.local/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env laravel'"},
|
||||
"install.development.php.symfony": {"icon":"","label":"Symfony","disabled":"omarchy-pkg-present symfony-cli","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env symfony'"},
|
||||
"install.development.elixir.elixir": {"icon":"","label":"Elixir","disabled":"[[ -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env elixir'"},
|
||||
"install.development.elixir.phoenix": {"icon":"","label":"Phoenix","disabled":"compgen -G \"$HOME/.mix/archives/phx_new*\"","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env phoenix'"},
|
||||
@@ -343,8 +343,8 @@
|
||||
"remove.development.javascript.node": {"icon":"","label":"Node.js","when":"[[ -d $HOME/.local/share/mise/installs/node ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env node'"},
|
||||
"remove.development.javascript.bun": {"icon":"","label":"Bun","when":"[[ -d $HOME/.local/share/mise/installs/bun ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env bun'"},
|
||||
"remove.development.javascript.deno": {"icon":"","label":"Deno","when":"[[ -d $HOME/.local/share/mise/installs/deno ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env deno'"},
|
||||
"remove.development.php.php": {"icon":"","label":"PHP","when":"omarchy-pkg-present php","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env php'"},
|
||||
"remove.development.php.laravel": {"icon":"","label":"Laravel","when":"[[ -x $HOME/.config/composer/vendor/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env laravel'"},
|
||||
"remove.development.php.php": {"icon":"","label":"PHP","when":"[[ -d $HOME/.local/share/mise/installs/php ]] || omarchy-pkg-present php","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env php'"},
|
||||
"remove.development.php.laravel": {"icon":"","label":"Laravel","when":"[[ -x $HOME/.local/bin/laravel || -x $HOME/.config/composer/vendor/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env laravel'"},
|
||||
"remove.development.php.symfony": {"icon":"","label":"Symfony","when":"omarchy-pkg-present symfony-cli","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env symfony'"},
|
||||
"remove.development.elixir.elixir": {"icon":"","label":"Elixir","when":"[[ -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env elixir'"},
|
||||
"remove.development.elixir.phoenix": {"icon":"","label":"Phoenix","when":"[[ -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env phoenix'"},
|
||||
|
||||
@@ -10,8 +10,8 @@ export TERMINAL=xdg-terminal-exec
|
||||
# Used by terminal programs to open files with the selected Omarchy default editor
|
||||
export EDITOR="omarchy-launch-editor --inline"
|
||||
|
||||
# Use a custom directory for screenshots (remember to make the directory!)
|
||||
# export OMARCHY_SCREENSHOT_DIR="$HOME/Pictures/Screenshots"
|
||||
# Use a custom directory for screenshots
|
||||
# export OMASNAP_SCREENSHOT_DIR="$HOME/Pictures/Screenshots"
|
||||
|
||||
# Use a custom directory for screenrecordings (remember to make the directory!)
|
||||
# export OMARCHY_SCREENRECORD_DIR="$HOME/Videos/Screencasts"
|
||||
@@ -116,7 +116,6 @@ default/** ──► omarchy-settings /usr/share/omarchy
|
||||
├─ hypr/toggles/*.lua (flags,
|
||||
│ single-window-aspect-ratio, window-no-gaps) /etc/skel/.local/state/omarchy/toggles/hypr/
|
||||
├─ nautilus-python/extensions/*.py /etc/skel/.local/share/nautilus-python/extensions/
|
||||
├─ tensaku/state.toml /etc/skel/.local/state/tensaku/state.toml
|
||||
├─ uwsm/env.d/10-omarchy /usr/share/uwsm/env.d/
|
||||
├─ environment.d/*.conf /usr/lib/environment.d/
|
||||
├─ fontconfig/conf.avail/50-omarchy.conf /usr/share/fontconfig/conf.avail/
|
||||
|
||||
+16
-2
@@ -20,7 +20,17 @@ wait).
|
||||
"author": "You",
|
||||
"description": "A clock that does cool things",
|
||||
"kinds": ["bar-widget"],
|
||||
"entryPoints": { "barWidget": "Widget.qml" }
|
||||
"entryPoints": { "barWidget": "Widget.qml" },
|
||||
"barWidget": {
|
||||
"displayName": "Cool clock",
|
||||
"category": "Time",
|
||||
"allowMultiple": false,
|
||||
"defaultSection": "left",
|
||||
"defaults": { "format": "HH:mm" },
|
||||
"schema": [
|
||||
{ "key": "format", "type": "string", "label": "Format" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -37,7 +47,7 @@ wait).
|
||||
|
||||
Only one full bar option is active at a time. The built-in `omarchy.bar` is
|
||||
used when `bar.id` is omitted or when a selected third-party bar cannot load.
|
||||
Panels, overlays, and menus are loaded when summoned. Plugins can set the top-level manifest key `keepLoaded: true` to survive between summons, and to keep a service mounted across plugin hot-reload (so `omarchy.lock` is not destroyed while Hyprland still holds the session lock). First-party services are loaded at startup.
|
||||
Panels, overlays, and menus are loaded when summoned. Plugins can set the top-level manifest key `keepLoaded: true` to survive between summons, and to keep a service mounted across plugin hot-reload (so `omarchy.lock` is not destroyed while Hyprland still holds the session lock). The kept service instance is not replaced, so changes to its code take effect on a shell restart. First-party services are loaded at startup.
|
||||
|
||||
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose `open(payloadJson)` and `close()` for summon/hide; on load the host injects `omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` / `barWidgetRegistry`) as properties. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades instead: ordinary plugins may look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are kept out of the host's public service map and QML object tree, and third-party registry/configuration snapshots can be changed only locally without mutating host state. The facades are API boundaries, not same-process QML sandboxes: a visual widget shares the host bar's scene and can walk its parent hierarchy to ordinary host objects. Sensitive state must not rely on the facade alone for isolation.
|
||||
|
||||
@@ -88,6 +98,10 @@ bar from the CLI — `use | reset | defaults | position | transparent | put |
|
||||
move | set`, with placement flags such as `--section` and `--index`.
|
||||
The lower-level IPC methods remain available through `omarchy-shell shell ...`.
|
||||
|
||||
## Elsewhen
|
||||
|
||||
Elsewhen (`omacom.elsewhen`) ships in the `elsewhen` package at `/usr/share/omarchy/shell/plugins/omacom.elsewhen`, where the shell discovers it automatically. New installs place it immediately before the clock; the migration uses `omarchy bar put omacom.elsewhen --before omarchy.clock`, which preserves an existing placement and uses Elsewhen's normal right-side placement if the clock is absent. Existing plugin directories and symlinks are left intact. The normal update flow restarts the shell after migrations; the migration does not interrupt plugin loading with an immediate restart.
|
||||
|
||||
## IPC
|
||||
|
||||
The shell exposes a `shell` target (the host also registers
|
||||
|
||||
+6
-10
@@ -52,6 +52,7 @@ directory or an installed Omarchy.
|
||||
Assertions are TAP-flavored and blunt:
|
||||
|
||||
- `pass "description"` prints `ok - description`.
|
||||
- `skip "description"` prints `ok - description # SKIP`. Include why the check could not run. Like `pass`, it returns normally; use `exit 0` afterwards only when the rest of the file cannot run either.
|
||||
- `fail "description" [detail]` prints the optional detail and
|
||||
`not ok - description` to stderr, then **exits the file**. There is no
|
||||
counting or continuing within a file: the first failed assertion ends it,
|
||||
@@ -59,19 +60,13 @@ Assertions are TAP-flavored and blunt:
|
||||
already invalidated.
|
||||
- `require_command <cmd>` fails the file when a needed tool is absent.
|
||||
|
||||
The runner compensates for that early exit: `./test/shell` continues past a
|
||||
failing file and summarizes the failures at the end. Aborting the whole run at
|
||||
the first bad file once let a single packaging failure mask 114 of 134 files.
|
||||
Failure granularity is therefore per file inside a run, per assertion inside a
|
||||
file.
|
||||
The runner compensates for that early exit: `./test/shell` continues past a failing file and summarizes the failures at the end. Aborting the whole run at the first bad file once let a single packaging failure mask 114 of 134 files. Failure granularity is therefore per file inside a run, per assertion inside a file.
|
||||
|
||||
The runner also lists files with skipped checks, including files that ran some checks or later failed. Skips do not fail a run. When no files fail, a run with skips is reported as completed without failures rather than having passed every check.
|
||||
|
||||
## Compositor-dependent tests
|
||||
|
||||
Some tests launch Quickshell or query Hyprland, but the suite must stay green
|
||||
on headless machines. `require_compositor "description"` handles this: when no
|
||||
compositor answers it prints `ok - no Wayland compositor; skipping ...` and
|
||||
exits 0 — a skip is a passing test — and otherwise returns so the file
|
||||
proceeds.
|
||||
Some tests launch Quickshell or query Hyprland, but the suite must stay green on headless machines. `require_compositor "description"` handles this: when no compositor answers it calls `skip` with the reason and exits 0, and otherwise returns so the file proceeds. The skip marker lets the runner distinguish unavailable runtime coverage from checks that passed.
|
||||
|
||||
The probe is more than an environment check, because `WAYLAND_DISPLAY` only
|
||||
proves the variable was inherited. Sandboxes pass the environment through
|
||||
@@ -119,6 +114,7 @@ only a live session can prove.
|
||||
|
||||
## Conventions worth copying
|
||||
|
||||
- **Redirect background output.** Send background fixtures' stdout to a log or `/dev/null` and clean up the processes on exit. An inherited output pipe can hold the runner open after the test exits.
|
||||
- **Stub the world, run the real code.** Tests build a scratch `bin/` of stub
|
||||
executables (`sudo`, `tmux`, `gsettings`, helper commands) that log their
|
||||
arguments to a file, prepend it to `PATH`, and then run the real script
|
||||
|
||||
@@ -26,6 +26,7 @@ docker-compose
|
||||
dosfstools
|
||||
dotnet-runtime
|
||||
dua-cli
|
||||
elsewhen
|
||||
evince
|
||||
exfatprogs
|
||||
expac
|
||||
@@ -95,6 +96,10 @@ omacalc
|
||||
omacut
|
||||
omawrite
|
||||
omarchy-nvim
|
||||
omasnap
|
||||
# OWE owns desktop video backgrounds and feeds the lock screen its frames.
|
||||
owe
|
||||
owe-lockfeed
|
||||
pacman-contrib
|
||||
pamixer
|
||||
pinta
|
||||
@@ -108,12 +113,9 @@ ttfx
|
||||
qemu-user-static-binfmt
|
||||
qrencode
|
||||
qt6-imageformats
|
||||
qt6-multimedia
|
||||
qt6-multimedia-ffmpeg
|
||||
quickshell
|
||||
ripgrep
|
||||
ruby
|
||||
tensaku
|
||||
sddm
|
||||
slurp
|
||||
socat
|
||||
|
||||
@@ -14,8 +14,11 @@ set -euo pipefail
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now \
|
||||
bt-agent.service \
|
||||
owed.service \
|
||||
omarchy-recover-internal-monitor.service \
|
||||
omarchy-sleep-lock.service \
|
||||
omarchy-migrate-notify.service \
|
||||
omarchy-fcitx5.service \
|
||||
omarchy-crash-watch.service
|
||||
|
||||
omarchy-hook-install theme-set /usr/share/owe/10-owe-sync
|
||||
@@ -30,6 +30,11 @@ if [[ -n $NODE_PACKAGE_DIR ]]; then
|
||||
mkdir -p "$NODE_INSTALL_DIR"
|
||||
tar -xzf "$NODE_TARBALL" --strip-components=1 -C "$NODE_INSTALL_DIR"
|
||||
mise use -g node@"$NODE_VERSION"
|
||||
|
||||
# That pinned the exact bundled version, which would exempt Node from
|
||||
# mise up forever. Loosen it to latest, like an online install gets:
|
||||
# mise resolves latest to the installed version while offline.
|
||||
mise config set tools.node latest --file "$HOME/.config/mise/config.toml"
|
||||
fi
|
||||
else
|
||||
mise use -g node@latest
|
||||
|
||||
@@ -13,13 +13,15 @@ Everything you can grab off the screen hangs off the Print Screen key. One key o
|
||||
|
||||
## Screenshots
|
||||
|
||||
Hit `Print Screen` and the screen freezes so nothing shifts under you while you aim. Drag a box for a freeform region, or just click once and the shot snaps to whatever rectangle you clicked in — a window if you landed on one, the whole monitor if you landed on the bar or in a gap. Changed your mind? Hit `Print Screen` again to dismiss the picker.
|
||||
Hit `Print Screen` and Omasnap captures the focused monitor before its overlay appears, so nothing shifts under you while you aim. Drag a freeform region, click a window to capture it, or click open space to capture the whole monitor. Press `S` before drawing to capture a scrolling region. Changed your mind? Hit `Print Screen` again to dismiss Omasnap.
|
||||
|
||||
The result goes two places at once: a PNG in your pictures directory, and the clipboard, so you can paste it straight into a chat window with `Super + V`. A notification pops up with a thumbnail. Click it (or hit `Super + Alt + ,` to invoke the last notification) and the shot opens in Tensaku, the annotation editor, where you can draw arrows and boxes on it before you send it.
|
||||
After you select an area, Omasnap copies the capture to the clipboard and shows a preview for 10 seconds. Use the preview's pin button or `Ctrl + P` to keep it on screen, or choose Edit to annotate it.
|
||||
|
||||
Files land in `~/Pictures` by default, named `screenshot-2026-08-13_14-22-05.png`. If you'd rather keep them in their own folder, set `OMARCHY_SCREENSHOT_DIR` — see [the FAQ](46-faq.md) for where to put session environment variables. Omarchy creates the directory for you if it isn't there. You can swap the editor too with `OMARCHY_SCREENSHOT_EDITOR`.
|
||||
The annotation editor can draw arrows, lines, shapes, highlights, numbered markers, text, and secure redactions; crop or cut out part of the image; OCR its text; and add a backdrop. In the editor, press `Enter` to copy and save the finished PNG, `Ctrl + C` to copy it without saving, or `Ctrl + S` to save it without copying.
|
||||
|
||||
From the terminal, `omarchy screenshot` takes the same shot, and you can be explicit about it: `omarchy capture screenshot region` for freeform only, `windows` to snap to window and monitor rectangles, or `fullscreen` to skip the picker entirely and grab the focused monitor. A second argument of `copy` puts the shot only on the clipboard, and `save` only on disk.
|
||||
Saved screenshots land in `~/Pictures/Screenshots` by default, with a name such as `screenshot-2026-08-13_14-22-05-firefox.png`. Set `OMASNAP_SCREENSHOT_DIR` to use another directory — see [the FAQ](46-faq.md) for where to put session environment variables. Omasnap creates the directory when it saves the first shot.
|
||||
|
||||
From the terminal, `omarchy screenshot` opens the same overlay, and you can choose its starting mode: `omarchy capture screenshot region`, `windows`, `fullscreen`, or `scroll`. A second argument of `copy` or `save` skips the preview and sends the shot straight to that destination. To edit before output, use `omarchy screenshot --editor=overlay` for a fullscreen editor or `omarchy screenshot --editor=window` for a separate window.
|
||||
|
||||
### Driving the picker from the keyboard
|
||||
|
||||
@@ -27,16 +29,18 @@ While the selection is up, you don't have to use the mouse at all:
|
||||
|
||||
| Key | Function |
|
||||
| --- | -------- |
|
||||
| `S` | Toggle scrolling-region mode |
|
||||
| `Super + Arrow keys` | Move among windows in Window mode |
|
||||
| `Return` | Capture the highlighted window |
|
||||
| `Ctrl + Return` | Capture the whole screen |
|
||||
| `Tab` / `Ctrl + Tab` | Highlight the next / previous window |
|
||||
| Arrow keys | Highlight the window in that direction |
|
||||
| `Ctrl + A` | Capture the full focused monitor |
|
||||
| `R` | Restore the last region drawn in this session |
|
||||
| `Esc` | Dismiss Omasnap |
|
||||
|
||||
The arrows and Tab move the cursor to the window they pick, so the highlight follows along and you can see what you're about to capture. These bindings only exist while a selection is on screen, so they can't collide with anything in your own config.
|
||||
These bindings only exist while Omasnap is open, so they can't collide with anything in your own config.
|
||||
|
||||
## Screen recording
|
||||
|
||||
`Alt + Print Screen` opens _Trigger > Capture > Screenrecord_, which asks what you want on the soundtrack: no audio, desktop audio, desktop plus microphone, or desktop plus microphone plus webcam. That last one only shows up if you actually have a camera plugged in. Pick one and you get the same picker as a screenshot: drag a region, or click a window or monitor.
|
||||
`Alt + Print Screen` opens _Trigger > Capture > Screenrecord_, which asks what you want on the soundtrack: no audio, desktop audio, desktop plus microphone, or desktop plus microphone plus webcam. That last one only shows up if you actually have a camera plugged in. Pick one and Omarchy's recording picker lets you drag a region or click a window or monitor.
|
||||
|
||||
Recording runs on gpu-screen-recorder, which encodes on the GPU at 60fps and falls back to the CPU if it has to. The result is an MP4 in `~/Videos`, named `screenrecording-2026-08-13_14-22-05.mp4`. Set `OMARCHY_SCREENRECORD_DIR` to change that — but note that unlike the screenshot directory, this one has to exist already, or the recording refuses to start.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Capture commands — Screenshots and screen recording:
|
||||
omarchy capture qr Decode a QR code from a screenshot region
|
||||
omarchy capture screenrecording [--fullscreen] [--with-desktop-audio] [--with-microphone-audio] [--with-webcam] [--webcam-device=<device>] [--webcam-size=<small|medium|large>] [--resolution=<size>] [--stop-recording] Start or stop screen recording
|
||||
omarchy capture screenrecording with webcam Pick a webcam and start a screen recording with it
|
||||
omarchy capture screenshot [smart|region|windows|fullscreen] [slurp|copy|save] [--editor=<name>] Take a screenshot
|
||||
omarchy capture screenshot [smart|region|windows|fullscreen|scroll] [copy|save] Take a screenshot
|
||||
omarchy capture text Extract text from a screenshot region with OCR
|
||||
omarchy capture webcam resize <smaller|larger|reset|small|medium|large> Resize the active webcam recording overlay
|
||||
```
|
||||
|
||||
@@ -95,7 +95,7 @@ omarchy plugin validate ./my-plugin
|
||||
|
||||
That runs the same checks the shell does at load time: the schema version, the required fields, an id that isn't reserved, entry points that are safe relative paths and actually exist, an entry point for every kind you claimed, and no symlinks anywhere inside the folder.
|
||||
|
||||
For the full picture, the source is the documentation: `shell/README.md` in the Omarchy repo covers the manifest schema, the shell's IPC contract, and the exact shape of `shell.json`, and `shell/plugins/README.md` lists every first-party plugin with its id, kinds, and entry points.
|
||||
For plugin development, see the [shell reference](https://github.com/omacom/omarchy/blob/quattro/docs/omarchy-shell.md) and [first-party plugin catalog](https://github.com/omacom/omarchy/blob/quattro/shell/plugins/README.md).
|
||||
|
||||
## Sharing yours with the world
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Every theme ships with its own set of backgrounds, and you can add extras of you
|
||||
|
||||
You can do this most easily by going to _Install > Style > Background_ in the Omarchy Menu. That'll bring up the folder where the backgrounds for that theme is stored. Hit `Super + Shift + F` to start another file manager, find your background, copy it over. Now it'll be included in the choices of backgrounds you can select between using `Super + Ctrl + Space`.
|
||||
|
||||
Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images, playing on a loop. Only your first monitor's wallpaper plays a video's sound track, through the default audio output at the system volume, and the lock screen stays silent. Playback stops on its own whenever nothing can see it — while a fullscreen window covers that monitor, while the screensaver is up, and once a locked screen has gone dark — but a video wallpaper still costs far more power than a still one, and each monitor decodes its own copy.
|
||||
Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images. Videos are played by the OWE wallpaper engine. It decodes the video once for all monitors and plays its sound through the default audio output, and it stops playback whenever nothing can see it. The lock screen draws the same decode, muted, through OWE. A video wallpaper still costs far more power than a still one.
|
||||
|
||||
You can find a huge collection of cool curated backgrounds on https://github.com/dharmx/walls.
|
||||
|
||||
Video backgrounds keep a cached still on the lock screen while playback is paused or unavailable. Animated GIFs play on the desktop and show a still frame on the lock screen.
|
||||
+3
-3
@@ -54,15 +54,15 @@ Automatic discovery, where printers on the network appear without being added, i
|
||||
|
||||
### How do I change where screenshots or screenrecordings are saved?
|
||||
|
||||
If you want screenshots to be saved to `~/Pictures/Screenshots` instead of just `~/Pictures`, you can add this to a file under `~/.config/uwsm/env.d/` (like `~/.config/uwsm/env.d/capture`):
|
||||
Saved screenshots go to `~/Pictures/Screenshots` by default. To use another directory, add this to a file under `~/.config/uwsm/env.d/` (like `~/.config/uwsm/env.d/capture`):
|
||||
|
||||
```
|
||||
export OMARCHY_SCREENSHOT_DIR="$HOME/Pictures/Screenshots"
|
||||
export OMASNAP_SCREENSHOT_DIR="$HOME/Pictures/Captures"
|
||||
```
|
||||
|
||||
You can do the same for screenrecordings using `OMARCHY_SCREENRECORD_DIR`.
|
||||
|
||||
Just remember to create the directory you want to save to and restart Omarchy for this to take effect.
|
||||
Omasnap creates its screenshot directory automatically. Create the screenrecording directory yourself, then restart Omarchy for either environment change to take effect.
|
||||
|
||||
### How do I get the speakers + webcam working on my Apple Studio Display?
|
||||
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
echo "Install native video wallpaper playback dependencies"
|
||||
|
||||
omarchy-pkg-add qt6-multimedia qt6-multimedia-ffmpeg
|
||||
echo "Native video dependencies are superseded by the OWE migration"
|
||||
@@ -0,0 +1,18 @@
|
||||
echo "Replace Satty and Tensaku with Omasnap"
|
||||
|
||||
omarchy-pkg-add omasnap
|
||||
|
||||
# The old source installer left a NoDisplay entry that overrides the packaged launcher.
|
||||
rm -f "$HOME/.local/share/applications/omasnap.desktop"
|
||||
|
||||
imv_config="$HOME/.config/imv/config"
|
||||
if [[ -f $imv_config ]]; then
|
||||
sed -i --follow-symlinks \
|
||||
-e 's/^# Edit the current image in Tensaku and quit the viewer$/# Edit the current image in Omasnap and quit the viewer/' \
|
||||
-e 's/^# Edit the current image in Satty and quit the viewer$/# Edit the current image in Omasnap and quit the viewer/' \
|
||||
-e 's|^<Ctrl+e> = exec tensaku-edit "$imv_current_file" & ; quit$|<Ctrl+e> = exec omasnap "$imv_current_file" \& ; quit|' \
|
||||
-e 's|^<Ctrl+e> = exec satty --filename "$imv_current_file" & ; quit$|<Ctrl+e> = exec omasnap "$imv_current_file" \& ; quit|' \
|
||||
"$imv_config"
|
||||
fi
|
||||
|
||||
omarchy-pkg-drop satty tensaku
|
||||
@@ -0,0 +1,14 @@
|
||||
echo "Install Elsewhen, the world clock plugin"
|
||||
|
||||
omarchy-pkg-add elsewhen
|
||||
|
||||
# Dev checkouts do not contain plugins installed by system packages.
|
||||
packaged_plugin="/usr/share/omarchy/shell/plugins/omacom.elsewhen"
|
||||
user_plugin="$HOME/.config/omarchy/plugins/omacom.elsewhen"
|
||||
if [[ ! $OMARCHY_PATH -ef /usr/share/omarchy && -d $packaged_plugin && ! -e $user_plugin && ! -L $user_plugin ]]; then
|
||||
mkdir -p "${user_plugin%/*}"
|
||||
ln -s "$packaged_plugin" "$user_plugin"
|
||||
fi
|
||||
|
||||
omarchy-shell shell rescanPlugins
|
||||
omarchy-bar put omacom.elsewhen --before omarchy.clock
|
||||
@@ -0,0 +1,17 @@
|
||||
echo "Enable OWE desktop video backgrounds and lock feed"
|
||||
|
||||
omarchy-pkg-add owe owe-lockfeed
|
||||
omarchy-hook-install theme-set /usr/share/owe/10-owe-sync
|
||||
|
||||
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||
if ! systemctl --user enable owed.service; then
|
||||
wants_dir="$HOME/.config/systemd/user/graphical-session.target.wants"
|
||||
mkdir -p "$wants_dir"
|
||||
ln -sfn /usr/lib/systemd/user/owed.service "$wants_dir/owed.service"
|
||||
fi
|
||||
|
||||
# A TTY update enables the next graphical login without starting a renderer
|
||||
# against a missing Wayland session. A failed live start leaves this pending.
|
||||
if [[ ${OMARCHY_UPGRADE_TO_QUATTRO_LIVE:-0} != 1 ]] && systemctl --user is-active --quiet graphical-session.target; then
|
||||
systemctl --user start owed.service
|
||||
fi
|
||||
@@ -61,7 +61,7 @@ QtObject {
|
||||
// Run an argv vector without a shell interpreting it: the constant `exec "$@"`
|
||||
// means the args only ever land in positional parameters, which bash expands
|
||||
// without re-tokenizing — so untrusted data ($(id), a filename) stays literal.
|
||||
// The login shell (-l) keeps the PATH/session env GUI targets (tensaku, mpv,
|
||||
// The login shell (-l) keeps the PATH/session env GUI targets (omasnap, mpv,
|
||||
// xdg-open) need. Prefer this over execDetached for anything built from input.
|
||||
function execArgv(argv) {
|
||||
Quickshell.execDetached(["bash", "-lc", 'exec "$@"', "bash"].concat(argv))
|
||||
|
||||
+6
-299
@@ -1,302 +1,9 @@
|
||||
# Omarchy shell
|
||||
|
||||
`omarchy-shell` is a single long-running [Quickshell](https://quickshell.org/)
|
||||
instance that hosts the Omarchy desktop. Hyprland autostart launches one shell
|
||||
per graphical session; everything else — the bar, background switcher, panels,
|
||||
and overlays — runs **inside** the shell as a plugin.
|
||||
The Omarchy desktop runs in one long-lived Quickshell process. Its bar, panels, overlays, menus, and services are plugins hosted by `shell.qml`.
|
||||
|
||||
Hosting everything inside one shell means:
|
||||
|
||||
- shared services and singletons live once, not once per process
|
||||
- summoning a panel is an IPC call into a process that is already running,
|
||||
not a fresh `quickshell -p ...` cold start
|
||||
- third-party plugins can be loaded from disk without changing any source
|
||||
code in Omarchy itself
|
||||
|
||||
The runtime layout:
|
||||
|
||||
```
|
||||
shell/
|
||||
shell.qml entry point (ShellRoot)
|
||||
services/
|
||||
PluginRegistry.qml discovers, validates plugins, looks up enabled state in shell.json
|
||||
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
|
||||
plugins/
|
||||
bar/ first-party plugins (see plugins/README.md)
|
||||
image-picker/
|
||||
menu/
|
||||
notifications/
|
||||
panels/
|
||||
audio/
|
||||
bluetooth/
|
||||
monitor/
|
||||
network/
|
||||
power/
|
||||
weather/
|
||||
agents/
|
||||
services/
|
||||
battery/
|
||||
idle/
|
||||
osd/
|
||||
polkit/
|
||||
```
|
||||
|
||||
The plugin discovery path is documented in [plugins/README.md](plugins/README.md).
|
||||
|
||||
## Plugin manifest
|
||||
|
||||
Every plugin ships a `manifest.json` describing what it is and how the
|
||||
shell should load it. Minimal example:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "my.org.cool-clock",
|
||||
"name": "Cool clock",
|
||||
"version": "1.0.0",
|
||||
"author": "You",
|
||||
"description": "A clock that does cool things",
|
||||
"kinds": ["bar-widget"],
|
||||
"entryPoints": { "barWidget": "Widget.qml" },
|
||||
"barWidget": {
|
||||
"displayName": "Cool clock",
|
||||
"category": "Time",
|
||||
"allowMultiple": false,
|
||||
"defaultSection": "left",
|
||||
"defaults": { "format": "HH:mm" },
|
||||
"schema": [
|
||||
{ "key": "format", "type": "string", "label": "Format" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported `kinds`:
|
||||
|
||||
| Kind | What it is |
|
||||
|--------------|--------------------------------------------------------------|
|
||||
| `bar-widget` | A component that the active bar can drop into a section |
|
||||
| `panel` | A persistent or summoned floating window (e.g. OSD) |
|
||||
| `overlay` | A fullscreen overlay (e.g. background switcher) |
|
||||
| `menu` | A summoned menu surface |
|
||||
| `service` | A headless singleton, no UI |
|
||||
| `bar` | A full bar option that can replace the built-in `omarchy.bar` |
|
||||
|
||||
Only one `bar` plugin is active at a time. Missing or invalid selections fall
|
||||
back to the built-in `omarchy.bar`, so users always have a safe path home.
|
||||
Panels, overlays, and menus are loaded when summoned. Plugins that need
|
||||
to outlive a single summon can set `keepLoaded: true` (e.g. the image
|
||||
picker keeps its overlay window mounted between summons). The same flag
|
||||
keeps a service mounted across plugin hot-reload, so tearing down a
|
||||
changed bar widget cannot destroy `omarchy.lock` while Hyprland still
|
||||
holds the session lock. The kept instance is not replaced, so code
|
||||
changes to a `keepLoaded` service itself only take effect on a shell
|
||||
restart. First-party services are loaded at startup.
|
||||
|
||||
Entry points may declare `omarchyPath`, `shell`, `manifest`, `pluginRegistry`, and `barWidgetRegistry` properties for host injection. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades: ordinary plugins can look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are retained outside the host's public service map and QML object tree, and changing a third-party registry or configuration snapshot cannot mutate host state. Facades do not isolate visual widgets from the parent hierarchy of the shared QML scene, so sensitive state must remain outside that reachable graph.
|
||||
|
||||
Widgets rendered by a third-party replacement bar receive a service-less entry facade with target-scoped lifecycle and settings operations. Their live service objects are available only when the trusted built-in bar hosts them; otherwise the replacement bar could request and retain any configured widget's service.
|
||||
|
||||
The full schema lives in `services/PluginRegistry.qml`.
|
||||
|
||||
## Installing a third-party plugin
|
||||
|
||||
A plugin is a **git repo** with a `manifest.json` at its root. Adding one
|
||||
clones it straight into `~/.config/omarchy/plugins/<id>/` (named by the
|
||||
manifest id); updating is a fast-forward pull of that checkout.
|
||||
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git
|
||||
omarchy plugin update acme.weather # fetches, shows a diff, fast-forwards
|
||||
omarchy plugin update # updates every git-managed plugin
|
||||
omarchy plugin remove acme.weather
|
||||
```
|
||||
|
||||
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns you before cloning, plugins land disabled so you can review the code before enabling, and updates show a diff of the changes before touching anything. The scoped QML interfaces remove direct authentication-service and generic replacement-bar service lookups, but visual plugins still share and can traverse the ordinary host scene. Only add repos whose code you are willing to run.
|
||||
|
||||
Each command is **interactive** when run bare in a terminal (gum pickers,
|
||||
confirmation, a diff to review) and fully **non-interactive** when given
|
||||
arguments. Pass `--yes` to skip every prompt — this is the path for scripts and
|
||||
AI agents:
|
||||
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable --yes
|
||||
omarchy plugin update --yes
|
||||
```
|
||||
|
||||
The installer never runs plugin code, install hooks, or sudo — it only clones
|
||||
files, validates the manifest, and toggles enabled state over shell IPC. Since
|
||||
an installed plugin is a plain git checkout, anything beyond add/update
|
||||
(pinning a ref, switching branches) is ordinary git in the plugin directory.
|
||||
|
||||
### Installing by hand
|
||||
|
||||
You can still drop a plugin in without git:
|
||||
|
||||
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
|
||||
plus the QML referenced from its `entryPoints`.
|
||||
2. `omarchy-shell shell rescanPlugins`.
|
||||
3. `omarchy plugin enable <id>`. Bar widgets start in
|
||||
`barWidget.defaultSection`, or in the center when it is omitted, and can be
|
||||
moved with `omarchy bar move`; a full bar replaces the one in use.
|
||||
|
||||
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
|
||||
`omarchy-shell shell enablePlugin <id> '{}'`, and `omarchy-shell shell listPlugins`.
|
||||
The `omarchy plugin` commands wrap those calls. `omarchy bar move` and
|
||||
`omarchy bar set` edit the persisted widget layout in `shell.json`.
|
||||
|
||||
To hack on a built-in plugin safely, clone it into user config instead of
|
||||
editing the built-in source. The complete plugin directory is copied, including
|
||||
every declared kind and local dependency. A built-in id such as
|
||||
`omarchy.clock` becomes `<username>.clock` (e.g. `dhh.clock`), with `My Clock`
|
||||
as its display name. The username prefix keeps shared clones from colliding
|
||||
with each other or with other plugin authors.
|
||||
|
||||
```bash
|
||||
omarchy plugin clone omarchy.clock
|
||||
```
|
||||
|
||||
Cloning switches from the built-in to the new personal plugin, preserving an
|
||||
existing bar widget's position and settings. Setup > Plugins > Clone provides
|
||||
the interactive picker, then opens the new `<username>.*` directory in `$EDITOR`.
|
||||
Existing shortcuts and shell IPC calls made to the built-in id are routed to
|
||||
the enabled clone, so cloning does not require changing its callers. Removing
|
||||
an active clone switches back to its built-in source.
|
||||
Saving a file anywhere under `~/.config/omarchy/plugins/` reloads plugin code
|
||||
automatically; `omarchy-shell shell rescanPlugins` remains available to force a reload.
|
||||
|
||||
First-party plugins under `shell/plugins/` are discovered the same way and load
|
||||
by default. Disabling a non-widget records it in `disabledPlugins[]`; disabling
|
||||
a widget removes it from the bar layout while leaving its component available
|
||||
to add again. A full bar has no off state and is replaced by enabling another.
|
||||
|
||||
## IPC contract
|
||||
|
||||
The shell exposes a single `shell` IPC target plus whatever extra targets
|
||||
individual plugins register (e.g. the bar's `bar` target for refresh
|
||||
hooks, the image picker's `image-selector` target). `omarchy-menu` uses the
|
||||
shell target to summon the first-party `omarchy.menu` plugin instead of
|
||||
running a separate Quickshell instance.
|
||||
|
||||
| Method | Returns | Effect |
|
||||
|------------------------------------------|---------|-------------------------------------------------------|
|
||||
| `ping` | `ok` | health check |
|
||||
| `summon <id> <payloadJson>` | `ok` / `unknown` | load + open a panel/overlay plugin |
|
||||
| `hide <id>` | — | close a previously-summoned plugin |
|
||||
| `toggle <id> <payloadJson>` | — | summon if closed, hide if open |
|
||||
| `call <id> <method> <arg>` | string | call a method on an already-loaded plugin |
|
||||
| `rescanPlugins` | — | re-walk plugin dirs and hot-reload plugin code |
|
||||
| `reloadConfig` | `ok` | reload `~/.config/omarchy/shell.json` |
|
||||
| `setPluginEnabled <id> <enabled>` | `ok` / `unknown` | flip the persisted enabled bit (see note) |
|
||||
| `listPlugins` | JSON | every discovered plugin, sorted by name |
|
||||
|
||||
Direct invocation:
|
||||
|
||||
```
|
||||
quickshell ipc -p $OMARCHY_PATH/shell call shell ping
|
||||
```
|
||||
|
||||
Hyprland autostart launches the shell directly with `quickshell -p
|
||||
$OMARCHY_PATH/shell`. Use `omarchy-restart-shell` to stop every running
|
||||
instance of that config and launch one fresh shell process.
|
||||
|
||||
A convenience wrapper, [`omarchy-shell`](../bin/omarchy-shell), forwards IPC
|
||||
calls to the running shell. It does not start the shell.
|
||||
|
||||
```
|
||||
omarchy-shell shell ping
|
||||
omarchy-shell shell toggle omarchy.menu '{"menu":"root"}'
|
||||
omarchy-shell shell listPlugins
|
||||
omarchy-shell shell rescanPlugins
|
||||
```
|
||||
|
||||
**Note on `setPluginEnabled`:** the `enabled` argument is a string. Only the
|
||||
literal `"true"` enables the plugin; every other value (including `"True"`,
|
||||
`"1"`, `"yes"`, or omitted) disables it. This keeps the IPC surface
|
||||
type-stable across QML's `string`-only IPC arguments.
|
||||
|
||||
## Persisted state
|
||||
|
||||
There is one user config file. Everything that distinguishes your
|
||||
customization from the shipped defaults lives in it.
|
||||
|
||||
| Path | Owner | Purpose |
|
||||
|-----------------------------------|----------------|--------------------------------------------------------|
|
||||
| `~/.config/omarchy/shell.json` | the shell | full layout + per-entry settings + enabled plugin list |
|
||||
| `~/.config/omarchy/plugins/<id>/` | user | drop-in third-party plugin source files |
|
||||
|
||||
The `config/omarchy/shell.json` default config describes the
|
||||
fresh-install state. When the user has no `shell.json`, the shell uses
|
||||
the defaults verbatim. Once the user customizes anything, `shell.json`
|
||||
becomes the authoritative file — we do **not** deep-merge defaults back in.
|
||||
|
||||
### shell.json shape
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"idle": {
|
||||
"screensaver": 150,
|
||||
"lock": 300
|
||||
},
|
||||
"bar": {
|
||||
"id": "omarchy.bar",
|
||||
"position": "top",
|
||||
"transparent": false,
|
||||
"centerAnchor": "omarchy.clock",
|
||||
"layout": {
|
||||
"left": [ { "id": "omarchy.menu" }, { "id": "omarchy.workspaces" } ],
|
||||
"center": [ { "id": "omarchy.clock", "format": "HH:mm" } ],
|
||||
"right": [
|
||||
{ "id": "omarchy.audio" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": []
|
||||
}
|
||||
```
|
||||
|
||||
### Storage rules
|
||||
|
||||
1. **The active bar option is `bar.id`.** Omit it or set it to `omarchy.bar`
|
||||
to use the built-in bar. Set it to another plugin id whose manifest declares
|
||||
`kind: "bar"` to replace the full bar.
|
||||
2. **Every plugin instance is one entry.** Either in `bar.layout.<section>`
|
||||
for bar widgets, or in `plugins[]` for panels, overlays, services,
|
||||
menus, and anything else non-bar.
|
||||
3. **Settings are inline on the entry.** No `config:` sub-object, no
|
||||
separate per-plugin settings file, no merge layers. The fields on each
|
||||
entry are the values the plugin sees.
|
||||
4. **Built-in widget ids are namespaced.** Use ids such as `omarchy.clock`,
|
||||
`omarchy.audio`, and `omarchy.network`. The migration rewrites older ids
|
||||
like `Clock` and `AudioPanel` forward.
|
||||
5. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff
|
||||
its id appears somewhere in shell.json. For full bar options, that means
|
||||
`bar.id`; for bar widgets, plugin enable/disable adds/removes layout entries;
|
||||
other plugin kinds are enabled the same way. First-party non-bar plugins
|
||||
are enabled unless listed in `disabledPlugins[]`.
|
||||
6. **Multiple instances** are allowed when a manifest sets
|
||||
`allowMultiple: true`. Each instance is independent — e.g. two clock
|
||||
widgets in different timezones are just two `{"id":"omarchy.clock", "timezone": ...}`
|
||||
entries with their own values.
|
||||
7. **Idle timings are top-level.** `idle.screensaver` and `idle.lock`
|
||||
are seconds since user idle began, so the default lock fires at 300s
|
||||
even if the 150s screensaver starts first.
|
||||
8. **`version: 1` is required** at the top level. The shell will fall back
|
||||
to defaults rather than load an unknown version.
|
||||
|
||||
## Implementation history
|
||||
|
||||
Built up in phases on this branch:
|
||||
|
||||
- Phase 1 — `omarchy-shell phase 1: host the existing bar in a single shell`
|
||||
- Phase 2 — `omarchy-shell phase 2: plugin registry and bar widget registry`
|
||||
- Phase 3 — `omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin`
|
||||
- Phase 4 — `omarchy-shell phase 4: absorb background-switcher as a plugin`
|
||||
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs`
|
||||
- Phase 6 — `omarchy-shell phase 6: reviewer cleanup (path traversal, collision, races)`
|
||||
- Phase 7 — `omarchy-shell phase 7: replace socket with IpcHandler, rename to image-picker`
|
||||
- Phase 8a — `omarchy-shell phase 8a: unified shell.json with inline plugin settings`
|
||||
|
||||
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
|
||||
out of scope here and deferred to a follow-up after a review pass.
|
||||
- [Shell reference](../docs/omarchy-shell.md) — plugin manifests, IPC, configuration, and shared UI contracts.
|
||||
- [Plugin user guide](../manual/32-shell-plugins.md) — install, configure, and clone plugins.
|
||||
- [First-party plugins](plugins/README.md) — bundled plugin IDs, kinds, and entry points.
|
||||
- [Shell development guide](../agents/skills/shell-dev.md) — conventions for changing the desktop.
|
||||
- [Theming reference](../docs/theming.md) — theme tokens, templates, and generated configuration.
|
||||
@@ -1,29 +1,19 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
|
||||
// The shell draws stills only. OWE owns video backgrounds on the desktop, and
|
||||
// it feeds the lock screen through its own socket.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string path: ""
|
||||
property int version: 0
|
||||
property bool playbackEnabled: true
|
||||
property bool audioEnabled: false
|
||||
// Bumped when the file behind an unchanged path may have been replaced.
|
||||
// Images cache-bust through version; a video is rebuilt, since FFmpeg
|
||||
// would read a query as part of the filename.
|
||||
property int reloads: 0
|
||||
property bool reloading: false
|
||||
readonly property var current: video ? videoLoader.item : imageLoader.item
|
||||
readonly property var current: imageLoader.item
|
||||
readonly property bool ready: current ? current.ready : false
|
||||
readonly property bool video: Util.isVideoPath(path)
|
||||
// Cache-bust images selected in a running lock session. FFmpeg treats the
|
||||
// query as part of a local filename, so videos must keep their plain URL.
|
||||
// Each URL is empty for the other kind, so a switch never hands the still
|
||||
// loader a video, or the player a still, in the moment before it unloads.
|
||||
// Both test the path directly: going through `video` lets a URL evaluate
|
||||
// against the stale flag and leak the wrong file for one pass.
|
||||
readonly property url imageUrl: path && !Util.isVideoPath(path) ? Util.fileUrl(path) + (version ? "?v=" + version : "") : ""
|
||||
readonly property url videoUrl: path && Util.isVideoPath(path) ? Util.fileUrl(path) : ""
|
||||
// Cache-bust images selected in a running lock session, so a theme switch
|
||||
// that replaces the file behind an unchanged path shows the new pixels.
|
||||
readonly property url imageUrl: path && !video ? Util.fileUrl(path) + (version ? "?v=" + version : "") : ""
|
||||
|
||||
Loader {
|
||||
id: imageLoader
|
||||
@@ -32,45 +22,6 @@ Item {
|
||||
sourceComponent: imageComponent
|
||||
}
|
||||
|
||||
// Loaded by URL rather than from a Component here, so QtMultimedia and its
|
||||
// audio dependency closure never map into a session that only shows images.
|
||||
Loader {
|
||||
id: videoLoader
|
||||
anchors.fill: parent
|
||||
active: root.path !== "" && root.video && !root.reloading
|
||||
source: "BackgroundVideo.qml"
|
||||
}
|
||||
|
||||
onReloadsChanged: {
|
||||
if (!video) return
|
||||
reloading = true
|
||||
Qt.callLater(function() { root.reloading = false })
|
||||
}
|
||||
|
||||
// A player on its way out keeps its source: pushing an empty one starts a
|
||||
// load of nothing that its destructor then cancels, which FFmpeg logs.
|
||||
Binding {
|
||||
target: videoLoader.item
|
||||
property: "mediaSource"
|
||||
value: root.videoUrl
|
||||
when: videoLoader.item !== null && Util.isVideoPath(root.path)
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: videoLoader.item
|
||||
property: "playbackEnabled"
|
||||
value: root.playbackEnabled
|
||||
when: videoLoader.item !== null
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: videoLoader.item
|
||||
property: "audioEnabled"
|
||||
value: root.audioEnabled
|
||||
when: videoLoader.item !== null
|
||||
}
|
||||
|
||||
Component {
|
||||
id: imageComponent
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
|
||||
// Deliberately a bare MediaPlayer and VideoOutput rather than the Video
|
||||
// convenience type: Video always builds an AudioOutput, and a muted sink still
|
||||
// decodes the audio stream and opens an audio client on every output.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property url mediaSource: ""
|
||||
property bool playbackEnabled: true
|
||||
property bool audioEnabled: false
|
||||
property int mediaGeneration: 0
|
||||
property bool priming: false
|
||||
property int primingGeneration: -1
|
||||
property bool frameReceived: false
|
||||
readonly property bool ready: player.hasVideo
|
||||
|
||||
onMediaSourceChanged: {
|
||||
mediaGeneration += 1
|
||||
priming = false
|
||||
primingGeneration = -1
|
||||
frameReceived = false
|
||||
primePauseTimer.stop()
|
||||
framePauseTimer.stop()
|
||||
output.clearOutput()
|
||||
}
|
||||
|
||||
onPlaybackEnabledChanged: {
|
||||
priming = false
|
||||
frameReceived = false
|
||||
primePauseTimer.stop()
|
||||
framePauseTimer.stop()
|
||||
if (playbackEnabled) player.play()
|
||||
else player.pause()
|
||||
}
|
||||
|
||||
function pauseAfterPrimedFrame() {
|
||||
if (!priming
|
||||
|| root.playbackEnabled
|
||||
|| primingGeneration !== root.mediaGeneration) return
|
||||
|
||||
priming = false
|
||||
primePauseTimer.stop()
|
||||
framePauseTimer.stop()
|
||||
player.pause()
|
||||
}
|
||||
|
||||
// A paused MediaPlayer can load a source without presenting its first frame.
|
||||
// Prime it until VideoOutput receives a frame, with a timeout so a stalled
|
||||
// decoder cannot keep running indefinitely on battery.
|
||||
Timer {
|
||||
id: primePauseTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
|
||||
onTriggered: root.pauseAfterPrimedFrame()
|
||||
}
|
||||
|
||||
// Receiving a frame means the decoder has produced it, but the scene graph
|
||||
// may not have committed it yet. Give VideoOutput a render cycle before
|
||||
// pausing so the first frame is not lost on a source switch.
|
||||
Timer {
|
||||
id: framePauseTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
|
||||
onTriggered: root.pauseAfterPrimedFrame()
|
||||
}
|
||||
|
||||
VideoOutput {
|
||||
id: output
|
||||
anchors.fill: parent
|
||||
fillMode: VideoOutput.PreserveAspectCrop
|
||||
}
|
||||
|
||||
// Sound is opted into per output: with a player per monitor, every output
|
||||
// playing the track would layer copies of it. The sink is only built once
|
||||
// the media reports a sound track, so a silent file never opens an audio
|
||||
// client or its threads. Priming a paused player must not be heard.
|
||||
Loader {
|
||||
id: audioLoader
|
||||
active: root.audioEnabled && player.hasAudio
|
||||
sourceComponent: AudioOutput {
|
||||
muted: root.priming || !root.playbackEnabled
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: player
|
||||
source: root.mediaSource
|
||||
videoOutput: output
|
||||
audioOutput: audioLoader.item
|
||||
loops: MediaPlayer.Infinite
|
||||
autoPlay: root.playbackEnabled
|
||||
onMediaStatusChanged: {
|
||||
if (mediaStatus !== MediaPlayer.LoadedMedia) return
|
||||
|
||||
if (!root.playbackEnabled) {
|
||||
root.priming = true
|
||||
root.primingGeneration = root.mediaGeneration
|
||||
root.frameReceived = false
|
||||
primePauseTimer.restart()
|
||||
}
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: output.videoSink
|
||||
function onVideoFrameChanged() {
|
||||
if (player.mediaStatus !== MediaPlayer.BufferedMedia) return
|
||||
if (!root.priming
|
||||
|| root.playbackEnabled
|
||||
|| root.primingGeneration !== root.mediaGeneration
|
||||
|| root.frameReceived) return
|
||||
|
||||
root.frameReceived = true
|
||||
framePauseTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ BarIndicator 1.0 BarIndicator.qml
|
||||
BarIconButton 1.0 BarIconButton.qml
|
||||
BarWidget 1.0 BarWidget.qml
|
||||
BackgroundMedia 1.0 BackgroundMedia.qml
|
||||
BackgroundVideo 1.0 BackgroundVideo.qml
|
||||
BorderOverlay 1.0 BorderOverlay.qml
|
||||
BorderSurface 1.0 BorderSurface.qml
|
||||
Button 1.0 Button.qml
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
@@ -17,7 +16,6 @@ Item {
|
||||
|
||||
property string currentBackground: ""
|
||||
property string displayedBackground: ""
|
||||
property int displayedReloads: 0
|
||||
property string incomingBackground: ""
|
||||
property string oldBackground: ""
|
||||
property bool finishingTransition: false
|
||||
@@ -28,23 +26,6 @@ Item {
|
||||
property string pendingShellRaw: ""
|
||||
property real revealProgress: 1
|
||||
|
||||
// Injected by the first-party service loader; used to reach the lock and idle
|
||||
// services so playback can stop whenever nothing can see the wallpaper.
|
||||
property var shell: null
|
||||
|
||||
// Stop a video wallpaper's decoding whenever it is covered. Qt's FFmpeg
|
||||
// engine drives its own clock, so an unseen player keeps decoding until it
|
||||
// is told not to — a locked laptop would otherwise decode until it died.
|
||||
readonly property var lockService: shell && shell.services ? shell.firstPartyServiceFor("omarchy.lock") : null
|
||||
readonly property var idleService: shell && shell.services ? shell.firstPartyServiceFor("omarchy.idle") : null
|
||||
readonly property var batteryService: shell && shell.services ? shell.firstPartyServiceFor("omarchy.battery") : null
|
||||
readonly property bool lockActive: lockService ? lockService.locked : false
|
||||
readonly property bool screensaverActive: idleService ? idleService.screensaverWindowCount > 0 : false
|
||||
readonly property bool powerSaverActive: batteryService ? batteryService.powerSaverOnBattery : false
|
||||
// A lock or a screensaver covers every output, so it is decided once here.
|
||||
// Fullscreen is decided per output below, because it only covers its own.
|
||||
readonly property bool sessionObscured: lockActive || screensaverActive
|
||||
|
||||
function isVideo(path) {
|
||||
return Util.isVideoPath(path)
|
||||
}
|
||||
@@ -78,9 +59,6 @@ Item {
|
||||
if (instant || !displayedBackground || isVideo(path) || isVideo(displayedBackground)) {
|
||||
oldBackground = ""
|
||||
incomingBackground = ""
|
||||
// A theme switch can replace the file behind an unchanged path, which
|
||||
// an unchanged property would never pick up.
|
||||
if (displayedBackground === finalPath) displayedReloads += 1
|
||||
displayedBackground = finalPath
|
||||
revealProgress = 1
|
||||
return
|
||||
@@ -225,23 +203,9 @@ Item {
|
||||
// Keep render updates enabled. The background layer has been observed to
|
||||
// lose its committed buffer while parked with updatesEnabled=false,
|
||||
// leaving a black desktop until omarchy-shell is restarted. A still
|
||||
// wallpaper costs nothing to keep enabled, and a video one is throttled
|
||||
// by pausing playback rather than by parking the layer.
|
||||
// wallpaper costs nothing to keep enabled. OWE manages video layers.
|
||||
updatesEnabled: true
|
||||
|
||||
// Pausing every wallpaper for one fullscreen window would freeze the one
|
||||
// still on show next to it, which costs a viewer more than it saves. The
|
||||
// workspace on show here knows whether a fullscreen window covers it,
|
||||
// wherever focus happens to be.
|
||||
readonly property var hyprlandMonitor: Hyprland.monitorFor(modelData)
|
||||
readonly property var visibleWorkspace: hyprlandMonitor ? hyprlandMonitor.activeWorkspace : null
|
||||
readonly property bool fullscreenHere: visibleWorkspace ? visibleWorkspace.hasFullscreen : false
|
||||
|
||||
// A sound track plays from one output only, or every monitor would
|
||||
// layer its own copy of it.
|
||||
readonly property bool firstScreen: Quickshell.screens.length > 0
|
||||
&& String(Quickshell.screens[0].name || "") === String(modelData.name || "")
|
||||
|
||||
property bool maskReady: false
|
||||
|
||||
function maybeStartReveal() {
|
||||
@@ -259,13 +223,12 @@ Item {
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// OWE owns video backgrounds. This layer draws stills, and stays empty
|
||||
// behind a video so OWE's own layer shows through.
|
||||
BackgroundMedia {
|
||||
id: base
|
||||
anchors.fill: parent
|
||||
path: root.displayedBackground
|
||||
reloads: root.displayedReloads
|
||||
playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere
|
||||
audioEnabled: panel.firstScreen
|
||||
onReadyChanged: {
|
||||
if (ready && root.finishingTransition) {
|
||||
root.incomingBackground = ""
|
||||
|
||||
@@ -14,7 +14,7 @@ the shell for its whole session.
|
||||
|
||||
## Customizing
|
||||
|
||||
The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`config/omarchy/shell.json`](../../../config/omarchy/shell.json). Once you customize anything via the bar gestures, `omarchy bar ...`, or by editing shell.json directly, your file is canonical — there is no deep-merge.
|
||||
The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../../docs/omarchy-shell.md#shelljson). Out of the box the shell uses [`config/omarchy/shell.json`](../../../config/omarchy/shell.json). Once you customize anything via the bar gestures, `omarchy bar ...`, or by editing shell.json directly, your file is canonical — there is no deep-merge.
|
||||
|
||||
The bar is configured directly on the bar itself: drag empty bar space (or click-and-hold) to move the bar to another screen edge, double-left-click empty center-bar space to toggle transparency, and drag widgets to reorder them. The `omarchy bar position`, `omarchy bar transparent`, `omarchy bar move`, and `omarchy bar set` commands do the same from scripts. Enable or disable widgets with `omarchy plugin enable` and `omarchy plugin disable` (widget ids come from `omarchy plugin list`).
|
||||
|
||||
@@ -172,9 +172,4 @@ richer popup plugins live in feature directories such as `../panels/audio/`,
|
||||
`manifest.json`. Bar layout ids are namespaced, e.g. `omarchy.audio`,
|
||||
`omarchy.network`, and `omarchy.clock`.
|
||||
|
||||
Third-party widgets ship as separate plugins under
|
||||
`~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json`
|
||||
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
|
||||
[../../README.md](../../README.md) for the manifest schema. Rescan, enable,
|
||||
and place third-party plugins with `omarchy-shell shell rescanPlugins`,
|
||||
`omarchy plugin enable`, and `omarchy bar move`.
|
||||
Third-party widgets ship as separate plugins under `~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json` declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See the [shell reference](../../../docs/omarchy-shell.md#plugin-manifest) for the manifest schema. Rescan, enable, and place third-party plugins with `omarchy-shell shell rescanPlugins`, `omarchy plugin enable`, and `omarchy bar move`.
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
import Owe.LockFeed
|
||||
|
||||
// Loaded through a Loader from LockView, so a system without the module shows
|
||||
// no lock video instead of losing the whole lock screen.
|
||||
LockFeed {
|
||||
}
|
||||
@@ -7,6 +7,7 @@ Item {
|
||||
id: root
|
||||
|
||||
property string backgroundPath: ""
|
||||
property string videoPosterPath: ""
|
||||
property int backgroundVersion: 0
|
||||
property bool fingerprintConfigured: false
|
||||
property bool authenticatingPassword: false
|
||||
@@ -43,6 +44,9 @@ Item {
|
||||
? Border.surfaceSpec("lock", "border-error", Color.lock.borderError, root.outlineThickness, "border-alpha")
|
||||
: Border.surfaceSpec("lock", "border-active", Color.lock.borderActive, root.outlineThickness, "border-alpha")
|
||||
|
||||
readonly property bool video: Util.isVideoPath(root.backgroundPath)
|
||||
readonly property bool feedActive: root.video && root.loadBackground && !root.displaysBlank && !root.powerSaverActive
|
||||
|
||||
signal submitPassword(string password)
|
||||
signal passwordTextEdited(string password)
|
||||
signal clearFailureRequested()
|
||||
@@ -88,16 +92,15 @@ Item {
|
||||
|
||||
BackgroundMedia {
|
||||
id: wallpaper
|
||||
objectName: "lockWallpaper"
|
||||
anchors.fill: parent
|
||||
path: root.loadBackground ? root.backgroundPath : ""
|
||||
path: root.loadBackground ? (root.video ? root.videoPosterPath : root.backgroundPath) : ""
|
||||
version: root.backgroundVersion
|
||||
playbackEnabled: root.loadBackground && !root.displaysBlank && !root.powerSaverActive
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: wallpaper
|
||||
source: wallpaper.video ? null : wallpaper
|
||||
visible: !wallpaper.video
|
||||
source: wallpaper
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: root.loadBackground && wallpaper.ready
|
||||
blur: 1.0
|
||||
@@ -106,11 +109,22 @@ Item {
|
||||
contrast: -0.08
|
||||
}
|
||||
|
||||
// Qt's video output cannot be sampled by MultiEffect on every renderer.
|
||||
// The cached poster stays behind the feed when policy pauses playback,
|
||||
// the module is unavailable, or a new connection has not received a frame.
|
||||
Loader {
|
||||
id: feedLoader
|
||||
objectName: "lockFeedLoader"
|
||||
anchors.fill: parent
|
||||
active: root.feedActive
|
||||
source: "LockFeedSurface.qml"
|
||||
visible: status === Loader.Ready
|
||||
}
|
||||
|
||||
// The feed item cannot be sampled by MultiEffect on every renderer.
|
||||
// Keep video wallpapers visible and darken them slightly for legibility.
|
||||
Rectangle {
|
||||
anchors.fill: wallpaper
|
||||
visible: wallpaper.video
|
||||
anchors.fill: feedLoader
|
||||
visible: root.video
|
||||
color: "#22000000"
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ Item {
|
||||
property string failureMessage: ""
|
||||
property int failedAttempts: 0
|
||||
property string backgroundPath: ""
|
||||
property string videoPosterPath: ""
|
||||
property int backgroundVersion: 0
|
||||
property string lastEvent: "init"
|
||||
property string lastEventAt: ""
|
||||
@@ -113,6 +114,16 @@ Item {
|
||||
if (!readlinkProc.running) readlinkProc.running = true
|
||||
}
|
||||
|
||||
function refreshPoster() {
|
||||
if (!root.videoBackground) {
|
||||
root.videoPosterPath = ""
|
||||
return
|
||||
}
|
||||
if (posterProc.running) return
|
||||
posterProc.sourcePath = root.backgroundPath
|
||||
posterProc.running = true
|
||||
}
|
||||
|
||||
function refreshFingerprintStatus() {
|
||||
if (!fingerprintCheckProc.running) fingerprintCheckProc.running = true
|
||||
}
|
||||
@@ -307,6 +318,7 @@ Item {
|
||||
id: lockView
|
||||
anchors.fill: parent
|
||||
backgroundPath: root.backgroundPath
|
||||
videoPosterPath: root.videoPosterPath
|
||||
backgroundVersion: root.backgroundVersion
|
||||
fingerprintConfigured: root.fingerprintConfigured
|
||||
authenticatingPassword: root.authenticatingPassword
|
||||
@@ -339,6 +351,7 @@ Item {
|
||||
LockView {
|
||||
anchors.fill: parent
|
||||
backgroundPath: root.backgroundPath
|
||||
videoPosterPath: root.videoPosterPath
|
||||
backgroundVersion: root.backgroundVersion
|
||||
fingerprintConfigured: root.fingerprintConfigured
|
||||
authenticatingPassword: false
|
||||
@@ -409,9 +422,25 @@ Item {
|
||||
onStreamFinished: {
|
||||
var next = String(text || "").trim()
|
||||
if (next !== root.backgroundPath) {
|
||||
root.videoPosterPath = ""
|
||||
root.backgroundPath = next
|
||||
root.backgroundVersion += 1
|
||||
}
|
||||
root.refreshPoster()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: posterProc
|
||||
property string sourcePath: ""
|
||||
command: ["bash", Quickshell.env("OMARCHY_PATH") + "/shell/plugins/lock/poster.sh", sourcePath]
|
||||
stdout: StdioCollector { id: posterOutput; waitForEnd: true }
|
||||
onExited: function(exitCode) {
|
||||
if (sourcePath !== root.backgroundPath) {
|
||||
root.refreshPoster()
|
||||
} else {
|
||||
root.videoPosterPath = exitCode === 0 ? String(posterOutput.text || "").trim() : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# One cached frame shared by all lock outputs, including when OWE is paused.
|
||||
set -euo pipefail
|
||||
|
||||
source_path=$1
|
||||
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/lock-poster"
|
||||
signature=$(stat -Lc '%s:%y:%z' "$source_path")
|
||||
key=$(printf '%s\n%s' "$source_path" "$signature" | sha256sum | cut -d ' ' -f 1)
|
||||
poster="$cache_dir/poster-$key.jpg"
|
||||
mkdir -p "$cache_dir"
|
||||
exec {lock_fd}>"$cache_dir/.lock"
|
||||
flock -w 10 "$lock_fd"
|
||||
|
||||
if [[ ! -s $poster ]]; then
|
||||
temporary=$(mktemp "$cache_dir/.poster-XXXXXX.jpg")
|
||||
trap 'rm -f "$temporary"' EXIT
|
||||
timeout -k 1 5 ffmpegthumbnailer -i "$source_path" -o "$temporary" -s 1920 -q 8 {lock_fd}>&-
|
||||
[[ -s $temporary ]]
|
||||
mv -f "$temporary" "$poster"
|
||||
find "$cache_dir" -maxdepth 1 -type f -name 'poster-*.jpg' ! -name "poster-$key.jpg" -delete
|
||||
fi
|
||||
|
||||
printf '%s\n' "$poster"
|
||||
+17
-2
@@ -21,16 +21,31 @@ fi
|
||||
# Keep going after a failing file. A test file exits at its first failed
|
||||
# assertion, so aborting the run there too would hide every file after it --
|
||||
# one packaging failure was masking 114 of 134 files.
|
||||
output=$(mktemp)
|
||||
trap 'rm -f "$output"' EXIT
|
||||
failed=()
|
||||
skipped=()
|
||||
for test in "${tests[@]}"; do
|
||||
printf '==> %s\n' "${test#$ROOT/}"
|
||||
bash "$test" || failed+=("${test#$ROOT/}")
|
||||
bash "$test" | tee "$output" || failed+=("${test#$ROOT/}")
|
||||
if grep -q '^ok - .* # SKIP$' "$output"; then
|
||||
skipped+=("${test#$ROOT/}")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#skipped[@]} > 0 )); then
|
||||
printf '\nSkipped checks in %d of %d test files:\n' "${#skipped[@]}" "${#tests[@]}"
|
||||
printf ' %s\n' "${skipped[@]}"
|
||||
fi
|
||||
|
||||
if (( ${#failed[@]} > 0 )); then
|
||||
printf '\n%d of %d test files failed:\n' "${#failed[@]}" "${#tests[@]}" >&2
|
||||
printf ' %s\n' "${failed[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\nAll %d test files passed.\n' "${#tests[@]}"
|
||||
if (( ${#skipped[@]} == 0 )); then
|
||||
printf '\nAll %d test files passed.\n' "${#tests[@]}"
|
||||
else
|
||||
printf '\n%d test files completed without failures; %d had skipped checks.\n' "${#tests[@]}" "${#skipped[@]}"
|
||||
fi
|
||||
@@ -43,7 +43,7 @@ if (( EUID != 0 )); then
|
||||
fi
|
||||
|
||||
if (( ! root_runtime_available )); then
|
||||
pass "no unprivileged user namespace; skipping the root lock-helper lookup matrix"
|
||||
skip "no unprivileged user namespace; skipping the root lock-helper lookup matrix"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_compositor "bar icon geometry test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping bar icon geometry test"
|
||||
skip "quickshell not installed; skipping bar icon geometry test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ trap cleanup EXIT
|
||||
require_compositor "bar widget contract test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping bar widget contract test"
|
||||
skip "quickshell not installed; skipping bar widget contract test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ pass() {
|
||||
printf 'ok - %s\n' "$1"
|
||||
}
|
||||
|
||||
skip() {
|
||||
printf 'ok - %s # SKIP\n' "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
local description="$1"
|
||||
local detail="${2:-}"
|
||||
@@ -72,7 +76,7 @@ require_compositor() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
pass "no Wayland compositor; skipping $description"
|
||||
skip "no Wayland compositor; skipping $description"
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ if [[ -n $real_hiddev ]]; then
|
||||
fail "wrapper did not trust a valid cached hiddev node: $real_hiddev"
|
||||
pass "wrapper trusts a cached hiddev character device without re-detecting"
|
||||
else
|
||||
pass "no /dev/hiddev* character device present; skipping the valid-cache case"
|
||||
skip "no /dev/hiddev* character device present; skipping the valid-cache case"
|
||||
fi
|
||||
|
||||
# --- With no XDG_RUNTIME_DIR, the predictable /tmp cache is not consulted ------
|
||||
@@ -148,5 +148,5 @@ if mkfifo "$tmp_cache" 2>/dev/null; then
|
||||
"it blocked reading the FIFO decoy at $tmp_cache"
|
||||
pass "wrapper ignores the /tmp cache path when XDG_RUNTIME_DIR is unset"
|
||||
else
|
||||
pass "$tmp_cache already present or not safely creatable; skipping the /tmp-fallback case"
|
||||
skip "$tmp_cache already present or not safely creatable; skipping the /tmp-fallback case"
|
||||
fi
|
||||
@@ -46,7 +46,7 @@ mode=$(stat -c '%a' "$write_dir/color.json")
|
||||
pass "theme colour writes a 0644 color.json"
|
||||
|
||||
if (( EUID == 0 )); then
|
||||
pass "running as root; skipping the mktemp-failure check"
|
||||
skip "running as root; skipping the mktemp-failure check"
|
||||
else
|
||||
chmod u+w "$write_dir"
|
||||
export TMPDIR=$test_tmp/missing-tmp
|
||||
|
||||
@@ -82,7 +82,7 @@ SH
|
||||
chmod +x "$stub_bin/sudo"
|
||||
|
||||
if ((EUID == 0)); then
|
||||
pass "running as root; skipping the elevation checks, which would rewrite this machine's browser policy"
|
||||
skip "running as root; skipping the elevation checks, which would rewrite this machine's browser policy"
|
||||
else
|
||||
elevation_for() {
|
||||
: >"$test_tmp/elevation"
|
||||
|
||||
@@ -20,7 +20,7 @@ JS
|
||||
require_compositor "Button hover geometry runtime test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping Button hover geometry runtime test"
|
||||
skip "quickshell not installed; skipping Button hover geometry runtime test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
# Exercise the privileged installer without writing to the host's /usr/share.
|
||||
if ! command -v bwrap >/dev/null || ! bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true 2>/dev/null; then
|
||||
pass "user namespaces unavailable; skipping isolated Claude extension installation"
|
||||
skip "user namespaces unavailable; skipping isolated Claude extension installation"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -277,12 +277,12 @@ printf '%s\n' "$1" >"$EDITOR_PATH_OUT"
|
||||
cat "$1" >"$EDITOR_TEXT_OUT"
|
||||
SH
|
||||
|
||||
cat >"$TMPDIR/bin/tensaku-edit" <<'SH'
|
||||
cat >"$TMPDIR/bin/omasnap" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >"$TENSAKU_OUT"
|
||||
printf '%s\n' "$*" >"$OMASNAP_OUT"
|
||||
SH
|
||||
|
||||
chmod +x "$TMPDIR/bin/wl-copy" "$TMPDIR/bin/wl-paste" "$TMPDIR/bin/wtype" "$TMPDIR/bin/omarchy-launch-browser" "$TMPDIR/bin/omarchy-launch-editor" "$TMPDIR/bin/tensaku-edit"
|
||||
chmod +x "$TMPDIR/bin/wl-copy" "$TMPDIR/bin/wl-paste" "$TMPDIR/bin/wtype" "$TMPDIR/bin/omarchy-launch-browser" "$TMPDIR/bin/omarchy-launch-editor" "$TMPDIR/bin/omasnap"
|
||||
|
||||
capture_output=$(XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" "$ROOT/shell/plugins/clipboard/capture.sh")
|
||||
[[ $capture_output == '{"type":"text","text":"terminal copy"}' ]] || fail "clipboard capture records normal text events"
|
||||
@@ -513,8 +513,8 @@ pass "clipboard open helper opens text entries in editor"
|
||||
[[ $(<"$TMPDIR/editor-path") == "$TMPDIR"/state/omarchy/clipboard-open/clipboard.*.txt ]] || fail "clipboard open helper writes text entries to a temporary file"
|
||||
pass "clipboard open helper writes text entries to a temporary file"
|
||||
|
||||
TENSAKU_OUT="$TMPDIR/tensaku" HOME="$TMPDIR/home" PATH="$TMPDIR/bin:$PATH" \
|
||||
OMASNAP_OUT="$TMPDIR/omasnap" HOME="$TMPDIR/home" PATH="$TMPDIR/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-clipboard-open" --history-index 2
|
||||
|
||||
[[ $(<"$TMPDIR/tensaku") == "$TMPDIR/image.png" ]] || fail "clipboard open helper opens image entries in Tensaku"
|
||||
pass "clipboard open helper opens image entries in Tensaku"
|
||||
[[ $(<"$TMPDIR/omasnap") == "$TMPDIR/image.png" ]] || fail "clipboard open helper opens image entries in Omasnap"
|
||||
pass "clipboard open helper opens image entries in Omasnap"
|
||||
@@ -62,7 +62,7 @@ run_guard() {
|
||||
' bash "$SHELL_TEST_DIR" 2>&1 || printf 'guard exited %s\n' "$?"
|
||||
}
|
||||
|
||||
skipped="ok - no Wayland compositor; skipping sample runtime test"
|
||||
skipped="ok - no Wayland compositor; skipping sample runtime test # SKIP"
|
||||
|
||||
output=$(run_guard -u WAYLAND_DISPLAY XDG_RUNTIME_DIR="$runtime_dir")
|
||||
[[ $output == "$skipped" ]] || fail "guard skips without a display" "$output"
|
||||
@@ -76,7 +76,7 @@ pass "guard skips when the socket is unreachable"
|
||||
|
||||
# Everything below needs a socket to stand in for a live or abandoned compositor.
|
||||
if (( ! socket_bound )); then
|
||||
pass "cannot bind a Unix socket here; skipping the cases that need one"
|
||||
skip "cannot bind a Unix socket here; skipping the cases that need one"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -33,6 +33,15 @@ jq -e '
|
||||
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||
pass "default center anchor exists in center layout"
|
||||
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
(.bar.layout.center | ids) as $ids |
|
||||
($ids | index("omacom.elsewhen")) as $elsewhen |
|
||||
($ids | index("omarchy.clock")) as $clock |
|
||||
$elsewhen != null and $clock == $elsewhen + 1
|
||||
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||
pass "default center layout puts elsewhen immediately before the clock"
|
||||
|
||||
jq -e '
|
||||
any(.bar.layout.center[]; (.id // .) == "omarchy.clock" and (.formatAlt // "") == "d MMMM \u0027W\u0027ww yyyy")
|
||||
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command jq
|
||||
require_command python3
|
||||
|
||||
migration="$ROOT/migrations/1786643346.sh"
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
home="$test_dir/home"
|
||||
profile_root="$home/.config/chromium"
|
||||
preferences="$profile_root/Default/Preferences"
|
||||
mkdir -p "$(dirname "$preferences")"
|
||||
|
||||
# Any id Chromium once derived from the extension's keyless load path; the
|
||||
# repair keys off the registered command name, not the id.
|
||||
ghost_id="ikkebdkaanlebnifjnbeiaklodhbjcci"
|
||||
pinned_id="bgpiichlckmfanooecilcjemknkcpngb"
|
||||
|
||||
write_stale_preferences() {
|
||||
jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {($ghost): {commands: {"copy-url": {suggested_key: "Alt+Shift+L", was_assigned: true}}}, ($pinned): {commands: {"copy-url": {suggested_key: "Alt+Shift+L"}}}}}}' >"$preferences"
|
||||
}
|
||||
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/python3" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 127
|
||||
STUB
|
||||
chmod +x "$stub_bin/python3"
|
||||
|
||||
# Test stubs must delegate to the system interpreter, not a user shim that can
|
||||
# route python3 back through the stubs and recurse.
|
||||
REAL_PYTHON=$(PATH="$stub_bin:$PATH" command -p -v python3)
|
||||
[[ $REAL_PYTHON != "$stub_bin/python3" ]] || fail "real Python resolution bypasses user shims"
|
||||
export REAL_PYTHON
|
||||
rm -f "$stub_bin/python3"
|
||||
|
||||
run_migration() {
|
||||
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# A running Chromium-family browser marks its profile root with a SingletonLock
|
||||
# symlink to <hostname>-<pid>, a target that never exists on disk. That lock —
|
||||
# not the mere presence of a browser process — is what the migration waits on.
|
||||
open_browser() {
|
||||
mkdir -p "$profile_root"
|
||||
ln -sfn "test-host-1234" "$profile_root/SingletonLock"
|
||||
}
|
||||
close_browser() {
|
||||
rm -f "$profile_root/SingletonLock"
|
||||
}
|
||||
|
||||
# The affected profile being open prompts for the windows to be closed;
|
||||
# declining (or having no terminal to ask in) defers the repair so a
|
||||
# rewrite-on-exit cannot revert it.
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum"
|
||||
chmod +x "$stub_bin/gum"
|
||||
write_stale_preferences
|
||||
open_browser
|
||||
|
||||
before_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
|
||||
|
||||
run_migration && fail "migration defers while the affected profile is open"
|
||||
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$before_hash" ]] ||
|
||||
fail "migration leaves preferences alone while the affected profile is open"
|
||||
pass "migration defers the repair while the affected profile is open"
|
||||
|
||||
# gum paints its prompt on stderr, so that stream has to stay attached:
|
||||
# suppressing it leaves gum reading keys behind an unpainted screen, which
|
||||
# reads as a hung update.
|
||||
cat >"$stub_bin/gum" <<'STUB'
|
||||
#!/bin/bash
|
||||
echo "gum-prompt-painted" >&2
|
||||
exit 1
|
||||
STUB
|
||||
prompt_stderr="$test_dir/prompt-stderr"
|
||||
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>"$prompt_stderr" &&
|
||||
fail "migration defers when the browser prompt is declined"
|
||||
grep -q "gum-prompt-painted" "$prompt_stderr" || fail "migration keeps the browser prompt visible"
|
||||
pass "migration keeps the browser prompt visible"
|
||||
|
||||
# A browser holding a different profile root cannot revert this repair, so it
|
||||
# must not hold the update: the repair goes through without ever reaching the
|
||||
# prompt, which the still-declining gum stub would otherwise fail.
|
||||
close_browser
|
||||
mkdir -p "$home/.config/google-chrome"
|
||||
ln -sfn "test-host-1234" "$home/.config/google-chrome/SingletonLock"
|
||||
write_stale_preferences
|
||||
run_migration || fail "migration repairs while a different profile root is open"
|
||||
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
|
||||
fail "migration repairs the shortcut while a different profile root is open"
|
||||
pass "migration ignores a browser on a different profile root"
|
||||
rm -f "$home/.config/google-chrome/SingletonLock" "$preferences.omarchy-copy-url-repair.bak"
|
||||
|
||||
# Closing the affected profile and confirming the prompt lets the repair
|
||||
# proceed.
|
||||
write_stale_preferences
|
||||
open_browser
|
||||
cat >"$stub_bin/gum" <<'STUB'
|
||||
#!/bin/bash
|
||||
"$CLOSE_BROWSER"
|
||||
touch "${GUM_CALLED:?}"
|
||||
exit 0
|
||||
STUB
|
||||
cat >"$stub_bin/close-browser" <<'STUB'
|
||||
#!/bin/bash
|
||||
rm -f "$HOME/.config/chromium/SingletonLock"
|
||||
STUB
|
||||
chmod +x "$stub_bin/gum" "$stub_bin/close-browser"
|
||||
GUM_CALLED="$test_dir/gum-called" CLOSE_BROWSER="$stub_bin/close-browser" \
|
||||
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 ||
|
||||
fail "migration proceeds once the profile is closed and the prompt confirmed"
|
||||
[[ -e $test_dir/gum-called ]] || fail "migration asks before repairing under a running browser"
|
||||
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
|
||||
fail "migration repairs after the browser prompt is confirmed"
|
||||
pass "migration asks to close the browser and repairs on confirmation"
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
|
||||
# With the affected profile closed the ghost registration moves to the pinned id.
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum"
|
||||
close_browser
|
||||
write_stale_preferences
|
||||
run_migration || fail "migration repairs the shortcut when no browser is running"
|
||||
|
||||
jq -e --arg ghost "$ghost_id" --arg pinned "$pinned_id" '
|
||||
.extensions.commands["linux:Alt+Shift+L"].extension == $pinned and
|
||||
(.extensions.settings | has($ghost) | not) and
|
||||
.extensions.settings[$pinned].commands["copy-url"].was_assigned == true
|
||||
' "$preferences" >/dev/null || fail "migration rebinds the Copy URL shortcut to the pinned extension id"
|
||||
[[ -f $preferences.omarchy-copy-url-repair.bak ]] ||
|
||||
fail "migration backs up preferences before the repair"
|
||||
pass "migration rebinds the Copy URL shortcut to the pinned extension id"
|
||||
|
||||
# A repaired profile has no ghost registration left, so nothing is pending —
|
||||
# even while that same profile is open.
|
||||
rm "$preferences.omarchy-copy-url-repair.bak"
|
||||
repaired_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
|
||||
open_browser
|
||||
run_migration || fail "migration reruns cleanly after the repair"
|
||||
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$repaired_hash" && ! -e $preferences.omarchy-copy-url-repair.bak ]] ||
|
||||
fail "migration is idempotent after the repair"
|
||||
pass "migration is idempotent after the repair"
|
||||
close_browser
|
||||
|
||||
# A remapped shortcut keeps the user's chosen key while moving to the pinned id.
|
||||
jq -n --arg ghost "$ghost_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences"
|
||||
run_migration || fail "migration repairs remapped shortcuts"
|
||||
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned' "$preferences" >/dev/null ||
|
||||
fail "migration keeps the remapped key while rebinding to the pinned id"
|
||||
pass "migration keeps remapped shortcut keys"
|
||||
|
||||
# When the pinned extension already holds a copy-url binding (the user fixed
|
||||
# it by hand), the ghost is dropped rather than doubled into a second binding.
|
||||
jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $pinned, global: false}, "linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences"
|
||||
run_migration || fail "migration cleans ghosts alongside a manual repair"
|
||||
jq -e --arg pinned "$pinned_id" '
|
||||
(.extensions.commands | has("linux:Alt+Shift+L") | not) and
|
||||
.extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned
|
||||
' "$preferences" >/dev/null || fail "migration drops the ghost instead of double-binding the pinned extension"
|
||||
pass "migration never double-binds the pinned extension"
|
||||
|
||||
# A browser starting mid-repair may write stale Preferences back on exit, so
|
||||
# the migration must stay pending for a later browser-free run to verify. A
|
||||
# stub hands the repair call through and opens the profile right after it.
|
||||
write_stale_preferences
|
||||
close_browser
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
cat >"$stub_bin/python3" <<'STUB'
|
||||
#!/bin/bash
|
||||
# Called as `python3 -c <script> <preferences> <pinned_id> <check|repair>`, and
|
||||
# the check calls report a surviving ghost through their exit status.
|
||||
"${REAL_PYTHON}" "$@"
|
||||
status=$?
|
||||
[[ ${5:-} == "repair" ]] && ln -sfn "test-host-1234" "$HOME/.config/chromium/SingletonLock"
|
||||
exit $status
|
||||
STUB
|
||||
chmod +x "$stub_bin/python3"
|
||||
if HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1; then
|
||||
fail "migration stays pending when a browser starts mid-repair"
|
||||
fi
|
||||
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
|
||||
fail "migration still repairs preferences before deferring on a late browser"
|
||||
pass "migration stays pending when a browser starts mid-repair"
|
||||
rm -f "$stub_bin/python3" "$preferences.omarchy-copy-url-repair.bak"
|
||||
close_browser
|
||||
|
||||
# A browser that started and exited mid-repair restores stale Preferences
|
||||
# before the final profile check; the post-repair file verification catches it.
|
||||
write_stale_preferences
|
||||
cp "$preferences" "$test_dir/stale-preferences"
|
||||
cat >"$stub_bin/python3" <<'STUB'
|
||||
#!/bin/bash
|
||||
"${REAL_PYTHON}" "$@"
|
||||
status=$?
|
||||
[[ ${5:-} == "repair" ]] && cp "${STALE_PREFERENCES:?}" "${REPAIRED_PREFERENCES:?}"
|
||||
exit $status
|
||||
STUB
|
||||
chmod +x "$stub_bin/python3"
|
||||
if HOME="$home" PATH="$stub_bin:$PATH" STALE_PREFERENCES="$test_dir/stale-preferences" \
|
||||
REPAIRED_PREFERENCES="$preferences" bash -euo pipefail "$migration" >/dev/null 2>&1; then
|
||||
fail "migration stays pending when a briefly-lived browser undoes the repair"
|
||||
fi
|
||||
pass "migration stays pending when a briefly-lived browser undoes the repair"
|
||||
rm -f "$stub_bin/python3"
|
||||
close_browser
|
||||
write_stale_preferences
|
||||
run_migration || fail "migration recovers after a reverted repair"
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
|
||||
# A repair attempted while the affected profile was open leaves its backup
|
||||
# behind. A rerun that sees a clean disk while that profile still runs must
|
||||
# stay pending — the browser can restore the ghost on exit — and only a
|
||||
# browser-free rerun verifies the repair and completes.
|
||||
write_stale_preferences
|
||||
run_migration || fail "repair run before the verification scenario"
|
||||
[[ -f $preferences.omarchy-copy-url-repair.bak ]] || fail "verification scenario has a repair backup"
|
||||
open_browser
|
||||
run_migration && fail "migration must not complete an unverified repair while a browser runs"
|
||||
pass "migration keeps an unverified repair pending while a browser runs"
|
||||
close_browser
|
||||
run_migration || fail "migration completes once the repair is verified with browsers closed"
|
||||
pass "migration verifies an attempted repair on a browser-free rerun"
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
|
||||
# An installed third-party extension with a command that happens to be named
|
||||
# copy-url keeps its own registration.
|
||||
jq -n '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", global: false}}, settings: {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: {path: "/home/user/.config/some-extension", commands: {}}}}}' >"$preferences"
|
||||
untouched_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
|
||||
run_migration || fail "migration leaves installed third-party extensions alone"
|
||||
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$untouched_hash" ]] ||
|
||||
fail "migration does not steal a third-party copy-url command registration"
|
||||
pass "migration leaves installed third-party extensions alone"
|
||||
@@ -81,13 +81,13 @@ SH
|
||||
rm -rf "$poison_dir"
|
||||
pass "root omarchy-dns resolves system helpers from a trusted PATH, not the invocation PATH"
|
||||
else
|
||||
pass "no unprivileged user namespace; skipping the root trusted-PATH probe"
|
||||
skip "no unprivileged user namespace; skipping the root trusted-PATH probe"
|
||||
fi
|
||||
|
||||
# require_root returns immediately for root, so the stubs below would not stand
|
||||
# between the script and the host's real NetworkManager and resolved config.
|
||||
if (( EUID == 0 )); then
|
||||
pass "running as root; skipping the elevation checks, which would rewrite this machine's DNS"
|
||||
skip "running as root; skipping the elevation checks, which would rewrite this machine's DNS"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_command jq
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
mkdir -p "$test_dir/bin" "$test_dir/home"
|
||||
export CALL_LOG="$test_dir/calls"
|
||||
|
||||
cat >"$test_dir/bin/omarchy-pkg-add" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'package %s\n' "$*" >>"$CALL_LOG"
|
||||
exit "${PACKAGE_STATUS:-0}"
|
||||
SH
|
||||
cat >"$test_dir/bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$CALL_LOG"
|
||||
[[ $2 != "rescanPlugins" ]] || exit "${SCAN_STATUS:-0}"
|
||||
printf '%s\n' "${TEST_PUT_RESULT:-ok}"
|
||||
SH
|
||||
cat >"$test_dir/bin/omarchy-restart-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
echo 'migration must leave the restart to omarchy update' >&2
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$test_dir/bin/"*
|
||||
|
||||
mkdir -p "$test_dir/packaged/shell/plugins/omacom.elsewhen"
|
||||
sed "s|/usr/share/omarchy|$test_dir/packaged|g" "$ROOT/migrations/1789581661.sh" >"$test_dir/migration.sh"
|
||||
|
||||
plugin="$test_dir/home/.config/omarchy/plugins/omacom.elsewhen"
|
||||
run_migration() {
|
||||
: >"$CALL_LOG"
|
||||
HOME="$test_dir/home" OMARCHY_PATH="${1:-$test_dir/packaged}" PATH="$test_dir/bin:$ROOT/bin:$PATH" \
|
||||
bash -euo pipefail "$test_dir/migration.sh" >"$test_dir/output" 2>&1
|
||||
}
|
||||
|
||||
if PACKAGE_STATUS=1 run_migration; then
|
||||
fail "package failure stops the migration"
|
||||
fi
|
||||
[[ ! -e $plugin && ! -L $plugin && $(cat "$CALL_LOG") == "package elsewhen" ]] ||
|
||||
fail "package failure leaves the plugin and shell untouched"
|
||||
pass "package failure stops before changing the shell"
|
||||
|
||||
run_migration
|
||||
[[ ! -e $plugin && ! -L $plugin ]] || fail "migration does not create a user plugin link"
|
||||
[[ ! -e $ROOT/config/omarchy/plugins/omacom.elsewhen && ! -L $ROOT/config/omarchy/plugins/omacom.elsewhen ]] || fail "fresh installs do not ship a user plugin link"
|
||||
pass "migration and fresh installs rely on the packaged plugin directory"
|
||||
|
||||
expected=$'package elsewhen\nshell rescanPlugins\nshell putBarWidget omacom.elsewhen {"before":"omarchy.clock"}'
|
||||
[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "install, scan and placement run in order" "$(cat "$CALL_LOG")"
|
||||
pass "real bar helper enables and places before the clock without restarting during reload"
|
||||
|
||||
run_migration
|
||||
[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "migration can be rerun"
|
||||
[[ ! -e $plugin && ! -L $plugin ]] || fail "rerunning the migration does not create a user plugin link"
|
||||
pass "migration can be rerun without a user plugin link"
|
||||
|
||||
run_migration "$ROOT"
|
||||
[[ $(readlink "$plugin") == "$test_dir/packaged/shell/plugins/omacom.elsewhen" ]] || fail "dev checkout links the packaged plugin"
|
||||
pass "dev checkout discovers Elsewhen through a user plugin link"
|
||||
run_migration "$ROOT"
|
||||
[[ $(readlink "$plugin") == "$test_dir/packaged/shell/plugins/omacom.elsewhen" ]] || fail "dev link survives a rerun"
|
||||
pass "dev link is idempotent"
|
||||
rm "$plugin"
|
||||
|
||||
mkdir -p "$plugin"
|
||||
printf 'local work\n' >"$plugin/notes"
|
||||
run_migration "$ROOT"
|
||||
[[ ! -L $plugin && $(cat "$plugin/notes") == "local work" ]] || fail "existing checkout is preserved"
|
||||
pass "existing checkout and local files are preserved"
|
||||
|
||||
rm "$plugin/notes"
|
||||
rmdir "$plugin"
|
||||
ln -s "$test_dir/custom-plugin" "$plugin"
|
||||
run_migration "$ROOT"
|
||||
[[ $(readlink "$plugin") == "$test_dir/custom-plugin" ]] || fail "existing symlink is preserved"
|
||||
pass "existing symlink is preserved, including a missing target"
|
||||
|
||||
for failure in 'SCAN_STATUS=1' 'TEST_PUT_RESULT=unknown'; do
|
||||
if env "$failure" HOME="$test_dir/home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" \
|
||||
bash -euo pipefail "$test_dir/migration.sh" >"$test_dir/output" 2>&1; then
|
||||
fail "$failure must leave the migration pending"
|
||||
fi
|
||||
pass "$failure leaves the migration pending"
|
||||
done
|
||||
@@ -8,7 +8,7 @@ if (( EUID != 0 )); then
|
||||
if unshare --user --map-root-user true 2>/dev/null; then
|
||||
exec unshare --user --map-root-user bash "$0"
|
||||
fi
|
||||
pass "no unprivileged user namespace; skipping factory account cleanup"
|
||||
skip "no unprivileged user namespace; skipping factory account cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
# The hook guards on the real /etc/pam.d path, which can't be mocked via PATH.
|
||||
if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]]; then
|
||||
pass "fingerprint invitation test skipped: host already has fingerprint auth configured"
|
||||
skip "fingerprint invitation test skipped: host already has fingerprint auth configured"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
property var view
|
||||
property var wallpaper
|
||||
property var feed
|
||||
property int step: 0
|
||||
|
||||
Item { id: host; width: 1000; height: 700 }
|
||||
|
||||
function findItem(item, name) {
|
||||
if (item.objectName === name) return item
|
||||
for (var i = 0; i < item.children.length; i++) {
|
||||
var found = findItem(item.children[i], name)
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function check(condition, message) {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 150
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: {
|
||||
try {
|
||||
if (root.step === 0) {
|
||||
var component = Qt.createComponent("file://" + Quickshell.env("OMARCHY_PATH") + "/shell/plugins/lock/LockView.qml")
|
||||
check(component.status === Component.Ready, component.errorString())
|
||||
root.view = component.createObject(host, {width: 1000, height: 700, backgroundPath: "/still.png", loadBackground: false})
|
||||
check(root.view !== null, component.errorString())
|
||||
root.wallpaper = findItem(root.view, "lockWallpaper")
|
||||
root.feed = findItem(root.view, "lockFeedLoader")
|
||||
check(root.wallpaper.path === "", "hidden still must not decode")
|
||||
root.view.backgroundPath = "/video.mp4"
|
||||
root.view.videoPosterPath = "/poster.jpg"
|
||||
} else if (root.step === 1) {
|
||||
check(!root.feed.active && root.feed.item === null, "hidden video must not load a feed client")
|
||||
root.view.loadBackground = true
|
||||
root.view.powerSaverActive = true
|
||||
} else if (root.step === 2) {
|
||||
check(root.wallpaper.path === "/poster.jpg", "power saver keeps the video poster")
|
||||
check(!root.feed.active, "power saver must not connect to the feed")
|
||||
root.view.powerSaverActive = false
|
||||
} else if (root.step === 3) {
|
||||
check(root.feed.active, "visible video enables the isolated feed loader")
|
||||
check(root.wallpaper.path === "/poster.jpg", "poster stays beneath the feed before its first frame")
|
||||
root.view.displaysBlank = true
|
||||
} else if (root.step === 4) {
|
||||
check(!root.feed.active && root.feed.item === null, "blanking releases the feed client")
|
||||
root.view.loadBackground = false
|
||||
} else {
|
||||
check(root.wallpaper.path === "", "unlock releases the poster image")
|
||||
console.log("OWE_LOCK_TEST_PASS")
|
||||
Qt.quit()
|
||||
}
|
||||
root.step++
|
||||
} catch (error) {
|
||||
console.error("OWE_LOCK_TEST_FAIL: " + error)
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ trap cleanup EXIT
|
||||
require_compositor "QML contract test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping QML contract test"
|
||||
skip "quickshell not installed; skipping QML contract test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -150,5 +150,5 @@ pass "font controls create missing Kitty overrides without restoring the theme i
|
||||
if "$ROOT/bin/omarchy-cmd-present" kitty; then
|
||||
kitty +runpy "$(cat "$ROOT/test/shell.d/fixtures/kitty/check-config.py")"
|
||||
else
|
||||
pass "Kitty not installed; skipping native config parser checks"
|
||||
skip "Kitty not installed; skipping native config parser checks"
|
||||
fi
|
||||
@@ -4,6 +4,9 @@ set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
# Start with color enabled; the no-color scenarios opt in below.
|
||||
unset NO_COLOR
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
|
||||
@@ -32,15 +32,6 @@ check(command == ["/usr/bin/updatedb", *options],
|
||||
"locate service runs updatedb directly with fixed Btrfs options")
|
||||
check("ConditionACPower=true" in (root / "etc/systemd/system/plocate-updatedb.service.d/ac-only.conf").read_text(),
|
||||
"scheduled locate indexing keeps its AC-power condition")
|
||||
check(not (root / "install/config/locate.sh").exists() and not (root / "migrations/1784809451.sh").exists(),
|
||||
"the retired locate configuration helper and migration are absent")
|
||||
for directory in ("bin", "install", "migrations"):
|
||||
for path in (root / directory).rglob("*"):
|
||||
if path.is_file():
|
||||
content = path.read_text()
|
||||
if "OMARCHY_UPDATEDB_CONF_PATH" in content or "config/locate.sh" in content:
|
||||
raise SystemExit("not ok - retired locate configuration path remains in " + str(path))
|
||||
check(True, "runtime and installation no longer reference the configuration rewrite")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="omarchy-locate-") as scratch:
|
||||
scratch = Path(scratch)
|
||||
|
||||
@@ -21,7 +21,7 @@ trap cleanup EXIT
|
||||
require_compositor "lock fingerprint indicator test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping lock fingerprint indicator test"
|
||||
skip "quickshell not installed; skipping lock fingerprint indicator test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ trap cleanup EXIT
|
||||
require_compositor "lock password overflow test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping lock password overflow test"
|
||||
skip "quickshell not installed; skipping lock password overflow test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ trap cleanup EXIT
|
||||
require_compositor "manifest entrypoint load test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping manifest entrypoint load test"
|
||||
skip "quickshell not installed; skipping manifest entrypoint load test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -7,6 +7,14 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mkdir -p "$tmp/bin"
|
||||
export QR_ROUTE_EXIT=0
|
||||
|
||||
# Loopback is never wireless, so exercise the connected-Wi-Fi fallback.
|
||||
cat >"$tmp/bin/ip" <<'EOF'
|
||||
#!/bin/bash
|
||||
[[ $QR_ROUTE_EXIT == "0" ]] || exit "$QR_ROUTE_EXIT"
|
||||
printf '1.1.1.1 dev lo\n'
|
||||
EOF
|
||||
|
||||
cat >"$tmp/bin/nmcli" <<'EOF'
|
||||
#!/bin/bash
|
||||
@@ -28,13 +36,13 @@ payload=$(</dev/stdin)
|
||||
printf '%s' "$payload" >"$QR_PAYLOAD_FILE"
|
||||
printf '## \n ## \n ##\n'
|
||||
EOF
|
||||
chmod +x "$tmp/bin/nmcli" "$tmp/bin/qrencode"
|
||||
chmod +x "$tmp/bin/ip" "$tmp/bin/nmcli" "$tmp/bin/qrencode"
|
||||
|
||||
run_success_case() {
|
||||
local description=$1 fields=$2 expected_payload=$3
|
||||
shift 3
|
||||
local output meta matrix payload arg with_meta=false
|
||||
local expected_matrix expected_security expected_ssid expected_iface="*"
|
||||
local expected_matrix expected_security expected_ssid expected_iface="wlan0"
|
||||
|
||||
for arg in "$@"; do
|
||||
[[ $arg == "--meta" ]] && with_meta=true || expected_iface=$arg
|
||||
@@ -49,9 +57,8 @@ run_success_case() {
|
||||
meta=$(head -n1 <<<"$output")
|
||||
matrix=$(tail -n +2 <<<"$output")
|
||||
|
||||
# The meta line leads with the shared interface, security, and SSID. With
|
||||
# no interface argument the helper detects one from the live host, so that
|
||||
# field is only pinned when the case pinned it.
|
||||
# The metadata reports the requested interface, or the connected Wi-Fi
|
||||
# interface supplied by the nmcli stub when no interface was requested.
|
||||
expected_security=${expected_payload#WIFI:T:}
|
||||
expected_security=${expected_security%%;*}
|
||||
expected_ssid=$(head -n1 <<<"$fields")
|
||||
@@ -88,6 +95,12 @@ run_success_case \
|
||||
'WIFI:T:WPA;S:Cafe Detected;P:secret;;' \
|
||||
--meta
|
||||
|
||||
QR_ROUTE_EXIT=2 run_success_case \
|
||||
"network QR helper detects Wi-Fi when route lookup fails" \
|
||||
$'Cafe No Route\nwpa-psk\nsecret\nno\n' \
|
||||
'WIFI:T:WPA;S:Cafe No Route;P:secret;;' \
|
||||
--meta
|
||||
|
||||
run_success_case \
|
||||
"network QR helper supports open networks" \
|
||||
$'Cafe Open\nnone\n\nno\n' \
|
||||
|
||||
@@ -140,7 +140,6 @@ export OMARCHY_PTL_REBUILD_MARKER="$scratch/state/1789095456"
|
||||
export OMARCHY_MIGRATION_STATE="$scratch/user-markers"
|
||||
touch "$OMARCHY_PTL_REBUILD_MARKER" "$OMARCHY_MIGRATION_STATE/1789095456.sh"
|
||||
cp "$migration" "$scratch/omarchy/migrations/"
|
||||
[[ ! -e $ROOT/migrations/1789095456.sh ]] || fail "the superseded migration must not install the PTL variant first"
|
||||
pending=$(OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" --pending)
|
||||
[[ $pending == "1789325478.sh" ]] || fail "the renamed migration is pending after completing the old migration"
|
||||
OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" > "$scratch/output" 2>&1
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
packages="$ROOT/install/omarchy-base.packages"
|
||||
migration="$ROOT/migrations/1788129995.sh"
|
||||
|
||||
grep -qxF omasnap "$packages" || fail "fresh installs include Omasnap"
|
||||
! grep -qxF tensaku "$packages" || fail "fresh installs no longer include Tensaku"
|
||||
! grep -qxF satty "$packages" || fail "fresh installs no longer include Satty"
|
||||
pass "fresh installs use Omasnap as the screenshot tool"
|
||||
|
||||
grep -Fq 'namespace = "^omasnap$"' "$ROOT/default/hypr/apps/screenshot-selection.lua" ||
|
||||
fail "Omasnap has a layer rule"
|
||||
grep -Fq 'no_screen_share = true' "$ROOT/default/hypr/apps/screenshot-selection.lua" ||
|
||||
fail "the Omasnap overlay is excluded from screen sharing"
|
||||
! grep -Fq 'dev.tensaku.Tensaku' "$ROOT/default/hypr/apps/system.lua" ||
|
||||
fail "the removed Tensaku window rules are gone"
|
||||
pass "Hyprland applies Omasnap's overlay policy without stale Tensaku rules"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
stub_bin="$test_tmp/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/omasnap" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\t%s\n' "${OMASNAP_SCREENSHOT_DIR:-}" "$*" >>"$OMASNAP_TEST_LOG"
|
||||
SH
|
||||
chmod +x "$stub_bin/omasnap"
|
||||
|
||||
capture_log="$test_tmp/capture.log"
|
||||
OMASNAP_TEST_LOG="$capture_log" PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-capture-screenshot"
|
||||
[[ $(<"$capture_log") == $'\t' ]] || fail "the default screenshot opens Omasnap without extra arguments"
|
||||
|
||||
: >"$capture_log"
|
||||
OMASNAP_TEST_LOG="$capture_log" OMARCHY_SCREENSHOT_DIR="$test_tmp/legacy-output" PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-capture-screenshot" windows copy
|
||||
[[ $(<"$capture_log") == "$test_tmp/legacy-output"$'\twindows --copy' ]] ||
|
||||
fail "the screenshot command maps the legacy directory and copy argument to Omasnap"
|
||||
|
||||
: >"$capture_log"
|
||||
OMASNAP_TEST_LOG="$capture_log" OMARCHY_SCREENSHOT_DIR="$test_tmp/legacy-output" OMASNAP_SCREENSHOT_DIR="$test_tmp/native-output" PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-capture-screenshot" fullscreen save
|
||||
[[ $(<"$capture_log") == "$test_tmp/native-output"$'\tfullscreen --save' ]] ||
|
||||
fail "the native Omasnap directory wins while legacy save syntax still works"
|
||||
|
||||
: >"$capture_log"
|
||||
OMASNAP_TEST_LOG="$capture_log" PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-capture-screenshot" region slurp
|
||||
[[ $(<"$capture_log") == $'\tregion' ]] || fail "the former default slurp argument remains a harmless compatibility no-op"
|
||||
|
||||
: >"$capture_log"
|
||||
OMASNAP_TEST_LOG="$capture_log" PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-capture-screenshot" scroll --save
|
||||
[[ $(<"$capture_log") == $'\tscroll --save' ]] || fail "native Omasnap modes and flags pass through unchanged"
|
||||
|
||||
pass "the Omarchy screenshot route delegates compatible arguments to Omasnap"
|
||||
|
||||
cat >"$stub_bin/omarchy-pkg-add" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'add\t%s\n' "$*" >>"$OMASNAP_MIGRATION_LOG"
|
||||
exit "${OMASNAP_PACKAGE_STATUS:-0}"
|
||||
SH
|
||||
cat >"$stub_bin/omarchy-pkg-drop" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'drop\t%s\n' "$*" >>"$OMASNAP_MIGRATION_LOG"
|
||||
SH
|
||||
chmod +x "$stub_bin/omarchy-pkg-add" "$stub_bin/omarchy-pkg-drop"
|
||||
|
||||
migration_home="$test_tmp/home"
|
||||
mkdir -p "$migration_home/.config/imv" "$migration_home/dotfiles"
|
||||
cat >"$migration_home/dotfiles/imv.config" <<'EOF'
|
||||
[binds]
|
||||
|
||||
# Edit the current image in Tensaku and quit the viewer
|
||||
<Ctrl+e> = exec tensaku-edit "$imv_current_file" & ; quit
|
||||
<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit
|
||||
EOF
|
||||
ln -s "$migration_home/dotfiles/imv.config" "$migration_home/.config/imv/config"
|
||||
|
||||
migration_log="$test_tmp/migration.log"
|
||||
run_migration() {
|
||||
: >"$migration_log"
|
||||
OMASNAP_MIGRATION_LOG="$migration_log" HOME="$migration_home" PATH="$stub_bin:$PATH" \
|
||||
bash -euo pipefail "$migration" >/dev/null
|
||||
}
|
||||
|
||||
run_migration
|
||||
|
||||
[[ $(sed -n '1p' "$migration_log") == $'add\tomasnap' ]] || fail "the migration installs Omasnap first"
|
||||
[[ $(sed -n '2p' "$migration_log") == $'drop\tsatty tensaku' ]] || fail "the migration removes Satty and Tensaku after Omasnap is ready"
|
||||
grep -Fq '# Edit the current image in Omasnap and quit the viewer' "$migration_home/.config/imv/config" ||
|
||||
fail "the migration updates the stock imv editor comment"
|
||||
grep -Fq '<Ctrl+e> = exec omasnap "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
|
||||
fail "the migration sends the stock imv edit binding to Omasnap"
|
||||
grep -Fq '<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
|
||||
fail "the migration preserves custom imv bindings"
|
||||
[[ -L $migration_home/.config/imv/config ]] || fail "the migration preserves a dotfile-managed imv symlink"
|
||||
[[ $(stat -c '%a' "$migration") == 644 ]] || fail "the Omasnap migration has mode 0644"
|
||||
|
||||
pass "the migration swaps packages and safely updates only the stock imv binding"
|
||||
|
||||
cat >"$migration_home/dotfiles/imv.config" <<'EOF'
|
||||
[binds]
|
||||
|
||||
# Edit the current image in Satty and quit the viewer
|
||||
<Ctrl+e> = exec satty --filename "$imv_current_file" & ; quit
|
||||
<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit
|
||||
EOF
|
||||
cp "$migration_home/dotfiles/imv.config" "$test_tmp/imv-before"
|
||||
legacy_desktop="$migration_home/.local/share/applications/omasnap.desktop"
|
||||
mkdir -p "$(dirname "$legacy_desktop")"
|
||||
cat >"$legacy_desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Omasnap
|
||||
Exec=omasnap
|
||||
NoDisplay=true
|
||||
EOF
|
||||
if OMASNAP_PACKAGE_STATUS=1 run_migration; then
|
||||
fail "Omasnap install failure stops the migration"
|
||||
fi
|
||||
[[ $(<"$migration_log") == $'add\tomasnap' ]] || fail "install failure leaves old screenshot packages installed"
|
||||
cmp -s "$test_tmp/imv-before" "$migration_home/dotfiles/imv.config" || fail "install failure leaves the imv binding untouched"
|
||||
[[ -f $legacy_desktop ]] || fail "install failure preserves the existing launcher entry"
|
||||
pass "Omasnap install failure preserves the previous screenshot setup"
|
||||
|
||||
run_migration
|
||||
[[ ! -e $legacy_desktop && ! -L $legacy_desktop ]] || fail "the migration removes the old user-local Omasnap desktop entry"
|
||||
pass "the migration removes the desktop entry that hides the packaged launcher"
|
||||
grep -Fq '# Edit the current image in Omasnap and quit the viewer' "$migration_home/.config/imv/config" ||
|
||||
fail "the migration updates the stock Satty comment"
|
||||
grep -Fq '<Ctrl+e> = exec omasnap "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
|
||||
fail "the migration replaces the stock Satty binding before removing Satty"
|
||||
grep -Fq '<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
|
||||
fail "the Satty migration preserves custom bindings"
|
||||
[[ -L $migration_home/.config/imv/config ]] || fail "the Satty migration preserves the imv symlink"
|
||||
pass "the migration upgrades the stock Satty binding to Omasnap"
|
||||
|
||||
cp "$migration_home/dotfiles/imv.config" "$test_tmp/imv-migrated"
|
||||
run_migration
|
||||
cmp -s "$test_tmp/imv-migrated" "$migration_home/dotfiles/imv.config" || fail "rerunning the migration preserves the updated imv config"
|
||||
pass "the migration can be rerun"
|
||||
|
||||
printf '<Ctrl+e> = exec custom-editor "$imv_current_file" & ; quit\n' >"$migration_home/dotfiles/imv.config"
|
||||
cp "$migration_home/dotfiles/imv.config" "$test_tmp/imv-custom"
|
||||
run_migration
|
||||
cmp -s "$test_tmp/imv-custom" "$migration_home/dotfiles/imv.config" || fail "the migration preserves a custom edit shortcut"
|
||||
pass "custom imv edit shortcuts remain unchanged"
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
mkdir -p "$work/bin" "$work/home"
|
||||
export TEST_CALLS="$work/calls"
|
||||
for command in omarchy-pkg-add omarchy-hook-install systemctl; do
|
||||
cat >"$work/bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >>"$TEST_CALLS"
|
||||
case "$*" in
|
||||
'--user enable owed.service') [[ ${TEST_TTY:-0} == 0 ]] ;;
|
||||
'--user is-active --quiet graphical-session.target') [[ ${TEST_TTY:-0} == 0 ]] ;;
|
||||
'--user start owed.service') [[ ${TEST_START_FAIL:-0} == 0 ]] ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
SH
|
||||
chmod +x "$work/bin/$command"
|
||||
done
|
||||
|
||||
migration="$ROOT/migrations/1789764927.sh"
|
||||
run_migration() {
|
||||
HOME="$work/home" PATH="$work/bin:$PATH" bash -euo pipefail "$migration"
|
||||
}
|
||||
run_migration
|
||||
run_migration
|
||||
grep -Fx 'omarchy-pkg-add owe owe-lockfeed' "$TEST_CALLS" >/dev/null
|
||||
grep -Fx 'omarchy-hook-install theme-set /usr/share/owe/10-owe-sync' "$TEST_CALLS" >/dev/null
|
||||
grep -Fx 'systemctl --user start owed.service' "$TEST_CALLS" >/dev/null
|
||||
pass "migration installs OWE, its theme refresh hook, and starts the graphical session service"
|
||||
|
||||
: >"$TEST_CALLS"
|
||||
TEST_TTY=1 run_migration
|
||||
TEST_TTY=1 run_migration
|
||||
[[ $(readlink "$work/home/.config/systemd/user/graphical-session.target.wants/owed.service") == /usr/lib/systemd/user/owed.service ]]
|
||||
if grep -F 'systemctl --user start' "$TEST_CALLS" >/dev/null; then
|
||||
fail "TTY migration must not start the renderer"
|
||||
fi
|
||||
pass "TTY migrations enable the next login without starting a renderer"
|
||||
|
||||
: >"$TEST_CALLS"
|
||||
OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 run_migration
|
||||
if grep -F 'systemctl --user start' "$TEST_CALLS" >/dev/null; then
|
||||
fail "Quattro upgrade must defer OWE until the new graphical session"
|
||||
fi
|
||||
pass "Quattro upgrade enables OWE without starting it in the old shell"
|
||||
|
||||
if TEST_START_FAIL=1 run_migration; then
|
||||
fail "a failed live service start leaves the migration pending"
|
||||
fi
|
||||
pass "a failed live service start fails the migration"
|
||||
|
||||
: >"$TEST_CALLS"
|
||||
HOME="$work/home" PATH="$work/bin:$PATH" bash "$ROOT/install/user/first-run/enable-user-units.sh"
|
||||
grep -E '^systemctl --user enable --now .*owed.service' "$TEST_CALLS" >/dev/null
|
||||
grep -Fx 'omarchy-hook-install theme-set /usr/share/owe/10-owe-sync' "$TEST_CALLS" >/dev/null
|
||||
pass "fresh installs enable OWE and install the theme refresh hook"
|
||||
|
||||
cat >"$work/bin/ffmpegthumbnailer" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'thumbnail\n' >>"$TEST_CALLS"
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
-o) output=$2; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
[[ ${TEST_POSTER_FAIL:-0} == 0 ]] || exit 1
|
||||
printf 'poster\n' >"$output"
|
||||
SH
|
||||
chmod +x "$work/bin/ffmpegthumbnailer"
|
||||
source_path="$work/video with 'quotes'.mp4"
|
||||
printf 'source\n' >"$source_path"
|
||||
poster() {
|
||||
PATH="$work/bin:$PATH" XDG_CACHE_HOME="$work/cache" bash "$ROOT/shell/plugins/lock/poster.sh" "$source_path"
|
||||
}
|
||||
: >"$TEST_CALLS"
|
||||
first=$(poster)
|
||||
[[ -s $first && $(poster) == "$first" ]]
|
||||
[[ $(wc -l <"$TEST_CALLS") == 1 ]]
|
||||
printf 'changed source\n' >>"$source_path"
|
||||
second=$(poster)
|
||||
[[ -s $second && $second != "$first" && ! -e $first ]]
|
||||
pass "lock posters are cached, refreshed after source changes, and old frames are evicted"
|
||||
printf 'broken source\n' >>"$source_path"
|
||||
if TEST_POSTER_FAIL=1 poster; then fail "failed poster generation must fail"; fi
|
||||
[[ -z $(find "$work/cache" -name '.poster-*.jpg' -print -quit) ]]
|
||||
pass "failed poster generation never publishes a partial image"
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
skip "quickshell unavailable; skipping lock feed QML lifecycle"
|
||||
exit 0
|
||||
fi
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
mkdir -p "$work/config" "$work/runtime" "$work/home"
|
||||
chmod 700 "$work/runtime"
|
||||
cp "$SHELL_TEST_DIR/fixtures/owe-lock/shell.qml" "$work/config/shell.qml"
|
||||
ln -s "$ROOT/shell/Ui" "$work/config/Ui"
|
||||
ln -s "$ROOT/shell/Commons" "$work/config/Commons"
|
||||
|
||||
# Offscreen rendering exercises the real QML bindings without taking a lock
|
||||
# or connecting to the user's compositor.
|
||||
HOME="$work/home" XDG_RUNTIME_DIR="$work/runtime" \
|
||||
OMARCHY_PATH="$ROOT" QT_QPA_PLATFORM=offscreen QT_QPA_PLATFORMTHEME= \
|
||||
QT_STYLE_OVERRIDE= QT_QUICK_BACKEND=software \
|
||||
timeout 15 quickshell -p "$work/config" --no-color >"$work/log" 2>&1 || {
|
||||
cat "$work/log" >&2
|
||||
fail "lock feed QML lifecycle runs"
|
||||
}
|
||||
if ! grep -q 'OWE_LOCK_TEST_PASS' "$work/log" || grep -q 'OWE_LOCK_TEST_FAIL' "$work/log"; then
|
||||
cat "$work/log" >&2
|
||||
fail "lock feed respects visibility and keeps its poster fallback"
|
||||
fi
|
||||
pass "lock feed respects visibility, power saver and blanking; hidden views release images"
|
||||
@@ -158,7 +158,7 @@ if script -qec true /dev/null >/dev/null 2>&1; then
|
||||
fail "plugin add reached git clone for an option-shaped gum-prompt URL"
|
||||
pass "plugin add guard rejects an option-shaped URL from the interactive prompt"
|
||||
else
|
||||
pass "script -qec unavailable; skipping the interactive gum-prompt guard case"
|
||||
skip "script -qec unavailable; skipping the interactive gum-prompt guard case"
|
||||
fi
|
||||
|
||||
# Legitimate URL forms pass the guard and reach git clone (stubbed, no network).
|
||||
|
||||
@@ -156,7 +156,7 @@ pass "service facades resolve enabled clones without widening replacement-bar ac
|
||||
require_compositor "plugin authentication boundary runtime test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping plugin authentication boundary runtime test"
|
||||
skip "quickshell not installed; skipping plugin authentication boundary runtime test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ trap cleanup EXIT
|
||||
require_compositor "plugin registry contract test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping plugin registry contract test"
|
||||
skip "quickshell not installed; skipping plugin registry contract test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ JS
|
||||
require_compositor "pointer movement gate runtime test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping pointer movement gate runtime test"
|
||||
skip "quickshell not installed; skipping pointer movement gate runtime test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ trap cleanup EXIT
|
||||
require_compositor "shell runtime smoke test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping shell runtime smoke test"
|
||||
skip "quickshell not installed; skipping shell runtime smoke test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,17 +21,17 @@ trap cleanup EXIT
|
||||
require_compositor "screenshot sanity test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping screenshot sanity test"
|
||||
skip "quickshell not installed; skipping screenshot sanity test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if pgrep -x slurp >/dev/null 2>&1; then
|
||||
pass "slurp is already running; skipping screenshot sanity test"
|
||||
if pgrep -x omasnap >/dev/null 2>&1; then
|
||||
skip "omasnap is already running; skipping screenshot sanity test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_command hyprctl
|
||||
require_command grim
|
||||
require_command omasnap
|
||||
require_command jq
|
||||
require_command python3
|
||||
|
||||
@@ -118,13 +118,13 @@ jq -e '
|
||||
fail_with_log "screenshot test shell rendered visible bar widgets"
|
||||
}
|
||||
|
||||
screenshot=$(
|
||||
OMARCHY_PATH="$test_root" \
|
||||
OMARCHY_SCREENSHOT_DIR="$screenshot_dir" \
|
||||
HOME="$test_home" \
|
||||
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy" capture screenshot fullscreen save 2>"$screenshot_err" | tail -n 1
|
||||
)
|
||||
OMARCHY_PATH="$test_root" \
|
||||
OMASNAP_SCREENSHOT_DIR="$screenshot_dir" \
|
||||
HOME="$test_home" \
|
||||
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy" capture screenshot fullscreen save >/dev/null 2>"$screenshot_err"
|
||||
|
||||
screenshot=$(find "$screenshot_dir" -maxdepth 1 -type f -name '*.png' -print -quit)
|
||||
|
||||
[[ -n $screenshot && -f $screenshot ]] || fail_with_log "fullscreen screenshot was captured"
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
mkdir -p "$test_dir/test/shell.d"
|
||||
cp "$ROOT/test/"{shell,all} "$test_dir/test/"
|
||||
cp "$SHELL_TEST_DIR/base-test.sh" "$test_dir/test/shell.d/"
|
||||
|
||||
# Run the real runners in a tiny fixture tree, without recursing into this test.
|
||||
run_suite() {
|
||||
status=0
|
||||
output=$(bash "$test_dir/test/${1:-shell}" 2>&1) || status=$?
|
||||
}
|
||||
|
||||
run_suite
|
||||
(( status == 1 )) && [[ $output == *"No shell tests found"* ]] ||
|
||||
fail "an empty suite fails instead of counting the helper as a test" "$output"
|
||||
pass "an empty suite fails instead of counting the helper as a test"
|
||||
|
||||
cat >"$test_dir/test/shell.d/a-pass-test.sh" <<'SH'
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
captured=$(skip "captured fixture output is not a skipped check")
|
||||
pass "the command skips an optional action"
|
||||
SH
|
||||
run_suite
|
||||
(( status == 0 )) && [[ $output == *"All 1 test files passed."* ]] ||
|
||||
fail "passing assertions and captured skip output do not mark a file skipped" "$output"
|
||||
pass "passing assertions and captured skip output do not mark a file skipped"
|
||||
|
||||
cat >"$test_dir/test/shell.d/b-partial-test.sh" <<'SH'
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
pass "static check"
|
||||
skip "first unavailable runtime check"
|
||||
skip "second unavailable runtime check"
|
||||
pass "check after skips"
|
||||
SH
|
||||
cat >"$test_dir/test/shell.d/c-skipped-test.sh" <<'SH'
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
unset WAYLAND_DISPLAY
|
||||
require_compositor "runtime fixture"
|
||||
fail "unreachable after compositor skip"
|
||||
SH
|
||||
cat >"$test_dir/test/shell.d/z-last-test.sh" <<'SH'
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
pass "last file ran"
|
||||
SH
|
||||
run_suite
|
||||
(( status == 0 )) && [[ $output == *"4 test files completed without failures; 2 had skipped checks."* ]] ||
|
||||
fail "whole and partial skips are counted once per file and remain successful" "$output"
|
||||
[[ $output == *"ok - check after skips"* && $output == *"ok - last file ran"* ]] ||
|
||||
fail "skip returns normally and later files still run" "$output"
|
||||
[[ $output == *$'Skipped checks in 2 of 4 test files:\n test/shell.d/b-partial-test.sh\n test/shell.d/c-skipped-test.sh\n'* ]] ||
|
||||
fail "the summary identifies only files with skipped checks" "$output"
|
||||
pass "whole and partial skips are visible without failing or stopping the suite"
|
||||
|
||||
cat >"$test_dir/test/shell.d/d-failed-test.sh" <<'SH'
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
skip "unavailable check before failure"
|
||||
echo "failure detail" >&2
|
||||
false
|
||||
pass "unreachable after failure"
|
||||
SH
|
||||
run_suite
|
||||
(( status == 1 )) && [[ $output == *$'1 of 5 test files failed:\n test/shell.d/d-failed-test.sh'* ]] ||
|
||||
fail "a failure after a skip still fails the suite through the output pipe" "$output"
|
||||
[[ $output == *"Skipped checks in 3 of 5 test files:"* && $output == *"failure detail"* && $output == *"ok - last file ran"* && $output != *"ok - unreachable after failure"* ]] ||
|
||||
fail "failures preserve skip reporting, diagnostics, errexit, and later tests" "$output"
|
||||
pass "failures remain fatal while skips and later test results stay visible"
|
||||
|
||||
printf '#!/bin/bash\necho "CLI fixture passed"\n' >"$test_dir/test/cli"
|
||||
chmod +x "$test_dir/test/cli"
|
||||
run_suite all
|
||||
(( status == 1 )) && [[ $output == *"CLI fixture passed"* && $output == *"Skipped checks in 3 of 5 test files:"* && $output == *$'1 of 2 suites failed:\n test/shell'* ]] ||
|
||||
fail "the aggregate runner preserves shell failures and skip reporting" "$output"
|
||||
pass "the aggregate runner preserves shell failures and skip reporting"
|
||||
@@ -173,7 +173,7 @@ if locale -a 2>/dev/null | grep -qix 'en_US.utf-\?8'; then
|
||||
|
||||
pass "the accepted set does not move with the desktop's locale"
|
||||
else
|
||||
pass "no en_US.UTF-8 locale; skipping the locale-pinning check"
|
||||
skip "no en_US.UTF-8 locale; skipping the locale-pinning check"
|
||||
fi
|
||||
|
||||
# basename reads a leading dash as an option once the scp-style prefix is gone.
|
||||
|
||||
@@ -25,7 +25,7 @@ trap cleanup EXIT
|
||||
require_compositor "tray menu activation test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping tray menu activation test"
|
||||
skip "quickshell not installed; skipping tray menu activation test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -36,7 +36,7 @@ python - <<'PY' || {
|
||||
import dbus
|
||||
import gi
|
||||
PY
|
||||
pass "python DBus bindings unavailable; skipping tray menu activation test"
|
||||
skip "python DBus bindings unavailable; skipping tray menu activation test"
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake" stop
|
||||
pass "stale update ownership preserves a newer Stay Awake choice"
|
||||
|
||||
# A stale PID is safe even if it has been reused by another process.
|
||||
sleep 30 &
|
||||
sleep 30 >/dev/null &
|
||||
unrelated_pid=$!
|
||||
unrelated_start_time=$(awk '{ print $22 }' "/proc/$unrelated_pid/stat")
|
||||
mkdir -p "$stay_awake_helper_state"
|
||||
|
||||
@@ -28,6 +28,8 @@ else
|
||||
expected_scope=""
|
||||
fi
|
||||
|
||||
# The baseline has no locale override; forwarding is tested below.
|
||||
unset LC_ALL
|
||||
run_helper -Syu --noconfirm
|
||||
[[ $(cat "$test_tmp/call") == "env OMARCHY_UPDATE_PACMAN=1 ${expected_scope}pacman -Syu --noconfirm" ]] ||
|
||||
fail "helper composes the guarded pacman invocation" "$(cat "$test_tmp/call")"
|
||||
|
||||
@@ -9,15 +9,13 @@ const fs = require('fs')
|
||||
|
||||
const utilQml = fs.readFileSync(path.join(root, 'shell/Commons/Util.qml'), 'utf8')
|
||||
const mediaQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundMedia.qml'), 'utf8')
|
||||
const videoQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundVideo.qml'), 'utf8')
|
||||
const backgroundQml = fs.readFileSync(path.join(root, 'shell/plugins/background/Background.qml'), 'utf8')
|
||||
const lockQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
|
||||
const lockFeedQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockFeedSurface.qml'), 'utf8')
|
||||
const themeSwitcher = fs.readFileSync(path.join(root, 'bin/omarchy-theme-switcher'), 'utf8')
|
||||
const quattroUpgrade = fs.readFileSync(path.join(root, 'bin/omarchy-upgrade-to-quattro'), 'utf8')
|
||||
const multimediaMigration = fs.readFileSync(path.join(root, 'migrations/1786609204.sh'), 'utf8')
|
||||
const barTextColor = fs.readFileSync(path.join(root, 'bin/omarchy-bar-text-color'), 'utf8')
|
||||
const menuImages = fs.readFileSync(path.join(root, 'bin/omarchy-menu-images'), 'utf8')
|
||||
const lockView = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
|
||||
const lockService = fs.readFileSync(path.join(root, 'shell/plugins/lock/Service.qml'), 'utf8')
|
||||
const batteryService = fs.readFileSync(path.join(root, 'shell/plugins/services/battery/Service.qml'), 'utf8')
|
||||
const themeSet = fs.readFileSync(path.join(root, 'bin/omarchy-theme-set'), 'utf8')
|
||||
@@ -29,54 +27,43 @@ assert(
|
||||
'shared media helper identifies video paths without truncating valid local names'
|
||||
)
|
||||
assert(
|
||||
videoQml.includes('loops: MediaPlayer.Infinite') &&
|
||||
videoQml.includes('autoPlay: root.playbackEnabled') &&
|
||||
videoQml.includes('fillMode: VideoOutput.PreserveAspectCrop') &&
|
||||
/imageUrl: path && !Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) &&
|
||||
/videoUrl: path && Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) : ""/.test(mediaQml),
|
||||
'background media plays aspect-cropped videos on a loop, and hands each loader only its own kind of file'
|
||||
!fs.existsSync(path.join(root, 'shell/Ui/BackgroundVideo.qml')),
|
||||
'the shell keeps no video player of its own'
|
||||
)
|
||||
assert(
|
||||
videoQml.includes('MediaPlayer.LoadedMedia') &&
|
||||
videoQml.includes('primePauseTimer') &&
|
||||
videoQml.includes('mediaGeneration') &&
|
||||
videoQml.includes('videoSink') &&
|
||||
videoQml.includes('onVideoFrameChanged') &&
|
||||
videoQml.includes('MediaPlayer.BufferedMedia') &&
|
||||
videoQml.includes('mediaStatus !== MediaPlayer.BufferedMedia') &&
|
||||
videoQml.includes('interval: 1000') &&
|
||||
videoQml.includes('interval: 50') &&
|
||||
videoQml.includes('frameReceived') &&
|
||||
videoQml.includes('output.clearOutput()') &&
|
||||
!videoQml.includes('KeepLastFrame') &&
|
||||
/onPlaybackEnabledChanged:[\s\S]*?if \(playbackEnabled\) player\.play\(\)[\s\S]*?else player\.pause\(\)/.test(videoQml) &&
|
||||
videoQml.includes('primingGeneration') &&
|
||||
videoQml.includes('player.play()') &&
|
||||
videoQml.includes('player.pause()'),
|
||||
'paused video sources are primed to display their first frame'
|
||||
/imageUrl: path && !video \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) &&
|
||||
/active: root\.path !== "" && !root\.video/.test(mediaQml) &&
|
||||
!mediaQml.includes('videoUrl') &&
|
||||
!mediaQml.includes('videoLoader') &&
|
||||
!mediaQml.includes('videoEnabled'),
|
||||
'background media is image-only: it loads stills and leaves videos to OWE'
|
||||
)
|
||||
assert(
|
||||
!/^\s*import QtMultimedia/m.test(mediaQml) &&
|
||||
mediaQml.includes('source: "BackgroundVideo.qml"'),
|
||||
'the still-image path never imports QtMultimedia, so image-only sessions do not map it'
|
||||
!/^\s*import QtMultimedia/m.test(backgroundQml) &&
|
||||
!/^\s*import QtMultimedia/m.test(lockQml) &&
|
||||
!backgroundQml.includes('MediaPlayer') &&
|
||||
!lockQml.includes('MediaPlayer'),
|
||||
'no shell surface builds a Qt Multimedia pipeline'
|
||||
)
|
||||
assert(
|
||||
!/^\s*Video\s*\{/m.test(videoQml) &&
|
||||
/property bool audioEnabled: false/.test(videoQml) &&
|
||||
/property bool audioEnabled: false/.test(mediaQml) &&
|
||||
/active: root\.audioEnabled && player\.hasAudio/.test(videoQml) &&
|
||||
/audioOutput: audioLoader\.item/.test(videoQml) &&
|
||||
/muted: root\.priming \|\| !root\.playbackEnabled/.test(videoQml) &&
|
||||
/property: "audioEnabled"\s*\n\s*value: root\.audioEnabled/.test(mediaQml) &&
|
||||
/firstScreen: Quickshell\.screens\.length > 0\s*\n\s*&& String\(Quickshell\.screens\[0\]\.name/.test(backgroundQml) &&
|
||||
backgroundQml.includes('audioEnabled: panel.firstScreen') &&
|
||||
!lockQml.includes('audioEnabled'),
|
||||
'a sound track plays from the first monitor only, a silent file builds no audio output, and the lock stays quiet'
|
||||
/^import Owe\.LockFeed$/m.test(lockFeedQml) &&
|
||||
lockFeedQml.includes('LockFeed {') &&
|
||||
lockQml.includes('source: "LockFeedSurface.qml"') &&
|
||||
lockQml.includes('active: root.feedActive') &&
|
||||
/feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml),
|
||||
'the lock screen shows video through the OWE lock feed, loaded so a missing module costs only the video'
|
||||
)
|
||||
assert(
|
||||
/property: "mediaSource"[\s\S]*?when: videoLoader\.item !== null && Util\.isVideoPath\(root\.path\)\s*\n\s*restoreMode: Binding\.RestoreNone/.test(mediaQml) &&
|
||||
!videoQml.includes('Component.onDestruction'),
|
||||
'a player on its way out keeps its source, so nothing is left loading for its destructor to cancel'
|
||||
!/^import Owe\./m.test(lockQml),
|
||||
'the lock view itself carries no foreign import, so the lock still loads without the feed module'
|
||||
)
|
||||
assert(
|
||||
lockQml.includes('path: root.loadBackground ? (root.video ? root.videoPosterPath : root.backgroundPath) : ""') &&
|
||||
lockQml.indexOf('id: feedLoader') > lockQml.indexOf('MultiEffect {') &&
|
||||
lockQml.includes('visible: root.video') &&
|
||||
!lockQml.includes('wallpaper.video'),
|
||||
'the lock screen keeps its image effect for stills and shows the feed for videos'
|
||||
)
|
||||
assert(
|
||||
!mediaQml.includes('mipmap'),
|
||||
@@ -86,33 +73,21 @@ assert(
|
||||
/instant \|\| !displayedBackground \|\| isVideo\(path\) \|\| isVideo\(displayedBackground\)[\s\S]*displayedBackground = finalPath/.test(backgroundQml),
|
||||
'video switches bypass the image-only reveal stack and use the durable background path'
|
||||
)
|
||||
assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share video-capable media rendering')
|
||||
assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share still rendering')
|
||||
assert(
|
||||
lockQml.includes('source: wallpaper.video ? null : wallpaper') &&
|
||||
lockQml.includes('visible: !wallpaper.video') &&
|
||||
lockQml.includes('visible: wallpaper.video'),
|
||||
'lock screen bypasses its image effect for video output'
|
||||
!backgroundQml.includes('videoEnabled') &&
|
||||
!backgroundQml.includes('displayedReloads') &&
|
||||
!mediaQml.includes('displayedReloads'),
|
||||
'the desktop hands video backgrounds to OWE with no player switch to carry'
|
||||
)
|
||||
assert(
|
||||
/sessionObscured:\s*lockActive \|\| screensaverActive/.test(backgroundQml) &&
|
||||
backgroundQml.includes('playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere') &&
|
||||
backgroundQml.includes('omarchy.lock') &&
|
||||
backgroundQml.includes('omarchy.idle') &&
|
||||
backgroundQml.includes('omarchy.battery'),
|
||||
'desktop playback stops while covered or on battery power-saver'
|
||||
)
|
||||
assert(
|
||||
backgroundQml.includes('Hyprland.monitorFor(modelData)') &&
|
||||
/fullscreenHere: visibleWorkspace \? visibleWorkspace\.hasFullscreen : false/.test(backgroundQml) &&
|
||||
!backgroundQml.includes('ToplevelManager.activeToplevel'),
|
||||
'a fullscreen window pauses only the output it covers, wherever focus is'
|
||||
)
|
||||
assert(
|
||||
/if \(displayedBackground === finalPath\) displayedReloads \+= 1/.test(backgroundQml) &&
|
||||
backgroundQml.includes('reloads: root.displayedReloads') &&
|
||||
/active: root\.path !== "" && root\.video && !root\.reloading/.test(mediaQml) &&
|
||||
/onReloadsChanged: \{[\s\S]*?reloading = true/.test(mediaQml),
|
||||
'a theme switch that keeps the video path still reopens the replaced file'
|
||||
!backgroundQml.includes('playbackEnabled') &&
|
||||
!backgroundQml.includes('sessionObscured') &&
|
||||
!backgroundQml.includes('fullscreenHere') &&
|
||||
!backgroundQml.includes('Hyprland.monitorFor') &&
|
||||
!backgroundQml.includes('omarchy.lock') &&
|
||||
!backgroundQml.includes('omarchy.battery'),
|
||||
'the desktop no longer carries the shell video pause policy'
|
||||
)
|
||||
assert(
|
||||
barTextColor.includes('magick "$background_path[0]"'),
|
||||
@@ -163,13 +138,12 @@ assert(
|
||||
'a locked video wallpaper follows what each panel actually did, not only what the lock asked for'
|
||||
)
|
||||
assert(
|
||||
lockView.includes('playbackEnabled: root.loadBackground && !root.displaysBlank') &&
|
||||
lockView.includes('&& !root.powerSaverActive') &&
|
||||
/feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml) &&
|
||||
/displaysBlank: root\.screenBlank\(/.test(lockService) &&
|
||||
/powerSaverActive: root\.powerSaverActive/.test(lockService) &&
|
||||
/function runBlank\(\) \{\s*\n\s*root\.displaysBlank = true/.test(lockService) &&
|
||||
/function runWake\(\) \{\s*\n\s*root\.displaysBlank = false/.test(lockService),
|
||||
'the lock screen stops playback once displays go dark or power-saver is active'
|
||||
'the lock feed stops once displays go dark or power-saver is active'
|
||||
)
|
||||
assert(
|
||||
batteryService.includes('property string activePowerProfile') &&
|
||||
@@ -186,10 +160,6 @@ assert(
|
||||
'theme switcher previews video-only themes, named preview files included'
|
||||
)
|
||||
assert(quattroUpgrade.includes("-iname '*.mp4'"), 'Quattro upgrade can seed a video-only theme background')
|
||||
assert(
|
||||
multimediaMigration.includes('omarchy-pkg-add qt6-multimedia qt6-multimedia-ffmpeg'),
|
||||
'existing Quattro installations receive video playback dependencies'
|
||||
)
|
||||
JS
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
@@ -303,8 +273,11 @@ timeout_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$timeout_cache" \
|
||||
timeout_marker=$(find "$timeout_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
|
||||
[[ -z $timeout_marker ]] || fail "a timed out video is left to retry rather than remembered as failed"
|
||||
|
||||
grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia runtime is a base package"
|
||||
grep -qx 'qt6-multimedia-ffmpeg' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia FFmpeg backend is a base package"
|
||||
grep -qx 'owe' "$ROOT/install/omarchy-base.packages" || fail "OWE is a base package"
|
||||
grep -qx 'owe-lockfeed' "$ROOT/install/omarchy-base.packages" || fail "the OWE lock feed module is a base package"
|
||||
if grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages"; then
|
||||
fail "Qt Multimedia is no longer needed by the shell"
|
||||
fi
|
||||
|
||||
pass "menu image generator creates thumbnails consumed by the picker"
|
||||
pass "direct picker generates and reuses still thumbnails"
|
||||
@@ -312,7 +285,7 @@ pass "direct picker omits videos whose thumbnails cannot be generated"
|
||||
pass "a rejected video is remembered so it costs nothing on the next open"
|
||||
pass "a timed out video is left to retry"
|
||||
pass "a locked video wallpaper follows the panels' real DPMS state"
|
||||
pass "Qt Multimedia playback dependencies are declared"
|
||||
pass "OWE and its lock feed module are declared"
|
||||
|
||||
source <(awk '
|
||||
/^(is_video_path|snapshot_background_path|background_transition_uses_snapshots|choose_theme_background|choose_staged_theme_background|set_theme_background)\(\) \{/ { copying=1 }
|
||||
|
||||
@@ -11,7 +11,7 @@ if [[ ${OMARCHY_WINDOWS_TEST_NAMESPACE:-0} != 1 ]]; then
|
||||
exec env OMARCHY_WINDOWS_TEST_NAMESPACE=1 \
|
||||
unshare --user --map-current-user --keep-caps --mount --propagation private bash "$0"
|
||||
fi
|
||||
pass "unprivileged mount namespaces unavailable; skipping Windows VM mount runtime tests"
|
||||
skip "unprivileged mount namespaces unavailable; skipping Windows VM mount runtime tests"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -241,7 +241,7 @@ race_swaps="$TMPDIR/concurrent-race-swaps"
|
||||
sleep 0.005
|
||||
fi
|
||||
done
|
||||
) &
|
||||
) >/dev/null &
|
||||
racer_pid=$!
|
||||
concurrent_dc_calls=0
|
||||
dc() {
|
||||
|
||||
@@ -8,7 +8,7 @@ if ((EUID != 0)); then
|
||||
if unshare --user --map-auto --map-root-user --mount true 2>/dev/null; then
|
||||
exec unshare --user --map-auto --map-root-user --mount --propagation private bash "$0"
|
||||
fi
|
||||
pass "automatic subordinate-id namespace unavailable; skipping root Windows VM boundary probe"
|
||||
skip "automatic subordinate-id namespace unavailable; skipping root Windows VM boundary probe"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
Reference in new issue
Block a user