Switching Monitor Inputs on Computer Wakeup

2021-04-25

Update: There are reports that the -p flag of ddcctl can permanently damage monitors. Don't use it! The flag was removed in ddcctl.
For Apple Silicon Macs, you may use m1ddc instead. With the risk of permanently damaging monitors, the undocumented commands m1ddc set standby 1 and m1ddc set standby 5 can be used to power on and power off monitors, respectively.

Switching peripherals between multiple computers can be achieved in a convenient fashion using KVM (Keyboard Video Mouse) switches. Unfortunately, high-quality KVM switches that support HiDPI monitors with high refresh rates are expensive.

As a more affordable solution, Haim Gelfenbeyn implemented software that detects when a specified USB device connects or disconnects and switches monitor inputs as desired based on configured settings. The monitor inputs are switched in software via DDC/CI. To physically switch between computers, this solution leverages an inexpensive USB switch to which multiple computers are connected. Pressing the button on the switch changes the active computer port, which results in the software switching the monitor inputs as configured. Moreover, the software turns the video output of the active computer on again if necessary by moving the mouse a bit on Windows or by running caffeinate on macOS.

Alternatively, HDMI switches constitute an inexpensive hardware solution. HDMI switches have a physical button that allows for selecting one of the monitors connected to the switch as the video output for a single connected computer or video device.

In case a USB switch or an HDMI switch is unavailable, it is still possible to switch monitor inputs when a computer wakes up with the aid of software. For macOS, this can be achieved via tools such as ddcctl in combination with SleepWatcher. While SleepWatcher is available via the Homebrew package manger, ddcctl can be installed via a release build or be built from source. Another useful tool is displayplacer, which allows for restoring monitor configurations. This tool is available via the author's Homebrew tap. Moreover, it can be installed from a release build or be built from source by running make in the checked out repository folder with the Makefile and copying the built binary to a folder in the PATH such as /usr/local/bin/.

SleepWatcher looks for executable files named .wakeup and .sleep in each user's home directory. The former file is executed when the computer wakes up, whereas the latter file is executed when the computer is about to go to sleep.

With a dual monitor setup, a ~/.wakeup file might look as follows:

#!/usr/bin/env bash

wait_for_displays () {
  # Wait until exactly 2 external displays are detected, trying up to 10 times:
  for i in $(seq 1 10)
  do
    /usr/local/bin/ddcctl 2> /dev/null | grep 'found 2 external displays' && return 0 || \
      echo 'Waiting for displays' && sleep 2
  done
  return 1
}

main () {
  # Between 7 PM and 7 AM, set the display brightness to 3 out of 100.
  # During other hours, set the display brightness to 25 out of 100.
  local -r hour=$(date '+%H')
  local brightness=25
  if [ $hour -lt 7 ] || [ $hour -ge 19 ]
  then
    brightness=3
  fi

  # Power each display on ("-p 1"),
  # set the brightness ("-b $brightness"),
  # set the desired input ("-i 15"):
  /usr/local/bin/ddcctl -d 1 -p 1 -b $brightness -i 15 
  /usr/local/bin/ddcctl -d 2 -p 1 -b $brightness -i 15 
  # Note: brightness doesn't update if the -b parameter comes after the -i
  # parameter

  # Restore the desired display configuration.
  # To configure your displays, open "System Preferences" and from there open the
  # "Displays" tool.
  # Run "displayplacer list" to obtain a displayplacer command for the current
  # configuration.
  /usr/local/bin/displayplacer 'id:display-1-id res:1080x1920 hz:60 color_depth:8 scaling:off origin:(0,0) degree:90' 'id:display-2-id res:1200x1920 hz:60 color_depth:8 scaling:off origin:(1080,0) degree:270'
}

date
wait_for_displays && main

Ensure to adjust the commands in the script above to suit your preferences and your monitor setup. In the displayplacer command above, display-1-id and display-2-id are merely placeholders for the actual display identifiers. Get a displayplacer command for your setup as instructed in the script. To obtain the ddcctl flags supported by your displays, run ddcctl with the flag to test followed by ?. For example, ddcctl -d 1 -i ? shows the current value as well as the maximum value of the -i flag that determines the input for the first display (-d 1), and ddcctl -d 2 -p ? shows the values of the -p flag that determines the power state for the second display (-d 2).

The wakeup script above sets the display brightness based on the local time when the script runs. A more sophisticated tool for setting the monitor brightness periodically is Lunar.

A ~/.sleep script that turns off both displays looks as follows:

#!/usr/bin/env sh

# Show time on stdout:
date
# Power off each display:
/usr/local/bin/ddcctl -d 1 -p 5
/usr/local/bin/ddcctl -d 2 -p 5

Make sure that the wakeup and sleep scripts are executable by running chmod 700 ~/.wakeup ~/.sleep. Homebrew installs launcher scripts under /usr/local/etc/sleepwatcher/. If the SleepWatcher daemon doesn't launch at system startup, a tutorial by Jason Pitoniak suggests to run the following commands to get the daemon up and running:

ln -sfv /usr/local/Cellar/sleepwatcher/2.2.1/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist

To obtain status and error output for sleepwatcher in /tmp/sleepwatcher.*, add the following lines before the line </dict> in ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist:

<key>StandardOutPath</key>
<string>/tmp/sleepwatcher.out</string>
<key>StandardErrorPath</key>
<string>/tmp/sleepwatcher.err</string>

Check whether the wakeup or sleep scripts are quarantined by running the following commands:

xattr /usr/local/Cellar/sleepwatcher/2.2.1/etc/sleepwatcher/rc.wakeup
xattr /usr/local/Cellar/sleepwatcher/2.2.1/etc/sleepwatcher/rc.sleep

If the output is com.apple.quarantine, then you can move the scripts out of quarantine as follows:

xattr -r -d com.apple.quarantine /usr/local/Cellar/sleepwatcher/2.2.1/etc/sleepwatcher/rc.wakeup
xattr -r -d com.apple.quarantine /usr/local/Cellar/sleepwatcher/2.2.1/etc/sleepwatcher/rc.sleep

Restart sleepwatcher to ensure it is running with the configured settings:

launchctl unload ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist
launchctl load -w ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist

The commands above run sleepwatcher as the local user after the specific user first logs in after a reboot. If you want to run the sleepwatcher scripts of each local user after a reboot, run the following commands:

sudo cp /usr/local/Cellar/sleepwatcher/2.2.1/de.bernhard-baehr.sleepwatcher-20compatibility.plist /Library/LaunchDaemons/
sudo chmod 0644 /Library/LaunchDaemons/de.bernhard-baehr.sleepwatcher-20compatibility.plist
launchctl unload -w ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist
sudo launchctl load -w /Library/LaunchDaemons/de.bernhard-baehr.sleepwatcher-20compatibility.plist

With this configuration in place, your laptop should change your monitor settings as desired whenever your computer wakes up or goes into sleep mode. See also this discussion on Hacker News for further ideas.