Files
dotfiles/dot_local/bin/executable_omarchy-cycle
T

177 lines
5.3 KiB
Bash
Raw Normal View History

#!/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. 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" '
[ .[]
| select(((.class // "") | test($re; "i")) or ((.initialClass // "") | test($re; "i")))
]
| 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 == "terminal" ]]; then
# Cycle all terminals.
INSTANCES=($(addresses_matching '(kitty|alacritty|ghostty|foot|wezterm)'))
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]}"