dotfiles/scripts/backlight.py

274 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
##############################################################################
# backlight.py -- A script for setting the backlight brightness using sysfs. #
# 2021 © Marcel Kapfer <opensource@mmk2410.org> #
# Licensed unter the MIT/Expat License #
# #
# It is expected that the running user has permission to change the relevant #
# sysfs file. See https://wiki.archlinux.org/title/Backlight#ACPI and the #
# related "Discussion" wiki page on how to grant users the necessary right #
##############################################################################
import sys
SYS_DIR = "/sys/class/backlight/"
GRAPHICS_CARD = "intel_backlight"
ACTUAL_BRIGHTNESS_FILE = "/actual_brightness"
MAX_BRIGHTNESS_FILE = "/max_brightness"
BRIGHTNESS_FILE = "/brightness"
def __read_file_as_int(path: str, function: str) -> int:
"""Read the value at the given `path` as integer.
It is expected that the value in te file can be casted to an integer.
Parameters
----------
path : str
Path to the file to read.
function : str
Information about the reason for writing the file. Used only for better error messages.
Returns
-------
int
Value read at the file at `path`.
"""
try:
with open(path, "r") as f:
value = f.readline()
except FileNotFoundError:
print("[ERROR] Could not read {}. File {} not found.".format(function, path))
sys.exit(2)
except PermissionError:
print("[ERROR] Could not read {}. Permission to read {} denied.".format(function, path))
sys.exit(3)
except IsADirectoryError:
print("[ERROR] Could not read {}. {} is a directory.".format(function, path))
sys.exit(4)
try:
value = int(value.strip())
except ValueError:
print("[ERROR] Could not parse {}.".format(function))
sys.exit(1)
return value
def __write_int_as_file(path: str, value: int, function: str) -> None:
"""Write the given integer `value` to the given `path`.
This functions *overwrites* the file at `path`.
Parameters
----------
path : str
Path to the file to write.
value : int
Value to write the file at `path`.
function : str
Information about the reason for writing the file. Used only for better error messages.
"""
try:
with open(path, "w") as f:
print(value, file=f)
except FileNotFoundError:
print("[ERROR] Could not write {}. File {} not found".format(function, path))
sys.exit(2)
except PermissionError:
print("[ERROR] Could not write {}. Permission to write {} denied.".format(function, path))
sys.exit(3)
except IsADirectoryError:
print("[ERROR] Could not write {}. {} is a directory.".format(function, path))
sys.exit(4)
def actual_brightness() -> int:
"""Get actual brightness value using sysfs.
Read the actual brightness value from the corresponding file in sysfs.
Returns
-------
int
Actual brightness value.
"""
path = SYS_DIR + GRAPHICS_CARD + ACTUAL_BRIGHTNESS_FILE
return __read_file_as_int(path, "actual brightness")
def max_brightness() -> int:
"""Get maximum brightness value using sysfs.
Read the maximum brightness value from the corresponding file in sysfs.
Returns
-------
int
Maximum brightness value.
"""
path = SYS_DIR + GRAPHICS_CARD + MAX_BRIGHTNESS_FILE
return __read_file_as_int(path, "maximal brightness")
def set_brightness(value: int) -> None:
"""Set a new brightness value using sysfs.
Set the given brightness `value` by writing the the corresponding file in sysfs.
Parameters
----------
value:
New brightness value.
"""
path = SYS_DIR + GRAPHICS_CARD + BRIGHTNESS_FILE
__write_int_as_file(path, value, "brightness")
def safe_set_brightness(new_brightness: int, max_brightness_value: int) -> None:
"""Set new brightness with obaying safety.
Set a new brightness value using `set_brightness` by checking the value for some pitfalls.
At the lower end the functions does not allow a brightness value lower than 5% of the maximum brightness.
At the upper end the functions does not allow a brightness value higher than 100% of the maximum brightness.
Parameters
----------
new_brightness : int
Value to set as brightness.
max_brightness_value : int
Maximum brightness value.
"""
min_brightness_value = int(0.05 * max_brightness_value)
if new_brightness < min_brightness_value:
new_brightness = min_brightness_value
elif new_brightness > max_brightness_value:
new_brightness = max_brightness_value
set_brightness(new_brightness)
def calc_brightness(value: float, actual_brightness_value: int, max_brightness_value: int, function: str) -> int:
"""Calculate brightness value based on actual and maximal brightness.
The function calculates a brightness value using the `function` string and the `value` as a percentage.
If `function` is empty the new brightness is the the `value` percentage of the maximum brightness.
If `function` is + or - then the `value` percentage of the maximum brightness is added/subtracted from the actual brightness.
Parameters
----------
value : float
The wanted change as a percentage.
actual_brightness_value : int
Value of the actual brightness.
max_brightness_value : int
Value of the maximum brightness.
function : str
Either "+" for addition to actual brightness, "-" for substraction of actual brightness or "" for relative to maximum brightness.
Returns
-------
int
Calculated brightness value.
"""
if function == "+":
new_brightness = actual_brightness_value + int(value * max_brightness_value)
elif function == "-":
new_brightness = actual_brightness_value - int(value * max_brightness_value)
else:
new_brightness = int(value * max_brightness_value)
return new_brightness
def parse_arg(arg: str) -> (int, str):
"""Parse argument and return value and modifier.
It is expected that the passed argument is the unaltered CLI argument of the form [+/-]NUMBER%.
The function will check the format and raise a `ValueError` if it is not compatible.
Parameters
----------
arg : str
String to parse
Returns
-------
int
Parsed value
mod
Parsed modifier, either +, - or an empty string.
Raises
------
ValueError
If given argument has not the required format.
"""
if len(arg) < 2 or len(arg) > 5:
raise ValueError("Wrong format. Expected lenght between 2 and 5 characters")
if arg[-1] != "%":
raise ValueError("Wrong format. Expected '%' at the end.")
mod = arg[0]
number_start_index = 0
if mod == "+" or mod == "-":
number_start_index = 1
else:
try:
int(mod)
except ValueError:
raise ValueError("Wrong format. Modifier neither +/- nor value of type integer.")
# Set modifier to empty string to return a halfway meaningful value.
mod = ""
try:
value = 0.01 * int(arg[number_start_index:-1])
except ValueError:
raise ValueError("Wrong format. Supplied value not a integer.")
return value, mod
def print_help() -> None:
"""Print help on using this script."""
print("Usage:")
print("backlight.py [+/-]NUMBER%")
print(" +: Increase brightness by NUMBER%")
print(" -: Decrease brightness by NUMBER%")
print(" : Set brightness to NUMBER%")
def main():
"""Main entry point."""
if len(sys.argv) != 2:
print("[ERROR] Unexpected amount of arguments.")
print_help()
sys.exit(5)
try:
value, mod = parse_arg(sys.argv[1])
except ValueError:
print("[ERROR] Malformed argument.")
print_help()
sys.exit(6)
actual_brightness_value = actual_brightness()
max_brightness_value = max_brightness()
new_brightness = calc_brightness(value, actual_brightness_value, max_brightness_value, mod)
safe_set_brightness(new_brightness, max_brightness_value)
if __name__ == "__main__":
main()