Almost every laptop keyboard include two function keys: one to decrease the brightness of the screen and other to increase it.
In most Linux desktop environments, those two keys work out-of-the-box for the laptop display, but… what about external displays?
Recently I got a new keyboard, and I was surprised to see those brightness keys. Since I’m using it for my desktop PC, at first I thought they would be useless and I assigned them to other actions.
However, a few days ago, I learnt about the existence of DDC/CI, a Common Interface standard that allows the computer to send commands to the display and to receive data from it over I2C, a serial communication bus. Those communication can be done in Linux thanks to the i2c-dev kernel module and, with a command line utility like ddcutil, it is as easy as issuing some commands in our shell.
We’re going to learn how to install and configure ddcutil and to use it to control the brightness of an indefinite number of external displays and to turn them off easily.
Of course, displays must be compatible with the aforementioned standards. But don’t worry, most modern ones are!
Installing and configuring ddcutil
Installation
ddcutil is available as a package in most distributions.
For example, for Arch Linux:
1
$ sudo pacman -S ddcutil
Configuration
Then, some additional configuration steps are necessary.
Load i2c-dev module
i2c-dev kernel module is required by ddcutil.
Check if it is already available (e.g. if it is built into the kernel):
1
$ lsmod | grep -F i2c_dev
If not, it must be loaded explicitly:
1
$ sudo bash -c 'echo i2c-dev > i2c-dev.conf'
A reboot is needed afterwards.
Give permissions to I2C devices
On some distributions, including Debian, Ubuntu and Arch Linux, all I2C devices are assigned to the group i2c. This is done by the i2c-tools package, which is a dependency of ddcutil.
In order for your user to have access to the devices, it must be added to that group:
1
$ sudo usermod $USER -aG i2c
Controlling brightness
Detect displays
First of all, run the following command to detect available displays:
1
$ ddcutil detect
You will get an output like this:
1
2
3
4
5
6
7
8
9
Display 1
I2C bus: /dev/i2c-1
DRM connector: card0-HDMI-A-2
(ommited output)
Display 2
I2C bus: /dev/i2c-4
DRM connector: card0-DP-1
(ommited output)
With that output, you can run commands that apply to the first display using the option --display 1
or --bus 1
(because it uses the bus /dev/i2c-1
), or to the second display with --display 2
or --bus 4
.
Get/set brightness
Displays have some settings coded into what is called VCP features. Their names and their values can be queried using the getvcp
command.
Take a look at all of them for one display:
1
$ ddcutil getvcp ALL --bus 1
You will get the code and the value for each one. This is the one for brightness:
1
VCP code 0x10 (Brightness ): current value = 100, max value = 100
That means brightness feature has code 10
, its current value is 100
and its maximum value is 100
.
That value can be set using the setvcp
command:
1
$ ddcutil setvcp 10 50 --bus 1
That command would set brightness to 50 % for display with bus number 1.
It also works with deltas:
1
2
$ ddcutil setvcp 10 + 5 --bus 1
$ ddcutil setvcp 10 - 5 --bus 1
Those commands would respectively increase and decrease brightness by 5 %.
Set brightness in all displays at once
ddcutil doesn’t have an option to run a command for multiple displays. A workaround is using xargs. Assuming you have 2 displays with bus numbers 1 and 4 and you want to increase brightness by 5 % for both of them:
1
$ echo "1 4" | xargs -n1 -P2 sh -c 'ddcutil setvcp 10 + 5 --bus $1' sh
would do the trick.
Important: use bus numbers rather than display numbers to reference displays. Otherwise, parallel commands won’t work.
Turning displays off
Another interesting VCP feature is Power mode, which can be set to turn on and off the display. In my case, it only works to turn them off, but I guess it depends on the display model.
The VCP code for this feature is d6:
1
$ ddcutil getvcp d6 --bus 1
1
VCP code 0xd6 (Power mode ): DPM: On, DPMS: Off (sl=0x01)
In order to turn off the display, the value has to be set to 5 and, to turn it on, it has to be set to 1, so
1
$ ddcutil setvcp d6 5 --bus 1
would turn off display with bus number 1.
Control brightness using the keyboard
Let’s get to the interesting part: doing all this conveniently using the keyboard!
Shell script
The following script can be used to easily set brightness in all displays to a fixed value or to increase or decrease it, or to turn them off.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env bash
progname="$(basename "$0")"
usage() {
echo "usage: $progname <up/down/set/off> [brightness %]" >&2
exit 1
}
[[ "$#" -eq 1 || "$#" -eq 2 ]] || usage
# Exit if an instance of the script is already running to avoid errors
# It has to be somewhere in $PATH for this to work
n_px=$(pgrep -f "^bash $(which $progname)" | wc -l)
[[ $n_px -gt 2 ]] && exit 1
# Get bus number for all displays and number of displays
displays=$(ddcutil detect --terse | grep -F "I2C bus:" | awk -F '-' '{print $2}')
n_displays=$(echo $displays | wc -w)
case "$1" in
up)
b="+\ $2"
;;
down)
b="-\ $2"
;;
set)
b="$2"
;;
off)
# Turn off all displays
echo $displays | xargs -n1 -P$n_displays sh -c 'ddcutil setvcp d6 5 --bus $1' sh
exit 0
;;
*)
usage
;;
esac
conf=$(for m in $displays; do echo $m $b; done)
echo $conf | xargs -n2 -P$n_displays sh -c 'ddcutil setvcp 10 $2 --bus $1' sh
You can find this script in my dotfiles, which includes an extra feature to use xob.
Save this script somewhere in your $PATH
. The following commands can be used, assuming we named it brightctl
:
up
/down
: increase/decrease brightness by x % (e.g.brightctl up 5
).set
: set brightness to x % (e.g.brightctl set 80
).off
: turn displays off (e.g.brightctl off
).
Keybindings
Now, we can assign calls to the script to the keyboard using, for example, sxhkd.
You can add this to your sxhkdrc
:
1
2
3
4
5
6
7
# brightness up/down
XF86MonBrightness{Up,Down}
bright-notif {up,down} 5
# custom brightness
ctrl + XF86MonBrightness{Up,Down}
bright-notif set {100,30}
This way, brightness can be increased/decreased by 5 % using the brightness keys and, with the Control modifier, it can be set to 30 % or 100 %.
You will notice that running one of these commands is much slower than when controlling the brightness of a native display.
Turning displays off conveniently
Shell script
We can use the turning off displays feature at our convenience: for example, after before locking the screen or just before suspending the system.
I use a script to lock the screen with two options: turning off displays and suspending the system.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env bash
usage() {
printf "%s" "\
usage: $(basename "$0") [options]
Options:
-h, --help show this help message and exit.
-o, --off turn all displays off.
-s, --suspend run systemctl suspend afterwards.
"
exit 1
}
get_args() {
TEMP=$(getopt \
-l 'help,off,suspend' \
-o 'hos' \
-n "$(basename "$0")" \
-- "$@") || exit 1
eval set -- "$TEMP"
unset TEMP
while true; do
case "$1" in
'-h' | '--help')
usage
;;
'-o' | '--off')
off=1
shift
continue
;;
'-s' | '--suspend')
susp=1
shift
continue
;;
'--')
shift
break
;;
*)
printf "error\n" >&2
exit 1
;;
esac
done
}
main() {
get_args "$@"
[[ -z "$(pgrep -f ^i3lock)" ]] || exit 1
# Comment if not using dunst
killall -SIGUSR1 dunst
[[ "$off" ]] && bright-notif off
# Use your favorite i3lock options
i3lock [options] &
if [[ "$susp" ]]; then
sleep 0.5
systemctl suspend
fi
wait
# Comment if not using dunst
killall -SIGUSR2 dunst
return 0
}
main "$@"
You can also find this script in my dotfiles.
When using --off
option, the displays are turned off before locking the screen. When using --suspend
option, the system is suspended after locking the screen.
Keybindings
You can add this to your sxhkdrc
:
1
2
3
4
5
6
7
# lock screen
super + {_,alt + }l
lock-screen{_, --off}
# suspend
super + shift + s
lock-screen --off --suspend
So, this way
- with Super + L, the screen is locked
- with Super + Alt + L, the displays turn off and the screen is locked
- with Super + Shift + S, the displays turn off, the screen is locked and the system is suspended