On Bspwm Tweaking
Posted by Jack on 2015-03-13 at 14:08Tagged: software
I've written before on my travels through the tiling WM landscape. It's been awhile though.
My most recent discovery is bspwm, which is a tiling WM that mixes automatic (think Xmonad) and manual (think ion3/notion) tiling as well as a hands off, but play nice approach to other desktop necessities like status bars and trays.
Why bspwm?
-
It's Minimal. Similar to Xmonad, bspwm does exactly one thing, but does it extremely well. It places windows. It doesn't have a built-in anything. No trays. No status bars. No menus. It doesn't even directly handle keybinds thanks to a companion program by the same author (Bastien Dejean) call
sxhkd
or Simple X HotKey Daemon which is a flexible tool to map keybinds to simple command execution similar toxbindkeys
but better. In my experience, built-in extra-features of WMs are often lacking the amount of flexibility I want, so no loss there. -
It's Scriptable. Bspwm has a companion program,
bspc
that can be used from the command line, or a script, to accomplish any action bspwm is capable of. In fact,bspc
's so complete that all of the defaultsxhkd
binds arebspc
commands and thebspwmrc
is nothing but a shell script callingbspc
to set configuration options, along with whatever other startup stuff you wish. -
It Communicates. It's a simple affair to extract information from bspwm, both via
bspc
and via the status FIFO that can be used to receive notification of bspwm internal events.
To summarize, bspwm
fits into the sweet spot where it has the minimalism of Xmonad, trading out the Haskell for shell scripts and a whole lot of scripting potential.
My Setup
First an obligatory double-wide screenshot.
Very simple, but there are a few upgrades from the examples in the git repo, despite the fact that the color scheme is the same.
-
Easy Named Desktops. I've written a couple of helper scripts around
bspc
anddmenu
that make it simple to create, switch to, rename, or destroy named desktops. -
Enumerated Desktops. They're also numbered along so that bspwm's default keybinds are more easily used along with their descriptive names.
-
Double Monitor Status Bar. I've split the monitor/desktop output of the status bar so that each monitor's information is displayed on it, rather than all on the left side of the bar.
-
Keybinds for Focusing/Sending to Monitors. A simple job for
bspc
, I was surprised there was no default bind for it, but these binds allow you to shift windows and focus from one screen to another without knowing which desktop is on which. -
Battery Monitor. I added a simple battery monitor on my laptop host.
-
A Tray. I've configured stalonetray to blend into my status bar.
-
Simplified Files. I much preferred having the panel configuration all in one file, instead of three (or four, if you count tweaking your
.profile
). -
Hostname Tweaks. Tweaks to make my single config work identically between my desktop and laptop.
Named Desktops
Bspwm natively supports named desktops, but they're cumbersome to use since you have to use bspc
from somewhere. The example config spawns 10 desktops by number and calls it good. Well, coming from notion (a fork of ion3), I got used to the ability to name desktops something useful to remember what the hell I was doing on them, and then shift between them easily.
As such, I've added two scripts, which are trivial wrappers around dmenu
and bspc
#!/bin/bash
DMENU\_NF="#A3A6AB"
DMENU\_NB="#34322E"
DMENU\_SF="#F6F9FF"
DMENU\_SB="#5C5955"
DESKTOP\_NAME=`bspc query -D | dmenu -b -nf "$DMENU\_NF" -nb "$DMENU\_NB" -sf "$DMENU\_SF" -sb "$DMENU\_SB" -p 'Desktop:'`
if [ -z $DESKTOP\_NAME ]; then
exit 0
fi
for existing\_desktop in `bspc query -D`; do
if [ "$DESKTOP\_NAME" == "$existing\_desktop" ]; then
bspc desktop "$DESKTOP\_NAME" -m `bspc query -M -m focused`
bspc desktop -f "$DESKTOP\_NAME"
exit 0
fi
done
bspc monitor -a "$DESKTOP\_NAME"
bspc desktop -f "$DESKTOP\_NAME"
#!/bin/bash
DMENU\_NF="#A3A6AB"
DMENU\_NB="#34322E"
DMENU\_SF="#F6F9FF"
DMENU\_SB="#5C5955"
DESKTOP\_NAME=`echo '' | dmenu -b -nf "$DMENU\_NF" -nb "$DMENU\_NB" -sf "$DMENU\_SF" -sb "$DMENU\_SB" -p 'Rename:'`
if [ -z $DESKTOP\_NAME ]; then
exit 0
fi
bspc desktop -n "$DESKTOP\_NAME"
I put these into my $PATH
someplace, I use ~/bin
for all of my custom scripts.
Then, in sxhkdrc
:
super + {\_,ctrl} + d
/home/jack/bin/bspwm\_{\_,re}name\_desktop
super + alt + d
bspc desktop focused -r
This allows you to use Super + d
to create a new named desktop, or switch to it if it already exists. Super + ctrl + d
will rename the currently focused desktop. Finally, Super + alt + d
will destroy the focused desktop (but only if it's empty and not the only desktop on its monitor, which is a restriction of bspwm).
Panel Improvements
I've combined the various panel example files into a custom single file that's still run from bspwmrc
.
This covers the enumerated desktops, split monitor status, tray, and hostname tweaks I mentioned above.
Here's the full panel script, commented with improvements:
#! /bin/sh
#
# Absorb all of the rote variable setting to make our panel self-contained
# From example .profile changes
PANEL\_FIFO="$HOME/.config/bspwm/panfifo"
PANEL\_HEIGHT="16"
PANEL\_FONT\_FAMILY="-*-terminus-medium-r-normal-*-12-*-*-*-c-*-*-1"
# From panel\_colors file
COLOR\_FOREGROUND='#FFA3A6AB'
COLOR\_BACKGROUND='#FF34322E'
COLOR\_ACTIVE\_MONITOR\_FG='#FF34322E'
COLOR\_ACTIVE\_MONITOR\_BG='#FF58C5F1'
COLOR\_INACTIVE\_MONITOR\_FG='#FF58C5F1'
COLOR\_INACTIVE\_MONITOR\_BG='#FF34322E'
COLOR\_FOCUSED\_OCCUPIED\_FG='#FFF6F9FF'
COLOR\_FOCUSED\_OCCUPIED\_BG='#FF5C5955'
COLOR\_FOCUSED\_FREE\_FG='#FFF6F9FF'
COLOR\_FOCUSED\_FREE\_BG='#FF6D561C'
COLOR\_FOCUSED\_URGENT\_FG='#FF34322E'
COLOR\_FOCUSED\_URGENT\_BG='#FFF9A299'
COLOR\_OCCUPIED\_FG='#FFA3A6AB'
COLOR\_OCCUPIED\_BG='#FF34322E'
COLOR\_FREE\_FG='#FF6F7277'
COLOR\_FREE\_BG='#FF34322E'
COLOR\_URGENT\_FG='#FFF9A299'
COLOR\_URGENT\_BG='#FF34322E'
COLOR\_LAYOUT\_FG='#FFA3A6AB'
COLOR\_LAYOUT\_BG='#FF34322E'
COLOR\_TITLE\_FG='#FFA3A6AB'
COLOR\_TITLE\_BG='#FF34322E'
COLOR\_STATUS\_FG='#FFA3A6AB'
COLOR\_STATUS\_BG='#FF34322E'
# Kill any panel processes older than us, instead of bailing like the example
# does. That caused one too many panel-less boots for me.
while [ $(pgrep -cx panel) -gt 1 ] ; do
pkill -ox -9 panel
done
# Kill any remaining trays / xtitle instances so we don't have multiples.
killall -9 stalonetray
killall -9 xtitle
# Setup taken from example, tell bspwm to avoid our status/tray and to start
# sending status updates to a FIFO
trap 'trap - TERM; kill 0' INT TERM QUIT EXIT
[ -e "$PANEL\_FIFO" ] && rm "$PANEL\_FIFO"
mkfifo "$PANEL\_FIFO"
bspc config top\_padding $PANEL\_HEIGHT
bspc control --subscribe > "$PANEL\_FIFO" &
# Here are the subprograms that add information to the status FIFO which are
# interpreted by panel\_bar, below. Each output is detected by its first
# character, which is how the bspwm internal information is presented.
# T - xtitle output
# S - date output (same as example)
# B - battery output
# Title
xtitle -sf 'T%s' > "$PANEL\_FIFO" &
# Simple date
function clock {
while true; do
date +"S%m/%d %H:%M"
sleep 1
done
}
clock > "$PANEL\_FIFO" &
# No frills battery monitor (Linux specific, probably)
# This is only enabled on certain hostnames, at the end of this file.
BAT="/sys/class/power\_supply/BAT0"
function bat\_percent {
while true; do
CHARGE\_NOW=`cat $BAT/charge\_now`
CHARGE\_FULL=`cat $BAT/charge\_full`
PERCENT=`echo "($CHARGE\_NOW * 100)/$CHARGE\_FULL" | bc`
STATUS=`cat $BAT/status`
if [ $STATUS == "Charging" ]; then
STATUS="+"
elif [ $STATUS == "Discharging" ]; then
STATUS="-"
else
STATUS=""
fi
echo "B$STATUS$PERCENT"
sleep 1
done
}
# Now panel\_bar, which was mostly taken from the example panel\_bar, with a
# handful of improvements.
# - functionified, from panel\_bar file of example
# - the output changes based on the number of monitors, to place a single
# monitors's information on that same monitor, instead of all in one corner.
# - added B header for battery
# - all the desktop indicators are enumerated
num\_mon=$(bspc query -M | wc -l)
wm\_info\_array=("" "" "" "" "")
function panel\_bar {
while read line < $PANEL\_FIFO; do
case $line in
S*)
# clock output
date="${line#?}"
;;
B*)
# battery output
percent="${line#?}"
;;
T*)
# xtitle output
title="%{F$COLOR\_TITLE\_FG}%{B$COLOR\_TITLE\_BG} ${line#?} %{B-}%{F-}"
;;
W*)
# bspwm internal state
wm\_infos=""
cur\_mon=-1
desktop\_num=1
IFS=':'
set -- ${line#?}
while [ $# -gt 0 ] ; do
item=$1
name=${item#?}
case $item in
M*)
# active monitor
cur\_mon=$((cur\_mon + 1))
wm\_infos=""
if [ $num\_mon -gt 1 ] ; then
wm\_infos="$wm\_infos %{F$COLOR\_ACTIVE\_MONITOR\_FG}%{B$COLOR\_ACTIVE\_MONITOR\_BG} ${name} %{B-}%{F-} "
fi
;;
m*)
# inactive monitor
cur\_mon=$((cur\_mon + 1))
wm\_infos=""
if [ $num\_mon -gt 1 ] ; then
wm\_infos="$wm\_infos %{F$COLOR\_INACTIVE\_MONITOR\_FG}%{B$COLOR\_INACTIVE\_MONITOR\_BG} ${name} %{B-}%{F-} "
fi
;;
O*)
# focused occupied desktop
wm\_infos="$wm\_infos%{F$COLOR\_FOCUSED\_OCCUPIED\_FG}%{B$COLOR\_FOCUSED\_OCCUPIED\_BG}%{U$COLOR\_FOREGROUND}%{+u} ${desktop\_num}. ${name} %{-u}%{B-}%{F-}"
desktop\_num=$((desktop\_num + 1))
;;
F*)
# focused free desktop
wm\_infos="$wm\_infos%{F$COLOR\_FOCUSED\_FREE\_FG}%{B$COLOR\_FOCUSED\_FREE\_BG}%{U$COLOR\_FOREGROUND}%{+u} ${desktop\_num}. ${name} %{-u}%{B-}%{F-}"
desktop\_num=$((desktop\_num + 1))
;;
U*)
# focused urgent desktop
wm\_infos="$wm\_infos%{F$COLOR\_FOCUSED\_URGENT\_FG}%{B$COLOR\_FOCUSED\_URGENT\_BG}%{U$COLOR\_FOREGROUND}%{+u} ${desktop\_num}. ${name} %{-u}%{B-}%{F-}"
desktop\_num=$((desktop\_num + 1))
;;
o*)
# occupied desktop
wm\_infos="$wm\_infos%{F$COLOR\_OCCUPIED\_FG}%{B$COLOR\_OCCUPIED\_BG} ${desktop\_num}. ${name} %{B-}%{F-}"
desktop\_num=$((desktop\_num + 1))
;;
f*)
# free desktop
wm\_infos="$wm\_infos%{F$COLOR\_FREE\_FG}%{B$COLOR\_FREE\_BG} ${desktop\_num}. ${name} %{B-}%{F-}"
desktop\_num=$((desktop\_num + 1))
;;
u*)
# urgent desktop
wm\_infos="$wm\_infos%{F$COLOR\_URGENT\_FG}%{B$COLOR\_URGENT\_BG} ${desktop\_num}. ${name} %{B-}%{F-}"
desktop\_num=$((desktop\_num + 1))
;;
L*)
# layout
wm\_infos="$wm\_infos %{F$COLOR\_LAYOUT\_FG}%{B$COLOR\_LAYOUT\_BG} ${name} %{B-}%{F-}"
;;
esac
shift
wm\_info\_array[cur\_mon]="$wm\_infos"
done
;;
esac
if [ $num\_mon -eq 1 ]; then
fmt="%{l}${wm\_info\_array[0]}%{c}${title}%{r}${percent} ${date}"
elif [ $num\_mon -eq 2 ]; then
fmt="%{l}${wm\_info\_array[0]}%{c}${title}%{S+}%{l}${wm\_info\_array[1]}%{r}${percent} ${date}"
else
# Same as 2 -- needs someone to test
fmt="%{l}${wm\_info\_array[0]}%{c}${title}%{S+}%{l}${wm\_info\_array[1]}%{r}${percent} ${date}"
fi
printf "%s\n" "$fmt"
done
}
# Actually invoking the panel and piping to bar
panel\_bar | bar -g x$PANEL\_HEIGHT -f "$PANEL\_FONT\_FAMILY" -F "$COLOR\_FOREGROUND" -B "$COLOR\_BACKGROUND" &
# Hostname tweaks, including forcing stalonetray into a reasonable place, and
# starting the battery monitor on my laptop
HOSTNAME=`hostname`
# Echelon dual monitor, tray should be upper right corner of left monitor
if [ $HOSTNAME == "echelon" ]; then
TRAY\_GEOM="1x1-1920"
# Toadite single monitor, avoid date and battery
elif [ $HOSTNAME == "toadite" ]; then
bat\_percent > "$PANEL\_FIFO" &
TRAY\_GEOM="1x1-100"
# Good for single monitor, just about enough room to avoid the date output
else
TRAY\_GEOM="1x1-75"
fi
stalonetray --geometry $TRAY\_GEOM -i $PANEL\_HEIGHT -bg "#34322e" --grow-gravity NE --kludges force\_icons\_size &
wait
I can't really explain any better than the comments, but I think I'm using a decent setup that will allow for future additions to the panel pretty easily just by mimicking the date and battery status functions and their invocations.
Monitor Binds
I simply added this to my sxhkdrc
super + {q,w}
bspc monitor -f {L,R}
super + {shift,alt} + {q,w}
bspc {window,desktop} -m {L,R}
This follows the Xmonad convention, where Q and W represent left and right monitors respectively, so super + q
focuses the left monitor, super + w
the right monitor. If I had three monitors, I would use QWE for Left, Center, Right.
The second set of binds sets up super + shift + q/w
to send windows to a specific monitor's desktop, and super + alt + q/w
to shift an entire desktop to another monitor. I find bspwm's desktop focus to be a bit wonky with multiple monitors (focusing a desktop will focus it on whatever monitor it's associated with, rather than the current monitor), but I still only rarely have to shift desktops between monitors.
In order for these binds to work correctly on multiple hosts, in bspwmrc
I added this block to rename my monitors consistently across machines.
HOSTNAME=`hostname`
if [ $HOSTNAME == "echelon" ]; then
bspc monitor HDMI-0 -n R
bspc monitor DVI-I-1 -n L
bspc monitor R -s L
bspc monitor L -d ""
bspc monitor R -d ""
elif [ $HOSTNAME == "toadite" ]; then
bspc monitor LVDS-0 -n L
bspc monitor L -d ""
fi
This also clears the desktops, down to a single unnamed desktop on each monitor.
Download
You can grab my config files here.
This includes everything you should need to run bspwm with my config. It untars such that the bspwm-config directory is like your HOME, so the config files are in .config, and won't be visible by default.
The only caveat is that you'll need to put the bin files in your $PATH someplace. Like the example config, it also expects that you have xtitle
and bar-ain't-recursive
installed.
If I end up making any more significant changes, I'll consider putting this up on my Github, but for now I'm pretty content with my setup and don't feel the need to version control it.