#!/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()