forked from udifuchs/icc-brightness
-
Notifications
You must be signed in to change notification settings - Fork 1
/
icc-brightness
executable file
·319 lines (259 loc) · 11 KB
/
icc-brightness
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
#! /usr/bin/env python
# Copyright 2017 - 2019, Udi Fuchs (original author)
# Copyright 2021, Kahlil Hodgson (modifications)
# SPDX-License-Identifier: MIT
"""Control display brightness by applying ICC color profiles.
icc-brightness set brightness max-brightness - set brightness manually
icc-brightness apply - apply brightness from system setting
icc-brightness watch - continuously update to system setting
icc-brightness clean - remove all profiles generated by us
icc-brightness list - list device models we can see
"""
import argparse
import fcntl
import logging
import os
import re
import signal
import subprocess
import sys
import threading
import time
import unicodedata
from types import SimpleNamespace
TEMP_FOLDER = "/tmp"
BACKLIGHT_PATH = "/sys/class/backlight/intel_backlight"
# Try some known working locations for the backlight device
if os.path.exists("/sys/class/backlight/intel_backlight"):
BACKLIGHT_PATH = "/sys/class/backlight/intel_backlight"
elif os.path.exists("/sys/class/backlight/acpi_video0"):
BACKLIGHT_PATH = "/sys/class/backlight/acpi_video0"
else:
logging.error("Could not find sys path to backlight device")
sys.exit(1)
BRIGHTNESS_PATH = os.path.join(BACKLIGHT_PATH, "brightness")
MAX_BRIGHTNESS_PATH = os.path.join(BACKLIGHT_PATH, "max_brightness")
CWD = os.path.dirname(__file__)
ICC_BRIGHTNESS_GEN = os.path.join(CWD, "icc-brightness-gen")
LOCK = threading.Lock()
target = SimpleNamespace(device=None, slug="")
def clean():
"""Find all profile generated by us and remove them."""
out = subprocess.check_output(["colormgr", "get-profiles"])
profile_path = None
filename = None
for line in out.decode("utf8").split("\n"):
if line.startswith("Object Path:"):
profile_path = line.split(":")[1].lstrip()
continue
if line.startswith("Filename:"):
filename = line.split(":")[1].lstrip()
if filename.find(f"icc/{target.slug}brightness_") < 0:
continue
logging.info("Removing: %s", filename)
subprocess.run(["colormgr", "delete-profile", profile_path], check=True)
subprocess.run(["rm", filename], check=True)
def list_devices():
"""List all device models visible to us"""
out = subprocess.check_output(["colormgr", "get-devices-by-kind", "display"])
for line in out.decode("utf8").split("\n"):
if line.startswith("Model:"):
print(line.split(":")[1].lstrip())
def find_profile_path(filename):
"""
Query colormgr for a profile via its filename
Returns the corresponding object path.
"""
try:
out = subprocess.check_output(
["colormgr", "find-profile-by-filename", filename]
)
except subprocess.CalledProcessError:
return None
object_path = None
for line in out.decode("utf8").split("\n"):
if line.startswith("Object Path:"):
object_path = line.split(":")[1].lstrip()
break
return object_path
def find_device_path():
"""
Query colormgr for the id of the target device
Returns the corresponding object path.
"""
out = subprocess.check_output(["colormgr", "get-devices-by-kind", "display"])
# If there is more than one device being managed, there will be multiple data blocks
# separated by blank lines. In each block the 'Object Path' line will always occur
# before the 'Model' or 'Embedded' line, so we repeatedly set the object_path and
# only break when we find an appropriate match. If we are not targeting a specific
# device, we just pick the first embedded device we find (i.e. the laptops screen).
object_path = None
for line in out.decode("utf8").split("\n"):
if line.startswith("Object Path:"):
object_path = line.split(":")[1].lstrip()
elif target.device is None:
if line.startswith("Embedded:"):
embedded = line.split(":")[1].lstrip()
if embedded == "Yes":
break
else:
if line.startswith("Model:"):
model_name = line.split(":")[1].lstrip()
if model_name.startswith(target.device):
break
return object_path
def rounded(brightness, max_brightness, factor=0.05):
"""
return BRIGHTNESS rounded to FACTOR increments of MAX_BRIGHTNESS
This avoids strange brightness settings like 0.61, due to float rounding anomalies.
"""
return int(
(((int((brightness / max_brightness) * 100)) // (factor * 100)) * factor)
* max_brightness
)
def icc_brightness(brightness, max_brightness):
"""Set brightness using an appropriate color profile"""
logging.debug("Searching for device")
device_path = find_device_path()
if device_path is None:
# This could happen during startup if the colord is not running yet
logging.warning("No matching device found yet")
return
brightness = rounded(brightness, max_brightness)
logging.info("Setting brightness ratio to %.2f", brightness / max_brightness)
icc_filename = "%sbrightness_%d_%d.icc" % (target.slug, brightness, max_brightness)
logging.debug("Searching for profile %s", icc_filename)
profile_path = find_profile_path(icc_filename)
if profile_path is None:
logging.debug("Create new profile %s", icc_filename)
icc_filepath = os.path.join(TEMP_FOLDER, icc_filename)
try:
subprocess.run(
[
ICC_BRIGHTNESS_GEN,
icc_filepath,
str(brightness),
str(max_brightness),
],
check=True,
)
except subprocess.CalledProcessError as ex:
# We should never get here ... but log the reason if we do
logging.error("Failed to create new profile: %s", ex.stdout)
logging.debug("Import new profile %s", icc_filepath)
try:
subprocess.run(["colormgr", "import-profile", icc_filepath], check=True)
except subprocess.CalledProcessError as ex:
# We should never get here ... but log the reason if we do
logging.error("Failed to import new profile: %s", ex.stdout)
logging.debug("Retrieve new profile %s", icc_filename)
profile_path = find_profile_path(icc_filename)
logging.debug("apply new profile to device %s", device_path)
try:
subprocess.run(
["colormgr", "device-add-profile", device_path, profile_path],
check=True,
)
except subprocess.CalledProcessError as ex:
# We should never get here ... but log the reason if we do
logging.error("Failed to add profile: %s", ex.stdout)
logging.debug("Make profile default for device %s", device_path)
try:
subprocess.run(
["colormgr", "device-make-profile-default", device_path, profile_path],
check=True,
)
except subprocess.CalledProcessError as ex:
# We should never get here ... but log the reason if we do
logging.error("Failed to add profile: %s", ex.stdout)
def icc_brightness_apply():
"""Apply a color profile corresponding to the sytsem brightness settings"""
with open(BRIGHTNESS_PATH) as infile:
brightness = int(infile.readline())
with open(MAX_BRIGHTNESS_PATH) as infile:
max_brightness = int(infile.readline())
icc_brightness(brightness, max_brightness)
def handler(signum, frame):
"""Respond to changes to the system brightness settings"""
logging.debug("Running handler for signum %d and frame %s", signum, frame)
if LOCK.acquire(blocking=False):
try:
icc_brightness_apply()
except subprocess.CalledProcessError as ex:
logging.exception("Error during call to icc_brightness")
logging.error(ex.stdout)
except BaseException:
logging.exception("Error during call to icc_brightness")
finally:
LOCK.release()
# Borrowed from Django
def slugify(value):
"""
Converts to lowercase, removes non-word characters (alphanumerics and
underscores) and converts spaces to hyphens. Also strips leading and
trailing whitespace.
"""
value = (
unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
)
value = re.sub(r"[^\w\s-]", "", value).strip().lower()
return re.sub(r"[-\s]+", "-", value)
def main():
log_format = "%(asctime)s - %(levelname)s: %(message)s"
logging.basicConfig(level=logging.INFO, format=log_format)
logger = logging.getLogger()
parser = argparse.ArgumentParser(
description="Control display brightness by applying ICC color profiles"
)
subparsers = parser.add_subparsers(dest="action")
parser.add_argument("--target", help="prefix of device models to target")
parser.add_argument("--loglevel", help="set the logging level")
parser.add_argument("--logfile", help="log to the specified file")
subparsers.add_parser("apply", help="apply brightness from system setting")
subparsers.add_parser("watch", help="continuously update to system setting")
subparsers.add_parser("clean", help="remove all profiles generated by us")
subparsers.add_parser("list", help="list visible device models")
set_parser = subparsers.add_parser("set", help="set brightness manually")
set_parser.add_argument("brightness", type=int)
set_parser.add_argument("max_brightness", type=int)
args = parser.parse_args()
if args.target is not None:
target.device = args.target
target.slug = slugify(target.device) + "-"
if args.loglevel is not None:
loglevel = args.loglevel.upper()
numeric_level = getattr(logging, loglevel, None)
if not isinstance(numeric_level, int):
raise ValueError("Invalid log level: %s" % args.loglevel)
logger.setLevel(numeric_level)
logger.info("Setting log level: %s", loglevel)
if args.logfile is not None:
logfile = args.logfile
fh = logging.FileHandler(logfile, mode="a")
fh.setFormatter(logging.Formatter(log_format))
logger.addHandler(fh)
logger.info("Logging to file: %s", logfile)
if args.action == "clean":
clean()
elif args.action == "apply":
icc_brightness_apply()
elif args.action == "watch":
try:
icc_brightness_apply()
except BaseException:
logging.exception("device-make-profile-default")
# Watch for changes to the system brightness settings
signal.signal(signal.SIGIO, handler)
fd = os.open(BACKLIGHT_PATH, os.O_RDONLY)
fcntl.fcntl(fd, fcntl.F_SETSIG, 0)
fcntl.fcntl(fd, fcntl.F_NOTIFY, fcntl.DN_MODIFY | fcntl.DN_MULTISHOT)
while True:
time.sleep(1000000)
elif args.action == "list":
list_devices()
elif args.action == "set":
icc_brightness(args.brightness, args.max_brightness)
else:
parser.print_help()
if __name__ == "__main__":
main()