Have you ever tried to change your display brightness only to find yourself frustrated by your display's unintuitive controls? Well, you're in luck because in this post, I'm going to show you how to do it from the comfort of your terminal.

To do that, we'll use the Display Data Channel Command Interface (DDC/CI) protocol which should be implemented on most displays today. Indeed, this is not a new standard, its first specification document dates back to 1998 and has support for CTR displays. It's built on top of I2C and works with HDMI, DisplayPort, DVI, and even VGA.

One way to take advantage of this protocol on Linux is to use the ddcutil utility.

Setting up

We first need to install it. I've made instructions for Archlinux and Ubuntu, since I use the former on my personnel computer and the latter at work.

On Archlinux

Start by installing the ddcutil and i2c-tools packages:

pacman -Sy ddcutil i2c-tools

You will need to load the i2c-dev module manually since it is not loaded automatically:

modprobe i2c-dev

To load it upon boot, you can create the /etc/modules-load.d/i2c_dev.conf with:

i2c_dev

You should now find /dev/i2c-* files. Note that by default, these device files are owned by the user and group root, so you will need to be root to interact with your displays through ddcutil.

To be able to change your display settings with a regular user, you can change these files' permissions. You first need to create the i2c group and add your user to it:

groupadd i2c
usermod -aG i2c $USER

Then create an udev rule by creating a /etc/udev/rules.d/45-ddcutil-i2c.rules file containing:

KERNEL=="i2c-[0-9]*", GROUP="i2c", MODE="0660"

This will change the group of the device file to i2c and make it readable and writable by users of this group. You will need to reboot to test that it works.

On Ubuntu

Install the ddcutil package:

apt get install ddcutil

You should not need to load the i2c-dev module yourself since it's either compiled with the kernel or loaded upon boot by default.

If you want to be able to interact with your displays without switching to root, you can add your user to the i2c group.

Overview

If things go well you should see your displays by running:

ddcutil detect
Display 1
   I2C bus:  /dev/i2c-4
   EDID synopsis:
      Mfg id:               ACI
      Model:                ASUS MG24U
      Product code:         9389
      Serial number:        REDACTED
      Binary serial number: REDACTED
      Manufacture year:     2016,  Week: 29
   VCP version:         2.2

To query your display settings, run:

ddcutil getvcp all
VCP code 0x02 (New control value             ): No new control values (0x01)
VCP code 0x0b (Color temperature increment   ): 2100 degree(s) Kelvin
VCP code 0x0c (Color temperature request     ): 3000 + 11 * (feature 0B color temp increment) degree(s) Kelvin
VCP code 0x10 (Brightness                    ): current value =    50, max value =   100
VCP code 0x12 (Contrast                      ): current value =    54, max value =   100
VCP code 0x14 (Select color preset           ): User 1 (0x0b), Tolerance: Unspecified (0x00)
VCP code 0x16 (Video gain: Red               ): current value =   100, max value =   100
VCP code 0x18 (Video gain: Green             ): current value =   100, max value =   100
VCP code 0x1a (Video gain: Blue              ): current value =   100, max value =   100
VCP code 0x52 (Active control                ): Value: 0x00
VCP code 0x60 (Input Source                  ): DisplayPort-1 (sl=0x0f)
VCP code 0x62 (Audio speaker volume          ): Volume level: 50 (00x32)
VCP code 0x6c (Video black level: Red        ): current value =    50, max value =   100
VCP code 0x6e (Video black level: Green      ): current value =    50, max value =   100
VCP code 0x70 (Video black level: Blue       ): current value =    50, max value =   100
VCP code 0x86 (Display Scaling               ): Max image, no aspect ration distortion (sl=0x02)
VCP code 0x87 (Sharpness                     ): current value =    50, max value =   100
VCP code 0x8a (Color Saturation              ): current value =    50, max value =   100
VCP code 0x8d (Audio mute/Screen blank       ): Unmute the audio (sl=0x02), Invalid value (sh=0x00)
VCP code 0xac (Horizontal frequency          ): 2228 hz
VCP code 0xae (Vertical frequency            ): 60.00 hz
VCP code 0xb6 (Display technology type       ): LCD (active matrix) (sl=0x03)
VCP code 0xc6 (Application enable key        ): 0x0083
VCP code 0xc8 (Display controller type       ): Mfg: Mstar (sl=0x05), controller number: mh=0xff, ml=0x16, sh=0x00
VCP code 0xc9 (Display firmware level        ): 0.0
VCP code 0xcc (OSD Language                  ): English (sl=0x02)
VCP code 0xd6 (Power mode                    ): DPM: On,  DPMS: Off (sl=0x01)
VCP code 0xdc (Display Mode                  ): Invalid value (sl=0x0e)
VCP code 0xdf (VCP Version                   ): 2.2

Each value is associated with a VCP code (Virtual Control Panel). For example, here we have the brightness controls which VCP code is 0x10 and which values can go from 0 to 100:

VCP code 0x10 (Brightness                    ): current value =    50, max value =   100

The all in ddcutil getvcp all tells ddcutil to query all the known settings. Other values are color to get all the color related features, scan to go through all the possible VCP codes from 0x00 to 0xff or a VCP code.

If we want to modify the brightness level to 55%, we would run the command:

ddcutil setvcp 10 55

Note that we lose the 0x in the VCP code. Some displays might use different values too. In that case, you would see a different max value.

If you have more than one display, you can select your display with the --display option:

ddcutil --display 1 setvcp 10 55

You can also specify relative values by adding a + or - argument before the value.

ddcutil setvcp 10 + 5
ddcutil setvcp 10 - 5

Now, let's say now that we want to modify the input source. In this case, you would need to use the VCP code 0x60:

VCP code 0x60 (Input Source                  ): DisplayPort-1 (sl=0x0f)

Its value is not a range, so to get the valid values you can run:

ddcutil prob

This command shows all the capabilities and features of your monitor. Here, we have the different values for our VCP:

   Feature: 60 (Input Source)
      Values (unparsed): 11 12 13 0F
      Values (  parsed):
         11: HDMI-1
         12: HDMI-2
         13: Unrecognized value
         0f: DisplayPort-1

You can then change the display source to HDMI-1 with:

ddcutil setvcp 60 0x11

Going further

Of course, controlling your display from the command line is probably not the most efficient way to go about it (okay... on some displays with a terrible OSD interface it might be a step-up already) but this enables to script and define interesting key bindings.

For example, here's a script I use to change the brightness of the display which is focused:

#!/bin/bash

# Modify the brightness level of the display which has
# the focus.
#
# Usage: ./ddc-brightness [+-] <level>
# Examples:
#  ./ddc-brightness + 10 # increase brightness level by 10
#  ./ddc-brightness   30 # set brightness level to 30

# First we identify and retrieve the serial number of
# the display which has the focus.
display_serial_num=$(
    swaymsg -t get_outputs |
    jq '.[] | select(.focused) | .serial' --raw-output)

# Then we change its brightness level.
ddcutil --sn "$display_serial_num" setvcp 10 $@

The script is then run with this Sway key binding configuration:

bindsym --locked Shift+XF86MonBrightnessUp exec ~/.bin/ddc-brightness + 5
bindsym --locked Shift+XF86MonBrightnessDown exec ~/.bin/ddc-brightness - 5

There are many other things you can do with ddcutil. To learn more about it, I encourage you to read its documentation if you are interested. You can also search for the "VESA Monitor Control Command Set Standard" specifications to read about the standard VCP codes.