BSPWM Quake

A couple of years ago, I wrote this tool called i3quake; it was a pop-up terminal for i3, and while it was fun, by the time I finished the project I realized that I could do most of it using just i3-msg and a bash script. The project was still useful, because there’s a race condition where you have to create the window to get an ID before you can attach a tag to the window with the ID, and it was easier to do that in a language that had threads, but ultimately, i3quake turned into a fancy luancher that set up a fancy shell.

A couple of months ago, I set myself up with bspwm and I needed my Quake terminal. It’s funny how little things become so ingrained after decades of using them that you almost can’t function without them. In any case, it turned out that scripting pop-up behavior on bspwm is even easier than on i3; so easy, it’s as easy to explain in a blog post as to upload a script. Note that you can do this for any X window, not just terminals. You can have pop-up Element chat windows, pop-up Gimp toolbars, whatever. Just give them unique IDs and bind the hide/expose commands to different characters.

What we’re looking for is a way to bind a hot-key to pop up a floating window in a specific geometry, and hide it again on the next hotkey press. In i3, we use the scratchpad, a hidden desktop that you can send windows to; a sort of oubliette. Bspwm doesn’t have a scratchpad, but it does respect X WM_HINTS, one of which is the hidden flag, and we can get all of the behavior we need with xdpyinfo, xdotool, and bspc.

First, pick a unique class for your quake window, say QUAKENAME=bspwmquake.

Next figure out the dimensions and offsets for your window. I like to have a tall window off the right side of my screen covering about half of the desktop. If you took any compsci classes, you inevitably figured out how to center a box: the x-offset is (total_width - $BOXWIDTH) / 2. Same with the y-offset: (total_height - $BOXHEIGHT) / 2. For example, let’s say my desktop is 1920px wide, and I want my terminal to be half that wide; my XOFFSET will be (1920 - (1920/2)) / 2, or 480. My terminal is going to be the total height of the window, so the YOFFSET is – trivially – 0.

Once you have the dimensions, you create a bspwm rule to enforce them. For that, you call bspc rule -a $QUAKENAME state=floating rectangle="$BOXWIDTH"x"$BOXHEIGHT"+"$XOFFSET"+"$YOFFSET".

Now start your applicationi and give the window the $QUAKENAME class you picked. Terminals usually have a command-line argument for this; for st it’s -c CLASS, for alacritty it’s --class. If your GUI app doesn’t have a CLI argument for this, you can use xprop to set the class name, a-la xprop -set WM_CLASS $QUAKENAME. xdotool set_window --class $QUAKENAME will do the same thing.

You’ll need to know the X window ID now so that you can run bspc commands on it. This is easy, because of all the work you went through to give the window a unique class: xdotool search --class $QUAKENAME will return the window ID for you; stick it in a variable, like $WIN_ID!

Next is to make sure the window is on the right desktop; otherwise, it’ll just pop up on whatever desktop it was created. You do that with bspc node $WIN_ID -d $(bspc query --desktops --desktop focused); the query figures out which desktop is focused, and the node command tells bspwm to send your window to that desktop.

Finally, you toggle the hidden flag and focused state; this command is: bspc node $WIN_ID -g hidden -f.

Bind the last two commands (send to focused desktop; toggle hidden) to your hotkey and just keep calling them, and the window will pop up and down.

I’m pasting a script that does all of this; it’s 51 lines, but much of what it does is to be portable to different desktop sizes, and there are a lot of comments. The real work is only a few lines setting up the window (which it does only once), and those two move & toggle commands. It’s more simple than you might think.

#!/usr/bin/zsh
#
# Quake terminal for BSP
# v1.0.0 Sean E. Russell <ser@ser1.net>
# BSD-3 Clause (https://opensource.org/licenses/BSD-3-Clause)
#
# Dependencies
# - zsh (https://www.zsh.org/)
# - xdpyinfo (https://xorg.freedesktop.org/)
# - bspc (https://github.com/baskerville/bspwm)
# - xdotool (https://www.semicomplete.com/projects/xdotool/)

##########################################################
# CONFIGURATION
##########################################################

# How much of your screen width do you want the terminal to cover?
WINDOWWIDTH=0.6
# How much of your screen height do you want the terminal to cover?
WINDOWHEIGHT=1.0
# X Position: 0 = Left, 1 = Right, 2 = Center
XPOS=1
# Y Position: 0 = Top, 1 = Bottom, 2 = Center
YPOS=2

# Some unique class to give the window. This should be fine.
QUAKENAME=bspwm_quake

##########################################################
# NE TUŜU!
##########################################################

WIN_ID=$(printf "0x%x" $(xdotool search --class $QUAKENAME))

if [[ $WIN_ID == "0x0" ]]; then
	bspc rule -r $QUAKENAME
	screenwidth=$(xdpyinfo | awk -F'[ x]+' '/dimensions:/{print $3}')
	screenheight=$(xdpyinfo | awk -F'[ x]+' '/dimensions:/{print $4}')
	windowwidth=$(printf "%d" $(($screenwidth * $WINDOWWIDTH)))
	windowheight=$(printf "%d" $(($screenheight * $WINDOWHEIGHT)))
	[[ $XPOS -eq 2 ]] && XPOS=0.5
	[[ $YPOS -eq 2 ]] && YPOS=0.5
	windowx=$(printf "%d" $((($screenwidth  - $windowwidth)  * $XPOS)))  # 1 = right,  0 = left, 0.5 = center
	windowy=$(printf "%d" $((($screenheight - $windowheight) * $YPOS)))  # 1 = bottom, 0 = top,  0.5 = center
	bspc rule -a $QUAKENAME state=floating rectangle="$windowwidth"x"$windowheight"+"$windowx"+"$windowy"
	st -c $QUAKENAME &!
else
    bspc node $WIN_ID -d $(bspc query --desktops --desktop focused)
	bspc node $WIN_ID -g hidden
	bspc node $WIN_ID -f
fi