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
# 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 matches a window, it will bring it to active/focus from any position or hide/close it.
- If there is no match it will 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 bonk.
-v Look only for visible windows for match, works well for sticky windows.
Default: lookup at all windows, hoping that its order is right.
Mandatory for matching: -c AND/OR -n
Mandatory for running: -m AND/OR -s
HEREDOC
}
requirements=( xdotool bonk )
for cmd in "${requirements[@]}"; do
if [[ -z $(command -v $cmd) ]]; then
cat <<- "HEREDOC"
Command $cmd could not be found.
Requirements:
- xdotool: sudo apt xdotool
- bonk: https://github.com/FascinatedBox/bonk'
HEREDOC
exit 1
fi
done
# default to lookup for any windows
_visibleMode="--all"
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="" ;;
\?) 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
# help to lower the risk of concurrencies
renice -n 10 $$
# ID focused windows
_winActiveId=$(bonk get-active 2>/dev/null)
# List matching window and select last
sleep 0.1
if [[ -z $_winClassPattern ]]; then
_winNameIds=$(bonk select $_visibleMode --title "${_winNamePattern}" 2>/dev/null)
_winMatchId=$(echo "${_winNameIds}" | tail -1)
elif [[ -z $_winNamePattern ]]; then
_winClassIds=$(bonk select $_visibleMode --classname "${_winClassPattern}" 2>/dev/null)
_winMatchId=$(echo "${_winClassIds}" | tail -1)
else
_winNameIds=$(bonk select $_visibleMode --title "${_winNamePattern}" 2>/dev/null)
_winClassIds=$(bonk select $_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 [id="${_winMatchId}"] move scratchpad
else
bonk close -w "${_winMatchId}" 2>/dev/null
fi
# a match exist
elif [[ -n $_winMatchId ]]; then
xdotool set_desktop_for_window "${_winMatchId}" "$(xdotool get_desktop)" 2>/dev/null
bonk activate -w "${_winMatchId}" 2>/dev/null
sleep 0.1
if [[ ! "${_winMatchId}" == "$(bonk get-active 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

  • 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