diff --git a/scripts/backlight.py b/scripts/backlight.py new file mode 100755 index 0000000..d3fb807 --- /dev/null +++ b/scripts/backlight.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 + +############################################################################## +# backlight.py -- A script for setting the backlight brightness using sysfs. # +# 2021 © Marcel Kapfer # +# 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()