mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 11:32:42 +00:00
190 lines
6.0 KiB
Bash
Executable File
190 lines
6.0 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Cycle through the windows of an app, or launch it if none are open
|
|
# omarchy:args=<app|browser|terminal|ai|webapp|webapp-NAME|kitty-CLASS>
|
|
|
|
set -uo pipefail
|
|
|
|
APP="${1:-}"
|
|
|
|
if [[ -z $APP ]]; then
|
|
echo "Usage: omarchy-cycle <app|browser|terminal|ai|webapp|webapp-NAME|kitty-CLASS>"
|
|
exit 1
|
|
fi
|
|
|
|
# Per-class launch commands for kitty-* (class -> command run via `kitty -e`).
|
|
# Unmapped classes just open a plain kitty with that class.
|
|
declare -A KITTY_CMDS=(
|
|
# [kitty-lazygit]="lazygit"
|
|
# [kitty-btop]="btop"
|
|
)
|
|
|
|
# --- Hyprland helpers --------------------------------------------------------
|
|
|
|
# Print the addresses of all windows whose class or initialClass matches the
|
|
# regex in $1. An optional regex in $2 excludes matches again — used to keep
|
|
# scratch windows out of a cycle. Windows are sorted by workspace, then by
|
|
# address, so the cycle order stays the same between calls.
|
|
addresses_matching() {
|
|
hyprctl clients -j | jq -r --arg re "$1" --arg not "${2:-}" '
|
|
[ .[]
|
|
| select(((.class // "") | test($re; "i")) or ((.initialClass // "") | test($re; "i")))
|
|
| select($not == "" or (
|
|
(((.class // "") | test($not; "i")) or ((.initialClass // "") | test($not; "i"))) | not
|
|
))
|
|
]
|
|
| sort_by(.workspace.id, .address)
|
|
| .[].address'
|
|
}
|
|
|
|
# Same, but drops site-specific app (SSA) windows, so real browsers stay apart
|
|
# from web apps that share a class prefix.
|
|
addresses_matching_no_ssa() {
|
|
hyprctl clients -j | jq -r --arg re "$1" '
|
|
[ .[]
|
|
| select(((.class // "") | test($re; "i")) or ((.initialClass // "") | test($re; "i")))
|
|
| select(((.class // "") | test("^(chrome-|webapp-)"; "i")) | not)
|
|
]
|
|
| sort_by(.workspace.id, .address)
|
|
| .[].address'
|
|
}
|
|
|
|
focused_address() {
|
|
hyprctl activewindow -j 2>/dev/null | jq -r '.address // empty'
|
|
}
|
|
|
|
focus_window() {
|
|
hyprctl dispatch "hl.dsp.focus({ window = \"address:$1\" })" >/dev/null 2>&1 ||
|
|
hyprctl dispatch focuswindow "address:$1" >/dev/null
|
|
}
|
|
|
|
spawn() {
|
|
exec setsid uwsm-app -- $1
|
|
}
|
|
|
|
# --- Find the windows --------------------------------------------------------
|
|
|
|
declare -a INSTANCES
|
|
|
|
if [[ $APP == "webapp" ]]; then
|
|
# Cycle all web apps: Firefox SSAs (WebApp-*) and Chromium/Brave SSAs (chrome-*).
|
|
# Real browsers are excluded, because their class has no SSA prefix.
|
|
INSTANCES=($(addresses_matching '^(webapp-|chrome-)'))
|
|
FALLBACK_APP="webapp-youtube"
|
|
|
|
elif [[ $APP == webapp-* ]]; then
|
|
# Extract the web app name (e.g. "webapp-claude" -> "claude").
|
|
WEBAPP_NAME="${APP#webapp-}"
|
|
# Firefox SSAs use the class WebApp-<Name>. Chromium SSAs use
|
|
# chrome-<host>__-Default, so the name is matched anywhere in the class.
|
|
INSTANCES=($(addresses_matching "webapp-${WEBAPP_NAME}|^chrome-.*${WEBAPP_NAME}"))
|
|
|
|
if ((${#INSTANCES[@]} == 0)); then
|
|
# No window is open. Launch from the desktop entry.
|
|
DESKTOP_FILE=$(find ~/.local/share/applications -iname "WebApp-${WEBAPP_NAME}*.desktop" -print -quit 2>/dev/null)
|
|
[[ -n $DESKTOP_FILE ]] ||
|
|
DESKTOP_FILE=$(find ~/.local/share/applications -iname "*${WEBAPP_NAME}*.desktop" -print -quit 2>/dev/null)
|
|
|
|
if [[ -n $DESKTOP_FILE ]]; then
|
|
exec setsid gtk-launch "$(basename "$DESKTOP_FILE")"
|
|
else
|
|
echo "No desktop file found for web app: ${WEBAPP_NAME}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
elif [[ $APP == "kitty" ]]; then
|
|
# Cycle all kitty windows, whatever the class (kitty, kitty-nvim, ...).
|
|
INSTANCES=($(addresses_matching '^kitty'))
|
|
DEFAULT_SPAWN="kitty"
|
|
|
|
elif [[ $APP == kitty-* ]]; then
|
|
# One kitty class, e.g. kitty-nvim -> class "kitty-nvim".
|
|
INSTANCES=($(addresses_matching "^${APP}$"))
|
|
|
|
if ((${#INSTANCES[@]} == 0)); then
|
|
cmd="${KITTY_CMDS[$APP]:-}"
|
|
if [[ -n $cmd ]]; then
|
|
spawn "kitty --class $APP -e $cmd"
|
|
else
|
|
spawn "kitty --class $APP"
|
|
fi
|
|
fi
|
|
|
|
elif [[ $APP == "ghostty" ]]; then
|
|
# Cycle Ghostty terminals (class com.mitchellh.ghostty).
|
|
INSTANCES=($(addresses_matching 'ghostty'))
|
|
DEFAULT_SPAWN="ghostty"
|
|
|
|
elif [[ $APP == "browser" ]]; then
|
|
# Cycle all browsers. Web apps are left out.
|
|
INSTANCES=($(addresses_matching_no_ssa '^(zen|brave|chromium|google-chrome|firefox|librewolf|vivaldi|microsoft-edge|helium)'))
|
|
DEFAULT_LAUNCHER="omarchy-launch-browser"
|
|
|
|
elif [[ $APP == "brave" ]]; then
|
|
# Cycle the Brave Origin browser and all of its web apps. Both the
|
|
# real browser (class brave-origin) and the SSAs (class brave-<host>)
|
|
# share the brave- prefix, so one regex catches them all.
|
|
INSTANCES=($(addresses_matching '^brave'))
|
|
DEFAULT_SPAWN="brave-origin"
|
|
|
|
elif [[ $APP == "terminal" ]]; then
|
|
# Cycle all terminals. kitty-herdr is the SUPER + A scratch terminal
|
|
# (omarchy-toggle-scratch owns it), so it stays out of the rotation.
|
|
INSTANCES=($(addresses_matching '(kitty|alacritty|ghostty|foot|wezterm)' '^kitty-herdr$'))
|
|
DEFAULT_LAUNCHER="omarchy-launch-terminal"
|
|
|
|
elif [[ $APP == "ai" ]]; then
|
|
# Cycle the AI desktop apps (Claude/Hermes).
|
|
INSTANCES=($(addresses_matching '(claude|hermes)'))
|
|
DEFAULT_SPAWN="hermes desktop"
|
|
|
|
else
|
|
# Any other app, matched on the full class.
|
|
INSTANCES=($(addresses_matching "^${APP}$"))
|
|
fi
|
|
|
|
# --- Launch if nothing is open -----------------------------------------------
|
|
|
|
if ((${#INSTANCES[@]} == 0)); then
|
|
if [[ -n ${DEFAULT_LAUNCHER:-} ]]; then
|
|
exec setsid "$DEFAULT_LAUNCHER"
|
|
fi
|
|
|
|
if [[ -n ${DEFAULT_SPAWN:-} ]]; then
|
|
spawn "$DEFAULT_SPAWN"
|
|
fi
|
|
|
|
if [[ -n ${FALLBACK_APP:-} ]]; then
|
|
exec "$0" "$FALLBACK_APP"
|
|
fi
|
|
|
|
if [[ $APP == "webapp" ]]; then
|
|
echo "No ${APP} windows running"
|
|
exit 0
|
|
fi
|
|
|
|
spawn "$APP"
|
|
fi
|
|
|
|
# --- Focus, or cycle to the next window --------------------------------------
|
|
|
|
if ((${#INSTANCES[@]} == 1)); then
|
|
focus_window "${INSTANCES[0]}"
|
|
exit 0
|
|
fi
|
|
|
|
FOCUSED=$(focused_address)
|
|
|
|
# If the focused window is one of ours, go to the next one. If it is not,
|
|
# go to the first one.
|
|
next_index=0
|
|
for i in "${!INSTANCES[@]}"; do
|
|
if [[ -n $FOCUSED && ${INSTANCES[$i]} == "$FOCUSED" ]]; then
|
|
next_index=$(((i + 1) % ${#INSTANCES[@]}))
|
|
break
|
|
fi
|
|
done
|
|
|
|
focus_window "${INSTANCES[$next_index]}"
|