#!/usr/bin/env bash # https://gitlab.com/wef/dotfiles/-/blob/master/bin/sway-select-window # shellcheck disable=SC2034 TIME_STAMP="20230129.084152" # Copyright (C) 2019-2023 Bob Hepple # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # CUSTOMISE HERE IF YOU'RE NOT GETTING ALL YOUR ICONS: # This array contains my most commonly used apps on f34, cached here # for speed. If they don't work for you, you can either just remove # these entries and rely on on-the-fly lookup of icons (which may be # slow). Or, for speedier operation, you can: # 1. get all your common apps running # 2. remove the entries in the array # 3. run the script from the CLI # 4. copy-paste the list of "[app]=icon" lines into the array # Also, if all else fails, you can hardcode your app here: declare -A icons=( [floating-kitty]=/usr/share/icons/hicolor/256x256/apps/kitty.png [kitty]=/usr/share/icons/hicolor/256x256/apps/kitty.png [foot]=/usr/share/icons/hicolor/48x48/apps/foot.png [emacs]=/usr/share/icons/breeze/apps/48/emacs.svg [firefox]=/usr/share/icons/hicolor/48x48/apps/firefox.png [mythfrontend]=/usr/share/pixmaps/mythfrontend.png [kitty]=/usr/share/icons/hicolor/256x256/apps/kitty.png [Gdcalc]=/usr/share/pixmaps/HP-16C-48.xpm [pavucontrol]=/usr/share/icons/gnome/48x48/apps/multimedia-volume-control.png [pulseeffects]=/usr/share/icons/hicolor/scalable/apps/pulseeffects.svg [blueman-manager]=/usr/share/icons/hicolor/scalable/apps/blueman.svg [gnome-system-monitor]=/usr/share/icons/hicolor/scalable/apps/org.gnome.SystemMonitor.svg [gnome-disks]=/usr/share/icons/hicolor/scalable/apps/org.gnome.DiskUtility.svg [org.gnome.Weather]=/usr/share/icons/hicolor/scalable/apps/org.gnome.Weather.svg [xfce4-appfinder]=/usr/share/icons/hicolor/scalable/apps/org.xfce.appfinder.svg [LibreWolf]=/var/lib/flatpak/exports/share/icons/hicolor/128x128/apps/io.gitlab.librewolf-community.png [firefox]=/var/lib/flatpak/exports/share/icons/hicolor/128x128/apps/org.mozilla.firefox.png ) KEYBOARD_DEVICE="/dev/input/event4" verbose="" PROG=$( basename "$0" ) case "$1" in -h|--help) echo "Usage: $PROG" echo echo "show running programs and select one to display (uses wofi)." echo "If evtest(1) is installed and sudo is available then you can press" echo "Shift-Enter to move the window here rather than move to" echo "the windows workspace. You may need to change KEYBOARD_DEVICE above." exit 0 ;; -v|--verbose) verbose="set" shift ;; esac jq_get_windows=' # descend to workspace or scratchpad .nodes[].nodes[] # save workspace name as .w | {"w": .name} + ( if (.nodes|length) > 0 then # workspace [recurse(.nodes[])] else # scratchpad [] end + .floating_nodes | .[] # select nodes with no children (windows) | select(.nodes==[]) )' jq_windows_to_tsv=' [ (.id | tostring), # remove markup and index from workspace name, replace scratch with "[S]" (.w | gsub("<[^>]*>|:$"; "") | sub("__i3_scratch"; "[S]")), # get app name (or window class if xwayland) (.app_id // .window_properties.class), (.name), (.pid) ] | @tsv' get_fs_win_in_ws() { id="${1:?}" swaymsg -t get_tree | jq -e -r --argjson id "$id" " [ $jq_get_windows ] | (.[]|select(.id == \$id)) as \$selected_workspace | .[] | select( .w == \$selected_workspace.w and .fullscreen_mode == 1 ) | $jq_windows_to_tsv " } get_hardcoded_icon() { icon="${icons[$1]}" echo "$icon" } get_desktop() { app="$1" p="/usr/share/applications" # fast and easy cases first: for prefix in "" org.kde. org.gnome. org.freedesktop.; do d="$p/$prefix$app.desktop" [[ -r "$d" ]] && echo "$d" && return done # maybe lowercase for prefix in "" org.kde. org.gnome. org.freedesktop.; do d="$p/$prefix${app,,}.desktop" [[ -r "$d" ]] && echo "$d" && return done # this is fairly reliable but slow: # look for a .desktop file with Exec=$app eg # gnome-disks (but exclude gnome-disk-image-writer.desktop) # gnome-font-viewer GREP='egrep -r' type rg &>/dev/null && GREP=rg d=$( $GREP -il "^exec=$app( %u)*[[:space:]]*$" $p | head -n 1) [[ -r "$d" ]] && echo "$d" && return # desperation - weird apps like com.github.wwmm.pulseeffects.desktop!! # shellcheck disable=SC2012 d=$( ls "$p/"*".$app.desktop" 2>/dev/null | head -n 1 ) [[ -r "$d" ]] && echo "$d" && return } get_icon() { icon_locations=( icons/hicolor/scalable/apps icons/hicolor/48x48/apps icons/hicolor/128x128/apps icons/hicolor/256x256/apps icons/gnome/48x48/apps icons/gnome/48x48/devices pixmaps ) app="$1" icon=$( get_hardcoded_icon "$app_name" ) [[ "$icon" && -r "$icon" ]] && echo "$icon" && return # let's go poke in the .desktop files: icon_name="" dt=$( get_desktop "$app" ) # sometimes we get the 'class' rather than the exe - so try this: [[ "$dt" ]] || { app=$( tr '\0' '\n' < "/proc/$pid/cmdline" | head -n 1 ) dt=$( get_desktop "$( basename "$app" )" ) } [[ "$dt" ]] && { icon_name=$( awk -v IGNORECASE="set" -F"=" '/^icon/ {print $2}' "$dt" ) [[ "$icon_name" ]] && { for d in "${icon_locations[@]}"; do for s in .svg .png ""; do icon=/usr/share/$d/$icon_name$s [[ -r $icon ]] && echo "$icon" && return done done } } # desperation street: icon=$( find /usr/share/icons /usr/share/pixmaps | grep -E -i "/$app.(png|svg)" | head -n 1 ) [[ "$icon" && -r "$icon" ]] && echo "$icon" && return # failed: #echo "/usr/share/icons/Adwaita/32x32/status/image-missing-symbolic.symbolic.png" echo "$HOME/.local/share/icons/img-missing-sym.png" } focus_window() { id="${1}" ws_name="${2}" app_name="${3}" win_title="${4}" [[ "$verbose" ]] && printf "focusing window (in workspace %s): [%s] (%s) \`%s\`\n" "$ws_name" "$id" "$app_name" "$win_title" >&2 # look for fullscreen among other windows in selected window's workspace if fs_win_tsv="$( get_fs_win_in_ws "$id" )" ; then read -r win_id ws_name app_name win_title <<<"$fs_win_tsv" if [ "$win_id" != "$id" ] ; then [[ "$verbose" ]] && printf 'found fullscreen window in target workspace (%s): [%s] (%s) "%s"\n' "$ws_name" "$win_id" "$app_name" "$win_title" >&2 swaymsg "[con_id=$win_id] fullscreen disable" fi fi swaymsg "[con_id=$id]" focus } id_fmt='%s' ws_fmt='%s' app_fmt='%s' title_fmt='%s' #id_fmt='%s' #ws_fmt='%s' #app_fmt='%s' #title_fmt='%s' # allow Alt+Tab etc to not be caught by sway: trap 'swaymsg "mode default"' EXIT swaymsg "mode passthrough" swaymsg -t get_tree | # get list of windows as tab-separated columns jq -r "$jq_get_windows | $jq_windows_to_tsv" | # align columns w/ spaces (keep tab as separator) column -s $'\t' -o $'\t' -t | # pango format the window list while IFS=$'\t' read -r win_id ws_name app_name win_title pid; do shopt -s extglob app_name="${app_name%%*( )}" icon=$( get_icon "$app_name" "$pid" ) [[ "$verbose" ]] && printf "[%s]=>%s\n" "$app_name" "$icon" >&2 # shellcheck disable=SC2059 # hide win_id for now #win_id="" # simplify ws_name instead of Scratch #(( ${#ws_name} > 8 )) && ws_name="${ws_name:0:1}" printf "img:$icon:text:${id_fmt}\t${ws_fmt}\t${app_fmt}\t${title_fmt}\n" \ "$win_id" \ "$ws_name" \ "$app_name" \ "$win_title" done | # sort by workspace name and then app_name: sort -k2 -k3 | # defeat the initial cache in order to preserve the order: wofi -m --cache-file=/dev/null --insensitive --allow-images --show dmenu --prompt='Focus a window' | { FS=$'\t' read -r win_id ws_name app_name win_title # printf "win_id='$win_id' ws_name='$ws_name' app_name='$app_name' win_title='$win_title'" >&2 # use evtest (if installed) to detect the state of the left shift # key and if it's 'down' then move the window here rather than move # to the window's workspace. Should be safe if evtest or sudo are # not available. ## i've disabled this type evtest && sudo evtest --query $KEYBOARD_DEVICE EV_KEY KEY_LEFTSHIFT SHIFT_STATE=$? if [[ "$win_id" ]]; then if (( SHIFT_STATE == 10 )); then # bring the window here swaymsg "[con_id=$win_id] move window to workspace current, focus" else focus_window "$win_id" "$ws_name" "$app_name" "$win_title" fi fi } exit 0 # this is the example documented in 'man 7 wofi' but it doesn't understand the scratchpad: swaymsg -t get_tree | jq -r '.nodes[].nodes[] | if .nodes then [recurse(.nodes[])] else [] end + .floating_nodes | .[] | select(.nodes==[]) | ((.id | tostring) + " " + .name)' | wofi --show dmenu | { read -r id name swaymsg "[con_id=$id]" focus } # Local Variables: # mode: shell-script # time-stamp-pattern: "4/TIME_STAMP=\"%:y%02m%02d.%02H%02M%02S\"" # eval: (add-hook 'before-save-hook 'time-stamp) # End: