diff --git a/dot_local/bin/executable_omarchy-cycle b/dot_local/bin/executable_omarchy-cycle new file mode 100755 index 0000000..fca79d7 --- /dev/null +++ b/dot_local/bin/executable_omarchy-cycle @@ -0,0 +1,177 @@ +#!/bin/bash + +# omarchy:summary=Cycle through the windows of an app, or launch it if none are open +# omarchy:args= + +set -uo pipefail + +APP="${1:-}" + +if [[ -z $APP ]]; then + echo "Usage: omarchy-cycle " + 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-. Chromium SSAs use + # chrome-__-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]}" diff --git a/dot_local/bin/executable_omarchy-cycle-browser b/dot_local/bin/executable_omarchy-cycle-browser new file mode 100755 index 0000000..a2e6834 --- /dev/null +++ b/dot_local/bin/executable_omarchy-cycle-browser @@ -0,0 +1,5 @@ +#!/bin/bash + +# omarchy:summary=Cycle Brave Origin Nightly windows; launch brave-origin-nightly if none are open. + +exec "$(dirname "$(readlink -f "$0")")/omarchy-cycle" brave-origin-nightly diff --git a/dot_local/bin/executable_omarchy-cycle-terminal b/dot_local/bin/executable_omarchy-cycle-terminal new file mode 100755 index 0000000..78d07da --- /dev/null +++ b/dot_local/bin/executable_omarchy-cycle-terminal @@ -0,0 +1,5 @@ +#!/bin/bash + +# omarchy:summary=Cycle through all terminals (ghostty/kitty/alacritty/foot/wezterm); launch the default if none are open. + +exec "$(dirname "$(readlink -f "$0")")/omarchy-cycle" terminal diff --git a/dot_local/bin/executable_yazi-nvim-open b/dot_local/bin/executable_yazi-nvim-open old mode 100644 new mode 100755