diff --git a/bin/omarchy-agent-skill-link b/bin/omarchy-agent-skill-link new file mode 100755 index 00000000000..532cad525bd --- /dev/null +++ b/bin/omarchy-agent-skill-link @@ -0,0 +1,74 @@ +#!/bin/bash + +# omarchy:summary=Link a skill directory into every agent's skills directory, or take those links back +# omarchy:args= [--remove] +# omarchy:examples=omarchy agent skill-link $OMARCHY_PATH/default/agents/optional-skills/cua-omarchy | omarchy agent skill-link $OMARCHY_PATH/default/agents/optional-skills/cua-omarchy --remove + +# omarchy-provision-user links every skill under default/agents/skills for +# everyone. A skill that only makes sense once the tool it describes is on the +# machine lives under default/agents/optional-skills instead, and the tool's +# install flow links it here into the same agent directories; the matching +# remove flow takes the links back with --remove. + +set -euo pipefail + +usage() { + echo "Usage: omarchy-agent-skill-link [--remove]" >&2 + exit 1 +} + +skill_dir="" +remove=false +for arg in "$@"; do + case $arg in + --remove) remove=true ;; + -*) usage ;; + *) skill_dir=$arg ;; + esac +done + +[[ -n $skill_dir ]] || usage + +if [[ $remove == false && ! -f $skill_dir/SKILL.md ]]; then + echo "No skill at $skill_dir: expected a directory holding a SKILL.md." >&2 + exit 1 +fi + +skill_dir=$(realpath -m -- "$skill_dir") +name=${skill_dir##*/} + +# Hermes profiles are only linked when they already exist; nothing here +# creates one. +agent_dirs=( + "$HOME/.agents/skills" + "$HOME/.claude/skills" + "$HOME/.codex/skills" + "$HOME/.pi/agent/skills" + "$HOME/.gemini/config/skills" + "$HOME/.hermes/skills" +) +for profile in "$HOME"/.hermes/profiles/*/; do + [[ -d $profile ]] && agent_dirs+=("${profile}skills") +done + +for dir in "${agent_dirs[@]}"; do + link="$dir/$name" + + if [[ $remove == true ]]; then + # Only a link to this skill: a skill of the same name the user keeps + # there themselves is not ours to remove. + if [[ -L $link && $(realpath -m -- "$link") == "$skill_dir" ]]; then + rm -- "$link" + fi + elif [[ -e $link && ! -L $link ]]; then + # ln would otherwise drop the link inside the directory that is there. + echo "Leaving $link alone: it is not a symlink." >&2 + elif [[ -L $link && -e $link && $(realpath -m -- "$link") != "$skill_dir" ]]; then + # A live link to some other skill is the user's choice; only a dangling + # one, or one already pointing here, gets replaced. + echo "Leaving $link alone: it points elsewhere." >&2 + else + mkdir -p -- "$dir" + ln -sfn -- "$skill_dir" "$link" + fi +done diff --git a/bin/omarchy-install-ai-cua b/bin/omarchy-install-ai-cua new file mode 100755 index 00000000000..e88acce4fec --- /dev/null +++ b/bin/omarchy-install-ai-cua @@ -0,0 +1,76 @@ +#!/bin/bash + +# omarchy:summary=Install Cua Driver, its agent skills, and the Cua Hyprland plugin +# omarchy:requires-sudo=true + +set -euo pipefail + +if (( EUID == 0 )); then + echo "Run this command as your desktop user, without sudo." >&2 + exit 1 +fi + +echo "Installing Cua Driver..." +omarchy-pkg-add cua-driver-bin + +# The driver takes its native Wayland backend only when this reaches the +# process serving the calls. default/hypr/envs.lua exports it to every session +# from the next login; this hands it to the one already running. +systemctl --user set-environment CUA_DRIVER_RS_ENABLE_WAYLAND=1 2>/dev/null || true +dbus-update-activation-environment CUA_DRIVER_RS_ENABLE_WAYLAND=1 2>/dev/null || true + +# The skill pack is what teaches an agent the driver's snapshot-then-act loop: +# the driver fetches the release matching its own version and links it into +# every agent skills directory that exists, and Omarchy's companion adds the +# package and Hyprland specifics upstream cannot know. A skill is read into +# every session of every agent on the machine, so it is offered rather than +# imposed. Without a terminal to ask in, the driver alone is the answer. +skills_installed=false +if [[ -t 0 ]]; then + echo "" + echo "Cua ships skills that teach Claude Code, Codex, Hermes, and the other" + echo "agents on this machine how to drive apps through the driver." + if gum confirm "Install the Cua agent skills?"; then + echo "Installing the Cua agent skills..." + if cua-driver skills install; then + omarchy-agent-skill-link "$OMARCHY_PATH/default/agents/optional-skills/cua-omarchy" + skills_installed=true + else + echo "The Cua skills did not install. Run 'cua-driver skills install' later to try again." >&2 + fi + fi +fi + +# The Hyprland plugin gives the driver a compositor route for input. It is +# built against one exact Hyprland release, so it can be absent from the +# channel or present but unresolvable against the Hyprland that is installed +# (edge moves first), and neither undoes the driver install above. -Sp resolves +# the transaction without root, so the mismatch is named instead of surfacing +# as a failed sudo pacman run. Installing the plugin loads nothing: activation +# is a deliberate step, omarchy-toggle-cua-input, because it enables compositor input. +plugin_installed=false +echo "" +if ! pacman -Si cua-hyprland-plugin &>/dev/null; then + echo "The Cua Hyprland plugin is not available in your package channel; Cua Driver works without it." +elif ! pacman -Sp cua-hyprland-plugin &>/dev/null; then + pinned=$(pacman -Si cua-hyprland-plugin | grep -o 'hyprland=[^ ]*' | head -1) + echo "The Cua Hyprland plugin is built for ${pinned:-another Hyprland release} and this machine has hyprland=$(pacman -Q hyprland | cut -d' ' -f2); skipping it. Cua Driver works without it." +else + echo "Installing the Cua Hyprland plugin..." + if omarchy-pkg-add cua-hyprland-plugin; then + plugin_installed=true + else + echo "The Cua Hyprland plugin did not install. Cua Driver works without it." >&2 + fi +fi + +echo "" +echo "Cua Driver has been installed." +if [[ $skills_installed == true ]]; then + echo "Agents pick up the Cua skills in their next session." +fi +if [[ $plugin_installed == true ]]; then + echo "Trigger > Toggle > Cua Input enables the plugin without changing your keyboard settings." + echo "After a plugin update, log out and back in before enabling it. The plugin pins the exact" + echo "Hyprland release, so updates that move Hyprland wait until it is removed." +fi diff --git a/bin/omarchy-remove-ai-cua b/bin/omarchy-remove-ai-cua new file mode 100755 index 00000000000..3259d9e39bd --- /dev/null +++ b/bin/omarchy-remove-ai-cua @@ -0,0 +1,44 @@ +#!/bin/bash + +# omarchy:summary=Remove Cua Driver, the Cua Hyprland plugin, and the agent skills they installed. +# omarchy:requires-sudo=true + +# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /. +set -euo pipefail + +# The driver knows which agent directories it linked its skill pack into, so +# it takes those links back before the package providing the command goes; +# --all removes the local copy too. A machine that never installed the skills +# has nothing to undo, and the driver says so without failing. +if omarchy-cmd-present cua-driver; then + cua-driver skills uninstall --all +fi +omarchy-agent-skill-link "$OMARCHY_PATH/default/agents/optional-skills/cua-omarchy" --remove + +# The flag goes even when the plugin package went first: a mapped module keeps +# reading it until logout. The toggle tells the user if Hyprland did not take it. +if omarchy-hyprland-toggle-enabled cua-input; then + omarchy-toggle-cua-input off || true +fi + +plugin_removed=false +if omarchy-pkg-present cua-hyprland-plugin; then + # The module itself stays mapped for the rest of the session, as the package requires. + omarchy-pkg-drop cua-hyprland-plugin + plugin_removed=true +fi +omarchy-pkg-drop cua-driver-bin + +# Installation and telemetry identity, update checks, and the daemon's socket +# and logs: nothing here is the user's work. Recordings land wherever the +# recording was started, which is none of these. +rm -rf \ + "$HOME/.cua-driver" \ + "$HOME/.config/cua" \ + "$HOME/.cache/cua-driver" + +echo "" +echo "Cua Driver has been removed." +if [[ $plugin_removed == true ]]; then + echo "A loaded Hyprland plugin stays in memory until this session ends." +fi diff --git a/bin/omarchy-toggle-cua-input b/bin/omarchy-toggle-cua-input new file mode 100755 index 00000000000..9224bff8639 --- /dev/null +++ b/bin/omarchy-toggle-cua-input @@ -0,0 +1,198 @@ +#!/bin/bash + +# omarchy:summary=Toggle the Cua Hyprland plugin's input route +# omarchy:args=[on|off|toggle|--status|--load] +# omarchy:examples=omarchy toggle cua-input | omarchy toggle cua-input off | omarchy-toggle-cua-input --status + +# Activation checks the package and running module without replacing a mapped +# plugin. Turning input off leaves the module loaded until the session ends. + +set -euo pipefail + +FLAG=cua-input +FLAG_FILE="$HOME/.local/state/omarchy/toggles/hypr/$FLAG.lua" +MODULE=/usr/lib/cua/hyprland/cua-hyprland-plugin.so +KIT=/usr/share/cua-hyprland-plugin +MIN_VERSION=0.32.0-3 +# nf-md-robot_excited, escaped so this file reads without a Nerd Font. +readonly GLYPH=$'\U000f16a4' + +usage() { + echo "Usage: omarchy-toggle-cua-input [on|off|toggle|--status|--load]" >&2 + exit 1 +} + +notify() { + omarchy-notification-send -g "$GLYPH" "$@" || true +} + +# Removes the flag, then switches the live option off: a reload that kept the old +# settings, as a config syntax error does, still answers ok. Off only once both took. +flag_off() { + local live + omarchy-hyprland-toggle "$FLAG" off >/dev/null || true + live=$(hyprctl eval 'if hl.get_config("plugin.cua.enabled") ~= nil then hl.config({ plugin = { cua = { enabled = false } } }) end' 2>/dev/null) + [[ $live == ok && ! -e $FLAG_FILE ]] +} + +refuse() { + local title="Cua input stays off" message=$1 + if omarchy-hyprland-toggle-enabled "$FLAG" && ! flag_off; then + title="Cua input may still be on" + message+=" Hyprland did not take the change; log out and back in to turn it off." + fi + notify "$title" "$message" + echo "$message" >&2 + exit 1 +} + +plugin_loaded() { + hyprctl -j plugin list 2>/dev/null | jq -e 'map(select(.name == "cua-hyprland-plugin")) | length > 0' >/dev/null +} + +# A module that does not report these predates the installed package: it is still mapped from before an update. +module_current() { + hyprctl -j cua:status 2>/dev/null | + jq -e '.keyboard_layout_independent == true and .foreground_numlock_compatible == true and has("ime_popup_guard")' >/dev/null +} + +# Without it, fcitx5 restarting while input is on can crash Hyprland 0.56.2. +ime_guard_active() { + hyprctl -j cua:status 2>/dev/null | jq -e '.ime_popup_guard == true' >/dev/null +} + +package_current() { + local version comparison + version=$(pacman -Q cua-hyprland-plugin | cut -d' ' -f2) || return 1 + comparison=$(vercmp "$version" "$MIN_VERSION") || return 1 + (( comparison >= 0 )) +} + +input_ready() { + hyprctl -j cua:status 2>/dev/null | jq -e '.configured == true and .transport.ready == true' >/dev/null +} + +# The package's consumer check: the installed module against the running +# compositor and runtime, needing no compiler. It also wants the digest of the +# kit provenance it was built from; pacman verified that file when it +# installed it, so the digest is read from the file rather than pinned here, +# where it would go stale with every requalified profile. +compatible() { + local digest + digest=$(sha256sum "$KIT/KIT-PROVENANCE.json" | cut -d' ' -f1) + python3 "$KIT/profile_verify.py" --kit "$KIT" --kit-sha256 "$digest" --consumer "$MODULE" >/dev/null 2>&1 +} + +# Hyprland reloads its configuration when a plugin loads, so an enabled flag is +# held at enabled = false until the module passes; nocreat keeps a concurrent off. +hold_flag() { + sed 's/enabled = true/enabled = false/' "$OMARCHY_PATH/default/hypr/toggles/$FLAG.lua" | + dd of="$FLAG_FILE" conv=nocreat status=none 2>/dev/null && hyprctl reload >/dev/null +} + +load_plugin() { + if ! plugin_loaded; then + hyprctl plugin load "$MODULE" >/dev/null + fi +} + +print_status() { + local enabled=false loaded=false ready=false + omarchy-hyprland-toggle-enabled "$FLAG" && enabled=true + plugin_loaded && loaded=true + input_ready && ready=true + jq -cn --argjson enabled "$enabled" --argjson loaded "$loaded" --argjson ready "$ready" \ + '{enabled:$enabled, loaded:$loaded, ready:$ready}' +} + +turn_on() { + # Land the flag held off before anything else: an off while the checks run + # removes it, and the restore below never creates it again. + mkdir -p "$(dirname "$FLAG_FILE")" + sed 's/enabled = true/enabled = false/' "$OMARCHY_PATH/default/hypr/toggles/$FLAG.lua" >"$FLAG_FILE" || + refuse "Cua input's configuration could not be written." + hyprctl reload >/dev/null || refuse "Hyprland could not reload its configuration." + + omarchy-pkg-present cua-hyprland-plugin || + refuse "The Cua Hyprland plugin is not installed; Install > AI > Cua adds it." + + package_current || refuse "Update cua-hyprland-plugin to $MIN_VERSION or newer before turning Cua Input on." + + compatible || refuse "The Cua Hyprland plugin does not match the running Hyprland; update it and log out and back in." + load_plugin || refuse "The Cua Hyprland plugin could not load; check the session logs." + if ! module_current; then + refuse "Log out and back in to use the updated Cua plugin, then turn Cua Input on again." + fi + ime_guard_active || refuse "The Cua plugin could not install its input-method crash guard; hyprctl -j cua:status has the details." + + if ! dd if="$OMARCHY_PATH/default/hypr/toggles/$FLAG.lua" of="$FLAG_FILE" conv=nocreat status=none 2>/dev/null; then + [[ -e $FLAG_FILE ]] && refuse "Cua input's configuration could not be restored; turn it on again." + exit 0 + fi + hyprctl reload >/dev/null || refuse "Hyprland could not reload its configuration." + + if input_ready; then + notify "Cua input on" "Your keyboard layout and remaps stay unchanged." + else + notify "Cua plugin loaded, input not ready" "hyprctl -j cua:status has the details." + fi +} + +turn_off() { + if ! flag_off; then + notify "Cua input may still be on" "Hyprland did not take the change; log out and back in to turn it off." + exit 1 + fi + notify "Cua input off" "The plugin stays loaded until you log out." +} + +# Session-start failures take the flag back and say whether that took. +startup_off() { + if flag_off; then + notify "Cua input turned off" "$1" + else + notify "Cua input may still be on" "$1 Hyprland did not take the change; log out and back in to turn it off." + fi + exit 0 +} + +# What the flag runs at session start. +load_at_start() { + omarchy-hyprland-toggle-enabled "$FLAG" || exit 0 + + if ! omarchy-pkg-present cua-hyprland-plugin || ! package_current || ! compatible; then + startup_off "Update the Cua Hyprland plugin and check that it matches the running Hyprland." + fi + + if ! plugin_loaded && ! hold_flag; then + [[ -e $FLAG_FILE ]] && startup_off "Its configuration could not be held off while the plugin loaded; turn Cua Input on again." + exit 0 + fi + + load_plugin || startup_off "The Cua Hyprland plugin could not load; check the session logs." + module_current || startup_off "Log out and back in to use the updated plugin, then turn Cua Input on again." + ime_guard_active || startup_off "The Cua plugin could not install its input-method crash guard; hyprctl -j cua:status has the details." + + # Refresh legacy keyboard overrides, but never recreate a flag removed by + # a concurrent off while verification ran. nocreat still truncates the file. + if dd if="$OMARCHY_PATH/default/hypr/toggles/$FLAG.lua" of="$FLAG_FILE" conv=nocreat status=none 2>/dev/null; then + hyprctl reload >/dev/null || startup_off "Hyprland could not reload its configuration; turn Cua Input on again." + elif [[ -e $FLAG_FILE ]]; then + startup_off "Its configuration could not be refreshed; turn Cua Input on again." + fi +} + +case "${1:-toggle}" in + on) turn_on ;; + off) turn_off ;; + toggle) + if omarchy-hyprland-toggle-enabled "$FLAG"; then + turn_off + else + turn_on + fi + ;; + --status | status) print_status ;; + --load) load_at_start ;; + *) usage ;; +esac diff --git a/default/agents/optional-skills/cua-omarchy/SKILL.md b/default/agents/optional-skills/cua-omarchy/SKILL.md new file mode 100644 index 00000000000..5cbc3c3383c --- /dev/null +++ b/default/agents/optional-skills/cua-omarchy/SKILL.md @@ -0,0 +1,48 @@ +--- +name: cua-omarchy +description: Operate native applications with Cua Driver on Omarchy and Hyprland, or diagnose its packaged installation and Wayland session. Complements the upstream cua-driver skill; use for computer-use tasks, not general desktop customization or plugin development. +--- + +# Cua on Omarchy + +Read the companion upstream skill, `cua-driver`, and its Linux guide for the action loop and tool contracts; `cua-driver skills install` links it beside this one. This skill adds the Omarchy package and Hyprland checks. If the companion is missing, inspect the installed CLI's help and schemas, and install its skills when setup is part of the requested task. + +## Establish the running driver + +Omarchy installs the driver as the `cua-driver-bin` package, so `command -v cua-driver` should resolve to `/usr/bin/cua-driver`, a link to `/usr/lib/cua-driver/cua-driver`. For setup, diagnosis, or a changed installation, check `readlink -f` of that path, `cua-driver --version`, and `pacman -Q cua-driver-bin`; `pacman -Qo` on the resolved executable establishes package ownership. A user-local link may legitimately point at the packaged binary, so its location alone does not identify a custom build. + +Check `cua-driver status` and the executable of the actual serving process when runtime behavior disagrees with the CLI: an older local service or an existing MCP connection can still run a different binary. Inspect the relevant unit names and executable paths; do not dump whole process environments or configuration files. A skill refresh does not reconnect MCP clients or change their configured executable. + +An executable link ending in `(deleted)` means its inode was replaced, not necessarily that the running bytes differ. Compare `sha256sum /proc//exe` with the resolved packaged executable before diagnosing version drift or proposing a restart. + +Use the desktop user's Wayland session and session bus. Omarchy exports `CUA_DRIVER_RS_ENABLE_WAYLAND=1` to every session, so a driver started from the session takes the native Wayland backend; a daemon started anywhere else needs it set explicitly, and exporting it in a new shell does not change an already running daemon. Inspect a service's configuration before changing it, and restart only the service the task covers. + +Run `cua-driver doctor` or `cua-driver call health_report '{}'` for diagnostics. Passing AT-SPI and Wayland checks establishes prerequisites, not successful window capture, input, or focus preservation. If a command is unavailable, consult `cua-driver --help` and `cua-driver describe ` for this installation. + +## Target and verify on Hyprland + +Obtain the current `pid` and `window_id` from `list_windows` and select the intended window explicitly. Hyprland addresses from `hyprctl -j clients` help with diagnosis but are not interchangeable with Cua's window IDs. After launching an application, enumerate its windows rather than assuming the launch result includes them. + +Take `get_window_state` for that exact target before acting. Prefer a returned `element_token`, or use `element_index` with its matching `snapshot_id`. Re-snapshot after an action and use the new handles. Keep a multi-call workflow on one persistent MCP connection as described upstream; a repeated session label across one-shot CLI processes does not preserve its lifecycle. Use only session parameters the current schemas advertise, and end sessions the workflow owns when finished. + +To exercise a running service over persistent MCP, pass the endpoint reported by `cua-driver status` to `cua-driver mcp --socket `. Bare `mcp` on Linux owns a separate runtime, so its results say nothing about that service. + +Read `hyprctl -j monitors` when diagnosing geometry: negative desktop origins are valid, and fractional scale means compositor coordinates and screenshot pixels are different domains. Use coordinates from the returned window PNG directly for window pixel actions; do not add monitor offsets or multiply by scale. If the display layout changes, take a fresh snapshot. A null `z_index`, `is_on_screen`, or a launch result's `active` flag does not establish keyboard focus; use the compositor's active window for diagnostic comparison. + +Hyprland's capture and virtual-input protocols do not guarantee exact capture of an arbitrary window or background raw input. If the driver cannot establish the target surface, use the accessibility result when it suffices; do not treat an output crop as proof of that window's pixels or dispatch pixel actions against unproven capture. + +Honor `background_unavailable`, `surface_identity_unproven`, and other capability refusals. Foreground delivery can affect focus, workspace, and the real pointer, so use it within the user's existing foreground authorization or obtain it. Do not bypass a refusal with `hyprctl` focus dispatches or generic input injection. Verify the requested postcondition from fresh state; a delivery acknowledgment alone is insufficient. + +An `unverifiable` result with a foreground escalation hint is not proof that input failed: a native GTK entry can show the typed text while its fresh AT-SPI tree still omits the value. Inspect a fresh screenshot before repeating text or advancing the delivery mode. If the pixels already satisfy the task, stop. If capture is unavailable, retain the uncertainty instead of replaying input that may have been delivered. + +## The Hyprland plugin + +Omarchy's Cua install also installs the `cua-hyprland-plugin` package when the package channel carries it. Installing it loads nothing: the module sits at `/usr/lib/cua/hyprland/cua-hyprland-plugin.so` until `omarchy-toggle-cua-input` (Trigger > Toggle > Cua Input) runs the package's compatibility check, loads it, and turns on its input route. This requires plugin 0.32.0-3 or newer: its background agent keyboards have independent US keymaps, the user's layout and remaps stay unchanged, and it keeps a restarting fcitx5 from crashing Hyprland 0.56.2 while input is on. Plain pointer operations do not require a US layout. Num Lock remains unchanged and is allowed for foreground keys whose meaning it does not affect. Caps Lock, held modifiers, unsupported layout groups, and incompatible key semantics can still prevent typing. Foreground keys still depend on the user's mapping and can refuse operations whose key or modifiers have a different meaning; this is not arbitrary-layout or Unicode/IME support. `omarchy-toggle-cua-input --status` reports enabled, loaded, and ready. Check `hyprctl -j plugin list` and `hyprctl -j cua:status` before attributing any behavior to the plugin; a loaded module, a healthy probe, and successful delivery are separate evidence. The background input route admits only the applications and exact versions Cua qualified (Inkscape 1.4.4-6 on stable at the time of writing); for anything else `background_unavailable` is the contract, not a fault to work around. Do not load, unload, or reconfigure the plugin by other means as part of a computer-use task: turning it on enables compositor input, and hot replacing a mapped module is unsupported. After upgrading a loaded plugin, log out and back in; the toggle refuses a loaded module without independent agent keymaps, the Num Lock compatibility fix, or an active input-method guard, and never unloads it. Older copied toggle flags are refreshed when enabled or loaded at session start to remove their keyboard overrides. + +## Maintain the package and skills separately + +The packaged driver updates through `omarchy update`; its own self-updater is disabled and declines. Do not replace it with a vendor bootstrap or a remembered custom-build path to repair a skills problem. + +Run skills management as the desktop user: `cua-driver skills status` reports the upstream pack and its agent links, and `cua-driver skills update` refreshes that pack in place. The installer removes its destination before downloading a replacement, so back up local edits to the upstream directory first, and keep this companion out of it: this skill is a link into `$OMARCHY_PATH` and updates with Omarchy. Newly installed skills may need a new agent session to be discovered. + +For tests, use a disposable application and keep screenshots limited to that target. Record discovery, capture, semantic input, raw input, and focus/cursor checks separately. Input into an already focused window does not prove input to an unfocused or occluded window, and before/after focus samples do not prove the absence of transient focus changes. diff --git a/default/fonts/omarchy/README.md b/default/fonts/omarchy/README.md index df93aa555ae..7375a1691f3 100644 --- a/default/fonts/omarchy/README.md +++ b/default/fonts/omarchy/README.md @@ -17,6 +17,7 @@ The private-use glyphs in `omarchy.ttf` are: - `U+E90C` — OpenClaw, traced from the lobster mascot the openclaw package ships as `dist/control-ui/favicon.svg`, since upstream publishes no monochrome SVG - `U+E90D` — Cursor, from - `U+E90E` — Claude, from +- `U+E90F` — Cua, the koala mark from , taken from with its seven paths merged into one in [cua.svg](cua.svg) so the font tool would take it The agent marks are monochrome so the menu can render them using the active theme's foreground and selection colors. diff --git a/default/fonts/omarchy/cua.svg b/default/fonts/omarchy/cua.svg new file mode 100644 index 00000000000..9954f664a82 --- /dev/null +++ b/default/fonts/omarchy/cua.svg @@ -0,0 +1,3 @@ + + + diff --git a/default/fonts/omarchy/omarchy.ttf b/default/fonts/omarchy/omarchy.ttf index 3c70d8031f1..c28a0b998db 100644 Binary files a/default/fonts/omarchy/omarchy.ttf and b/default/fonts/omarchy/omarchy.ttf differ diff --git a/default/hypr/envs.lua b/default/hypr/envs.lua index 4eb9c94522d..1ad47d624ae 100644 --- a/default/hypr/envs.lua +++ b/default/hypr/envs.lua @@ -24,6 +24,11 @@ hl.env("XDG_SESSION_DESKTOP", "Hyprland") -- Use XCompose file. hl.env("XCOMPOSEFILE", paths.home .. "/.XCompose") +-- Cua Driver only takes its native Wayland backend when told to, and on a +-- Wayland-only desktop that is the backend to have, so every session says so +-- whether or not the driver is installed yet. +hl.env("CUA_DRIVER_RS_ENABLE_WAYLAND", "1") + -- hyprctl setenv doesn't reach keybind dispatcher env; use hl.env. hl.env("OMARCHY_PATH", paths.omarchy_path) diff --git a/default/hypr/toggles/cua-input.lua b/default/hypr/toggles/cua-input.lua new file mode 100644 index 00000000000..2255e142659 --- /dev/null +++ b/default/hypr/toggles/cua-input.lua @@ -0,0 +1,10 @@ +-- Agent keyboards own their keymaps; this flag leaves human input unchanged. +-- The option exists only while the module is loaded; setting it earlier is a config error at login. +if hl.get_config("plugin.cua.enabled") ~= nil then + hl.config({ plugin = { cua = { enabled = true } } }) +end + +-- A mapped module does not survive logout; verify it again each session. +hl.on("hyprland.start", function() + hl.exec_cmd("omarchy-toggle-cua-input --load") +end) diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 6cd399f3458..91913b005f5 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -99,6 +99,7 @@ "trigger.toggle.window-gaps": {"icon":"","label":"Window Gaps","action":"omarchy-hyprland-window-gaps-toggle"}, "trigger.toggle.animations": {"icon":"󰕟","label":"Animations","action":"omarchy-toggle-animations"}, "trigger.toggle.one-window-ratio": {"icon":"","label":"1-Window Ratio","action":"omarchy-hyprland-window-single-square-aspect-toggle"}, + "trigger.toggle.cua-input": {"icon":"","iconFont":"omarchy","label":"Cua Input","when":"omarchy-pkg-present cua-hyprland-plugin","action":"omarchy-toggle-cua-input"}, "trigger.tests.network-speedtest": {"icon":"󰓅","label":"Network Speed Test","action":"omarchy-shell shell summon omarchy.speedtest"}, "trigger.tests.disk-speedtest": {"icon":"󰋊","label":"Disk Speed Test","action":"omarchy-shell shell summon omarchy.disk-speedtest"}, @@ -247,6 +248,7 @@ "install.terminal.kitty": {"icon":"","label":"Kitty","disabled":"omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"}, "install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"}, "install.ai.claude": {"icon":"","iconFont":"omarchy","label":"Claude Desktop","disabled":"omarchy-pkg-present claude-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-claude"}, + "install.ai.cua": {"icon":"","iconFont":"omarchy","label":"Cua","disabled":"omarchy-pkg-present cua-driver-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-cua"}, "install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"}, "install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"}, "install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","disabled":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"}, @@ -318,6 +320,7 @@ "remove.service.ssh-agent": {"icon":"󰣀","label":"SSH Agent","when":"systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-service-ssh-agent"}, "remove.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","when":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-chatgpt"}, "remove.ai.claude": {"icon":"","iconFont":"omarchy","label":"Claude Desktop","when":"omarchy-pkg-present claude-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-claude"}, + "remove.ai.cua": {"icon":"","iconFont":"omarchy","label":"Cua","when":"omarchy-pkg-present cua-driver-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-cua"}, "remove.ai.dictation": {"icon":"","label":"Dictation","when":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-remove"}, "remove.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"omarchy-pkg-present grok-bot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-grok-bot"}, "remove.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"omarchy-pkg-present lmstudio-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-lm-studio"}, diff --git a/docs/file-layout.md b/docs/file-layout.md index 5caaba5669d..128ba1dc477 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -204,7 +204,7 @@ Runs once per user. It does **not** copy `~/.config/**`, `~/.bashrc`, `flags.lua`, or the nautilus extensions — `/etc/skel` already seeded those. It only does the things `/etc/skel` can't: -- Skill symlinks into `~/.agents/skills/`, `~/.claude/skills/`, `~/.codex/skills/`, `~/.pi/agent/skills/`, `~/.gemini/config/skills/` (Antigravity), `~/.hermes/skills/`, and each existing `~/.hermes/profiles/*/skills/` → `$OMARCHY_PATH/default/agents/skills/`, looping over every skill directory there (currently `omarchy` and `diagnose-crash`) so new skills need no edit. Symlinks (not copies) so `omarchy dev link` against a dev checkout repoints them correctly. Hermes profile dirs are only linked when they already exist — provision does not create Hermes profiles. +- Skill symlinks into `~/.agents/skills/`, `~/.claude/skills/`, `~/.codex/skills/`, `~/.pi/agent/skills/`, `~/.gemini/config/skills/` (Antigravity), `~/.hermes/skills/`, and each existing `~/.hermes/profiles/*/skills/` → `$OMARCHY_PATH/default/agents/skills/`, looping over every skill directory there (currently `omarchy` and `diagnose-crash`) so new skills need no edit. Symlinks (not copies) so `omarchy dev link` against a dev checkout repoints them correctly. Hermes profile dirs are only linked when they already exist — provision does not create Hermes profiles. A skill that only makes sense once an optional tool is on the machine lives under `default/agents/optional-skills/` instead (currently `cua-omarchy`): the tool's install flow links it into the same directories through `omarchy-agent-skill-link`, and its remove flow takes the links back with `--remove`. - `xdg-user-dirs-update` (Templates/Public/Desktop folded back into `$HOME`) and `~/.config/gtk-3.0/bookmarks` (needs `$HOME` expansion). - Hyprland's package-owned default input reads `XKBLAYOUT` / `XKBVARIANT` diff --git a/manual/17-ai.md b/manual/17-ai.md index aa661fff186..a8b17835bb7 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -68,6 +68,10 @@ Hermes Desktop is the one to know about, because a machine has one Hermes, and i OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw lives in `~/.openclaw` and updates itself: use the Control UI's "Update Gateway" button or run `openclaw update`, and the gateway restarts on the new version. Omarchy only chooses the release a fresh install starts from. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside OpenClaw itself and the plugin runtimes it downloads; the default keeps it, and installing OpenClaw again picks it back up. +### Computer use with Cua + +[Cua Driver](https://cua.ai/docs/cua-driver) lets a coding agent operate the apps on your desktop: it snapshots a window's accessibility tree, acts on the elements it finds there, and verifies the result from fresh state. _Install > AI > Cua_ installs the driver and then offers to install the Cua agent skills, which teach Claude Code, Codex, Hermes, and the other agents the driver's loop, together with an Omarchy companion skill covering the Hyprland and package specifics. Say no and the driver is installed on its own; `cua-driver skills install` adds the skills later. Where your package channel carries the Cua Hyprland plugin, the install adds that too, but never loads it. _Trigger > Toggle > Cua Input_ (or `omarchy toggle cua-input`) runs the package's compatibility check, loads the plugin, and switches on its input route; it needs plugin 0.32.0-3 or newer, which keeps your keyboard layout and remaps unchanged while its background agent keyboards use their own US keymaps, and keeps a restarting input method from crashing Hyprland. Num Lock stays unchanged and does not block keys whose meaning it leaves unaffected. Foreground typing can still refuse incompatible keys or shortcuts, Caps Lock, held modifiers, or unsupported layout groups. After a plugin update, log out and back in before enabling it. The plugin is built against one exact Hyprland release, and while it is installed an update that moves Hyprland waits until the plugin is removed. _Remove > AI > Cua_ takes the driver, the plugin, the skills, and the driver's own state back out. + ### Local LLMs Omarchy recommends two ways of running local LLM models: LM Studio and Ollama. LM Studio provides a GUI interface for finding open-weight models, installing them, and running them. It's a great way to get going easily. Ollama offers a CLI for doing so similarly. But if you're new to local models, I'd start with LM Studio. You can install either under _Install > AI_ in the Omarchy Menu. diff --git a/test/shell.d/agent-skill-link-test.sh b/test/shell.d/agent-skill-link-test.sh new file mode 100644 index 00000000000..5da1f95ea75 --- /dev/null +++ b/test/shell.d/agent-skill-link-test.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +link="$ROOT/bin/omarchy-agent-skill-link" +skill="$tmp_dir/skills/example" +mkdir -p "$skill" +printf -- '---\nname: example\n---\n' >"$skill/SKILL.md" + +agent_dirs=(.agents/skills .claude/skills .codex/skills .pi/agent/skills .gemini/config/skills .hermes/skills) + +fresh_home() { + rm -rf "$tmp_dir/home" + mkdir -p "$tmp_dir/home" + export HOME="$tmp_dir/home" +} + +# Every agent directory gets the link, created where it is missing, and an +# existing Hermes profile is covered without a profile being invented. +fresh_home +mkdir -p "$HOME/.hermes/profiles/work" +"$link" "$skill" + +for dir in "${agent_dirs[@]}" .hermes/profiles/work/skills; do + [[ -L $HOME/$dir/example && $(realpath "$HOME/$dir/example") == "$(realpath "$skill")" ]] || + fail "skill link reaches every agent skills directory" "$dir" +done +pass "skill link reaches every agent skills directory" + +[[ ! -e $HOME/.hermes/profiles/default ]] || fail "skill link does not invent Hermes profiles" +pass "skill link does not invent Hermes profiles" + +# A real directory of the same name is the user's, not a place to drop a link. +fresh_home +mkdir -p "$HOME/.codex/skills/example" +touch "$HOME/.codex/skills/example/SKILL.md" +"$link" "$skill" 2>/dev/null + +[[ ! -L $HOME/.codex/skills/example && ! -e $HOME/.codex/skills/example/example ]] || + fail "skill link leaves a user's directory of the same name alone" +[[ -L $HOME/.claude/skills/example ]] || fail "skill link still reaches the other agent directories" +pass "skill link leaves a user's directory of the same name alone" + +# --remove takes back only links to this skill: one of the same name pointing +# somewhere else stays. +fresh_home +mkdir -p "$tmp_dir/theirs" "$HOME/.claude/skills" +ln -s "$tmp_dir/theirs" "$HOME/.claude/skills/example" +"$link" "$skill" +"$link" "$skill" --remove + +for dir in .agents/skills .codex/skills .pi/agent/skills .gemini/config/skills .hermes/skills; do + [[ ! -e $HOME/$dir/example && ! -L $HOME/$dir/example ]] || fail "skill link --remove takes back the links it made" "$dir" +done +pass "skill link --remove takes back the links it made" + +[[ -L $HOME/.claude/skills/example && $(readlink "$HOME/.claude/skills/example") == "$tmp_dir/theirs" ]] || + fail "skill link --remove keeps a foreign link of the same name" +pass "skill link --remove keeps a foreign link of the same name" + +# Removing what was never linked is not an error. +fresh_home +"$link" "$skill" --remove || fail "skill link --remove succeeds with nothing to remove" +pass "skill link --remove succeeds with nothing to remove" + +# Linking needs a skill to point at. +fresh_home +if "$link" "$tmp_dir/nowhere" 2>/dev/null; then + fail "skill link refuses a directory without a SKILL.md" +fi +pass "skill link refuses a directory without a SKILL.md" diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index fe0da845709..f4e3ac2ee3f 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -664,5 +664,5 @@ assert( JS font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf") -[[ $font_charset == *"e900-e90e"* ]] || fail "Omarchy icon font includes every custom menu glyph" +[[ $font_charset == *"e900-e90f"* ]] || fail "Omarchy icon font includes every custom menu glyph" pass "Omarchy icon font includes the official agent marks" diff --git a/test/shell.d/remove-ai-test.sh b/test/shell.d/remove-ai-test.sh index 03887c53f3d..21335a1f2dc 100644 --- a/test/shell.d/remove-ai-test.sh +++ b/test/shell.d/remove-ai-test.sh @@ -40,7 +40,9 @@ export TEST_GUM_LOG="$tmp_dir/gum-log" touch "$TEST_GUM_LOG" export TEST_LOG="$tmp_dir/log" -export PATH="$tmp_dir/bin:$PATH" +# The checkout's own helpers come after the stubs but ahead of any installed +# copy, so a remover that leans on a helper shipped here finds this one. +export PATH="$tmp_dir/bin:$ROOT/bin:$PATH" fresh_home() { rm -rf "$tmp_dir/home" @@ -400,3 +402,91 @@ rc=0 ! grep -q '^drop:openclaw$' "$TEST_LOG" || fail "OpenClaw removal aborts when systemd cannot be reached" "package dropped anyway" pass "OpenClaw removal aborts when systemd cannot be reached" + +# Cua's skill pack is the driver's own to unlink, so the driver is asked before +# its package goes; Omarchy's companion link goes too, but a skill of the same +# name the user placed themselves does not. The driver and package presence are +# stubbed: neither is on the machine running this. +cat >"$tmp_dir/bin/cua-driver" <<'SCRIPT' +#!/bin/bash +printf 'cua-driver:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +chmod +x "$tmp_dir/bin/cua-driver" + +cat >"$tmp_dir/bin/omarchy-pkg-present" <<'SCRIPT' +#!/bin/bash +for pkg in "$@"; do + [[ " ${TEST_PRESENT_PKGS-} " == *" $pkg "* ]] || exit 1 +done +SCRIPT +chmod +x "$tmp_dir/bin/omarchy-pkg-present" + +# The real toggle would reload Hyprland; the flag it owns is what is checked. +cat >"$tmp_dir/bin/omarchy-toggle-cua-input" <<'SCRIPT' +#!/bin/bash +printf 'cua-input:%s\n' "$*" >>"$TEST_LOG" +rm -f "$HOME/.local/state/omarchy/toggles/hypr/cua-input.lua" +exit "${TEST_TOGGLE_STATUS:-0}" +SCRIPT +chmod +x "$tmp_dir/bin/omarchy-toggle-cua-input" + +fresh_home +mkdir -p "$HOME/.cua-driver/skills" "$HOME/.config/cua" "$HOME/.cache/cua-driver" \ + "$HOME/.agents/skills" "$HOME/.claude/skills" "$tmp_dir/their-cua-omarchy" \ + "$HOME/.local/state/omarchy/toggles/hypr" +touch "$HOME/.local/state/omarchy/toggles/hypr/cua-input.lua" +ln -s "$ROOT/default/agents/optional-skills/cua-omarchy" "$HOME/.agents/skills/cua-omarchy" +ln -s "$tmp_dir/their-cua-omarchy" "$HOME/.claude/skills/cua-omarchy" +: >"$TEST_LOG" +OMARCHY_PATH="$ROOT" TEST_PRESENT_PKGS="cua-hyprland-plugin" "$ROOT/bin/omarchy-remove-ai-cua" >/dev/null + +[[ $(head -1 "$TEST_LOG") == "cua-driver:skills uninstall --all" ]] || + fail "Cua removal lets the driver take back its skill pack before the package goes" "$(cat "$TEST_LOG")" +pass "Cua removal lets the driver take back its skill pack before the package goes" + +grep -q '^drop:cua-hyprland-plugin$' "$TEST_LOG" && grep -q '^drop:cua-driver-bin$' "$TEST_LOG" || + fail "Cua removal drops the plugin and the driver" "$(cat "$TEST_LOG")" +pass "Cua removal drops the plugin and the driver" + +# The input toggle is switched off, and before the plugin package goes. +[[ $(grep -n '^cua-input:off$' "$TEST_LOG" | cut -d: -f1) -lt $(grep -n '^drop:cua-hyprland-plugin$' "$TEST_LOG" | cut -d: -f1) ]] || + fail "Cua removal turns the input toggle off before dropping the plugin" "$(cat "$TEST_LOG")" +[[ ! -e $HOME/.local/state/omarchy/toggles/hypr/cua-input.lua ]] || + fail "Cua removal turns the input toggle off before dropping the plugin" "flag kept" +pass "Cua removal turns the input toggle off before dropping the plugin" + +[[ ! -e $HOME/.agents/skills/cua-omarchy && ! -L $HOME/.agents/skills/cua-omarchy ]] || + fail "Cua removal unlinks the Omarchy companion skill" +pass "Cua removal unlinks the Omarchy companion skill" + +[[ $(readlink "$HOME/.claude/skills/cua-omarchy") == "$tmp_dir/their-cua-omarchy" ]] || + fail "Cua removal keeps a companion skill the user placed themselves" +pass "Cua removal keeps a companion skill the user placed themselves" + +for gone in .cua-driver .config/cua .cache/cua-driver; do + [[ ! -e $HOME/$gone ]] || fail "Cua removal deletes the driver's own state" "$gone" +done +pass "Cua removal deletes the driver's own state" + +# Without the plugin installed there is nothing of it to drop, and no toggle to touch. +: >"$TEST_LOG" +fresh_home +OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-remove-ai-cua" >/dev/null +! grep -q '^drop:cua-hyprland-plugin$' "$TEST_LOG" || fail "Cua removal drops the plugin only when it is installed" +! grep -q '^cua-input:' "$TEST_LOG" || fail "Cua removal drops the plugin only when it is installed" "toggle touched" +grep -q '^drop:cua-driver-bin$' "$TEST_LOG" || fail "Cua removal drops the plugin only when it is installed" "driver kept" +pass "Cua removal drops the plugin only when it is installed" + +# A plugin package removed on its own leaves the flag behind; removal still +# turns it off, and goes on removing the driver even if Hyprland did not take it. +for toggle_status in 0 1; do + : >"$TEST_LOG" + fresh_home + mkdir -p "$HOME/.local/state/omarchy/toggles/hypr" + touch "$HOME/.local/state/omarchy/toggles/hypr/cua-input.lua" + OMARCHY_PATH="$ROOT" TEST_TOGGLE_STATUS=$toggle_status "$ROOT/bin/omarchy-remove-ai-cua" >/dev/null + grep -q '^cua-input:off$' "$TEST_LOG" || fail "Cua removal turns off a flag the plugin package left (toggle $toggle_status)" "$(cat "$TEST_LOG")" + ! grep -q '^drop:cua-hyprland-plugin$' "$TEST_LOG" || fail "Cua removal turns off a flag the plugin package left (toggle $toggle_status)" "dropped an absent package" + grep -q '^drop:cua-driver-bin$' "$TEST_LOG" || fail "Cua removal turns off a flag the plugin package left (toggle $toggle_status)" "driver kept" + pass "Cua removal turns off a flag the plugin package left (toggle $toggle_status)" +done diff --git a/test/shell.d/toggle-cua-input-test.sh b/test/shell.d/toggle-cua-input-test.sh new file mode 100644 index 00000000000..ea4b826a3bb --- /dev/null +++ b/test/shell.d/toggle-cua-input-test.sh @@ -0,0 +1,500 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +mkdir -p "$tmp_dir/bin" +export TEST_LOG="$tmp_dir/log" + +# hyprctl answers from the environment; loads and reloads are logged. +cat >"$tmp_dir/bin/hyprctl" <<'SCRIPT' +#!/bin/bash +case "$*" in + "-j plugin list") + if [[ ${TEST_PLUGIN_LOADED:-0} == 1 ]]; then echo '[{"name":"cua-hyprland-plugin","handle":"1"}]'; else echo '[]'; fi ;; + "-j cua:status") + if [[ ${TEST_CUA_READY:-0} == 1 ]]; then ready=true; else ready=false; fi + numlock_capability="" + if [[ ${TEST_NUMLOCK_COMPATIBLE:-true} != missing ]]; then + numlock_capability=",\"foreground_numlock_compatible\":${TEST_NUMLOCK_COMPATIBLE:-true}" + fi + guard_capability="" + if [[ ${TEST_IME_GUARD:-true} != missing ]]; then + guard_capability=",\"ime_popup_guard\":${TEST_IME_GUARD:-true}" + fi + printf '{"configured":%s,"transport":{"ready":%s},"keyboard_layout_independent":%s%s%s}\n' "$ready" "$ready" "${TEST_LAYOUT_INDEPENDENT:-true}" "$numlock_capability" "$guard_capability" ;; + "-j getoption input:kb_layout") printf '{"str":"%s"}\n' "${TEST_LAYOUT:-us}" ;; + "-j getoption input:kb_variant") printf '{"str":"%s"}\n' "${TEST_VARIANT:-}" ;; + "plugin load "*) + printf 'hyprctl:%s\n' "$*" >>"$TEST_LOG" + if grep -q 'enabled = false' "$HOME/.local/state/omarchy/toggles/hypr/cua-input.lua" 2>/dev/null; then echo 'flag-at-load:held' >>"$TEST_LOG"; fi + exit "${TEST_LOAD_STATUS:-0}" ;; + "eval "*) + printf 'hyprctl:%s\n' "$*" >>"$TEST_LOG" + if [[ ${TEST_EVAL_STATUS:-0} == 0 ]]; then echo ok; else echo "error: eval failed"; fi ;; + reload) + printf 'hyprctl:%s\n' "$*" >>"$TEST_LOG" + reloads=$(($(cat "$TEST_LOG.reloads" 2>/dev/null || echo 0) + 1)) + echo "$reloads" >"$TEST_LOG.reloads" + if [[ -n ${TEST_RELOAD_FAIL_FROM:-} ]] && ((reloads >= TEST_RELOAD_FAIL_FROM)); then exit 1; fi + exit "${TEST_RELOAD_STATUS:-0}" ;; + *) printf 'hyprctl:%s\n' "$*" >>"$TEST_LOG" ;; +esac +SCRIPT + +# dd fails on demand for copies from a file, which is how the flag is restored. +cat >"$tmp_dir/bin/dd" <<'SCRIPT' +#!/bin/bash +if [[ ${TEST_RESTORE_FAIL:-0} == 1 && " $* " == *" if="* ]]; then exit 1; fi +exec /usr/bin/dd "$@" +SCRIPT + +# The package's consumer check and the digest it takes: pass or fail on demand. +cat >"$tmp_dir/bin/python3" <<'SCRIPT' +#!/bin/bash +printf 'python3:%s\n' "$*" >>"$TEST_LOG" +if [[ -n ${TEST_VERIFY_PAUSE:-} ]]; then + touch "$TEST_VERIFY_PAUSE.ready" + for ((attempt = 0; attempt < 500; attempt++)); do + [[ -e $TEST_VERIFY_PAUSE.release ]] && break + sleep 0.01 + done + [[ -e $TEST_VERIFY_PAUSE.release ]] || exit 1 +fi +exit "${TEST_VERIFY_STATUS:-0}" +SCRIPT +cat >"$tmp_dir/bin/sha256sum" <<'SCRIPT' +#!/bin/bash +echo "0000000000000000000000000000000000000000000000000000000000000000 $1" +SCRIPT + +cat >"$tmp_dir/bin/pacman" <<'SCRIPT' +#!/bin/bash +[[ $* == "-Q cua-hyprland-plugin" ]] || exit 1 +if [[ -n ${TEST_PACMAN_PAUSE:-} ]]; then + touch "$TEST_PACMAN_PAUSE.ready" + for ((attempt = 0; attempt < 500; attempt++)); do + [[ -e $TEST_PACMAN_PAUSE.release ]] && break + sleep 0.01 + done +fi +printf 'cua-hyprland-plugin %s\n' "${TEST_PLUGIN_VERSION:-0.32.0-3}" +SCRIPT + +cat >"$tmp_dir/bin/omarchy-pkg-present" <<'SCRIPT' +#!/bin/bash +[[ ${TEST_PLUGIN_INSTALLED:-1} == 1 ]] +SCRIPT + +cat >"$tmp_dir/bin/omarchy-notification-send" <<'SCRIPT' +#!/bin/bash +printf 'notify:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +chmod +x "$tmp_dir/bin/"* + +export PATH="$tmp_dir/bin:$ROOT/bin:$PATH" +export OMARCHY_PATH="$ROOT" + +toggle="$ROOT/bin/omarchy-toggle-cua-input" +flag_file() { echo "$HOME/.local/state/omarchy/toggles/hypr/cua-input.lua"; } + +fresh_home() { + rm -rf "$tmp_dir/home" + mkdir -p "$tmp_dir/home" + export HOME="$tmp_dir/home" + : >"$TEST_LOG" +} + +# Nothing to turn on without the package. +fresh_home +rc=0 +TEST_PLUGIN_INSTALLED=0 "$toggle" on 2>/dev/null || rc=$? +[[ $rc != 0 && ! -e $(flag_file) ]] || fail "cua input refuses without the plugin package" +grep -q '^notify:.*Cua input stays off' "$TEST_LOG" || fail "cua input refuses without the plugin package" "no notification" +pass "cua input refuses without the plugin package" + +# Neither non-US layouts nor XKB variants prevent enabling background input. +for layout in dk us; do + fresh_home + TEST_LAYOUT="$layout" TEST_VARIANT=intl "$toggle" on + [[ -e $(flag_file) ]] || fail "cua input accepts $layout with remaps" + pass "cua input accepts $layout with remaps" +done + +# Executing the shipped Lua must leave the user's keyboard settings intact, and +# must not name the plugin's option before the module is loaded: at login that +# is an unknown config key, which Hyprland reports as a config error. +require_command lua +for loaded in false true; do + lua - "$ROOT/default/hypr/toggles/cua-input.lua" "$loaded" <<'LUA' +local loaded = arg[2] == "true" +local calls = 0 +hl = { + get_config = function(key) + assert(key == "plugin.cua.enabled") + if loaded then return false end + return nil + end, + config = function(config) + calls = calls + 1 + assert(config.input == nil, "toggle must not write human keyboard settings") + assert(config.plugin.cua.enabled == true, "toggle must enable Cua input") + end, + on = function(event, callback) + assert(event == "hyprland.start") + hl.exec_cmd = function(command) assert(command == "omarchy-toggle-cua-input --load") end + callback() + end, +} +dofile(arg[1]) +assert(calls == (loaded and 1 or 0), "plugin option set " .. calls .. " times with the module " .. (loaded and "loaded" or "unloaded")) +LUA +done +pass "cua input config preserves human keyboard settings" +pass "cua input config names the plugin option only once the module is loaded" + +# Older packages cannot supply the runtime capability, even if a stub reports it. +fresh_home +rc=0 +TEST_PLUGIN_VERSION=0.28.2-2 "$toggle" on 2>/dev/null || rc=$? +[[ $rc != 0 && ! -e $(flag_file) ]] || fail "cua input refuses older packages" +! grep -q '^hyprctl:plugin load' "$TEST_LOG" || fail "cua input refuses older packages" "plugin loaded anyway" +pass "cua input refuses older packages" + +# An upgraded package does not replace an older module already in memory. +fresh_home +mkdir -p "$(dirname "$(flag_file)")" +printf 'hl.config({input={kb_layout="us",kb_options=""}})\n' >"$(flag_file)" +rc=0 +TEST_PLUGIN_LOADED=1 TEST_LAYOUT_INDEPENDENT=false "$toggle" on 2>/dev/null || rc=$? +[[ $rc != 0 && ! -e $(flag_file) ]] || fail "cua input disables a stale loaded module" +! grep -q '^hyprctl:plugin ' "$TEST_LOG" || fail "cua input disables a stale loaded module" "module replaced" +grep -q 'Log out and back in' "$TEST_LOG" || fail "cua input disables a stale loaded module" "restart instruction missing" +grep -q '^hyprctl:reload$' "$TEST_LOG" || fail "cua input disables a stale loaded module" "old keymap not restored" +pass "cua input disables a stale loaded module without replacing it" + +# Package 4 already has independent keymaps, but lacks the Num Lock fix. +# Both interactive enable and startup must reject that still-mapped module. +for action in on --load; do + fresh_home + mkdir -p "$(dirname "$(flag_file)")" + cp "$ROOT/default/hypr/toggles/cua-input.lua" "$(flag_file)" + rc=0 + TEST_PLUGIN_LOADED=1 TEST_NUMLOCK_COMPATIBLE=missing "$toggle" "$action" 2>/dev/null || rc=$? + if [[ $action == on ]]; then + [[ $rc != 0 ]] || fail "cua input refuses the old Num Lock guard during $action" + fi + [[ ! -e $(flag_file) ]] || fail "cua input refuses the old Num Lock guard during $action" "flag remains" + ! grep -q '^hyprctl:plugin ' "$TEST_LOG" || fail "cua input refuses the old Num Lock guard during $action" "module replaced" + grep -q 'Log out and back in' "$TEST_LOG" || fail "cua input refuses the old Num Lock guard during $action" "restart instruction missing" + pass "cua input refuses the old Num Lock guard during $action without replacing the module" +done + +# A module mapped before the input-method guard, or one whose hooks did not +# install, must not get input: fcitx5 restarting could crash Hyprland. Only +# the old module is fixed by logging out; a failed hook would fail again. +for guard in missing false; do + if [[ $guard == missing ]]; then expected='Log out and back in'; else expected='could not install its input-method crash guard'; fi + for action in on --load; do + fresh_home + mkdir -p "$(dirname "$(flag_file)")" + cp "$ROOT/default/hypr/toggles/cua-input.lua" "$(flag_file)" + rc=0 + TEST_PLUGIN_LOADED=1 TEST_IME_GUARD=$guard "$toggle" "$action" 2>/dev/null || rc=$? + if [[ $action == on ]]; then + [[ $rc != 0 ]] || fail "cua input refuses a $guard input-method guard during $action" + fi + [[ ! -e $(flag_file) ]] || fail "cua input refuses a $guard input-method guard during $action" "flag remains" + ! grep -q '^hyprctl:plugin ' "$TEST_LOG" || fail "cua input refuses a $guard input-method guard during $action" "module replaced" + grep -q "$expected" "$TEST_LOG" || fail "cua input refuses a $guard input-method guard during $action" "wrong instruction: $(cat "$TEST_LOG")" + if [[ $guard == false ]] && grep -q 'Log out and back in' "$TEST_LOG"; then + fail "cua input refuses a $guard input-method guard during $action" "a failed hook is not fixed by logging out" + fi + pass "cua input refuses a $guard input-method guard during $action without replacing the module" + done +done + +# Compiled capabilities are available before input is enabled or ready. +fresh_home +TEST_CUA_READY=0 "$toggle" on +[[ -e $(flag_file) ]] || fail "cua input checks compiled capabilities before input is ready" +grep -q '^notify:.*Cua plugin loaded, input not ready' "$TEST_LOG" || fail "cua input checks compiled capabilities before input is ready" "not-ready state hidden" +pass "cua input checks compiled capabilities before input is ready" + +# A failed load must clear a legacy flag rather than leave its keyboard override. +for action in on --load; do + fresh_home + mkdir -p "$(dirname "$(flag_file)")" + printf 'hl.config({input={kb_layout="us",kb_options=""}})\n' >"$(flag_file)" + rc=0 + TEST_LOAD_STATUS=1 "$toggle" "$action" 2>/dev/null || rc=$? + [[ ! -e $(flag_file) ]] || fail "cua input clears old flags when $action cannot load" + grep -q '^hyprctl:reload$' "$TEST_LOG" || fail "cua input clears old flags when $action cannot load" "old keymap not restored" + pass "cua input clears old flags when $action cannot load" +done + +# A failed compatibility check leaves the module unloaded. +fresh_home +rc=0 +TEST_VERIFY_STATUS=1 "$toggle" on 2>/dev/null || rc=$? +[[ $rc != 0 && ! -e $(flag_file) ]] || fail "cua input refuses when the compatibility check fails" +! grep -q '^hyprctl:plugin load' "$TEST_LOG" || fail "cua input refuses when the compatibility check fails" "plugin loaded anyway" +pass "cua input refuses when the compatibility check fails" + +# on: check, load, land the flag, reload, report. +fresh_home +TEST_CUA_READY=1 "$toggle" on +grep -q '^python3:.*profile_verify.py --kit /usr/share/cua-hyprland-plugin --kit-sha256 0000.* --consumer /usr/lib/cua/hyprland/cua-hyprland-plugin.so' "$TEST_LOG" || + fail "cua input runs the package's consumer check before loading" "$(cat "$TEST_LOG")" +pass "cua input runs the package's consumer check before loading" + +grep -q '^hyprctl:plugin load /usr/lib/cua/hyprland/cua-hyprland-plugin.so$' "$TEST_LOG" || fail "cua input loads the plugin" "$(cat "$TEST_LOG")" +pass "cua input loads the plugin" + +cmp -s "$(flag_file)" "$ROOT/default/hypr/toggles/cua-input.lua" || fail "cua input lands the shipped toggle flag" +grep -q '^hyprctl:reload$' "$TEST_LOG" || fail "cua input lands the shipped toggle flag" "no reload" +pass "cua input lands the shipped toggle flag" + +grep -q '^notify:.*Cua input on Your keyboard layout and remaps stay unchanged.' "$TEST_LOG" || fail "cua input reports unchanged keyboard settings" "$(cat "$TEST_LOG")" +pass "cua input reports unchanged keyboard settings" + +[[ $(TEST_CUA_READY=1 "$toggle" --status) == '{"enabled":true,"loaded":false,"ready":true}' ]] || fail "cua input reports status" "$(TEST_CUA_READY=1 "$toggle" --status)" +pass "cua input reports status" + +# An already loaded module with independent keymaps is not loaded twice. +: >"$TEST_LOG" +TEST_PLUGIN_LOADED=1 TEST_LAYOUT=us "$toggle" on +! grep -q '^hyprctl:plugin load' "$TEST_LOG" || fail "cua input does not reload a loaded plugin" +pass "cua input does not reload a loaded plugin" + +# off: the flag goes and Hyprland reloads; the module is left alone. +: >"$TEST_LOG" +"$toggle" off +[[ ! -e $(flag_file) ]] || fail "cua input off removes the flag" +grep -q '^hyprctl:reload$' "$TEST_LOG" || fail "cua input off removes the flag" "no reload" +! grep -q '^hyprctl:plugin unload' "$TEST_LOG" || fail "cua input off removes the flag" "unloaded the module" +pass "cua input off removes the flag" + +[[ $("$toggle" --status) == '{"enabled":false,"loaded":false,"ready":false}' ]] || fail "cua input reports status when off" +pass "cua input reports status when off" + +# toggle flips between the two. +: >"$TEST_LOG" +TEST_CUA_READY=1 "$toggle" +[[ -e $(flag_file) ]] || fail "cua input toggles on from off" +pass "cua input toggles on from off" +"$toggle" +[[ ! -e $(flag_file) ]] || fail "cua input toggles off from on" +pass "cua input toggles off from on" + +# --load at session start: with the flag on, load again after the check. +fresh_home +TEST_CUA_READY=1 "$toggle" on >/dev/null +: >"$TEST_LOG" +"$toggle" --load +grep -q '^hyprctl:plugin load' "$TEST_LOG" || fail "cua input reloads the plugin at session start" "$(cat "$TEST_LOG")" +[[ -e $(flag_file) ]] || fail "cua input reloads the plugin at session start" "flag removed" +pass "cua input reloads the plugin at session start" + +# The load reloads Hyprland's configuration before the module is checked, so +# input stays held off through it and is restored only after the checks. +grep -qx 'flag-at-load:held' "$TEST_LOG" || fail "cua input holds input off while a fresh module loads" "$(cat "$TEST_LOG")" +[[ $(grep -nm1 '^hyprctl:reload$' "$TEST_LOG" | cut -d: -f1) -lt $(grep -nm1 '^hyprctl:plugin load' "$TEST_LOG" | cut -d: -f1) ]] || + fail "cua input holds input off while a fresh module loads" "held flag not reloaded before the load" +cmp -s "$(flag_file)" "$ROOT/default/hypr/toggles/cua-input.lua" || fail "cua input holds input off while a fresh module loads" "flag not restored" +pass "cua input holds input off while a fresh module loads" + +# A fresh module whose guard did not install never gets the enabled flag back. +: >"$TEST_LOG" +TEST_IME_GUARD=false "$toggle" --load +grep -qx 'flag-at-load:held' "$TEST_LOG" || fail "cua input keeps a fresh unguarded module off at session start" "not held during load" +[[ ! -e $(flag_file) ]] || fail "cua input keeps a fresh unguarded module off at session start" "flag remains" +pass "cua input keeps a fresh unguarded module off at session start" +"$toggle" on >/dev/null + +# Neither a failed hold nor a failed restore may leave a held flag that +# reports Cua Input on while input stays off. +for failure in TEST_RELOAD_STATUS=1 TEST_RELOAD_FAIL_FROM=2 TEST_RESTORE_FAIL=1; do + : >"$TEST_LOG" + rm -f "$TEST_LOG.reloads" + rc=0 + env "$failure" "$toggle" --load 2>/dev/null || rc=$? + [[ ! -e $(flag_file) ]] || fail "cua input clears a held flag when $failure" "flag left: $(cat "$(flag_file)")" + grep -q '^notify:.*Cua input turned off' "$TEST_LOG" || fail "cua input clears a held flag when $failure" "no notification" + pass "cua input clears a held flag when $failure" + "$toggle" on >/dev/null +done + +# on with the flag already set and the module not yet loaded holds input off +# through the load as well, and refuses a module whose guard did not install. +for guard in true false; do + fresh_home + mkdir -p "$(dirname "$(flag_file)")" + cp "$ROOT/default/hypr/toggles/cua-input.lua" "$(flag_file)" + rc=0 + TEST_CUA_READY=1 TEST_IME_GUARD=$guard "$toggle" on 2>/dev/null || rc=$? + grep -qx 'flag-at-load:held' "$TEST_LOG" || fail "cua input on holds input off while loading (guard $guard)" "$(cat "$TEST_LOG")" + if [[ $guard == true ]]; then + cmp -s "$(flag_file)" "$ROOT/default/hypr/toggles/cua-input.lua" || fail "cua input on holds input off while loading (guard $guard)" "flag not restored" + else + [[ $rc != 0 && ! -e $(flag_file) ]] || fail "cua input on holds input off while loading (guard $guard)" "unguarded module kept its flag" + fi + pass "cua input on holds input off while loading (guard $guard)" +done + +# A failed reload while landing the flag must not leave it reporting on. +fresh_home +rm -f "$TEST_LOG.reloads" +rc=0 +TEST_RELOAD_STATUS=1 "$toggle" on 2>/dev/null || rc=$? +[[ $rc != 0 && ! -e $(flag_file) ]] || fail "cua input on clears the flag when the reload fails" +grep -q '^notify:.*Cua input stays off Hyprland could not reload' "$TEST_LOG" || fail "cua input on clears the flag when the reload fails" "no notification: $(cat "$TEST_LOG")" +pass "cua input on clears the flag when the reload fails" + +# Turning off while on verifies must win: on's held flag goes and is not recreated. +fresh_home +rc=0 +TEST_CUA_READY=1 TEST_VERIFY_PAUSE="$tmp_dir/onverify" "$toggle" on 2>/dev/null & +on_pid=$! +for ((attempt = 0; attempt < 200; attempt++)); do + [[ -e $tmp_dir/onverify.ready ]] && break + sleep 0.01 +done +[[ -e $tmp_dir/onverify.ready ]] || fail "cua input on reached its verification pause" +"$toggle" off +touch "$tmp_dir/onverify.release" +wait "$on_pid" || rc=$? +[[ ! -e $(flag_file) ]] || fail "cua input on preserves a concurrent off" "flag recreated" +! grep -q '^notify:.*Cua input on ' "$TEST_LOG" || fail "cua input on preserves a concurrent off" "reported on: $(cat "$TEST_LOG")" +pass "cua input on preserves a concurrent off" +rm -f "$tmp_dir/onverify".* + +# A failed reload while turning off falls back to switching the live option off. +fresh_home +"$toggle" on >/dev/null +: >"$TEST_LOG" +TEST_RELOAD_STATUS=1 "$toggle" off +[[ ! -e $(flag_file) ]] || fail "cua input off switches the live option off when the reload fails" "flag kept" +grep -q '^hyprctl:eval .*enabled = false' "$TEST_LOG" || fail "cua input off switches the live option off when the reload fails" "$(cat "$TEST_LOG")" +grep -q '^notify:.*Cua input off' "$TEST_LOG" || fail "cua input off switches the live option off when the reload fails" "not reported off" +pass "cua input off switches the live option off when the reload fails" + +# When neither takes, off says so rather than claiming input is off. +fresh_home +"$toggle" on >/dev/null +: >"$TEST_LOG" +rc=0 +TEST_RELOAD_STATUS=1 TEST_EVAL_STATUS=1 "$toggle" off 2>/dev/null || rc=$? +[[ $rc != 0 ]] || fail "cua input off reports input that may still be on" "exited 0" +grep -q '^notify:.*Cua input may still be on' "$TEST_LOG" || fail "cua input off reports input that may still be on" "$(cat "$TEST_LOG")" +! grep -q '^notify:.*Cua input off' "$TEST_LOG" || fail "cua input off reports input that may still be on" "claimed off" +pass "cua input off reports input that may still be on" + +# A refusal says the same when it cannot take the setting back. +fresh_home +"$toggle" on >/dev/null +: >"$TEST_LOG" +rc=0 +TEST_RELOAD_STATUS=1 TEST_EVAL_STATUS=1 "$toggle" on 2>/dev/null || rc=$? +[[ $rc != 0 ]] || fail "cua input refusal reports input that may still be on" "exited 0" +grep -q '^notify:.*Cua input may still be on' "$TEST_LOG" || fail "cua input refusal reports input that may still be on" "$(cat "$TEST_LOG")" +pass "cua input refusal reports input that may still be on" + +# A reload that keeps the old settings still answers ok, so off always sets the live option. +fresh_home +"$toggle" on >/dev/null +: >"$TEST_LOG" +"$toggle" off +grep -q '^hyprctl:eval .*enabled = false' "$TEST_LOG" || fail "cua input off sets the live option even when the reload answers ok" "$(cat "$TEST_LOG")" +pass "cua input off sets the live option even when the reload answers ok" + +# An off during on's package checks wins too: on's held flag is its first step. +fresh_home +TEST_CUA_READY=1 TEST_PACMAN_PAUSE="$tmp_dir/onpkg" "$toggle" on 2>/dev/null & +on_pid=$! +for ((attempt = 0; attempt < 200; attempt++)); do + [[ -e $tmp_dir/onpkg.ready ]] && break + sleep 0.01 +done +[[ -e $tmp_dir/onpkg.ready ]] || fail "cua input on reached its package check pause" +"$toggle" off +touch "$tmp_dir/onpkg.release" +wait "$on_pid" || true +[[ ! -e $(flag_file) ]] || fail "cua input on preserves an off during its package checks" "flag recreated" +! grep -q '^notify:.*Cua input on ' "$TEST_LOG" || fail "cua input on preserves an off during its package checks" "reported on" +pass "cua input on preserves an off during its package checks" +rm -f "$tmp_dir/onpkg".* + +# A session-start failure that cannot take the setting back says so. +fresh_home +"$toggle" on >/dev/null +: >"$TEST_LOG" +TEST_PLUGIN_VERSION=0.28.2-2 TEST_RELOAD_STATUS=1 TEST_EVAL_STATUS=1 "$toggle" --load +grep -q '^notify:.*Cua input may still be on' "$TEST_LOG" || fail "cua input startup reports input that may still be on" "$(cat "$TEST_LOG")" +! grep -q '^notify:.*Cua input turned off' "$TEST_LOG" || fail "cua input startup reports input that may still be on" "claimed off" +pass "cua input startup reports input that may still be on" + +# A flag that cannot be deleted would enable input again at the next reload. +fresh_home +"$toggle" on >/dev/null +: >"$TEST_LOG" +chmod a-w "$(dirname "$(flag_file)")" +rc=0 +"$toggle" off 2>/dev/null || rc=$? +chmod u+w "$(dirname "$(flag_file)")" +[[ $rc != 0 ]] || fail "cua input off reports a flag it could not delete" "exited 0" +grep -q '^notify:.*Cua input may still be on' "$TEST_LOG" || fail "cua input off reports a flag it could not delete" "$(cat "$TEST_LOG")" +pass "cua input off reports a flag it could not delete" + +# Existing copied flags are replaced, removing their legacy keyboard overrides. +for ((line = 0; line < 20; line++)); do + printf 'hl.config({input={kb_layout="us",kb_options=""}})\n' +done >"$(flag_file)" +: >"$TEST_LOG" +"$toggle" --load +cmp -s "$(flag_file)" "$ROOT/default/hypr/toggles/cua-input.lua" || fail "cua input refreshes old flags at session start" +grep -q '^hyprctl:reload$' "$TEST_LOG" || fail "cua input refreshes old flags at session start" "no reload" +pass "cua input refreshes old flags at session start" + +# Turning off while startup verification runs must not recreate the flag. +: >"$TEST_LOG" +TEST_VERIFY_PAUSE="$tmp_dir/verify" "$toggle" --load & +load_pid=$! +for ((attempt = 0; attempt < 200; attempt++)); do + [[ -e $tmp_dir/verify.ready ]] && break + sleep 0.01 +done +[[ -e $tmp_dir/verify.ready ]] || fail "cua input startup verifier reached the pause" +"$toggle" off +[[ ! -e $(flag_file) ]] || fail "cua input off removes the flag during startup verification" +touch "$tmp_dir/verify.release" +wait "$load_pid" +[[ ! -e $(flag_file) ]] || fail "cua input startup preserves a concurrent off" +pass "cua input startup preserves a concurrent off" +"$toggle" on >/dev/null + +# A stale module found at startup also removes old keyboard overrides safely. +: >"$TEST_LOG" +TEST_PLUGIN_LOADED=1 TEST_LAYOUT_INDEPENDENT=false "$toggle" --load +[[ ! -e $(flag_file) ]] || fail "cua input disables stale loaded module at session start" +! grep -q '^hyprctl:plugin ' "$TEST_LOG" || fail "cua input disables stale loaded module at session start" "module replaced" +pass "cua input disables stale loaded module at session start" +"$toggle" on >/dev/null + +# A check that no longer passes turns the flag off. +: >"$TEST_LOG" +TEST_VERIFY_STATUS=1 "$toggle" --load +[[ ! -e $(flag_file) ]] || fail "cua input turns itself off when the check fails at session start" +! grep -q '^hyprctl:plugin load' "$TEST_LOG" || fail "cua input turns itself off when the check fails at session start" "loaded anyway" +grep -q '^notify:.*Cua input turned off' "$TEST_LOG" || fail "cua input turns itself off when the check fails at session start" "no notification" +pass "cua input turns itself off when the check fails at session start" + +# With the flag off, session start does nothing. +: >"$TEST_LOG" +"$toggle" --load +[[ ! -s $TEST_LOG ]] || fail "cua input --load is a no-op when off" "$(cat "$TEST_LOG")" +pass "cua input --load is a no-op when off"