Monitor hot-plugging with X

(This is part of a larger series on finding your footing on Arch Linux and part 2 in a two-part monitor sequence. You should be familiar with part 1 first.)

Last modified: 29 May 2022

Goal: use udev to detect when a monitor cable is physically connected or disconnected, and use xrandr to automatically update the monitor display in response.

References:

Procedure

I’ll first concisely describe the steps to get things working, and then explain the reasoning and some of the technicalities at the end of this guide. By the way, the approach used here comes from Arch Forums: udev rule doesn’t work (hotplug monitor).

TLDR:

  1. Create a shell script that uses xrandr to update displays based HDMI/DisplayPort connection status in the sysfs file system.
  2. Wrap the shell script in a systemd service.
  3. Create a udev rule that runs the systemd service in response to video cable hot-plugging.

Check monitor connection with sysfs

Goal: learn how to check a video output’s connected/disconnected status using the device files in /sys/class/drm.

First make sure you know the xrandr name (e.g. HDMI-1, DP-1, etc.) of the video output from part 1 that you plan on using to connect your monitor—I’ll use HDMI-1 in this guide, but change this as needed for your own case.

Then check the contents of the directory /sys/class/drm. Here’s what this looks like on my computer:

$ ls /sys/class/drm
card0-DP-1
card0-DP-2
card0-eDP-1
card0-HDMI-A-1
card0-HDMI-A-2

Each directory represents a display device. The names should match the video outputs shown by xrandr in part 1; for example, card0-HDMI-A-1 corresponds to HDMI-1. There should be a status file inside card0 directory that shows the connected/disconnected state of each device. For example:

# *Connects HDMI cable to computer*
$ cat /sys/class/drm/card0-HDMI-A-1
connected

# *Unplugs HDMI cable from computer*
$ cat /sys/class/drm/card0-HDMI-A-1
disconnected

Check-in point: you should know the sysfs directory name (e.g. card0-HDMI-A-1, card0-DP-A-1) of the device you’ll use to connect your monitor. We’ll need this name in the following shell script.

Hotplug script

Here’s the shell script to implement hotplug and display-switching logic (explanation follows).

I’d suggest naming the script hotplug-monitor.sh and placing it in /usr/local/bin (a conventional location for local user programs not controlled by a package manager), but any system-wide readable location should work.

#!/bin/sh
# File location: /usr/local/bin/hotplug-monitor.sh
# Description: Sends X display to an external monitor and turns internal
# display off when an HDMI cable is physically connected; turns monitor display
# off and internal display back on when HDMI cable is physically disconnected.

# Specify your username and user ID
USER_NAME=<your-username>   # find with `id -un` or `whoami`
USER_ID=<your-user-id>      # find with `id -u`
# Example: `USER_NAME=ejmastnak`
# Example: `USER_NAME=1000`

# Export user's X-related environment variables
export DISPLAY=":0"
export XAUTHORITY="/home/${USER_NAME}/.Xauthority"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${USER_ID}/bus"

# Video output and device names recogized by xrandr/sysfs
internal="eDP-1"         # change as needed
external="HDMI-1"        # change as needed
device="card0-HDMI-A-1"  # change as needed

# If external display was just physically connected, turn external display on
# and (optionally) turn internal display off to save battery.
if [ $(cat /sys/class/drm/${device}/status) == "connected" ];
then
  xrandr --output "${external}" --auto  # sends display to monitor
  xrandr --output "${internal}" --off   # optionally turn internal display off

# If external display was just physically disconnected, turn 
# external display off and turn internal display on.
elif [ $(cat /sys/class/drm/${device}/status) == "disconnected" ];
then
  xrandr --output "${external}" --off   # turn monitor display off
  xrandr --output "${internal}" --auto  # turn internal display on (if needed)
else  # Do nothing if device status is unreadable
  exit
fi

Some comments:

systemd unit

We’ll run the above shell script from a systemd service.

Create a systemd unit /etc/systemd/system/hotplug-monitor.service with the following contents:

[Unit]
Description=Monitor hotplug service

[Service]
Type=simple

# Make the service run as your user and not as root
User=<your-username>  # add your username here

# Change path to hotplug script as needed
ExecStart=/usr/local/bin/hotplug-monitor.sh

[Install]
WantedBy=multi-user.target

Make sure to update the User=<your-username> field with your username, then run systemctl daemon-reload to make systemd register the new service file. No need to enable or start the service manually—it will started as needed from the following udev rule.

udev rule

Create a udev rule in /etc/udev/rules.d/85-drm-hotplug.rules with the following contents:

ACTION=="change", KERNEL=="card0", SUBSYSTEM=="drm", RUN+="/usr/bin/systemctl start hotplug-monitor.service"

This rule runs the above hotplug-monitor service whenever udev detects chages in devices with kernel name card0 in the drm subsystem—basically when you plug/unplug display cables. (DRM stands for “Direct Rendering Manager”—you can read more at Wikipedia: DRM.)

Appendix: Technicalities

There are (at least) two fair questions here:

  1. Why are we complicating things with a systemd service instead of running the hotplug script directly from the udev rule, as in e.g. in these ArchWiki examples?

  2. Why are we checking device connections using the sysfs device file sys/class/drm/*/status instead of just running xrandr, as in part 1?

Based on my current understanding of udev and systemd best practices and some empirical testing, here are my answers:

  1. udev rules are meant to run only short processes, and will block events from the triggering device until the initial process completes (see e.g. ArchWiki: udev/Spawning long-running processes and the references therein). This is why we use a systemd service to run the shell script, and then only use the udev rule to start the service.

  2. From my own testing (and the experience of at least one other user), the udev rule for hotplugging a display device can be triggered before xrandr becomes aware of the corresponding video output’s updated connected/disconnected status. As far as I can tell, in such cases /sys/class/drm/*/status will show the correct connection status, but xrandr will still be lagging behind by a few hundred milliseconds or so.

    For hot-plugging, it is thus more reliable to check a display’s connection state from the contents of /sys/class/drm/*/status than to use grep to parse the output of the xrandr command, as in e.g. part 1.