Automatic Display switch for OpenBSD laptop

       1776 words, 9 minutes

Most of my workstations are laptops. But because “age”, they are connected to an external 27" 4K monitor. It is used as my primary display and the laptop’s screen is disabled. And as I use WindowMaker as my daily window manager, I sometimes blank myself when I unplug the USB-C cable from the laptop.

There must be a way to automatically switch to the proper display when some USB-C monitors are (dis)connected… Other than switching to using Xfce, KDE, Gnome and other DEs that already implement this feature.

Warning: if you connect your monitor(s) using VGA, DVI, HDMI or DisplayPort, this post will probably not be useful.

Using autorandr

Spoiler Alert: this is not what I’m going to use as it doesn’t meet all my needs. I’m just reporting it so you can know about what it does and what it doesn’t.

I gave a try to autorandr , a tool that:

Automatically select a display configuration based on connected devices.

“Automatic” must be well understood. What this means is: each time you launch the software, it will check which connected devices you have, check the display configuration you saved and applied one of those. What it doesn’t mean is: it will wait in the background, detect that you have connected or disconnected a monitor and apply a saved display configuration accordingly.

Installing it on OpenBSD 7.6/amd64 is straightforward:

$ doas pkg_add autorandr

Then you shall configure your display as you wish and save it.

$ autorandr --save mobile
$ autorandr --save docked

I’m using redshift to have the display colour change according to the time of day. So I had applied the recommended setting to autorandr.

$ cat > ~/.config/autorandr/settings.ini << EOF
[config]
skip-options=gamma
EOF

I can now list my display profiles and apply the one I want.

$ autorandr
docked (detected) (1st match) (current)
mobile (detected) (2nd match)

$ autorandr --default mobile docked
$ autorandr mobile

This is nice but:

Using hotplugd, xrandr (and arandr)

Let’s get modular! After all, we’re using a UNIX-like operating system.

Configure and Save display configurations

A native tool to manage X11 display settings is xrandr(1).
Read the man page for more information; there are a lot.

To list the actual display configuration, just call xrandr.

$ xrandr
Screen 0: minimum 320 x 200, current 3840 x 2160, maximum 16384 x 16384
eDP connected (normal left inverted right x axis y axis)
   1920x1080     60.02 +  48.00
   1680x1050     60.02
   1280x1024     60.02
   1440x900      60.02
   1280x800      60.02
   1280x720      60.02
   1024x768      60.02
   800x600       60.02
   640x480       60.02
HDMI-A-0 disconnected (normal left inverted right x axis y axis)
DisplayPort-0 disconnected (normal left inverted right x axis y axis)
DisplayPort-1 connected primary 3840x2160+0+0 (normal left inverted right x axis y axis) 597mm x 336mm
   3840x2160     60.00*+  29.98
   2560x1440     59.95
   2048x1280     60.20
   2048x1152     60.00
   1920x1200     59.88
   2048x1080     24.00
   1920x1080     60.00    60.00    50.00    59.94    24.00    23.98
   1600x1200     60.00
   1680x1050     59.95
   1280x1024     75.02    60.02
   1440x900      60.00
   1280x800      59.81
   1152x864      75.00
   1280x720      60.00    50.00    59.94
   1024x768      75.03    60.00
   800x600       75.00    60.32
   720x576       50.00
   720x480       60.00    59.94
   640x480       75.00    60.00    59.94
   720x400       70.08

To use the external monitor as a display, I can run:

$ xrandr --output eDP --off --output DisplayPort-1 --auto

To use the laptop’s LCD as a display, I can run:

$ xrandr --output eDP --auto --output DisplayPort-1 --off

To use both monitors, my laptop being just under the external monitor, I can run:

$ xrandr \
  --output eDP           --mode 1920x1080 --pos 960x2160 \
  --output DisplayPort-1 --mode 3840x2160 --pos 0x0 --primary

Saving those commands in a script allows switching from one configuration to another easily.

Another (lazy) option is to use ARandR , Another XRandR GUI. This tool allows to setup a display configuration in a really easy way. It can apply, save and restore the configurations; in the form of executable shell scripts.

$ pkg_add arandr

$ arandr

$ ls -alh ~/.screenlayout/
total 32
drwxr-xr-x   2 joel  joel   512B Nov  7 22:57 ./
drwxr-xr-x  31 joel  joel   1.5K Nov  7 22:51 ../
-rwx------   1 joel  joel   165B Nov  7 19:05 docked.sh*
-rwx------   1 joel  joel   165B Nov  7 19:07 laptop.sh*

Detect USB-C monitor (dis)connection

The external monitor I am using is connected via USB-C and ships with a USB hub. There is a difference in the USB stack depending on whether it is plugged in or not.

$ sdiff /tmp/laptop /tmp/docked
Bus 000 Device 001: ID 1022:0000 Shinko Shoji Co., Ltd            Bus 000 Device 001: ID 1022:0000 Shinko Shoji Co., Ltd
Bus 001 Device 001: ID 1022:0000 Shinko Shoji Co., Ltd            Bus 000 Device 002: ID 0bda:5483 Realtek Semiconductor Corp.
                                                                > Bus 000 Device 003: ID 0bda:5483 Realtek Semiconductor Corp.
                                                                > Bus 000 Device 004: ID 05ac:024f Apple, Inc.
                                                                > Bus 000 Device 005: ID 046d:c52b Logitech, Inc. Unifying Receiv
                                                                > Bus 000 Device 006: ID 0bda:1100 Realtek Semiconductor Corp.
                                                                > Bus 000 Device 007: ID 0bda:0483 Realtek Semiconductor Corp.
                                                                > Bus 000 Device 008: ID 0bda:0483 Realtek Semiconductor Corp.
                                                                > Bus 000 Device 009: ID 0bda:8153 Realtek Semiconductor Corp.
                                                                > Bus 001 Device 001: ID 1022:0000 Shinko Shoji Co., Ltd
Bus 001 Device 002: ID 05e3:0610 Genesys Logic, Inc. 4-port hub   Bus 001 Device 002: ID 05e3:0610 Genesys Logic, Inc. 4-port hub
Bus 001 Device 003: ID 06cb:009a Synaptics, Inc.                  Bus 001 Device 003: ID 06cb:009a Synaptics, Inc.
Bus 001 Device 004: ID 05e3:0610 Genesys Logic, Inc. 4-port hub   Bus 001 Device 004: ID 05e3:0610 Genesys Logic, Inc. 4-port hub
Bus 001 Device 005: ID 04f2:b613 Chicony Electronics Co., Ltd     Bus 001 Device 005: ID 04f2:b613 Chicony Electronics Co., Ltd
Bus 001 Device 006: ID 0bda:b023 Realtek Semiconductor Corp.      Bus 001 Device 006: ID 0bda:b023 Realtek Semiconductor Corp.
Bus 001 Device 007: ID 04f2:b604 Chicony Electronics Co., Ltd     Bus 001 Device 007: ID 04f2:b604 Chicony Electronics Co., Ltd

The “Apple” is my NuPhy USB keyboard. The “Logitech” is the dongle on which my wireless mouse is attached. The Ethernet adapter from the dock is not obvious here; using lsusb -v reveals that it is “Bus 000 Device 009: ID 0bda:8153”.

hotplugd(8) is a daemon that monitors hot plug devices and reacts on signal send by connection/disconnection of those. It may run a custom script when I unplug any of the USB devices that are connected to the monitor’s hub; and when I plug them back.

When started, it will log device events in /var/log/daemon.

$ doas rcctl enable hotplugd
$ doas rcctl start hotplugd

$ tail -f /var/log/daemon

Here’s what’s logged when I connect the external monitor via the USB-C cable:

hotplugd[24739]: uhub4 attached, class 0
hotplugd[24739]: uhub5 attached, class 0
hotplugd[24739]: wskbd1 attached, class 5
hotplugd[24739]: ukbd0 attached, class 0
hotplugd[24739]: uhidev0 attached, class 0
hotplugd[24739]: wskbd2 attached, class 5
hotplugd[24739]: ukbd1 attached, class 0
hotplugd[24739]: uhid0 attached, class 0
hotplugd[24739]: wskbd3 attached, class 5
hotplugd[24739]: ucc0 attached, class 0
hotplugd[24739]: uhid1 attached, class 0
hotplugd[24739]: wsmouse2 attached, class 5
hotplugd[24739]: ums0 attached, class 0
hotplugd[24739]: uhidev1 attached, class 0
hotplugd[24739]: wsmouse3 attached, class 5
hotplugd[24739]: ums1 attached, class 0
hotplugd[24739]: uhidev2 attached, class 0
hotplugd[24739]: wskbd4 attached, class 5
hotplugd[24739]: ukbd2 attached, class 0
hotplugd[24739]: uhidev3 attached, class 0
hotplugd[24739]: wsmouse4 attached, class 5
hotplugd[24739]: ums2 attached, class 0
hotplugd[24739]: wskbd5 attached, class 5
hotplugd[24739]: ucc1 attached, class 0
hotplugd[24739]: uhid2 attached, class 0
hotplugd[24739]: uhid3 attached, class 0
hotplugd[24739]: uhidev4 attached, class 0
hotplugd[24739]: uhidpp0 attached, class 0
hotplugd[24739]: uhid4 attached, class 0
hotplugd[24739]: uhid5 attached, class 0
hotplugd[24739]: uhidev5 attached, class 0
hotplugd[24739]: uhid6 attached, class 0
hotplugd[24739]: uhidev6 attached, class 0
hotplugd[24739]: uhub6 attached, class 0
hotplugd[24739]: uhub7 attached, class 0
hotplugd[24739]: ure0 attached, class 3

Here’s what’s logged when I unplug the USB-C cable:

hotplugd[24739]: uhub3 detached, class 0
hotplugd[24739]: wskbd1 detached, class 5
hotplugd[24739]: ukbd0 detached, class 0
hotplugd[24739]: uhidev0 detached, class 0
hotplugd[24739]: wskbd2 detached, class 5
hotplugd[24739]: ukbd1 detached, class 0
hotplugd[24739]: uhid0 detached, class 0
hotplugd[24739]: wskbd3 detached, class 5
hotplugd[24739]: ucc0 detached, class 0
hotplugd[24739]: uhid1 detached, class 0
hotplugd[24739]: wsmouse2 detached, class 5
hotplugd[24739]: ums0 detached, class 0
hotplugd[24739]: uhidev1 detached, class 0
hotplugd[24739]: wsmouse3 detached, class 5
hotplugd[24739]: ums1 detached, class 0
hotplugd[24739]: uhidev2 detached, class 0
hotplugd[24739]: wskbd4 detached, class 5
hotplugd[24739]: ukbd2 detached, class 0
hotplugd[24739]: uhidev3 detached, class 0
hotplugd[24739]: wsmouse4 detached, class 5
hotplugd[24739]: ums2 detached, class 0
hotplugd[24739]: wskbd5 detached, class 5
hotplugd[24739]: ucc1 detached, class 0
hotplugd[24739]: uhid2 detached, class 0
hotplugd[24739]: uhid3 detached, class 0
hotplugd[24739]: uhidev4 detached, class 0
hotplugd[24739]: sensordev detached, class 0
hotplugd[24739]: uhidpp0 detached, class 0
hotplugd[24739]: uhid4 detached, class 0
hotplugd[24739]: uhid5 detached, class 0
hotplugd[24739]: uhidev5 detached, class 0
hotplugd[24739]: uhid6 detached, class 0
hotplugd[24739]: uhidev6 detached, class 0
hotplugd[24739]: uhub2 detached, class 0
hotplugd[24739]: ure0 detached, class 3
hotplugd[24739]: uhub5 detached, class 0
hotplugd[24739]: uhub4 detached, class 0

I could choose any of those devices as a signal that I (un)plugged the external monitor. There is little chance that I ever plug an USB Ethernet adapter on this laptop. So I decided ure0 would be the witness device.

Following the example of the hotplugd man page, I created an “attach” script that calls the previously created docked.sh script and a “detach” script that calls the laptop.sh one.

# cat > /etc/hotplug/attach
#!/bin/sh

DEVCLASS=$1
DEVNAME=$2

if [ "$DEVCLASS" -eq 3 ] && [ "$DEVNAME" = "ure0" ]; then
	env DISPLAY=:0.0 su joel -c /home/joel/.screenlayout/docked.sh
fi

exit 0
#EOF
^D
# cat > /etc/hotplug/detach
#!/bin/sh

DEVCLASS=$1
DEVNAME=$2

if [ "$DEVCLASS" -eq 3 ] && [ "$DEVNAME" = "ure0" ]; then
    env DISPLAY=:0.0 su joel -c /home/joel/.screenlayout/laptop.sh
fi

exit 0
#EOF
^D
# chmod 0755 /etc/hotplug/attach /etc/hotplug/detach

# rcctl restart hotplugd

One more thing WindowMaker

WindowMaker doesn’t always feel happy when I play with display configuration. When it is restarted, it feels better. But doing it manually is not great. So I send it a SIGUSR1 from the xrandr scripts that were saved using arandr in the previous section.

$ cat ~/.screenlayout/laptop.sh
#!/bin/sh

logger -p daemon.info -t hotplug/laptop "switching to laptop monitor"

xrandr --output eDP --primary --mode 1920x1080 --pos 0x0 --rotate normal \
--output HDMI-A-0 --off --output DisplayPort-0 --off --output DisplayPort-1 --off

pkill -usr1 -lf "/usr/local/bin/wmaker --for-real"

exit 0
#EOF
$ cat .screenlayout/docked.sh
#!/bin/sh

logger -p daemon.info -t hotplug/docked "switching to external monitor"

xrandr --output eDP --off --output HDMI-A-0 --off --output DisplayPort-0 --off \
--output DisplayPort-1 --primary --mode 3840x2160 --pos 0x0 --rotate normal

pkill -usr1 -lf "/usr/local/bin/wmaker --for-real"

exit 0
#EOF

Now I can disconnect my OpenBSD laptop from the monitor / power source and go to the couch without having to type a single command.