#!/usr/bin/env bash set -euo pipefail # Script to launch an application or focus it if already running in sway # Usage: launch-or-focus [args...] # Set DEBUG=1 to enable debug logging DEBUG="${DEBUG:-0}" debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*" >&2 || true } if [ $# -eq 0 ]; then echo "Usage: $0 [args...]" >&2 exit 1 fi COMMAND="$1" shift ARGS=("$@") APP_NAME=$(basename "$COMMAND") debug "Command: $COMMAND" debug "App name: $APP_NAME" debug "Args: ${ARGS[*]}" # Extract --class and --title values from args if present # Handle both --class VALUE and --class=VALUE formats CLASS_NAME="" TITLE_NAME="" for i in "${!ARGS[@]}"; do if [[ "${ARGS[$i]}" =~ ^--class=(.+)$ ]]; then CLASS_NAME="${BASH_REMATCH[1]}" debug "Found class (format --class=VALUE): $CLASS_NAME" elif [ "${ARGS[$i]}" = "--class" ] && [ $((i+1)) -lt ${#ARGS[@]} ]; then CLASS_NAME="${ARGS[$((i+1))]}" debug "Found class (format --class VALUE): $CLASS_NAME" elif [[ "${ARGS[$i]}" =~ ^--title=(.+)$ ]]; then TITLE_NAME="${BASH_REMATCH[1]}" debug "Found title (format --title=VALUE): $TITLE_NAME" elif [ "${ARGS[$i]}" = "--title" ] && [ $((i+1)) -lt ${#ARGS[@]} ]; then TITLE_NAME="${ARGS[$((i+1))]}" debug "Found title (format --title VALUE): $TITLE_NAME" fi done debug "Extracted CLASS_NAME: '$CLASS_NAME'" debug "Extracted TITLE_NAME: '$TITLE_NAME'" find_window_by_class() { local class="$1" debug "Searching for window by class: $class" local result result=$(swaymsg -t get_tree | jq -r --arg class "$class" ' recurse(.nodes[]?, .floating_nodes[]?) | select( ((.window_properties.class | type) == "string" and .window_properties.class == $class) or ((.app_id | type) == "string" and .app_id == $class) ) | .id ' | head -n 1) debug "find_window_by_class result: '$result'" echo "$result" } find_window_by_title() { local title="$1" local app="$2" local result debug "Searching for window by title: '$title' and app: '$app'" # Search for windows matching both app and title (most specific) result=$(swaymsg -t get_tree | jq -r --arg title "$title" --arg app "$app" ' recurse(.nodes[]?, .floating_nodes[]?) | select( ( ((.app_id | type) == "string" and (.app_id == $app or (.app_id | test($app; "i")))) or ((.window_properties.class | type) == "string" and (.window_properties.class == $app or (.window_properties.class | test($app; "i")))) ) and ((.name | type) == "string" and .name == $title) ) | .id ' | head -n 1) debug "find_window_by_title (app+title) result: '$result'" [ -n "$result" ] && [ "$result" != "null" ] && echo "$result" && return # Fall back to title-only exact match debug "Falling back to title-only exact match" result=$(swaymsg -t get_tree | jq -r --arg title "$title" ' recurse(.nodes[]?, .floating_nodes[]?) | select((.name | type) == "string" and .name == $title) | .id ' | head -n 1) debug "find_window_by_title (title only) result: '$result'" [ -n "$result" ] && [ "$result" != "null" ] && echo "$result" && return # Final fallback: title-only case-insensitive match debug "Falling back to title-only case-insensitive match" result=$(swaymsg -t get_tree | jq -r --arg title "$title" ' recurse(.nodes[]?, .floating_nodes[]?) | select((.name | type) == "string" and (.name | test($title; "i"))) | .id ' | head -n 1) debug "find_window_by_title (case-insensitive) result: '$result'" echo "$result" } find_window() { local app_name="$1" debug "Searching for window by app name: $app_name" local result result=$(swaymsg -t get_tree | jq -r --arg app "$app_name" ' recurse(.nodes[]?, .floating_nodes[]?) | select( ((.app_id | type) == "string" and (.app_id == $app or (.app_id | test($app; "i")))) or ((.window_properties.class | type) == "string" and (.window_properties.class == $app or (.window_properties.class | test($app; "i")))) or ((.name | type) == "string" and (.name | test($app; "i"))) ) | .id ' | head -n 1) debug "find_window result: '$result'" echo "$result" } focus_window() { local window_id="$1" debug "Focusing window ID: $window_id" swaymsg "[con_id=$window_id]" focus } WINDOW_ID="" debug "Starting window search..." # Only search by class/title if provided - don't fall back to generic app search # This prevents matching the wrong window if [ -n "$CLASS_NAME" ]; then WINDOW_ID=$(find_window_by_class "$CLASS_NAME") debug "After class search: WINDOW_ID='$WINDOW_ID'" fi if ([ -z "$WINDOW_ID" ] || [ "$WINDOW_ID" = "null" ]) && [ -n "$TITLE_NAME" ]; then WINDOW_ID=$(find_window_by_title "$TITLE_NAME" "$APP_NAME") debug "After title search: WINDOW_ID='$WINDOW_ID'" fi # Only fall back to generic app search if we don't have class or title # This means the user wants to focus any instance of the app if ([ -z "$WINDOW_ID" ] || [ "$WINDOW_ID" = "null" ]) && [ -z "$CLASS_NAME" ] && [ -z "$TITLE_NAME" ]; then WINDOW_ID=$(find_window "$APP_NAME") debug "After app name search (no class/title): WINDOW_ID='$WINDOW_ID'" ([ -z "$WINDOW_ID" ] || [ "$WINDOW_ID" = "null" ]) && WINDOW_ID=$(find_window "${APP_NAME,,}") debug "After lowercase app name search: WINDOW_ID='$WINDOW_ID'" fi if [ -n "$WINDOW_ID" ] && [ "$WINDOW_ID" != "null" ] && [ "$WINDOW_ID" -eq "$WINDOW_ID" ] 2>/dev/null; then debug "Window found! Focusing window ID: $WINDOW_ID" focus_window "$WINDOW_ID" exit 0 fi debug "No matching window found, launching new instance" "$COMMAND" "${ARGS[@]}" >/dev/null 2>&1 & disown