Automatic display profile switching
The cable dance
I have a multi-monitor setup that changes throughout the day. Sometimes I’m using all three screens, sometimes two of them, and sometimes I turn one off everything except one to focus or game. Every time a monitor goes on or off, I used to manually run xrandr commands to rearrange the displays. It got old fast.
What is autorandr?
Auto-detect connected display hardware and load appropriate X11 setup using xrandr.
autorandr saves your current xrandr configuration as a named profile. When you run it with the flag --change, it compares the currently connected outputs against all saved profiles and applies the matching one.
There’s a package available on Arch Linux:
pacman -S autorandr
Saving profiles
The idea is simple: arrange your displays the way you want them, then save that arrangement as a profile. autorandr doesn’t configure displays itself — it snapshots whatever xrandr state is currently active. So you need to set up the display layout first using xrandr (or a GUI like arandr), and then save it.
My setup looks like this — two landscape monitors and one rotated to portrait on the right:
┌──────────────┐
┌─────────────┐ ┌─────────────┐ │ │
│ │ │ │ │ │
│ DP-4 │ │ DP-2 │ │ DP-0 │
│ 2560x1440 │ │ 2560x1440 │ │ 1440x2560 │
│LG ULTRAGEAR │ │ ASUS XG32VC │ │ LG ULTRAGEAR │
│ │ │ (primary) │ │ │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ │ │
└──────────────┘
Before configuring anything, it helps to know what you’re working with.
xrandr --listmonitors gives you a clean overview of active monitors with their output names, resolutions and physical dimensions:
xrandr --listmonitors
Monitors: 3
0: +*DP-2 2560/697x1440/392+2560+730 DP-2
1: +DP-0 1440/600x2560/340+5120+0 DP-0
2: +DP-4 2560/600x1440/340+0+630 DP-4
The physical dimensions (697mm x 392mm vs 600mm x 340mm) can help distinguish between different monitor models. If you have two identical monitors (like my two LG 27UP850s both showing 600mm x 340mm), look at the output name (DP-0 vs DP-4), the current resolution and position instead. The * marks the primary output.
You can also check which GPU providers are available with xrandr --listproviders, useful if you have multiple GPUs:
xrandr --listproviders
Providers: number : 1
Provider 0: id: 0x1b7 cap: 0x1, Source Output crtcs: 4 outputs: 8 associated providers: 0 name:NVIDIA-0
Identifying monitor models
If you want to know which model is behind each output name, you can decode the EDID data. On systems with open-source drivers (amdgpu, i915, nouveau), this is straightforward with edid-decode and the DRM sysfs:
edid-decode /sys/class/drm/card1-DP-2/edid
However, if you’re using the NVIDIA proprietary driver, things get confusing. NVIDIA runs two separate display management paths: the kernel DRM subsystem (/sys/class/drm/) and the NVIDIA X driver, which talks directly to the GPU hardware and manages connectors on its own.
In other words, the output names don’t match between them — the kernel might expose DP-1, DP-2, DP-3 in sysfs, while xrandr shows DP-0 through DP-5. There’s no documented mapping.
You can see NVIDIA’s internal names in the Xorg log:
grep -E "DFP-[0-9]+\): connected" /var/log/Xorg.0.log
(--) NVIDIA(GPU-0): LG Electronics LG ULTRAGEAR (DFP-1): connected
(--) NVIDIA(GPU-0): Asustek Computer Inc ASUS XG32VC (DFP-4): connected
(--) NVIDIA(GPU-0): LG Electronics LG ULTRAGEAR (DFP-6): connected
This tells you which monitors are connected, but the DFP-N names are NVIDIA’s own — not the DP-N names xrandr uses. For day-to-day use, stick with xrandr output names. Those are what autorandr, xrandr, and arandr all use.
If you don’t want to deal with xrandr commands directly, arandr provides a visual GUI where monitors show up as labeled rectangles you can drag around and arrange. Hit apply, and the xrandr configuration is set for you.
pacman -S arandr
Otherwise, you can configure displays manually with xrandr. For example, if you have a single-monitor setup with a 1440p resolution and 164 Hz:
xrandr --output DP-2 --mode 2560x1440 --rate 164.85 --primary
Once the displays are arranged, save it for later:
autorandr --save single
Connect your other monitors in whatever order/options you desire, and save again:
autorandr --save multi
Repeat for each configuration you use. I have four profiles:
autorandr
left (left screen only)
multi (left, middle and right screens)
right (right screen)
single (middle screen)
The profiles are stored under ~/.config/autorandr/, with each profile containing a config and setup file. The config file holds the xrandr output settings, and the setup file stores the display fingerprint (EDIDs) used to detect which profile matches.
Here’s what a profile config looks like for a single monitor:
output DP-2
crtc 0
mode 2560x1440
pos 2560x730
primary
rate 164.85
And for the multi-monitor setup, the same file grows to include all connected outputs with their positions, refresh rates and rotations.
Applying profiles
Once you have your profiles saved, you can apply the correct one manually:
autorandr --change --default single
The --change flag tells autorandr to detect the current hardware and switch to the matching profile. The --default single flag specifies a fallback: if no saved profile matches the current setup, it falls back to the single profile.
Running this once is fine. Running it every time you plug or unplug a cable is not. We need something that listens for display changes and triggers autorandr automatically.
Listening for display changes
The X11 RandR extension
If you’re running X11, the RandR (Resize and Rotate) extension is what manages your display outputs. More importantly, it provides a notification mechanism: you can ask the X server to notify you when outputs change.
We can tap into this with python-xlib, a Python library that provides access to the X11 protocol:
pacman -S python-xlib
Or via pip:
pip install python-xlib
The watcher script
Here’s the script I wrote to listen for RandR events and trigger autorandr:
#!/usr/bin/env python3
"""Listen for X11 RandR events and run autorandr on display changes."""
import subprocess
import time
from Xlib import display
from Xlib.ext import randr
d = display.Display()
root = d.screen().root
root.xrandr_select_input(
randr.RROutputChangeNotifyMask | randr.RRScreenChangeNotifyMask
)
# Apply correct profile on startup
subprocess.run(["autorandr", "--change", "--default", "single"])
while True:
d.next_event()
time.sleep(2) # debounce — wait for all connectors to settle
while d.pending_events():
d.next_event()
subprocess.run(["autorandr", "--change", "--default", "single"])
Let’s break this down:
- Connect to the X display and get the root window.
- Register for RandR notifications using
xrandr_select_input. We’re interested in two types of events:RROutputChangeNotifyMask— fires when an output is connected or disconnected.RRScreenChangeNotifyMask— fires when the screen configuration changes (resolution, rotation, etc).
- Apply the correct profile on startup — so the displays are set up correctly when the script starts, not just when something changes.
- Event loop —
d.next_event()blocks until a RandR event arrives. When it does:- Sleep for 2 seconds to debounce. Display changes come in bursts — a single cable plug can generate multiple events as connectors settle.
- Drain any remaining events that piled up during the sleep.
- Run autorandr.
Save this to ~/.local/bin/autorandr-watch.py and make it executable:
chmod +x ~/.local/bin/autorandr-watch.py
The C alternative
The Python version works well, but it pulls in the entire Python interpreter and python-xlib — sitting at ~20 MB RSS while idle. If that bothers you, the same logic can be written in C using libX11 and libXrandr directly:
/*
* Listen for X11 RandR events and run autorandr on display changes.
* Compile: cc -O2 -o autorandr-watch autorandr-watch.c -lX11 -lXrandr
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrandr.h>
static void run_autorandr(void)
{
const char *argv[] = {
"autorandr", "--change", "--default", "single", NULL
};
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], (char *const *)argv);
perror("execvp");
_exit(1);
}
}
static void drain_events(Display *dpy)
{
XEvent ev;
while (XPending(dpy))
XNextEvent(dpy, &ev);
}
int main(void)
{
Display *dpy = XOpenDisplay(NULL);
if (!dpy) {
fprintf(stderr, "Cannot open display\n");
return 1;
}
int rr_event_base, rr_error_base;
if (!XRRQueryExtension(dpy, &rr_event_base, &rr_error_base)) {
fprintf(stderr, "RandR extension not available\n");
return 1;
}
Window root = DefaultRootWindow(dpy);
XRRSelectInput(dpy, root,
RROutputChangeNotifyMask | RRScreenChangeNotifyMask);
/* Apply correct profile on startup */
run_autorandr();
XEvent ev;
for (;;) {
XNextEvent(dpy, &ev);
sleep(2); /* debounce */
drain_events(dpy);
run_autorandr();
}
XCloseDisplay(dpy);
return 0;
}
The logic is identical — connect to the X display, register for RandR notifications, block on events, debounce, run autorandr. The difference is the footprint:
| Version | RSS |
|---|---|
| Python | ~20 MB |
| C | ~2.8 MB |
On Arch Linux, libxrandr and libx11 are already installed if you’re running X11. Compile it with:
cc -O2 -o ~/.local/bin/autorandr-watch ~/.local/bin/autorandr-watch.c -lX11 -lXrandr
Both versions are functionally equivalent — pick whichever you prefer.
Running it as a systemd user service
Rather than starting the script manually, we can run it as a systemd user service that starts with the graphical session.
Create ~/.config/systemd/user/autorandr-watch.service:
[Unit]
Description=autorandr display hotplug watcher
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/home/twfox/.local/bin/autorandr-watch
Restart=always
RestartSec=2
[Install]
WantedBy=graphical-session.target
The ExecStart points to the compiled C binary. If you prefer the Python version, change it to /home/twfox/.local/bin/autorandr-watch.py instead.
A few things worth noting:
After=graphical-session.targetandPartOf=graphical-session.target— the service starts after the graphical session is up and stops when it ends.Restart=alwayswithRestartSec=2— if the script crashes (e.g. X server restarts), systemd will bring it back after 2 seconds.- Since this is a user service, it inherits the user’s environment, including
DISPLAYandXAUTHORITY. No extra configuration needed.
Enable and start it:
systemctl --user daemon-reload
systemctl --user enable --now autorandr-watch.service
Verify it’s running:
systemctl --user status autorandr-watch.service
● autorandr-watch.service - autorandr display hotplug watcher
Loaded: loaded (/home/twfox/.config/systemd/user/autorandr-watch.service; enabled; preset: enabled)
Active: active (running)
Now every time you plug, unplug, or toggle a monitor, the script detects the change and autorandr applies the correct profile.
What about udev?
If you’ve read my previous articles on udev, you might be wondering: why not use udev rules instead? Some monitors — like my LG 27UP850 — expose a USB HID device for DDC/CI control, and you can see it in udev:
UDEV [7959.210385] bind /devices/pci0000:00/0000:00:14.0/usb1/1-13/1-13.2/1-13.2.3/1-13.2.3:1.0/0003:043E:9A39.001D (hid)
ACTION=bind
SUBSYSTEM=hid
HID_NAME=LG Electronics Inc. LG Monitor Controls
In theory, you could write a udev rule that triggers autorandr when this device appears or disappears. I actually tried this with a system-level oneshot service triggered by udev, and it works — but only for monitors that drop their USB connection when powered off.
The problem is that not all monitors behave this way. In my setup, one monitor generates USB events when toggled; the other doesn’t. The USB HID device stays connected even when the screen is off, so udev never fires for it.
The X11 RandR approach doesn’t have this limitation. It listens at the display protocol level, so it catches all output changes regardless of how the monitor is connected or whether it exposes USB devices.
Postswitch hooks
autorandr supports hooks that run after a profile is applied. These live in ~/.config/autorandr/postswitch.d/ and execute as shell scripts after every profile switch.
I use one to restore my wallpaper, since feh needs to re-apply it after the display configuration changes:
#!/bin/sh
feh --bg-fill /usr/local/share/wallpapers/titanfall2.png
Make sure the scripts are executable:
chmod +x ~/.config/autorandr/postswitch.d/wallpaper
You can add any number of scripts here — restarting your compositor, adjusting DPI, repositioning bars, etc. They run in alphabetical order.
Wrapping up
And that’s all for today, it’s a simple but it works.