launch-or-focus 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # Script to launch an application or focus it if already running in sway
  4. # Usage: launch-or-focus <command> [args...]
  5. # Set DEBUG=1 to enable debug logging
  6. DEBUG="${DEBUG:-0}"
  7. debug() {
  8. [ "$DEBUG" = "1" ] && echo "[DEBUG] $*" >&2 || true
  9. }
  10. if [ $# -eq 0 ]; then
  11. echo "Usage: $0 <command> [args...]" >&2
  12. exit 1
  13. fi
  14. COMMAND="$1"
  15. shift
  16. ARGS=("$@")
  17. APP_NAME=$(basename "$COMMAND")
  18. debug "Command: $COMMAND"
  19. debug "App name: $APP_NAME"
  20. debug "Args: ${ARGS[*]}"
  21. # Extract --class and --title values from args if present
  22. # Handle both --class VALUE and --class=VALUE formats
  23. CLASS_NAME=""
  24. TITLE_NAME=""
  25. for i in "${!ARGS[@]}"; do
  26. if [[ "${ARGS[$i]}" =~ ^--class=(.+)$ ]]; then
  27. CLASS_NAME="${BASH_REMATCH[1]}"
  28. debug "Found class (format --class=VALUE): $CLASS_NAME"
  29. elif [ "${ARGS[$i]}" = "--class" ] && [ $((i+1)) -lt ${#ARGS[@]} ]; then
  30. CLASS_NAME="${ARGS[$((i+1))]}"
  31. debug "Found class (format --class VALUE): $CLASS_NAME"
  32. elif [[ "${ARGS[$i]}" =~ ^--title=(.+)$ ]]; then
  33. TITLE_NAME="${BASH_REMATCH[1]}"
  34. debug "Found title (format --title=VALUE): $TITLE_NAME"
  35. elif [ "${ARGS[$i]}" = "--title" ] && [ $((i+1)) -lt ${#ARGS[@]} ]; then
  36. TITLE_NAME="${ARGS[$((i+1))]}"
  37. debug "Found title (format --title VALUE): $TITLE_NAME"
  38. fi
  39. done
  40. debug "Extracted CLASS_NAME: '$CLASS_NAME'"
  41. debug "Extracted TITLE_NAME: '$TITLE_NAME'"
  42. find_window_by_class() {
  43. local class="$1"
  44. debug "Searching for window by class: $class"
  45. local result
  46. result=$(swaymsg -t get_tree | jq -r --arg class "$class" '
  47. recurse(.nodes[]?, .floating_nodes[]?) |
  48. select(
  49. ((.window_properties.class | type) == "string" and .window_properties.class == $class) or
  50. ((.app_id | type) == "string" and .app_id == $class)
  51. ) |
  52. .id
  53. ' | head -n 1)
  54. debug "find_window_by_class result: '$result'"
  55. echo "$result"
  56. }
  57. find_window_by_title() {
  58. local title="$1"
  59. local app="$2"
  60. local result
  61. debug "Searching for window by title: '$title' and app: '$app'"
  62. # Search for windows matching both app and title (most specific)
  63. result=$(swaymsg -t get_tree | jq -r --arg title "$title" --arg app "$app" '
  64. recurse(.nodes[]?, .floating_nodes[]?) |
  65. select(
  66. (
  67. ((.app_id | type) == "string" and (.app_id == $app or (.app_id | test($app; "i")))) or
  68. ((.window_properties.class | type) == "string" and (.window_properties.class == $app or (.window_properties.class | test($app; "i"))))
  69. ) and
  70. ((.name | type) == "string" and .name == $title)
  71. ) |
  72. .id
  73. ' | head -n 1)
  74. debug "find_window_by_title (app+title) result: '$result'"
  75. [ -n "$result" ] && [ "$result" != "null" ] && echo "$result" && return
  76. # Fall back to title-only exact match
  77. debug "Falling back to title-only exact match"
  78. result=$(swaymsg -t get_tree | jq -r --arg title "$title" '
  79. recurse(.nodes[]?, .floating_nodes[]?) |
  80. select((.name | type) == "string" and .name == $title) |
  81. .id
  82. ' | head -n 1)
  83. debug "find_window_by_title (title only) result: '$result'"
  84. [ -n "$result" ] && [ "$result" != "null" ] && echo "$result" && return
  85. # Final fallback: title-only case-insensitive match
  86. debug "Falling back to title-only case-insensitive match"
  87. result=$(swaymsg -t get_tree | jq -r --arg title "$title" '
  88. recurse(.nodes[]?, .floating_nodes[]?) |
  89. select((.name | type) == "string" and (.name | test($title; "i"))) |
  90. .id
  91. ' | head -n 1)
  92. debug "find_window_by_title (case-insensitive) result: '$result'"
  93. echo "$result"
  94. }
  95. find_window() {
  96. local app_name="$1"
  97. debug "Searching for window by app name: $app_name"
  98. local result
  99. result=$(swaymsg -t get_tree | jq -r --arg app "$app_name" '
  100. recurse(.nodes[]?, .floating_nodes[]?) |
  101. select(
  102. ((.app_id | type) == "string" and (.app_id == $app or (.app_id | test($app; "i")))) or
  103. ((.window_properties.class | type) == "string" and (.window_properties.class == $app or (.window_properties.class | test($app; "i")))) or
  104. ((.name | type) == "string" and (.name | test($app; "i")))
  105. ) |
  106. .id
  107. ' | head -n 1)
  108. debug "find_window result: '$result'"
  109. echo "$result"
  110. }
  111. focus_window() {
  112. local window_id="$1"
  113. debug "Focusing window ID: $window_id"
  114. swaymsg "[con_id=$window_id]" focus
  115. }
  116. WINDOW_ID=""
  117. debug "Starting window search..."
  118. # Only search by class/title if provided - don't fall back to generic app search
  119. # This prevents matching the wrong window
  120. if [ -n "$CLASS_NAME" ]; then
  121. WINDOW_ID=$(find_window_by_class "$CLASS_NAME")
  122. debug "After class search: WINDOW_ID='$WINDOW_ID'"
  123. fi
  124. if ([ -z "$WINDOW_ID" ] || [ "$WINDOW_ID" = "null" ]) && [ -n "$TITLE_NAME" ]; then
  125. WINDOW_ID=$(find_window_by_title "$TITLE_NAME" "$APP_NAME")
  126. debug "After title search: WINDOW_ID='$WINDOW_ID'"
  127. fi
  128. # Only fall back to generic app search if we don't have class or title
  129. # This means the user wants to focus any instance of the app
  130. if ([ -z "$WINDOW_ID" ] || [ "$WINDOW_ID" = "null" ]) && [ -z "$CLASS_NAME" ] && [ -z "$TITLE_NAME" ]; then
  131. WINDOW_ID=$(find_window "$APP_NAME")
  132. debug "After app name search (no class/title): WINDOW_ID='$WINDOW_ID'"
  133. ([ -z "$WINDOW_ID" ] || [ "$WINDOW_ID" = "null" ]) && WINDOW_ID=$(find_window "${APP_NAME,,}")
  134. debug "After lowercase app name search: WINDOW_ID='$WINDOW_ID'"
  135. fi
  136. if [ -n "$WINDOW_ID" ] && [ "$WINDOW_ID" != "null" ] && [ "$WINDOW_ID" -eq "$WINDOW_ID" ] 2>/dev/null; then
  137. debug "Window found! Focusing window ID: $WINDOW_ID"
  138. focus_window "$WINDOW_ID"
  139. exit 0
  140. fi
  141. debug "No matching window found, launching new instance"
  142. "$COMMAND" "${ARGS[@]}" >/dev/null 2>&1 &
  143. disown