Config time

My river configuration is created by extracting the code blocks from within this file. It sounds far more complex than it needs to be, but it fits in to an elaborate ninja configuration that I use to generate my home directories. The advantage to me is that I can mix-and-match software versions on different machines, but it also means that the repository structure for individual configurations looks overcomplicated from the outside.

Tip

The output from tools/rst2zsh includes comment markers that can be used to navigate back to the source in this file. For example, in vim calling gF will jump to the section under the cursor.

Software versions

Package

Version

foot

1.17.2

river

v0.3.0-39-gccd676e[1]

sandbar

v0.1-13-gaa3f203[2]

swayidle

1.8.0

wideriver

1.2.0-2-g6a11a25[2]

wob

0.14.2

Note

These are the base versions from upstream, there may be local additions exposed in the installation packages. However, they will not introduce breaking changes.

General setup

We’re going to use zsh as it is always available on any system I use:

#! /bin/zsh -x

Note

We set -x here because it is gives us a lazy logging mechanism to catch and report errors at practically zero cost. In the initial run output will end up in river’s log, and in subsequent runs it will be in the executing terminal.

We’ll want stricter defaults out of the box:

setopt err_exit no_unset warn_create_global

… along with extended globs for better matching support:

setopt extended_glob

autoload functions we’ll need later:

autoload -Uz add-zsh-hook

Utility functions

Fetch socket path for systemd .socket units:

find_socket() {
    systemctl --user show $1@$WAYLAND_DISPLAY.socket --property=Listen |
        sed 's,.*=\(.*\) .*,\1,'
}

Populate a wob progress bar, if possible, as we move through the init file:

LINES=${#${(@f)"$(< $0)"}}
_progress() {
    setopt local_options no_xtrace
    [[ -z ${wob_pipe:-} ]] && return
    float line=${funcfiletrace[1]##*:}
    integer pcnt=$(((line - 1) / LINES * 100))
    echo $pcnt >>$wob_pipe
}
add-zsh-hook preexec _progress

While startup is fast enough that a progress marker isn’t necessary, I find it quite useful as a smoketest that quickly highlights an error in the configuration if the progress bar doesn’t reach the end. Also, I’ll be honest, it felt like a fun hack.

Note

This doesn’t strictly require add_zsh_hook, but I prefer the interface offered by it over simply setting the hook by hand.

Calculate a tag mask given a list of tags:

tag_mask() {
    integer r n
    for n ($@) {
        r+=$((1 << (n-1)))
    }
    echo $r
}
ALL_TAGS=$(tag_mask {1..32})

Configure environment

Configure environment variables used by freedesktop.org specifications:

export XDG_SESSION_TYPE=wayland XDG_{CURRENT,SESSION}_DESKTOP=river

Warning

It is important to be aware that river is not a standard compliant value for XDG_*_DESKTOP, but I’m already using it locally to trigger behaviour. I’ll change it if a better option appears later.

Make important environment variables available to dbus and systemd units:

envvars=(
    PATH
    WAYLAND_DISPLAY
    XDG_SESSION_TYPE
    XDG_{CURRENT,SESSION}_DESKTOP
)
if (( $+commands[dbus-update-activation-environment] )) {
    dbus-update-activation-environment --systemd $envvars
} else {
    systemctl --user import-environment $envvars
}

Run background services

I manage all my background services with a systemd user session. systemd handles all the gory details of process supervision, so that — for example — you don’t need to implement your own hot reloading for your status script.

The interesting thing to notice below is that I use instances keyed off of WAYLAND_DISPLAY so that it is possible to run multiple sessions, which comes in handy for testing as you can simply start a new nested session.

Start foot server:

systemctl --user start foot-server@$WAYLAND_DISPLAY.socket

Start sandbar:

systemctl --user start sandbar@$WAYLAND_DISPLAY.socket
sandbar_pipe=$(find_socket sandbar)
systemctl --user start sandbar_status@$WAYLAND_DISPLAY

Note

We fetch the sandbar socket location so that we can issue commands to it from within this file.

Start swayidle:

systemctl --user start swayidle@$WAYLAND_DISPLAY

Start wideriver:

systemctl --user start wideriver@$WAYLAND_DISPLAY

Start wob:

systemctl --user start wob@$WAYLAND_DISPLAY.socket
wob_pipe=$(find_socket wob)

Note

We fetch the socket location so that we can use it for a progress bar within this file.

Keybindings

General bindings:

riverctl map normal Super+Shift Q exit

riverctl map normal Super Page_Up focus-output next
riverctl map normal Super Page_Down focus-output previous

riverctl map normal Super B \
    spawn "echo all toggle-visibility >>$sandbar_pipe"
riverctl map normal Super+Shift B \
    spawn "echo all toggle-location >>$sandbar_pipe"

Extended keys

Configure function keys:

for mode (normal locked) {
    riverctl map $mode None XF86MonBrightnessUp \
        spawn "brightness_toggle up"
    riverctl map $mode None XF86MonBrightnessDown \
        spawn "brightness_toggle down"

    riverctl map $mode None XF86AudioPlay spawn "dtas-ctl play_pause"
    riverctl map $mode None XF86AudioNext spawn "dtas-ctl skip"

    riverctl map $mode None XF86AudioMute spawn "amixer sset Master toggle"
    riverctl map -repeat $mode None XF86AudioRaiseVolume \
        spawn "amixer sset Master 5%+"
    riverctl map -repeat $mode None XF86AudioLowerVolume \
        spawn "amixer sset Master 5%-"
}

Note

Media and function keys perform tasks that should work regardless of screen lock state.

Passthrough mode for testing configuration

A really great idea from the example river init file giving a quick toggle to make keys a no-op for testing nested compositors:

riverctl declare-mode passthrough

riverctl map normal Super F11 enter-mode passthrough
riverctl map passthrough Super F11 enter-mode normal

Tag management

Direct key access for manipulation of tags one through nine:

for tag ({1..9}) {
    tag_id=$(tag_mask $tag)

    riverctl map normal Super $tag set-focused-tags $tag_id
    riverctl map normal Super+Shift $tag set-view-tags $tag_id
    riverctl map normal Super+Control $tag toggle-focused-tags $tag_id
    riverctl map normal Super+Shift+Control $tag toggle-view-tags $tag_id
}

Show all, which you can treat it like a weak Apple’s Exposé:

riverctl map normal Super 0 set-focused-tags $ALL_TAGS

Window management

State bindings:

riverctl map normal Super+Shift Return zoom
riverctl map normal Super+Shift C close
riverctl map normal Super+Shift 0 set-view-tags $ALL_TAGS

riverctl map normal Super+Control Space toggle-float
riverctl map normal Super F toggle-fullscreen

Navigation bindings:

riverctl map normal Super Tab focus-view next
riverctl map normal Super+Shift Tab focus-view previous

riverctl map normal Super+Control Tab swap next
riverctl map normal Super+Control+Shift Tab swap previous

Output bindings:

riverctl map normal Super+Shift Page_up send-to-output next
riverctl map normal Super+Shift Page_down send-to-output previous

Floating support

ARROW_KEYS=(Left Down Up Right)

Declare floating mode:

riverctl declare-mode float
riverctl map normal Super R enter-mode float
riverctl map float None Escape enter-mode normal

Note

We declare a full mode here to make large scale changes to windows easier to accomplish. For quick changes all the modifiers aren’t a problem, but big changes are easier in the dedicated mode.

Basic movement bindings:

for key ($ARROW_KEYS) {
    riverctl map normal Super+Alt $key move $key:l 100
    riverctl map float None $key move $key:l 100
}

Cardinal movement bindings:

for key ($ARROW_KEYS) {
    riverctl map normal Super+Alt+Control $key snap $key:l
    riverctl map float Control $key snap $key:l
}

Basic resizing bindings:

xs=(horizontal vertical)
integer i=0 delta
for key dir (${ARROW_KEYS:^^xs}) {
    delta=$((i++ % 2 ? 1 : -1))00
    riverctl map normal Super+Alt+Shift $key resize $dir $delta
    riverctl map float Shift $key resize $dir $delta
}

Common applications

Spawn a foot client instance:

riverctl map normal Super Return spawn "footclient --no-wait"

Attempt to pick the most useful to me browser that is available:

riverctl map normal Super Z spawn \
    "exec ${commands[firefox]:-${commands[chromium]:-sensible-browser}}"

Mouse bindings

Configure “standard” mouse bindings:

riverctl map-pointer normal Super BTN_LEFT move-view
riverctl map-pointer normal Super BTN_RIGHT resize-view

It is nice to have a simple way to flip the float bit on a window:

riverctl map-pointer normal Super BTN_MIDDLE toggle-float

Using back and forward to manipulate the stack feels really quite natural:

riverctl map-pointer normal Super BTN_FORWARD swap next
riverctl map-pointer normal Super BTN_BACK swap previous

… and by extension back and forward to shuffle across outputs works well:

riverctl map-pointer normal Super+Shift BTN_FORWARD send-to-output next
riverctl map-pointer normal Super+Shift BTN_BACK send-to-output previous

Theming

Use monokai-pro palette:

riverctl background-color 0x1b1d1e
riverctl border-color-focused 0xa6e22e
riverctl border-color-unfocused 0x75715e
riverctl border-color-urgent 0xf92672

Note

This should really be configured more centrally, but for the time being it works.

Input devices

Wait 300 milliseconds and then repeat keys 50 times per second:

riverctl set-repeat 50 300

Configure non-standard options for keyboard:

declare -A _xkb_opts=(
    [caps]=escape_shifted_capslock
    [compose]=paus
    [keypad]=future
    [parens]=swap_brackets
)
xkb_opts_full=${(kj:,:)_xkb_opts/(#m)*/$MATCH:$_xkb_opts[$MATCH]}

Note

The globbing flags used here require extended_glob.

Perhaps those obscure keyboard options deserve an explanation:

Option

Description

escape_shifted_capslock

Make Capslock an alternative Escape key, but keep Capslock available with Shift+Capslock

paus

Use Pause as compose key

future

Unicode mathematics operators, noting that ASCII operators already exist on the main section

swap_brackets

Swap square bracket and parenthesis position

Configure a subset without bracket swaps for editing square bracket heavy code:

_xkb_opts_toggle=(parens)
xkb_opts_toggle=${(kj:,:)${(k)_xkb_opts:|_xkb_opts_toggle}/(#m)*/$MATCH:$_xkb_opts[$MATCH]}

Default to swap_brackets behaviour:

riverctl keyboard-layout -options $xkb_opts_full gb

Configure host specific touchpad settings:

if [[ $HOST =~ ^(camille|corale)$ ]] {
    riverctl input pointer-2-14-ETPS/2_Elantech_Touchpad tap enabled
    riverctl input pointer-2-14-ETPS/2_Elantech_Touchpad pointer-accel 0.8
}

We’ll declare a mode to wrap our input bindings, mainly as their use is uncommon and we won’t lose a lot of keys this way:

riverctl declare-mode input
riverctl map normal Super I enter-mode input
riverctl map input None Escape enter-mode normal

if [[ $HOST == ^(camille|corale)$ ]] {
    riverctl map input None T input pointer-2-14-ETPS/2_Elantech_Touchpad \
        events disabled
    riverctl map input Shift T input pointer-2-14-ETPS/2_Elantech_Touchpad \
        events enabled
}
riverctl map input None K spawn "riverctl keyboard-layout \
    -options $xkb_opts_full gb"
riverctl map input Shift K spawn "riverctl keyboard-layout \
    -options $xkb_opts_toggle gb"

Window rules

Sloppy focus is the only focus model that makes any sense to me:

riverctl focus-follows-cursor normal

Allow some rules to be stored outside default init to make it easier to share across different machines. For example, I need conflicting rules for outputs depending on location.

[[ -f $0:a:h/local_rules ]] && source $0:a:h/local_rules

Decades of use at this point means I always like the “second” tag — or workspace 2 for non-tagging interfaces — to contain a browser by default:

riverctl rule-add -app-id "chromium" tags $(tag_mask 2)
riverctl rule-add -app-id "firefox-esr" tags $(tag_mask 2)

I treat the “third” tag as media zone by default:

riverctl rule-add -app-id "mpv" tags $(tag_mask 3)

Note

It may make more sense to use a custom application identifier for the default apps, so that we can push them to their common tags but keep regular instances attached to current tag.

Layout

wideriver is the layout engine that is the closest match to the behaviour I’m used to with awesomewm, and makes a great default:

riverctl default-layout wideriver

We’ll declare a layout mode to make it quicker — and easier on the hands — to cycle layout controls when trying to pin down a comfortable setup:

riverctl declare-mode layout
riverctl map normal Super L enter-mode layout
riverctl map layout None Escape enter-mode normal

Layout format manipulation bindings:

riverctl map layout None M send-layout-cmd wideriver "--layout monocle"
riverctl map layout None T send-layout-cmd wideriver "--layout left"
riverctl map layout Shift T send-layout-cmd wideriver "--layout wide"
riverctl map layout Control T send-layout-cmd wideriver "--layout right"
riverctl map layout None Space send-layout-cmd wideriver "--layout-toggle"

Layout style manipulation bindings:

riverctl map layout None E send-layout-cmd wideriver "--stack even"
riverctl map layout None W send-layout-cmd wideriver "--stack dwindle"
riverctl map layout None I send-layout-cmd wideriver "--stack diminish"

Main window ratio manipulation bindings:

riverctl map layout None Equal send-layout-cmd wideriver "--ratio 0.52"
riverctl map layout None H send-layout-cmd wideriver "--ratio +0.05"
riverctl map layout None L send-layout-cmd wideriver "--ratio -0.05"

Bindings to adjust the number of windows in main stack:

riverctl map layout Shift Equal send-layout-cmd wideriver "--count 1"
riverctl map layout Shift H send-layout-cmd wideriver "--count +1"
riverctl map layout Shift L send-layout-cmd wideriver "--count -1"

Add top level bindings for monocle and tile-left, as they’re my most common layouts that I want quick access to:

riverctl map normal Super M send-layout-cmd wideriver "--layout monocle"
riverctl map normal Super T send-layout-cmd wideriver "--layout left"

Configure initial per-tag layouts:

for n ({2..32..2}) {
    riverctl set-focused-tags $(tag_mask $n)
    riverctl send-layout-cmd wideriver "--layout monocle"
}
riverctl set-focused-tags $(tag_mask 1)

Note

This reflects — what is at this point — my decades old tradition of defaulting to fullscreen on even tags. It doesn’t really make sense, but I’m quite accustomed to it.

Finalising

Allow a private machine specific configuration to be loaded:

[[ -f $0:a:h/local_init ]] && source $0:a:h/local_init

Show sandbar:

echo all show >>$sandbar_pipe

Note

sandbar is spawned hidden to allow us to issue per-tag layout changes or launch default applications without all the bar flashes that would result.

Footnotes