bctl 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bctl-0.0.1/PKG-INFO +252 -0
- bctl-0.0.1/README.md +229 -0
- bctl-0.0.1/bctl/__init__.py +0 -0
- bctl-0.0.1/bctl/bin/__init__.py +0 -0
- bctl-0.0.1/bctl/bin/main_client.py +145 -0
- bctl-0.0.1/bctl/bin/main_daemon.py +18 -0
- bctl-0.0.1/bctl/bin/main_daemon_sim.py +41 -0
- bctl-0.0.1/bctl/client.py +58 -0
- bctl-0.0.1/bctl/common.py +136 -0
- bctl-0.0.1/bctl/config.py +104 -0
- bctl-0.0.1/bctl/daemon.py +462 -0
- bctl-0.0.1/bctl/debouncer.py +20 -0
- bctl-0.0.1/bctl/display.py +230 -0
- bctl-0.0.1/bctl/exceptions.py +9 -0
- bctl-0.0.1/bctl/notify.py +58 -0
- bctl-0.0.1/bctl/udev_monitor.py +40 -0
- bctl-0.0.1/bctl.egg-info/PKG-INFO +252 -0
- bctl-0.0.1/bctl.egg-info/SOURCES.txt +22 -0
- bctl-0.0.1/bctl.egg-info/dependency_links.txt +1 -0
- bctl-0.0.1/bctl.egg-info/entry_points.txt +4 -0
- bctl-0.0.1/bctl.egg-info/requires.txt +7 -0
- bctl-0.0.1/bctl.egg-info/top_level.txt +1 -0
- bctl-0.0.1/pyproject.toml +50 -0
- bctl-0.0.1/setup.cfg +4 -0
bctl-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bctl
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: service for simultaneously controlling brightness of laptop & external displays
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: homepage, https://github.com/laur89/bctl
|
|
7
|
+
Project-URL: issues, https://github.com/laur89/bctl/issues
|
|
8
|
+
Keywords: brightness,brightless-control,display-brightness,screen-brightness
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
+
Classifier: Topic :: System :: Hardware
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: click
|
|
17
|
+
Requires-Dist: tendo
|
|
18
|
+
Requires-Dist: aiofiles
|
|
19
|
+
Requires-Dist: pyudev
|
|
20
|
+
Requires-Dist: desktop-notify
|
|
21
|
+
Requires-Dist: pydash
|
|
22
|
+
Requires-Dist: dbus-next
|
|
23
|
+
|
|
24
|
+
# BCTL - brightness control
|
|
25
|
+
|
|
26
|
+
This is a simple daemon for controlling displays' brightnesses. It
|
|
27
|
+
consists of a daemon process listening for user requests (e.g. changing brightness)
|
|
28
|
+
and a client to send commands to the daemon. udev events are monitored
|
|
29
|
+
for screen (dis)connections. Desktop notifications are shown on brightness change.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
`$ pipx instal bctl`
|
|
34
|
+
|
|
35
|
+
Note this will install the client & daemon executables, but it's user
|
|
36
|
+
responsibility to launch the daemon process, covered below.
|
|
37
|
+
|
|
38
|
+
## Why?
|
|
39
|
+
|
|
40
|
+
Main reason for this program is to provide a simple, general-puropse central point
|
|
41
|
+
for controlling brightness of _all_ the connected screens simultaneously and keeping
|
|
42
|
+
track of their current brightness levels.
|
|
43
|
+
|
|
44
|
+
Controlling laptops' internal screen is generally the easiest, as its device
|
|
45
|
+
object is exposed under `/sys/class/backlight/` dir -- it's the external
|
|
46
|
+
displays that tend to be trickier.
|
|
47
|
+
|
|
48
|
+
For controlling external screens' brightness there are roughly two main
|
|
49
|
+
methods, [explained below](#managing-external-displays). As the recommended
|
|
50
|
+
method -- ddcutil -- takes in some cases non-trivial amount of time to execute
|
|
51
|
+
(up to ~200ms), it can be slightly jarring to change brightness when spamming the key.
|
|
52
|
+
|
|
53
|
+
As this solution caches set brighness values there's no need to query it
|
|
54
|
+
from ddcutil, making e.g. desktop notification generation simpler.
|
|
55
|
+
|
|
56
|
+
Also screen connections & disconnections are kept track of via an udev monitor,
|
|
57
|
+
and there's an option to force all the screens' brightnesses to be kept in sync.
|
|
58
|
+
|
|
59
|
+
## Managing external displays
|
|
60
|
+
|
|
61
|
+
### [`ddcci` kernel driver](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux)
|
|
62
|
+
|
|
63
|
+
This kernel module _should_ detect the devices and expose 'em under
|
|
64
|
+
`/sys/class/backlight/`, just like the laptop's internal display (e.g.
|
|
65
|
+
`/sys/class/backlight/amdgpu_bl0`) is by default. This requires installation
|
|
66
|
+
of [ddcci-dkms](https://packages.debian.org/sid/ddcci-dkms) package and loading
|
|
67
|
+
`ddcci` kernel module.
|
|
68
|
+
|
|
69
|
+
Note as of '25 there are loads of issues w/ this kernel module's auto-detection
|
|
70
|
+
logic, e.g. see issues [7](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux/-/issues/7),
|
|
71
|
+
[42](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux/-/issues/42),
|
|
72
|
+
[46](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux/-/issues/46)
|
|
73
|
+
|
|
74
|
+
Current workaround seems to be manually enabling displays as per [this reddit post](https://old.reddit.com/r/gnome/comments/efkoya/using_ddccidriverlinux_you_can_get_native/fc0xrx6/):
|
|
75
|
+
|
|
76
|
+
- Before state (no external display devices listed/avail):
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
$ ls -l /sys/class/backlight
|
|
80
|
+
total 0
|
|
81
|
+
lrwxrwxrwx 1 root root 0 Sep 7 09:44 amdgpu_bl0 -> ../../devices/pci0000:00/0000:00:08.1/0000:07:00.0/drm/card0/card0-eDP-1/amdgpu_bl0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Enable manually:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
$ echo 'ddcci 0x37' | sudo tee /sys/bus/i2c/devices/i2c-11/new_device
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- After (`ddcci11` external screen avail):
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
$ ls -l /sys/class/backlight
|
|
94
|
+
total 0
|
|
95
|
+
lrwxrwxrwx 1 root root 0 Sep 7 09:44 amdgpu_bl0 -> ../../devices/pci0000:00/0000:00:08.1/0000:07:00.0/drm/card0/card0-eDP-1/amdgpu_bl0
|
|
96
|
+
lrwxrwxrwx 1 root root 0 Sep 7 10:41 ddcci11 -> ../../devices/pci0000:00/0000:00:08.1/0000:07:00.0/i2c-11/11-0037/ddcci11/backlight/ddcci11
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### [`ddcutil`](https://github.com/rockowitz/ddcutil)
|
|
100
|
+
|
|
101
|
+
**This is the recommended backend** for controlling external displays. Requires `i2c`
|
|
102
|
+
kernel module, but as of [v1.4](https://www.ddcutil.com/config_steps/) "_ddcutil
|
|
103
|
+
installation should automatically install this file, making manual configuration
|
|
104
|
+
unnecessary_"
|
|
105
|
+
|
|
106
|
+
**Note**: [arch wiki states](https://wiki.archlinux.org/title/Backlight):
|
|
107
|
+
> Using ddcci and i2c-dev simultaneously may result in resource conflicts such as
|
|
108
|
+
a Device or resource busy error
|
|
109
|
+
|
|
110
|
+
Meaning it's best to choose one of the options, not both.
|
|
111
|
+
|
|
112
|
+
## Usage
|
|
113
|
+
|
|
114
|
+
### Daemon
|
|
115
|
+
|
|
116
|
+
As mentioned earlier, a daemon process needs to be started that keeps track of
|
|
117
|
+
the displays. Easiest way to do so would be utilizing your OS's process
|
|
118
|
+
manager. An example of a systemd user service file (e.g.
|
|
119
|
+
`~/.config/systemd/user/bctld.service`) would be:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
[Unit]
|
|
123
|
+
Description=bctld aka brightness control daemon
|
|
124
|
+
PartOf=graphical-session.target
|
|
125
|
+
StartLimitIntervalSec=200
|
|
126
|
+
StartLimitBurst=15
|
|
127
|
+
|
|
128
|
+
[Service]
|
|
129
|
+
Type=simple
|
|
130
|
+
ExecStart=%h/.local/bin/bctld
|
|
131
|
+
Restart=on-failure
|
|
132
|
+
RestartSec=10
|
|
133
|
+
RestartPreventExitStatus=100
|
|
134
|
+
|
|
135
|
+
[Install]
|
|
136
|
+
WantedBy=graphical-session.target
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Enable & start this unit by running
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
$ systemctl --user enable --now bctld.service
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Client
|
|
146
|
+
|
|
147
|
+
With demon running, the client is used to send commands to the daemon. List
|
|
148
|
+
available commands via `bctl --help`
|
|
149
|
+
|
|
150
|
+
Some examples:
|
|
151
|
+
|
|
152
|
+
- `bctl up` - bump brightness up by `brightness_step` config
|
|
153
|
+
- `bctl down` - bump brightness down by `brightness_step` config
|
|
154
|
+
- `bctl delta 20` - bump brightness up by 20%
|
|
155
|
+
- `bctl delta -- -20` - bump brightness down by 20%
|
|
156
|
+
- `bctl set 55` - set brightness to 55%
|
|
157
|
+
- `bctl get` - returns current brightness level in %
|
|
158
|
+
- `bctl setvcp D6 01` - set vcp feature D6 to value 01 for all detected DDC displays;
|
|
159
|
+
this is simply shortcut for `ddcutil setvcp D6 01`
|
|
160
|
+
|
|
161
|
+
The daemon also registers signal handlers for `SIGUSR1` & `SIGUSR2`, so
|
|
162
|
+
sending said signals to the daemon process allows bumping brightness up
|
|
163
|
+
and down respectively; e.g.: `kill -s SIGUSR1 "$(pgrep -x bctld)"` or
|
|
164
|
+
`killall -s SIGUSR1 bctld`
|
|
165
|
+
|
|
166
|
+
### Socket
|
|
167
|
+
|
|
168
|
+
The client and daemon communicate over a unix socket set via `socket_path` config.
|
|
169
|
+
If using the provided client is too slow (e.g. for querying brightness), it's
|
|
170
|
+
possible to talk to the daemon directly over this socket. For instance current
|
|
171
|
+
brightness can be fetched via following command, which is equivalent to `bctl get`:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
$ socat - UNIX-CONNECT:/tmp/.bctld-ipc.sock <<< '["get",0,0]' | jq -re '.[1]'
|
|
175
|
+
75
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Please note there will be no guarantees about the stability of this api as it's
|
|
179
|
+
part of internal comms spec.
|
|
180
|
+
|
|
181
|
+
## Configuration
|
|
182
|
+
|
|
183
|
+
User configuration file is read from `$XDG_CONFIG_HOME/bctl/config.json`.
|
|
184
|
+
For full config list see the [config.py](./bctl/config.py) file that defines the defaults,
|
|
185
|
+
but the most important ones you might want to be aware of or change are:
|
|
186
|
+
|
|
187
|
+
| Config | Type | Default | Description |
|
|
188
|
+
| --- | --- | --- | --- |
|
|
189
|
+
| `msg_consumption_window_sec` | float | 0.1 | event consumption window in seconds |
|
|
190
|
+
| `udev_event_debounce_sec` | float | 3.0 | udev event debounce window in seconds |
|
|
191
|
+
| `brightness_step` | int | 5 | percentage to bump brightness up or down per change |
|
|
192
|
+
| `sync_brightness` | bool | False | whether to keep screens' brightnesses in sync |
|
|
193
|
+
| `main_display_ctl` | str | DDCUTIL | backend for brightness control |
|
|
194
|
+
| `internal_display_ctl` | str | RAW | backend for controlling internal display |
|
|
195
|
+
| `notify.icon.root_dir` | str | '' | notification icon directory |
|
|
196
|
+
| `fatal_exit_code` | int | 100 | error code daemon should exit with when restart shouldn't be attempted. you might want to use this value in systemd unit file w/ [`RestartPreventExitStatus`](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#RestartPreventExitStatus=) config |
|
|
197
|
+
|
|
198
|
+
#### `msg_consumption_window_sec`
|
|
199
|
+
|
|
200
|
+
Defines an event consumption window, meaning if say 'brightness up' key is spammed
|
|
201
|
+
5x during said window, ddcutil is invoked just once bumping up the brightness by
|
|
202
|
+
5x<brightness_step> value, as opposed to running ddcutil 5 times bumping
|
|
203
|
+
1x<brightness_step> each time.
|
|
204
|
+
|
|
205
|
+
#### `main_display_ctl`
|
|
206
|
+
|
|
207
|
+
This config sets the main backend for controlling the brightness. Available options:
|
|
208
|
+
- `DDCUTIL` - controls _external_ displays via ddcutil, requires
|
|
209
|
+
[`ddcutil`](https://github.com/rockowitz/ddcutil) to be on PATH, described above.
|
|
210
|
+
- `RAW` - all displays are controlled via the device interfaces under `/sys/class/backlight`
|
|
211
|
+
directory. In order to control external displays using this backend, you'd
|
|
212
|
+
likely need the installation of [`ddcci` kernel driver](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux),
|
|
213
|
+
described above.
|
|
214
|
+
- `BRIGHTNESSCTL` - all displays are controlled via [`brightnessctl`](https://github.com/Hummer12007/brightnessctl)
|
|
215
|
+
program.
|
|
216
|
+
- `BRILLO` - all displays are controlled via [`brillo`](https://gitlab.com/cameronnemo/brillo)
|
|
217
|
+
program.
|
|
218
|
+
|
|
219
|
+
#### `internal_display_ctl`
|
|
220
|
+
|
|
221
|
+
This config sets the backend used only for controlling the internal display
|
|
222
|
+
brightness, as that's not what ddcutil does. Only in effect if
|
|
223
|
+
`main_display_ctl=DDCUTIL` and we're running on a laptop. Available options are
|
|
224
|
+
`RAW | BRIGHTNESSCTL | BRILLO`
|
|
225
|
+
|
|
226
|
+
#### `notify.icon.root_dir`
|
|
227
|
+
|
|
228
|
+
Notification icon directory. Icon is chosen based on brightness level, and final used icon
|
|
229
|
+
will be `notify.icon.root_dir` + `notify.icon.brightness_{full,high,medium,low,off}`.
|
|
230
|
+
|
|
231
|
+
Note either half of final value may be an empty string, so if you want to use
|
|
232
|
+
single icon for all levels, set icon full path to `notify.icon.root_dir` and
|
|
233
|
+
set `notify.icon.brightness_{full,high,medium,low,off}` values to an empty string.
|
|
234
|
+
|
|
235
|
+
## Troubleshooting
|
|
236
|
+
|
|
237
|
+
### External display (dis)connection not detected
|
|
238
|
+
|
|
239
|
+
Current implementation relies on listening for `drm` subsystem `change` action
|
|
240
|
+
udev events. Some graphic cards (and/or monitors, unsure) are known to either
|
|
241
|
+
not emit said events, emit them only sometimes, or emit different ones. Recommend
|
|
242
|
+
you try debugging it via running `$ udevadm monitor` that starts listening for udev
|
|
243
|
+
events, then connect or disconnect your monitor and see what events are printed out.
|
|
244
|
+
With that info feel free to open an issue.
|
|
245
|
+
|
|
246
|
+
As a hacky workaround it's also possible to enable periodic polling by setting
|
|
247
|
+
`periodic_init_sec` to seconds at which interval display detection should
|
|
248
|
+
happen. Wouldn't set it to anything lower than 30.
|
|
249
|
+
|
|
250
|
+
Additionally you may opt out of udev monitoring altoghether (see [config.py](./bctl/config.py)),
|
|
251
|
+
and rely on your own custom detection; in that case daemon can be asked to
|
|
252
|
+
re-initialize its state by sending init command via the client: `$ bctl init`
|
bctl-0.0.1/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# BCTL - brightness control
|
|
2
|
+
|
|
3
|
+
This is a simple daemon for controlling displays' brightnesses. It
|
|
4
|
+
consists of a daemon process listening for user requests (e.g. changing brightness)
|
|
5
|
+
and a client to send commands to the daemon. udev events are monitored
|
|
6
|
+
for screen (dis)connections. Desktop notifications are shown on brightness change.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
`$ pipx instal bctl`
|
|
11
|
+
|
|
12
|
+
Note this will install the client & daemon executables, but it's user
|
|
13
|
+
responsibility to launch the daemon process, covered below.
|
|
14
|
+
|
|
15
|
+
## Why?
|
|
16
|
+
|
|
17
|
+
Main reason for this program is to provide a simple, general-puropse central point
|
|
18
|
+
for controlling brightness of _all_ the connected screens simultaneously and keeping
|
|
19
|
+
track of their current brightness levels.
|
|
20
|
+
|
|
21
|
+
Controlling laptops' internal screen is generally the easiest, as its device
|
|
22
|
+
object is exposed under `/sys/class/backlight/` dir -- it's the external
|
|
23
|
+
displays that tend to be trickier.
|
|
24
|
+
|
|
25
|
+
For controlling external screens' brightness there are roughly two main
|
|
26
|
+
methods, [explained below](#managing-external-displays). As the recommended
|
|
27
|
+
method -- ddcutil -- takes in some cases non-trivial amount of time to execute
|
|
28
|
+
(up to ~200ms), it can be slightly jarring to change brightness when spamming the key.
|
|
29
|
+
|
|
30
|
+
As this solution caches set brighness values there's no need to query it
|
|
31
|
+
from ddcutil, making e.g. desktop notification generation simpler.
|
|
32
|
+
|
|
33
|
+
Also screen connections & disconnections are kept track of via an udev monitor,
|
|
34
|
+
and there's an option to force all the screens' brightnesses to be kept in sync.
|
|
35
|
+
|
|
36
|
+
## Managing external displays
|
|
37
|
+
|
|
38
|
+
### [`ddcci` kernel driver](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux)
|
|
39
|
+
|
|
40
|
+
This kernel module _should_ detect the devices and expose 'em under
|
|
41
|
+
`/sys/class/backlight/`, just like the laptop's internal display (e.g.
|
|
42
|
+
`/sys/class/backlight/amdgpu_bl0`) is by default. This requires installation
|
|
43
|
+
of [ddcci-dkms](https://packages.debian.org/sid/ddcci-dkms) package and loading
|
|
44
|
+
`ddcci` kernel module.
|
|
45
|
+
|
|
46
|
+
Note as of '25 there are loads of issues w/ this kernel module's auto-detection
|
|
47
|
+
logic, e.g. see issues [7](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux/-/issues/7),
|
|
48
|
+
[42](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux/-/issues/42),
|
|
49
|
+
[46](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux/-/issues/46)
|
|
50
|
+
|
|
51
|
+
Current workaround seems to be manually enabling displays as per [this reddit post](https://old.reddit.com/r/gnome/comments/efkoya/using_ddccidriverlinux_you_can_get_native/fc0xrx6/):
|
|
52
|
+
|
|
53
|
+
- Before state (no external display devices listed/avail):
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
$ ls -l /sys/class/backlight
|
|
57
|
+
total 0
|
|
58
|
+
lrwxrwxrwx 1 root root 0 Sep 7 09:44 amdgpu_bl0 -> ../../devices/pci0000:00/0000:00:08.1/0000:07:00.0/drm/card0/card0-eDP-1/amdgpu_bl0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Enable manually:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
$ echo 'ddcci 0x37' | sudo tee /sys/bus/i2c/devices/i2c-11/new_device
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- After (`ddcci11` external screen avail):
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
$ ls -l /sys/class/backlight
|
|
71
|
+
total 0
|
|
72
|
+
lrwxrwxrwx 1 root root 0 Sep 7 09:44 amdgpu_bl0 -> ../../devices/pci0000:00/0000:00:08.1/0000:07:00.0/drm/card0/card0-eDP-1/amdgpu_bl0
|
|
73
|
+
lrwxrwxrwx 1 root root 0 Sep 7 10:41 ddcci11 -> ../../devices/pci0000:00/0000:00:08.1/0000:07:00.0/i2c-11/11-0037/ddcci11/backlight/ddcci11
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### [`ddcutil`](https://github.com/rockowitz/ddcutil)
|
|
77
|
+
|
|
78
|
+
**This is the recommended backend** for controlling external displays. Requires `i2c`
|
|
79
|
+
kernel module, but as of [v1.4](https://www.ddcutil.com/config_steps/) "_ddcutil
|
|
80
|
+
installation should automatically install this file, making manual configuration
|
|
81
|
+
unnecessary_"
|
|
82
|
+
|
|
83
|
+
**Note**: [arch wiki states](https://wiki.archlinux.org/title/Backlight):
|
|
84
|
+
> Using ddcci and i2c-dev simultaneously may result in resource conflicts such as
|
|
85
|
+
a Device or resource busy error
|
|
86
|
+
|
|
87
|
+
Meaning it's best to choose one of the options, not both.
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### Daemon
|
|
92
|
+
|
|
93
|
+
As mentioned earlier, a daemon process needs to be started that keeps track of
|
|
94
|
+
the displays. Easiest way to do so would be utilizing your OS's process
|
|
95
|
+
manager. An example of a systemd user service file (e.g.
|
|
96
|
+
`~/.config/systemd/user/bctld.service`) would be:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
[Unit]
|
|
100
|
+
Description=bctld aka brightness control daemon
|
|
101
|
+
PartOf=graphical-session.target
|
|
102
|
+
StartLimitIntervalSec=200
|
|
103
|
+
StartLimitBurst=15
|
|
104
|
+
|
|
105
|
+
[Service]
|
|
106
|
+
Type=simple
|
|
107
|
+
ExecStart=%h/.local/bin/bctld
|
|
108
|
+
Restart=on-failure
|
|
109
|
+
RestartSec=10
|
|
110
|
+
RestartPreventExitStatus=100
|
|
111
|
+
|
|
112
|
+
[Install]
|
|
113
|
+
WantedBy=graphical-session.target
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Enable & start this unit by running
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
$ systemctl --user enable --now bctld.service
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Client
|
|
123
|
+
|
|
124
|
+
With demon running, the client is used to send commands to the daemon. List
|
|
125
|
+
available commands via `bctl --help`
|
|
126
|
+
|
|
127
|
+
Some examples:
|
|
128
|
+
|
|
129
|
+
- `bctl up` - bump brightness up by `brightness_step` config
|
|
130
|
+
- `bctl down` - bump brightness down by `brightness_step` config
|
|
131
|
+
- `bctl delta 20` - bump brightness up by 20%
|
|
132
|
+
- `bctl delta -- -20` - bump brightness down by 20%
|
|
133
|
+
- `bctl set 55` - set brightness to 55%
|
|
134
|
+
- `bctl get` - returns current brightness level in %
|
|
135
|
+
- `bctl setvcp D6 01` - set vcp feature D6 to value 01 for all detected DDC displays;
|
|
136
|
+
this is simply shortcut for `ddcutil setvcp D6 01`
|
|
137
|
+
|
|
138
|
+
The daemon also registers signal handlers for `SIGUSR1` & `SIGUSR2`, so
|
|
139
|
+
sending said signals to the daemon process allows bumping brightness up
|
|
140
|
+
and down respectively; e.g.: `kill -s SIGUSR1 "$(pgrep -x bctld)"` or
|
|
141
|
+
`killall -s SIGUSR1 bctld`
|
|
142
|
+
|
|
143
|
+
### Socket
|
|
144
|
+
|
|
145
|
+
The client and daemon communicate over a unix socket set via `socket_path` config.
|
|
146
|
+
If using the provided client is too slow (e.g. for querying brightness), it's
|
|
147
|
+
possible to talk to the daemon directly over this socket. For instance current
|
|
148
|
+
brightness can be fetched via following command, which is equivalent to `bctl get`:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
$ socat - UNIX-CONNECT:/tmp/.bctld-ipc.sock <<< '["get",0,0]' | jq -re '.[1]'
|
|
152
|
+
75
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Please note there will be no guarantees about the stability of this api as it's
|
|
156
|
+
part of internal comms spec.
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
User configuration file is read from `$XDG_CONFIG_HOME/bctl/config.json`.
|
|
161
|
+
For full config list see the [config.py](./bctl/config.py) file that defines the defaults,
|
|
162
|
+
but the most important ones you might want to be aware of or change are:
|
|
163
|
+
|
|
164
|
+
| Config | Type | Default | Description |
|
|
165
|
+
| --- | --- | --- | --- |
|
|
166
|
+
| `msg_consumption_window_sec` | float | 0.1 | event consumption window in seconds |
|
|
167
|
+
| `udev_event_debounce_sec` | float | 3.0 | udev event debounce window in seconds |
|
|
168
|
+
| `brightness_step` | int | 5 | percentage to bump brightness up or down per change |
|
|
169
|
+
| `sync_brightness` | bool | False | whether to keep screens' brightnesses in sync |
|
|
170
|
+
| `main_display_ctl` | str | DDCUTIL | backend for brightness control |
|
|
171
|
+
| `internal_display_ctl` | str | RAW | backend for controlling internal display |
|
|
172
|
+
| `notify.icon.root_dir` | str | '' | notification icon directory |
|
|
173
|
+
| `fatal_exit_code` | int | 100 | error code daemon should exit with when restart shouldn't be attempted. you might want to use this value in systemd unit file w/ [`RestartPreventExitStatus`](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#RestartPreventExitStatus=) config |
|
|
174
|
+
|
|
175
|
+
#### `msg_consumption_window_sec`
|
|
176
|
+
|
|
177
|
+
Defines an event consumption window, meaning if say 'brightness up' key is spammed
|
|
178
|
+
5x during said window, ddcutil is invoked just once bumping up the brightness by
|
|
179
|
+
5x<brightness_step> value, as opposed to running ddcutil 5 times bumping
|
|
180
|
+
1x<brightness_step> each time.
|
|
181
|
+
|
|
182
|
+
#### `main_display_ctl`
|
|
183
|
+
|
|
184
|
+
This config sets the main backend for controlling the brightness. Available options:
|
|
185
|
+
- `DDCUTIL` - controls _external_ displays via ddcutil, requires
|
|
186
|
+
[`ddcutil`](https://github.com/rockowitz/ddcutil) to be on PATH, described above.
|
|
187
|
+
- `RAW` - all displays are controlled via the device interfaces under `/sys/class/backlight`
|
|
188
|
+
directory. In order to control external displays using this backend, you'd
|
|
189
|
+
likely need the installation of [`ddcci` kernel driver](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux),
|
|
190
|
+
described above.
|
|
191
|
+
- `BRIGHTNESSCTL` - all displays are controlled via [`brightnessctl`](https://github.com/Hummer12007/brightnessctl)
|
|
192
|
+
program.
|
|
193
|
+
- `BRILLO` - all displays are controlled via [`brillo`](https://gitlab.com/cameronnemo/brillo)
|
|
194
|
+
program.
|
|
195
|
+
|
|
196
|
+
#### `internal_display_ctl`
|
|
197
|
+
|
|
198
|
+
This config sets the backend used only for controlling the internal display
|
|
199
|
+
brightness, as that's not what ddcutil does. Only in effect if
|
|
200
|
+
`main_display_ctl=DDCUTIL` and we're running on a laptop. Available options are
|
|
201
|
+
`RAW | BRIGHTNESSCTL | BRILLO`
|
|
202
|
+
|
|
203
|
+
#### `notify.icon.root_dir`
|
|
204
|
+
|
|
205
|
+
Notification icon directory. Icon is chosen based on brightness level, and final used icon
|
|
206
|
+
will be `notify.icon.root_dir` + `notify.icon.brightness_{full,high,medium,low,off}`.
|
|
207
|
+
|
|
208
|
+
Note either half of final value may be an empty string, so if you want to use
|
|
209
|
+
single icon for all levels, set icon full path to `notify.icon.root_dir` and
|
|
210
|
+
set `notify.icon.brightness_{full,high,medium,low,off}` values to an empty string.
|
|
211
|
+
|
|
212
|
+
## Troubleshooting
|
|
213
|
+
|
|
214
|
+
### External display (dis)connection not detected
|
|
215
|
+
|
|
216
|
+
Current implementation relies on listening for `drm` subsystem `change` action
|
|
217
|
+
udev events. Some graphic cards (and/or monitors, unsure) are known to either
|
|
218
|
+
not emit said events, emit them only sometimes, or emit different ones. Recommend
|
|
219
|
+
you try debugging it via running `$ udevadm monitor` that starts listening for udev
|
|
220
|
+
events, then connect or disconnect your monitor and see what events are printed out.
|
|
221
|
+
With that info feel free to open an issue.
|
|
222
|
+
|
|
223
|
+
As a hacky workaround it's also possible to enable periodic polling by setting
|
|
224
|
+
`periodic_init_sec` to seconds at which interval display detection should
|
|
225
|
+
happen. Wouldn't set it to anything lower than 30.
|
|
226
|
+
|
|
227
|
+
Additionally you may opt out of udev monitoring altoghether (see [config.py](./bctl/config.py)),
|
|
228
|
+
and rely on your own custom detection; in that case daemon can be asked to
|
|
229
|
+
re-initialize its state by sending init command via the client: `$ bctl init`
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# TODO: consider pyro5 for rpc, as opposed to json over AF_UNIX
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import bctl.client as client
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group
|
|
10
|
+
@click.pass_context
|
|
11
|
+
@click.option(
|
|
12
|
+
'--debug',
|
|
13
|
+
is_flag=True,
|
|
14
|
+
help='Enables logging at debug level')
|
|
15
|
+
def main(ctx, debug):
|
|
16
|
+
"""Client for sending messages to BCTLD"""
|
|
17
|
+
ctx.obj = client.Client(debug=debug)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@main.command
|
|
21
|
+
@click.pass_obj
|
|
22
|
+
@click.argument('delta', type=int, default=5)
|
|
23
|
+
def up(ctx, delta):
|
|
24
|
+
"""Bump up screens' brightness.
|
|
25
|
+
|
|
26
|
+
:param ctx: context
|
|
27
|
+
:param delta: % delta to bump brightness up by
|
|
28
|
+
"""
|
|
29
|
+
assert delta > 0, 'brightness % to bump up by needs to be positive int'
|
|
30
|
+
ctx.send_cmd(['delta', delta])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@main.command
|
|
34
|
+
@click.pass_obj
|
|
35
|
+
@click.argument('delta', type=int, default=5)
|
|
36
|
+
def down(ctx, delta):
|
|
37
|
+
"""Bump down screens' brightness.
|
|
38
|
+
|
|
39
|
+
:param ctx: context
|
|
40
|
+
:param delta: % delta to bump brightness down by
|
|
41
|
+
"""
|
|
42
|
+
assert delta > 0, 'brightness % to bump down by needs to be positive int'
|
|
43
|
+
ctx.send_cmd(['delta', -delta])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@main.command
|
|
47
|
+
@click.pass_obj
|
|
48
|
+
@click.argument('delta', type=int)
|
|
49
|
+
def delta(ctx, delta):
|
|
50
|
+
"""Change screens' brightness by given %
|
|
51
|
+
|
|
52
|
+
:param ctx: context
|
|
53
|
+
:param delta: % delta to change brightness down by
|
|
54
|
+
"""
|
|
55
|
+
ctx.send_cmd(['delta', delta])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@main.command
|
|
59
|
+
@click.pass_obj
|
|
60
|
+
@click.argument('value', type=int)
|
|
61
|
+
def set(ctx, value):
|
|
62
|
+
"""Change screens' brightness to given %
|
|
63
|
+
|
|
64
|
+
:param ctx: context
|
|
65
|
+
:param value: % value to change brightness to
|
|
66
|
+
"""
|
|
67
|
+
assert value >= 0, 'brightness % to set to needs to be >= 0'
|
|
68
|
+
ctx.send_cmd(['set', value])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@main.command
|
|
72
|
+
@click.pass_obj
|
|
73
|
+
@click.argument('args', nargs=-1, type=str)
|
|
74
|
+
def setvcp(ctx, args: tuple[str, ...]):
|
|
75
|
+
"""Set VCP feature value(s) for all detected DDC displays
|
|
76
|
+
|
|
77
|
+
:param ctx: context
|
|
78
|
+
"""
|
|
79
|
+
assert len(args) >= 2, 'minimum 2 args needed, read ddcutil manual on [setvcp] command'
|
|
80
|
+
ctx.send_receive_cmd(['setvcp', args])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@main.command
|
|
84
|
+
@click.pass_obj
|
|
85
|
+
@click.argument('args', nargs=-1, type=str)
|
|
86
|
+
def getvcp(ctx, args: tuple[str, ...]):
|
|
87
|
+
"""Get VCP feature value(s) for all detected DDC displays
|
|
88
|
+
|
|
89
|
+
:param ctx: context
|
|
90
|
+
"""
|
|
91
|
+
assert args, 'minimum 1 feature needed, read ddcutil manual on [getvcp] command'
|
|
92
|
+
ctx.send_receive_cmd(['getvcp', args])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@main.command
|
|
96
|
+
@click.pass_obj
|
|
97
|
+
@click.option(
|
|
98
|
+
'-i', '--individual',
|
|
99
|
+
is_flag=True,
|
|
100
|
+
help='retrieve brightness levels per screen')
|
|
101
|
+
@click.option(
|
|
102
|
+
'-r', '--raw',
|
|
103
|
+
is_flag=True,
|
|
104
|
+
help='retrieve raw brightness value')
|
|
105
|
+
def get(ctx, individual: bool, raw: bool):
|
|
106
|
+
"""Get screens' brightness (%)
|
|
107
|
+
|
|
108
|
+
:param ctx: context
|
|
109
|
+
"""
|
|
110
|
+
ctx.send_receive_cmd(['get', individual, raw])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@main.command
|
|
114
|
+
@click.pass_obj
|
|
115
|
+
def init(ctx):
|
|
116
|
+
"""Re-initialize displays.
|
|
117
|
+
|
|
118
|
+
:param ctx: context
|
|
119
|
+
"""
|
|
120
|
+
ctx.send_cmd(['init'])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@main.command
|
|
124
|
+
@click.pass_obj
|
|
125
|
+
def sync(ctx):
|
|
126
|
+
"""Synchronize screens' brightness levels.
|
|
127
|
+
|
|
128
|
+
:param ctx: context
|
|
129
|
+
"""
|
|
130
|
+
ctx.send_cmd(['sync'])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@main.command
|
|
134
|
+
@click.pass_obj
|
|
135
|
+
def kill(ctx):
|
|
136
|
+
"""Terminate the daemon process.
|
|
137
|
+
|
|
138
|
+
:param ctx: context
|
|
139
|
+
"""
|
|
140
|
+
ctx.send_cmd(['kill'])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == '__main__':
|
|
144
|
+
main()
|
|
145
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import bctl.daemon as daemon
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.command
|
|
8
|
+
@click.option(
|
|
9
|
+
'--debug',
|
|
10
|
+
is_flag=True,
|
|
11
|
+
help='Enables logging at debug level.')
|
|
12
|
+
def main(debug):
|
|
13
|
+
daemon.main(debug)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == '__main__':
|
|
17
|
+
main()
|
|
18
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import bctl.daemon as daemon
|
|
5
|
+
from ..config import SimConf
|
|
6
|
+
|
|
7
|
+
@click.command
|
|
8
|
+
@click.option(
|
|
9
|
+
'--debug',
|
|
10
|
+
is_flag=True,
|
|
11
|
+
help='Enables logging at debug level.')
|
|
12
|
+
@click.option('-n', '--number', default=3,
|
|
13
|
+
help='Number of simulated displays')
|
|
14
|
+
@click.option('-w', '--wait', type=float, required=True,
|
|
15
|
+
help='How long to wait for work simulation')
|
|
16
|
+
@click.option('-b', '--brightness',
|
|
17
|
+
help='Initial brightness', default=50)
|
|
18
|
+
@click.option('-f', '--fail', type=str,
|
|
19
|
+
help='Failure mode to simulate')
|
|
20
|
+
@click.option('-e', '--exit', default=1,
|
|
21
|
+
help='code to exit chosen failmode with')
|
|
22
|
+
def main(debug, number, wait, brightness, fail, exit):
|
|
23
|
+
failmodes = ['i', 's']
|
|
24
|
+
assert number > 0, 'number of simulated displays must be positive'
|
|
25
|
+
assert fail in failmodes + [None], f'allowed failmodes are {failmodes}'
|
|
26
|
+
assert number >= 0, 'exit code needs to be >= 0'
|
|
27
|
+
|
|
28
|
+
sim: SimConf = {
|
|
29
|
+
'ndisplays': number,
|
|
30
|
+
'wait_sec': wait,
|
|
31
|
+
'initial_brightness': brightness,
|
|
32
|
+
'failmode': fail,
|
|
33
|
+
'exit_code': exit,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
daemon.main(debug, sim)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == '__main__':
|
|
40
|
+
main()
|
|
41
|
+
|