| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- #!/usr/bin/env bash
- set -euo pipefail
- # Script to launch an application or focus it if already running in sway
- # Usage: launch-or-focus <command> [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 <command> [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
|