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:
- I still need an extra tool to actually configure my display before I can save and restore states.
- It isn’t triggered automatically when a monitor is (dis)connected.
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.