Last active
December 15, 2024 21:01
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
- 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.
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\\""
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
Found a new usage, I can add block.click commands in i3status-rust to toogle open/hide terminal apps. Example with cava: