--- title: A minimal graphical session with River, PipeWire and OpenRC slug: minimal-graphical-session-with-river-pipewire-and-openrc date: 2025-08-08 draft: false authors: - luc tags: - Gentoo Linux categories: - Graphical session --- Graphical sessions are often full of unnecessary features, increasing (unnecessarily) the overall complexity of a desktop system. In this blog entry we intend to go back to the bare minimum, which defines itself as a reasonable step forward in practicality compared the default `tty`. Like being able to run graphical applications in windows and being able to manage these windows efficiently. We will meet but not exceed these requirements with [River](https://isaacfreund.com/software/river/) as our tiling window manager, [PipeWire](https://pipewire.org/) as our multimedia framework, and [OpenRC](https://wiki.gentoo.org/wiki/Project:OpenRC) as our user-service manager. ## User services with OpenRC As of `openrc-0.60` user services are supported by `openrc` (1). To enable a `openrc` user session we require a runtime dir (`XDG_RUNTIME_DIR`), which can be supplied by [turnstile](https://github.com/chimera-linux/turnstile) (2). Therefore emerge `turnstile`: { .annotate } 1. Presently the versions `>=0.60` of `openrc` are unstable. Enable unstable releases of `openrc` with: ``` shell title="/etc/portage/package.accept_keyword/openrc" sys-apps/openrc ~amd64 ``` 2. The `turnstile` ebuild can be found in the [portage-ample](https://git.ampel.dev/ampel/portage-ample) repository. ``` shell-session sh# emerge -a sys-auth/turnstile ``` and enable runtime dir management by `turnstile` by setting `manage_rundir` to yes in `turnstiled.conf`: ``` shell title="/etc/turnstile/turnstiled.conf" manage_rundir = yes ``` Add the `turnstile` daemon to the default runlevel: ``` shell-session sh# rc-update add turnstiled default ``` `pam` enables us to generate this `rundir` and start the `openrc` user session when the user is logged in by enabling the `turnstile` and `openrc` `pam` modules in `pam.d/system-login`: ``` shell title="/etc/pam.d/system-login" session optional pam_turnstile.so session optional pam_openrc.so ``` This user session contains the usual runlevels (`boot`, `default`, `shutdown`) that are run in their respective regimes (1). One approach is to add the window manager in the `default` runlevel, such that it will be started at login. But there are particular use cases where the graphical session is not required or wanted (only taking up precious resources), it is therefore wise to configure the graphical session in its own runlevel, to have the freedom to enable it any time you want with `openrc -U ` (2). { .annotate } 1. Though `boot` and `shutdown` should rather be interpreted as `login` and `logout` of the user session in this particular case. 2. Or to link it to the `default` runlevel. Besides the ``, denoted with `g`, we will also define a ``, denoted with `h`, for the services depending on the graphical-session. The creation of these runlevels can be simply performed with `mkdir`: ``` shell-session sh$ mkdir ~/.config/rc/runlevels/g sh$ mkdir ~/.config/rc/runlevels/h ``` and stack them accordingly: ``` shell-session sh$ ln -s /home//.config/rc/runlevels/default /home//.config/rc/runlevels/g sh$ ln -s /home//.config/rc/runlevels/g /home//.config/rc/runlevels/h ``` A set of minimal applications consisting of the terminal emulator (`foot`), application launcher (`fuzzel`(1)), notification daemon (`mako`) and display configurator (`kanshi`(2)) that will be of use in the graphical session may be emerged via: { .annotate } 1. Presently `fuzzel` is unstable in `GURU`. Enable unstable releases of `fuzzel` with: ``` shell title="/etc/portage/package.accept_keywords/fuzzel" gui-apps/fuzzel ~amd64 ``` 2. Presently `kanshi` and its dependency `libsfcg` is unstable in `GURU`. Enable unstable releases of `kanshi` and `libscfg` with: ``` shell title="/etc/portage/package.accept_keywords/kanshi" gui-apps/kanshi ~amd64 dev-libs/libscfg ~amd64 ``` ``` shell-session sh# emerge -a gui-apps/foot gui-apps/fuzzel gui-apps/mako gui-apps/kanshi ``` These applications require `WAYLAND_DISPLAY` to be set by the window manager. Therefore allow this variable to propagate in the user session: ``` shell title="~/.config/rc/rc.conf" rc_env_allow="WAYLAND_DISPLAY" ``` The terminal emulator (`foot`) requires the backend `footserver` for its clients. Therefore create a user service that will supervise `footserver`: ``` shell title="~/.config/rc/init.d/footserver" #!/usr/bin/openrc-run depend() { need graphical-session } supervisor=supervise-daemon command="/usr/bin/foot" command_args="--server" ``` Also the notification daemon (`mako`) and the display configurator (`kanshi`) require to be supervised, thus create a user service for them as well: ``` shell title="~/.config/rc/init.d/mako" #!/usr/bin/openrc-run depend() { need graphical-session } supervisor=supervise-daemon command="/usr/bin/mako" ``` ``` shell title="~/.config/rc/init.d/kanshi" #!/usr/bin/openrc-run depend() { need graphical-session } supervisor=supervise-daemon command="/usr/bin/kanshi" ``` Adding these user services to `` can simply be performed with `rc-update -U`: ``` shell-session sh$ rc-update -U add footserver h sh$ rc-update -U add mako h sh$ rc-update -U add kanshi h ``` ## Window management with River We require a seat manager as the mediating layer between the window manager and the hardware, to be able to run the window manager in the user session. We will use `seatd` as the seat management daemon, which requires us to set the following `USE` flags: ``` shell title="/etc/portage/package.use/seatd" sys-auth/seatd server -builtin ``` Now emerge `seatd`: ``` shell-session sh# emerge -a seatd ``` and add the daemon to the default runlevel: ``` shell-session sh# rc-update add seatd default ``` > We need to be part of the `seat` group to effectively communicate with the seat manager: > > ``` shell-session > sh# usermod --append --groups seat > ``` Video drivers are necessary for a graphical session to function. In Gentoo it is as simple as specifying what video card the system uses using the `USE` flags: ``` shell title="/etc/portage/package.use/video" */* VIDEO_CARDS: -* ``` By setting the `VIDEO_CARDS` `USE` flag the dependencies of `river` are compiled for the video card of the system, thus emerge `river` (1): { .annotate } 1. Presently `river` is unstable in `GURU`. Enable unstable releases of `river` with: ``` shell title="/etc/portage/package.accept_keywords/river" gui-wm/river ~amd64 ``` ``` shell-session sh# emerge -a gui-wm/river ``` > Buckle up, this is quite a hefty emerge. `river` starts by executing an init (POSIX shell) script present in `~/.conig/river/init`, this scipt should contain or link to all the configuration of `river`. A minimal configuration of the `river/init` script is given below: ``` shell title="~/.config/river/init" #!/bin/sh # Set background and colours swaybg -m fill -c 000000 -i $HOME/.local/share/backgrounds/ & #(1)! riverctl border-color-focused 0xFFFFFFCC riverctl border-color-unfocsed 0xFFFFFF66 riverctl border-color-urgent 0xFFFFFF # Set border-width riverctl border-width 2 # Set layout riverctl default-layout rivertile rivertile --view-padding 4 --outer-padding 4 & # Source the keybinds source ~/.config/river/binds # Switch to post-graphical runlevel openrc -U h ## Device input settings ## # Keyboard layout riverctl keyboard-layout qwerty # Set keyboard repeat rate riverctl set-repeat 50 300 # Set focus-follow-cursor riverctl focus-follow-cursor normal ``` 1. This line is optional, but if you want to spruce up your graphical session, then emerge: ``` shell-session sh# emerge -a gui-apps/swaybg ``` with the `river/binds` given by: ``` shell title="~/.config/river/binds" #!/bin/sh # Super+Shift+Q to exit river riverctl map normal Super+Shift Q spawn "openrc -U g && openrc -U default" ## Aplication mapping ## # Super+Backspace to spawn waylock riverctl map normal Super BackSpace spawn "waylock -ignore-empty-password" #(1)! # Super+Return to spawn foot riverctl map normal Super Return spawn footclient # Super+Space to spawn fuzzel riverctl map normal Super Space spawn fuzzel # Super+D to dismiss mako notification riverctl map normal Super D spawn "makoctl dismiss" # Super+T to spawn datetime notification riverctl map normal Super T spawn "$HOME/.local/bin/datetime" #(2)! # Super+B to spawn battery state notification riverctl map normal Super B spawn "$HOME/.local/bin/battery" #(3)! # MonBrightnessUp or MonBrightnessDown to spawn brightness notification (4) rivcertl map normal None XF86_MonBrightnessUp spawn "$HOME/.local/bin/brightness" riverctl map normal None XF86_MonBrightnessDown spawn "$HOME/.local/bin/brightness" # Print to spawn interactive screenshot riverctl map normal None Print spawn "grim -g "(slurp -d)" - | wl-copy -t image/jpeg" #(5)! # Shift+Print to spawn screenshot riverctl map normal Shift Print spawn "grim - | wl-copy -t image/jpeg" #(6)! ## Focused view mapping ## # Super+Q to close the focused view riverctl map normal Super Q close # Super+F to fullscreen toggle the focused view riverctl map normal Super F toggle-fullscreen # Super+BTN_MIDDLE to toggle float of the focused view riverctl map-pointer normal Super BTN_MIDDLE toggle-float # Super+BTN_LEFT to move the floated focused view riverctl map-pointer normal Super BTN_LEFT move-view # Super+BTN_RIGHT to resize the floated focused view riverctl map-pointer normal Super BTN_RIGHT resize-view # Super+{K;J} to focus next/previous view riverctl map normal Super K focus-view next riverctl map normal Super J focus-view previous # Super+Shift+{K;J} to swap the focused view with the next/previous view riverctl map normal Super+Shift K swap next riverctl map normal Super+Shift J swap previous # Super+{.;,} to focus the next/previous output riverctl map normal Super Period focus-output next riverctl map normal Super Comma focus-output previous # Super+Shift+{.;,} to send the focused view to the next/previous output riverctl map normal Super+Shift Period send-to-output next riverctl map normal Super+Shift Comma send-to-output previous ## Rivertile mapping ## # Super+{Up;Right;Down;Left} to change layout orientation riverctl map normal Super Up send-layout-cmd rivertile "main-location top" riverctl map normal Super Right send-layout-cmd rivertile "main-location right" riverctl map normal Super Down send-layout-cmd rivertile "main-location down" riverctl map normal Super Left send-layout-cmd rivertile "main-location left" # Super+Shift+{Up;Down} to increment/decrement the main count of rivertile riverctl map normal Super+Shift Up send-layout-cmd rivertile "main-count +1" riverctl map normal Super+Shift Down send-layout-cmd rivertile "main-count -1" # Super+Shift+{Right;Left} to increase/decrease the main ratio of rivertile riverctl map normal Super+Shift Right send-layout-cmd rivertile "main-ratio +0.05" riverctl map normal Super+Shift Left send-layout-cmd rivertile "main-ratio -0.05" ## Tag mapping ## for i in $(seq 1 9) do j=$((1 << ($i - 1))) # Super+[1-9] to focus tag [0-8] riverctl map normal Super $i set-focused-tags $j # Super+Shift+[1-9] to tag focused view with tag [0-8] riverctl map normal Super+Shift $i set-view-tags $j # Super+Control+[1-9] to toggle focus of tag [0-8] riverctl map normal Super+Control $i toggle-focused-tags $j # Super+Shift+Control+[1-9] to toggle tag [0-8] of focused view riverctl map normal Super+Shift+Control $i toggle-view-tags $j done all_tags=$(((1 << 32) - 1)) # Super+O to focus all tags riverctl map normal Super O set-focused-tags $all_tags # Super+Shift+O to tag focused view with all tags riverctl map normal Super+Shift O set-view-tags $all_tags ## Media mapping ## (7) # AudioRaiseVolume to increase volume audio sink by 5% riverctl map normal None XF86AudioRaiseVolume spawn "$HOME/.local/bin/audio sink volup" # AudioLowerVolume to decrease volume audio sink by 5% riverctl map normal None XF86AudioLowerVolume spawn "$HOME/.local/bin/audio sink voldown" # AudioMute to toggle mute audio sink riverctl map normal None XF86AudioMute spawn "$HOME/.local/bin/audio sink toggle" # AudioMicRaiseVolume to increase volume audio source by 5% riverctl map normal None XF86AudioMicRaiseVolume spawn "$HOME/.local/bin/audio source volup" # AudioMicLowerVolume to decrease volume audio source by 5% riverctl map normal None XF86AudioMicLowerVolume spawn "$HOME/.local/bin/audio source voldown" # AudioMicMute to toggle mute audio source riverctl map normal None XF86AudioMute spawn "$HOME/.local/bin/audio source toggle" ``` 1. Optionally, it could be useful to lock your session, therefore emerge `waylock`(1): { .annotate } 1. Presently `waylock` is unstable in `GURU`. Enable unstable releases of `waylock` with: ``` shell title="/etc/portage/package.accept_keywords/fuzzel" gui-apps/waylock ~amd64 ``` ``` shell-session sh# emerge -a gui-apps/waylock ``` 2. Optionally, it could be useful to see the datetime as a notification when you long for it, which is part of [ampel/notifiers](https://git.ampel.dev/ampel/notifiers). 3. Optionally, it could be useful to see the battery state as a notification, especially when you have a laptop. This functionality is part of [ampel/notifiers](https://git.ampel.dev/ampel/notifiers). 4. Optionally, it could be useful to see the brightness state of your display as a notification, which is part of [ampel/notifiers](https://git.ampel.dev/ampel/notifiers). As an additional remark, the default of configuration of `acpid` in Gentoo sucks, setting brightness capability with the same keys requires some changes in the configuration that can be found [here](https://git.lucbijl.nl/luc/gentoo-desktop/src/branch/main/etc/acpi). 5. Optional screenshot capability. 6. Optional screenshot capability. 7. Optionally, it could be useful to set and get the state of the audio sink and source, which is part of [ampel/notifiers](https://git.ampel.dev/ampel/notifiers). To be able to run our window manager (`river`) supervised, we require the `river` user service: ``` shell title="~/.config/rc/init.d/river" #!/usr/bin/openrc-run depend() { need dbus provide graphical-session } supervisor=supervise-daemon command="/usr/bin/river" ``` and add it to ``: ``` sh$ rc-update -U add river g ``` ## Interfacing audio and video with PipeWire We will interface audio and video with `pipewire` with `wireplumber` as fronted and `pulseaudio` support, which requires us to set the following `USE` flags: ``` shell title="/etc/portage/package.use/pipewire" media-video/pipewire sound-server loudness echo-cancel bluetooth media-sound/pulseaudio -daemon ``` Emerge `libpulse` for `pulseaudio` support, `pipewire` and `wireplumber`: ``` shell-session sh# emerge -a media-libs/libpulse media-video/pipewire media-video/wireplumber ``` and copy the default configuration of `pipewire` and `wireplumber` to their respective `~/.config` folders: ``` shell-session sh$ cp /usr/share/pipewire/pipewire.conf ~/.config/pipewire/ sh$ cp /usr/share/wirelumber/wireplumber.conf ~/.config/wireplumber/ ``` Now create the corresponding user services for the `pipewire-pulse` support layer, `pipewire` itself, and `wireplumber`: ``` shell title="~/.config/rc/init.d/pipewire-pulse" #!/usr/bin/openrc-run depend() { need audio-session } supervisor=supervise-daemon command="/usr/bin/pipewire-pulse" ``` ``` shell title="~/.config/rc/init.d/pipewire" #!/usr/bin/openrc-run depend() { need graphical-session provide audio-session } supervisor=supervise-daemon command="/usr/bin/pipewire" ``` ``` shell title="~/.config/rc/init.d/wireplumber" #!/usr/bin/openrc-run depend() { need audio-session } supervisor=supervise-daemon command="/usr/bin/wireplumber" ``` and add them to ``: ``` shell-session sh$ rc-update -U add pipewire h sh$ rc-update -U add pipewire-pulse h sh$ rc-update -U add wireplumber h ``` > We need to be part of the `pipewire` group for `pipewire` and dependencies to work accordingly: > > ``` shell-session > sh# usermod --append --groups pipewire > ``` For screencast and screenshot support in the graphical session we require the `xdg-desktop-portal` layer for rivers' backend `wlroots`, together with some tools (`grim`, `slurp`, `wl-clipboard`) to perform and save screenshots to the clipboard: ``` shell-session sh# emerge -a gui-libs/xdg-desktop-portal-wlr gui-apps/grim gui-app/slurp gui-apps/wl-clipboard ``` The `xdg-desktop-portal` should be run supervised, therefore create the `xdg-desktop-portal-wlr` user service: ``` shell title="~/.config/rc/init.d/xdg-desktop-portal-wlr" depend() { need graphical-session need audio-session } supervisor=supervise-daemon command="/usr/libexec/xdg-desktop-portal-wlr" ``` and add it to ``: ``` sh$ rc-update -U add xdg-desktop-portal-wlr h ``` ## Concluding remarks Well, I think we are there, a minimal graphical session suitable for everyday use. Regarding the customisation of the installed gui-apps, you may have a look at [my gentoo-desktop repository](https://git.lucbijl.nl/luc/gentoo-desktop), or if you think you have a better taste for aesthetic, then do it yourself you cynical connoisseur!