#!/bin/bash APP=$1 declare -a INSTANCES # 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" ) # Handle webapp cycling and launching if [[ "$APP" == "webapp" ]]; then # Cycle all WebApps: Firefox SSAs (WebApp-*) and Chromium/Brave SSAs (chrome-*-Default). # Falls back to launching YouTube if none are open. Excludes real browsers (chromium/brave-*). INSTANCES=($(niri msg -j windows | jq -r '.[] | select((.app_id // "") | ascii_downcase | test("^(webapp-|chrome-)")) | .id')) FALLBACK_APP="webapp-youtube" elif [[ "$APP" == webapp-* ]]; then # Extract the webapp name (e.g., "webapp-claude" -> "Claude") WEBAPP_NAME=$(echo "$APP" | sed 's/webapp-//' | sed 's/.*/\u&/') # Find matching WebApp by prefix (case-insensitive match) APP_ID=$(niri msg -j windows | jq -r ".[] | select(.app_id | test(\"WebApp-${WEBAPP_NAME}\"; \"i\")) | .app_id" | head -1) if [ -z "$APP_ID" ]; then # No instance running, launch via gtk-launch DESKTOP_FILE=$(find ~/.local/share/applications -iname "WebApp-${WEBAPP_NAME}*.desktop" -print -quit 2>/dev/null) if [ -n "$DESKTOP_FILE" ]; then gtk-launch "$(basename "$DESKTOP_FILE")" else echo "No desktop file found for WebApp-${WEBAPP_NAME}" fi exit 0 fi INSTANCES=($(niri msg -j windows | jq -r ".[] | select(.app_id | test(\"WebApp-${WEBAPP_NAME}\"; \"i\")) | .id")) elif [[ "$APP" == "kitty" ]]; then # Cycle through ALL kitty windows (any class: kitty, kitty-nvim, ...) INSTANCES=($(niri msg -j windows | jq -r '.[] | select((.app_id // "") | ascii_downcase | startswith("kitty")) | .id')) elif [[ "$APP" == kitty-* ]]; then # Specific kitty class, e.g. kitty-nvim -> app_id "kitty-nvim" INSTANCES=($(niri msg -j windows | jq -r --arg cls "$APP" \ '.[] | select((.app_id // "") | ascii_downcase == ($cls | ascii_downcase)) | .id')) if [ ${#INSTANCES[@]} -eq 0 ]; then cmd="${KITTY_CMDS[$APP]:-}" if [ -n "$cmd" ]; then niri msg action spawn -- kitty --class "$APP" -e $cmd else niri msg action spawn -- kitty --class "$APP" fi exit 0 fi elif [[ "$APP" == "ghostty" ]]; then # Cycle through Ghostty terminals (case-insensitive app_id match) INSTANCES=($(niri msg -j windows | jq -r '.[] | select(.app_id | test("ghostty"; "i")) | .id')) elif [[ "$APP" == "browser" ]]; then # Cycle through all browsers (zen/brave/chromium/firefox), launch default if none INSTANCES=($(niri msg -j windows | jq -r '.[] | select((.app_id // "") | ascii_downcase | test("^(zen|brave|chromium|firefox)")) | .id')) DEFAULT_SPAWN="zen" elif [[ "$APP" == "terminal" ]]; then # Cycle through all terminals (ghostty/kitty/alacritty), launch the OS # default (xdg-terminal-exec, ~/.config/xdg-terminals.list) if none INSTANCES=($(niri msg -j windows | jq -r '.[] | select((.app_id // "") | ascii_downcase | test("kitty|alacritty|ghostty")) | .id')) DEFAULT_SPAWN="xdg-terminal-exec" elif [[ "$APP" == "ai" ]]; then # Cycle through AI desktop apps (Claude/Hermes), launch Hermes if none INSTANCES=($(niri msg -j windows | jq -r '.[] | select((.app_id // "") | ascii_downcase | test("claude|hermes")) | .id')) DEFAULT_SPAWN="hermes desktop" else INSTANCES=($(niri msg -j windows | jq -r ".[] | select(.app_id==\"${APP}\") | .id")) fi # Launch if no instances if [ ${#INSTANCES[@]} -eq 0 ]; then if [ -n "${DEFAULT_SPAWN:-}" ]; then niri msg action spawn -- $DEFAULT_SPAWN exit 0 fi if [ -n "${FALLBACK_APP:-}" ]; then exec "$0" "$FALLBACK_APP" fi # Removed "kitty" from this block so it falls through to the spawn command if [[ "$APP" == "webapp" || "$APP" == "ghostty" ]]; then echo "No ${APP} windows running" exit 0 fi niri msg action spawn -- $APP exit 0 fi # Focus logic if [ ${#INSTANCES[@]} -eq 1 ]; then niri msg action focus-window --id ${INSTANCES[0]} else ISORTED=($(printf "%s\n" "${INSTANCES[@]}" | sort -n)) FOCUSED=$(niri msg -j focused-window | jq '.id') if [[ ! " ${ISORTED[@]} " =~ ${FOCUSED} ]]; then niri msg action focus-window --id ${ISORTED[0]} else new_index=0 for i in "${!ISORTED[@]}"; do if [ "${ISORTED[$i]}" -eq "${FOCUSED}" ]; then new_index=$(( (i + 1) % ${#ISORTED[@]} )) break fi done niri msg action focus-window --id ${ISORTED[$new_index]} fi fi