Over-Engineering a Display Toggle for Great Good
After dealing with a touchbar MacBook Pro at work I finally made the decision
to buy a personal machine that didn't include Apple hardware. That also meant
going home to Linux1 after a few years hiatus. In the intervening years since
I last tried Linux systemd
firmly supplanted other init systems, Unity came
and went on Ubuntu-family distributions, and in general, the Linux userland
experience became quite a bit more polished.
Despite all these advances in the Linux userland, however, there are still some
rough edges compared to non-free alternatives like macOS2 and Windows.
If one doesn't want to run a full-blown desktop environment like KDE or GNOME,
the landscape for tools that manage displays gets low-level fairly quickly.
The defacto tool appears to be xrandr
along with a collection of homebrew
scripts for managing one's displays. While this works for most people, I want
there to be a little more Just Works™ magic in my tooling.
The following misadventure details how I've gone from a collection of homebrew
scripts using xrandr
to a full blown Haskell tool that's invoked via udev
rules to manage my displays, affectionately called togglemon
.
Stating Our Intent Clearly
To be fair, the xrandr
approach works well in most cases, but it's
laborious typing out a script name every time I want to plug in an external
monitor when other operating systems or desktop environments just detect and
extend displays with some default configuration3. So let's clearly state what
I'm after. In doing so at this early stage I'm able to give better definition
to the solution I'm looking for. The clearer I am about intent, the easier it
is to define intermediate milestones and prevent scope creep that can kill any
motivation I have to continue working past the first day.
I would like a tool that allows me to plug in a display, expect that it is set as the active display, and disables my laptop screen. When I unplug the monitor, I expect this behavior to be reversed, preserving the configuration I had before. This behavior should work the same irrespective of the port the monitor is plugged into.
It's really the last sentence that gets me into trouble. The scripts I've
hastily thrown together are hardcoded to specific ports on my system. xrandr
likes to name displays things like DP1
, roughly corresponding to the port that
your graphics card configures for your system4. If you change the port
that you plug the monitor into, say DP2
instead of DP1
, my script
no longer works.
Could I parse the output of xrandr
in a shell script? Sure, but after taking
a quick look at the output I decided I'd rather drink battery acid.
See for yourself:
$> xrandr
Screen 0: minimum 8 x 8, current 3840 x 2160, maximum 32767 x 32767
eDP1 connected primary 3840x2160+0+0 (normal left inverted right x axis y axis) 290mm x 170mm
3840x2160 60.00*+ 59.97 48.00
3200x1800 59.96 60.00 59.94
2880x1620 60.00 59.96 59.97
2560x1600 59.99 59.97
2560x1440 59.96 60.00 59.95
2048x1536 60.00
1920x1440 60.00
1856x1392 60.01
1792x1344 60.01
2048x1152 60.00 59.90 59.91
1920x1200 59.88 59.95
1920x1080 59.96 60.00 59.93
1600x1200 60.00
1680x1050 59.95 59.88
1400x1050 59.98
1600x900 60.00 59.95 59.82
1280x1024 60.02
1400x900 59.96 59.88
1280x960 60.00
1368x768 60.00 59.88 59.85
1280x800 59.81 59.91
1280x720 59.86 60.00 59.74
1024x768 60.00
1024x576 60.00 59.90 59.82
960x540 60.00 59.63 59.82
800x600 60.32 56.25
864x486 60.00 59.92 59.57
640x480 59.94
720x405 59.51 60.00 58.99
640x360 59.84 59.32 60.00
DP1 disconnected (normal left inverted right x axis y axis)
DP2 disconnected (normal left inverted right x axis y axis)
VIRTUAL1 disconnected (normal left inverted right x axis y axis)
An experienced shell wizard might look at this output and come up with a clever one-liner to parse this mess. Dear reader, if that shell wizard is you, I would be most interested in your solution. In the absence of your solution, however, I decided to pursue another tack. Linux typically documents much of it's resources as files on the filesystem, if you only know where to look...
Everything is a File
It turns out that sysfs
documents kernel resources that are available
pretty well, including one particular kernel sub-system called the
Direct Rendering Manager, or DRM for short5. This means that
/sys/class/
on my filesystem lists a bunch of kernel resources, drm
among them. Here's mine:
$> ls /sys/class/drm
card0
card0-DP-1
card0-DP-2
card0-eDP-1
renderD128
version
So what are we looking at here? DRM exposes information about monitors according
to what graphics card it's associated with. This machine in particular only has
a single graphics card, so all peripheral ports are labeled with a card0
prefix. The next three entries are the available ports on my system, with eDP
appearing to be the internal display on the laptop itself. I don't know much
more about how DRM goes about naming these available ports, though I suspect
it is largely left up to the graphics card manufacturer. The astute observer
will note that the same display names we use in our xrandr
command is in the
middle segment of each directory name.
For the purposes of our investigation, renderD128
and version
don't contain
any relevant information that we would need to manage our displays, though if
you're interested in rabbit-holing on this one point you can read up on DRM
or sysfs rules in more detail.
Now let's explore what one of these display ports contains:
$> ls -Ahltr /sys/class/drm/card0-eDP-1/
total 0
-rw-r--r-- 1 root root 4.0K Aug 2 16:53 uevent
lrwxrwxrwx 1 root root 0 Aug 2 16:53 subsystem -> ../../../../../../class/drm
drwxr-xr-x 3 root root 0 Aug 2 16:53 intel_backlight
drwxr-xr-x 3 root root 0 Aug 2 16:53 i2c-5
drwxr-xr-x 3 root root 0 Aug 2 16:53 drm_dp_aux0
drwxr-xr-x 2 root root 0 Aug 2 16:53 power
-r--r--r-- 1 root root 4.0K Aug 2 16:53 enabled
-r--r--r-- 1 root root 4.0K Aug 2 16:53 dpms
-rw-r--r-- 1 root root 4.0K Aug 4 08:16 status
-r--r--r-- 1 root root 4.0K Aug 4 08:35 modes
-r--r--r-- 1 root root 0 Aug 4 08:35 edid
lrwxrwxrwx 1 root root 0 Aug 4 08:35 device -> ../../card0
Inside of each display port we'll find a number of pieces of interesting
information6. cat
-ing these files tells us various things about the monitor
that is currently plugged in. status
and enabled
will be most useful for
our purposes today, but feel free to explore what some of these other files
and directories contain (modes might be useful in configuring a certain
resolution for your display, for example).
The status
and enabled
files contain information about whether a display is
currently connected and currently in use respectively. status
values can
either be disabled
or enabled
, while enabled
files can either be
connected
or disconnected
. With these two pieces of information we should
be able to reliably determine which monitor is currently plugged in and in use,
and which monitor is plugged in just waiting for instructions.
We now have enough information to start designing a simple solution to our problem.
Modeling a Solution to our Problem
Here's a summary of the information we have available to us:
- We want to be able to automatically toggle displays when we plug in a monitor, regardless of the port it's plugged into
- Information about displays live in
/sys/class/drm
, prefixed by the graphics card that owns the port - Individual display ports contain information about whether a monitor is connected and enabled
- This information can be mapped to equivalent
xrandr
commands
While we're still a ways off from using udev
to automatically toggle monitors,
it's likely a safe bet that an invokable command line tool is the winning
strategy here.
With that in mind, here's a first pass at an algorithm to solve the problem:
- List the contents of
/sys/class/drm
- Filter contents to just directories
- For each directory, read the contents of the
status
andenabled
files
- If these files don't exist, do nothing as we do not have a monitor7
- Construct an active / passive display configuration based on connected monitors
- A monitor is active if it is enabled
- A monitor is passive if it is disabled
- Construct an
xrandr
command based on the active / passive display configuration - Execute the
xrandr
command
Let's walk through an example of how this would work, using the following diagram as a guide:
We start by reading the contents of /sys/class/drm
and find two displays:
- Our internal laptop display, labeled
eDP1
- Our external display, labeled
DP2
We then read the status
and enabled
files and find that we're currently
using our laptop screen, but our external display is currently connected. In
other words, our laptop display is Active and our external display is Passive,
so we construct a configuration that says as much.
Then, we use that configuration to construct a valid xrandr
command that will
toggle the displays for us and finally execute it.
By breaking up the problem in this way we can visually see that there are some objects, or types, we can define to model the problem space neatly. Each block in the diagram might be a function that transitions from one type of data to another. This strategy is common when designing programs that follow functional programming principles: define types that model your problem space, then define functions that form the transitions between each state. If you're skeptical of this decision I'll refer you to the litany of long dissertations about the benefits of functional programming, so that we avoid turning this post into another such dissertation.
In the next post, we'll begin implementing our solution using my functional language of choice: Haskell. We'll see how Haskell makes it straightforward for us to model the problem space, define functions to get us from one state to another, and put it all together in a neatly tested package. We'll also start to explore how monad transformers and the [ReaderT pattern] can help us effectively test our application.
Until next time, and happy hacking!
Footnotes
-
I'd just like to interject for a moment. What you're referring to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX. Many computer users run a modified version of the GNU system every day, without realizing it. Through a peculiar turn of events, the version of GNU which is widely used today is often called "Linux", and many of its users are not aware that it is basically the GNU system, developed by the GNU Project. There really is a Linux, and these people are using it, but it is just a part of the system they use. Linux is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. Linux is normally used in combination with the GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the so-called "Linux" distributions are really distributions of GNU/Linux. ↩
-
That said, the fact that macOS, as polished as it can be at times, still can't remember my display configuration when I plug my monitors back in is frustrating. [hurd]: https://www.gnu.org/software/hurd/hurd.html ↩
-
<insert crocodile tears here> ↩
-
If you're already familiar with DRM you may recognize there's a bit more nuance to it than this, though. ↩
-
No, not that kind of DRM. ↩
-
It should be noted, however, that the information within these directories is not guaranteed to be consistent. That is, if I plug in an external monitor that doesn't expose backlight functionality, I shouldn't expect to see an
intel_backlight
file in the tree. ↩ -
This'll become useful for filtering directories like
card0
, that represent the graphics card and not the display. ↩