Skip to content

Instantly share code, notes, and snippets.

@tkapias
Last active April 8, 2024 08:23
Show Gist options
  • Save tkapias/0443d4930c1d7b520f4496e1ff391625 to your computer and use it in GitHub Desktop.
Save tkapias/0443d4930c1d7b520f4496e1ff391625 to your computer and use it in GitHub Desktop.
Toggle or launch a new instance of a window with i3wm on a single screen setup.
#!/usr/bin/env bash
# Warning:
# xdotool fixed a race condition bug in 2021
# Related issues:
# https://github.com/jordansissel/xdotool/issues/60
# https://github.com/jordansissel/xdotool/pull/335
# Debian testing still uses v2016 in 2024, build the latest:
# https://github.com/jordansissel/xdotool
# Example for i3wm exec binding: toggle or run urxvtc in client mode (systemd daemon urxvtd), with setenv to bypass TMUX in my bashrc.
# bindcode $mod+Shift+49 exec --no-startup-id "/home/user/.config/i3/scripts/wtoggle.sh -i -c \\"^URxvt$\\" -n \\"^Terminal\sURxvt$\\" -m \\"urxvtc -title 'Terminal URxvt' -e sh -c 'TMUX=false bash'\\" -s \\"urxvtd\\""
# locale
export LC_ALL="C.UTF-8"
export TZ=:/etc/localtime
Help()
{
cat <<- 'HEREDOC'
This script is useful for i3wm.
Currently for single screen setup only.
- If it match a window it toggles it to focus from any position or hide/close it.
- If there is no match it launch a new command/service or both in daemon/client mode.
Syntax: ./wtoggle.sh [-h] [Options...]
options:
-h Print this Help.
-c "<winClassPattern>" Regex pattern for the matching window classname.
-n "<winNamePattern>" Regex pattern for the matching window name or title.
-m "<Cmd>" Command to launch if there is no match.
-s "<Service>" Systemd service to check & launch in priority if there is no match.
-i Use i3wm to send the active window to the scratchpad if it match.
Default: gracefully close the window with wmctrl.
-v Look ontly for visible windows for match, works well for sticky windows.
Default: lookup at all the stack, hoping that its order is right.
Mandatory for matching: -c AND/OR -n
Mandatory for running: -m AND/OR -s
HEREDOC
}
while getopts ":hc:n:m:s:iv" option; do
case $option in
h ) Help; exit 0 ;;
c ) _winClassPattern="${OPTARG}" ;;
n ) _winNamePattern="${OPTARG}" ;;
m ) _Cmd="${OPTARG}" ;;
s ) _Service="${OPTARG}" ;;
i ) _i3Mode=true ;;
v ) _visibleMode="--onlyvisible" ;;
\?) echo -e "Unknown option: -$OPTARG \n" >&2; Help; exit 1;;
: ) echo -e "Missing argument for -$OPTARG \n" >&2; Help; exit 1;;
* ) echo -e "Unimplemented option: -$option \n" >&2; Help; exit 1;;
esac
done
# Mandatory
if [[ -z $_winClassPattern ]] && [[ -z $_winNamePattern ]]; then
echo -e "Error: option -c or -n is mandatory.\n" >&2; Help; exit 1
fi
if [[ -z $_Cmd ]] && [[ -z $_Service ]]; then
echo -e "Error: option -m and/or -s are mandatory.\n" >&2; Help; exit 1
fi
# ID focused windows
_winActiveId=$(xdotool getactivewindow 2>/dev/null)
# List matching window and select last
if [[ -z $_winClassPattern ]]; then
_winNameIds=$(xdotool search $_visibleMode --name "${_winNamePattern}" 2>/dev/null)
_winMatchId=$(echo "${_winNameIds}" | tail -1)
elif [[ -z $_winNamePattern ]]; then
_winClassIds=$(xdotool search $_visibleMode --classname "${_winClassPattern}" 2>/dev/null)
_winMatchId=$(echo "${_winClassIds}" | tail -1)
else
_winNameIds=$(xdotool search $_visibleMode --name "${_winNamePattern}" 2>/dev/null)
_winClassIds=$(xdotool search $_visibleMode --classname "${_winClassPattern}" 2>/dev/null)
_winMatchIds=$(echo -e "${_winNameIds}\n${_winClassIds}")
for _id in $_winMatchIds; do
count=$(echo "$_winMatchIds" | grep -c "$_id")
[[ ! "$count" == "1" ]] && _winMatchId+=$(echo -e "\n$_id")
done
_winMatchId=$(echo "$_winMatchId" | tail -1)
fi
# if match is focused
if [[ "${_winActiveId:-0}" == "${_winMatchId:-1}" ]]; then
if [[ -n $_i3Mode ]]; then
i3-msg -q move scratchpad
else
# xdotool windowkill or windowclose are too brutal, waiting for windowquit https://github.com/jordansissel/xdotool/pull/306
wmctrl -i -c "${_winMatchId}" 2>/dev/null
fi
# a match exist
elif [[ -n $_winMatchId ]]; then
xdotool set_desktop_for_window "${_winMatchId}" "$(xdotool get_desktop)" 2>/dev/null
xdotool windowactivate "${_winMatchId}" 2>/dev/null
if [[ ! "${_winMatchId}" == "$(xdotool getactivewindow 2>/dev/null)" ]]; then
eval "${_Cmd}"
fi
# no match and there is a service to check
elif [[ -n $_Service ]]; then
if ! systemctl --user is-active --quiet "${_Service}"; then
systemctl --user start "${_Service}" 2>/dev/null
fi
if [[ -n $_Cmd ]]; then
sleep 1
eval "${_Cmd}"
fi
# no match and no service to check
else
eval "${_Cmd}"
fi
@tkapias
Copy link
Author

tkapias commented Apr 22, 2023

Found a new usage, I can add block.click commands in i3status-rust to toogle open/hide terminal apps. Example with cava:

[[block.click]]
button = "left"
cmd = "~/.config/i3/scripts/wtoggle.sh -i -c ^Terminal-Cava$ -m 'urxvtc -name Terminal-Cava -e sh -c cava' -s urxvtd"
update = true
  • If my terminal daemon is not running it will launch urxvtd first.
  • If no instance is running it will open a new terminal with a custom window classname and run the program.
  • If it's the currently focused window it will hide in the scratchpadi.
  • Or it will bring it to focus from anywhere else.

@tkapias
Copy link
Author

tkapias commented Apr 22, 2023

  • Added -v option, to address the case where the main window is not at the bottom of the window stack; like for KeePassXC (issue #9350).

With this option the window need to be always visible (sticky) when not in the scratchpad or closed. With the advantage that we exclude other hidden windows with the same class from the lookup.

@tkapias
Copy link
Author

tkapias commented Feb 11, 2024

I finally found why I still had some failure looking like race conditions, even if it seemed fixed for everyone else using xdotool: Debian testing still provide an old xdotool from 2016, and the bug was fixed in 2021.

Now, it works perfectly.

Some more examples from my i3wm config:

# Start/Toggle a floating single instance terminal (49=azerty's "²")
bindcode $mod+49 exec --no-startup-id "~/.config/i3/scripts/wtoggle.sh -i -c \\"^URxvt$\\" -n \\"^Terminal\sTmux-URxvt$\\" -m \\"urxvtc -title 'Terminal Tmux-URxvt'\\" -s \\"urxvtd\\""
# without Tmux
bindcode $mod+Shift+49 exec --no-startup-id "~/.config/i3/scripts/wtoggle.sh -i -c \\"^URxvt$\\" -n \\"^Terminal\sURxvt$\\" -m \\"urxvtc -title 'Terminal URxvt' -e sh -c 'TMUX=false bash'\\" -s \\"urxvtd\\""

# Start/Toggle a single instance xfe file explorer
bindsym $mod+Tab exec --no-startup-id "~/.config/i3/scripts/wtoggle.sh -i -c \\"^Xfe$\\" -n \\"^Xfe - \\" -m \\"xfe $HOME/.marks/\\""

# Start/Toggle a single instance keepassxc
bindsym $mod+x exec --no-startup-id "~/.config/i3/scripts/wtoggle.sh -c ^keepassxc$ -n Tomasz -m keepassxc"

# Start/Toggle a single instance mpv
bindsym $mod+Shift+p exec --no-startup-id "~/.config/i3/scripts/wtoggle.sh -i -c \\"^gl$\\" -n \\" - mpv$\\" -m \\"mpv --player-operation-mode=pseudo-gui --\\""

# Start/Toggle a single instance ncmpcnn
bindsym $mod+p exec --no-startup-id "~/.config/i3/scripts/wtoggle.sh -i -c \\"^Terminal-ncmpcpp$\\" -m \\"urxvtc -name Terminal-ncmpcpp -e sh -c ncmpcpp\\" -s \\"urxvtd\\""

# Start/Toggle a single instance qalculate-gtk
bindsym $mod+c exec --no-startup-id "~/.config/i3/scripts/wtoggle.sh -i -c \\"qalculate-gtk\\" -n \\"^Qalculate!$\\" -m \\"qalculate-gtk\\"" 

@tkapias
Copy link
Author

tkapias commented Apr 8, 2024

I just discovered a new xdotool alternative called bonk.

As I am still experiencing some concurrency issues sometime with xdotool, I updated the script with bonk to try if for some time.

The old version using xdotool and wmctrl is in the third revision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment