On Bspwm Tweaking

Posted by Jack on 2015-03-13 at 14:08
Tagged: 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?

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.

2015-03-12-000528_3840x1080_scrot

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.

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.