From fd62f4b29dec8fe777439f02667647c6ded19757 Mon Sep 17 00:00:00 2001 From: Rodolfo P A <6721075+rodoufu@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:54:45 -0300 Subject: [PATCH 1/3] Create bluetooth_battery.py --- contrib/bluetooth_battery.py | 184 +++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 contrib/bluetooth_battery.py diff --git a/contrib/bluetooth_battery.py b/contrib/bluetooth_battery.py new file mode 100644 index 00000000..f51b69c7 --- /dev/null +++ b/contrib/bluetooth_battery.py @@ -0,0 +1,184 @@ +import argparse +import json +import subprocess +import sys +from enum import Enum +from typing import List + + +class bcolors(Enum): + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + def colored(self, text: str) -> str: + return f"{self.value}{text}{self.ENDC.value}" + + +def call_process_get_output(cli: List[str]) -> str: + process = subprocess.Popen(cli, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + process.wait() + output, _errors = process.communicate() + return output.decode() + + +def get_bluetooth_battery( + is_debug: bool, + battery_red: int, + battery_yellow: int, + ignore_colors: bool, +) -> List[str]: + devices_output = call_process_get_output(["bluetoothctl", "devices"]).splitlines() + device_name_id_map = { + x[1].strip(): x[0].strip() + for x in map( + lambda x: [x[: x.find(" ")], x[x.find(" ") + 1 :]], + map(lambda x: x.lstrip("Device").strip(), devices_output), + ) + } + + line = [] + for device_name, device_id in device_name_id_map.items(): + device_info = call_process_get_output( + ["bluetoothctl", "info", device_id] + ).splitlines() + + def find_and_clean_up(info, to_find): + return map( + lambda x: x[x.find(": ") + 1 :].strip(), + filter(lambda x: x.find(to_find) != -1, info), + ) + + icon = next(find_and_clean_up(device_info, "Icon"), None) + is_connected = next(find_and_clean_up(device_info, "Connected"), "no") + is_connected = is_connected == "yes" + battery = next( + map( + lambda x: float(x[x.find("(") + 1 : x.find(")")]), + find_and_clean_up(device_info, "Battery Percentage"), + ), + None, + ) + + if is_debug: + print( + f"name: {device_name}, icon: {icon}, connected: {is_connected}, battery: {battery}" + ) + + icon_name_symbol = { + "input-mouse": "󰍽", + "input-keyboard": "", + "audio-headset": "", + "audio-headphones": "", + } + + if icon: + icon = icon_name_symbol.get(icon) + + if is_connected and battery is not None: + batery_text = f"{battery:.0f}%" + if icon: + batery_text = f"{icon} {batery_text}" + else: + batery_text = f"{device_name} {batery_text}" + + if not ignore_colors: + if battery_red <= battery <= battery_yellow: + batery_text = bcolors.WARNING.colored(batery_text) + elif battery < battery_red: + batery_text = bcolors.FAIL.colored(batery_text) + + line.append(batery_text) + + return line + + +def print_line(message): + """Non-buffered printing to stdout.""" + sys.stdout.write(message + "\n") + sys.stdout.flush() + + +def read_line(): + """Interrupted respecting reader for stdin.""" + # try reading a line, removing any extra whitespace + try: + line = sys.stdin.readline().strip() + # i3status sends EOF, or an empty line + if not line: + sys.exit(3) + return line + # exit on ctrl-c + except KeyboardInterrupt: + sys.exit() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--debug", "-d", help="Run in debug mode", action="store_true") + parser.add_argument( + "--ignore-colors", help="Ignore printing using colors", action="store_true" + ) + parser.add_argument( + "--battery_red", help="Battery level to print red", type=int, default=20 + ) + parser.add_argument( + "--battery_yellow", help="Battery level to print red", type=int, default=50 + ) + args = parser.parse_args() + + # FIXME: the colors are not working anymore in the status bar + + if args.debug: + content = " ".join( + get_bluetooth_battery( + is_debug=args.debug, + battery_yellow=args.battery_yellow, + battery_red=args.battery_red, + ignore_colors=args.ignore_colors, + ) + ) + print(f"args: {args}") + print(content) + else: + # Skip the first line which contains the version header. + print_line(read_line()) + + # The second line contains the start of the infinite array. + print_line(read_line()) + + while True: + line, prefix = read_line(), "" + # ignore comma at start of lines + if line.startswith(","): + line, prefix = line[1:], "," + + content = " ".join( + get_bluetooth_battery( + is_debug=args.debug, + battery_yellow=args.battery_yellow, + battery_red=args.battery_red, + ignore_colors=args.ignore_colors, + ) + ) + + j = json.loads(line) + # insert information into the start of the json, but could be anywhere + # CHANGE THIS LINE TO INSERT SOMETHING ELSE + j.insert( + 0, + { + "full_text": "%s" % content, + "name": "bluetooth_battery", + }, + ) + # and echo back new encoded json + print(prefix + json.dumps(j)) + # print_line(prefix + json.dumps(j)) + sys.stdout.flush() From 2cb08c161526b86eb4b234423c4f56b9ea669508 Mon Sep 17 00:00:00 2001 From: Rodolfo P A <6721075+rodoufu@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:07:38 -0300 Subject: [PATCH 2/3] Fix print and add icon when collor is disabled --- contrib/bluetooth_battery.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/contrib/bluetooth_battery.py b/contrib/bluetooth_battery.py index f51b69c7..9c3e318f 100644 --- a/contrib/bluetooth_battery.py +++ b/contrib/bluetooth_battery.py @@ -82,19 +82,24 @@ def find_and_clean_up(info, to_find): icon = icon_name_symbol.get(icon) if is_connected and battery is not None: - batery_text = f"{battery:.0f}%" + battery_text = f"{battery:.0f}%" if icon: - batery_text = f"{icon} {batery_text}" + battery_text = f"{icon} {battery_text}" else: - batery_text = f"{device_name} {batery_text}" + battery_text = f"{device_name} {battery_text}" - if not ignore_colors: - if battery_red <= battery <= battery_yellow: - batery_text = bcolors.WARNING.colored(batery_text) - elif battery < battery_red: - batery_text = bcolors.FAIL.colored(batery_text) + if battery_red <= battery <= battery_yellow: + if ignore_colors: + battery_text = f"{battery_text} 󰥄" + else: + battery_text = bcolors.WARNING.colored(battery_text) + elif battery < battery_red: + if ignore_colors: + battery_text = f"{battery_text} 󰤾" + else: + battery_text = bcolors.FAIL.colored(battery_text) - line.append(batery_text) + line.append(battery_text) return line @@ -179,6 +184,4 @@ def read_line(): }, ) # and echo back new encoded json - print(prefix + json.dumps(j)) - # print_line(prefix + json.dumps(j)) - sys.stdout.flush() + print_line(prefix + json.dumps(j)) From fd9c0af5a9d146ad4b07e631c012b41d6d05f15e Mon Sep 17 00:00:00 2001 From: Rodolfo P A <6721075+rodoufu@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:41:48 -0300 Subject: [PATCH 3/3] Fix issue with colors --- contrib/bluetooth_battery.py | 107 +++++++++++++++-------------------- 1 file changed, 47 insertions(+), 60 deletions(-) diff --git a/contrib/bluetooth_battery.py b/contrib/bluetooth_battery.py index 9c3e318f..bb1bce8a 100644 --- a/contrib/bluetooth_battery.py +++ b/contrib/bluetooth_battery.py @@ -2,23 +2,7 @@ import json import subprocess import sys -from enum import Enum -from typing import List - - -class bcolors(Enum): - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKCYAN = "\033[96m" - OKGREEN = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - ENDC = "\033[0m" - BOLD = "\033[1m" - UNDERLINE = "\033[4m" - - def colored(self, text: str) -> str: - return f"{self.value}{text}{self.ENDC.value}" +from typing import List, Optional def call_process_get_output(cli: List[str]) -> str: @@ -28,12 +12,27 @@ def call_process_get_output(cli: List[str]) -> str: return output.decode() +class I3Module: + def __init__( + self, + full_text: str, + name: str, + color: Optional[str] = None, + markup: Optional[str] = None, + ): + self.full_text = full_text + self.name = name + if color: + self.color = color + if markup: + self.markup = markup + + def get_bluetooth_battery( is_debug: bool, battery_red: int, battery_yellow: int, - ignore_colors: bool, -) -> List[str]: +) -> List[I3Module]: devices_output = call_process_get_output(["bluetoothctl", "devices"]).splitlines() device_name_id_map = { x[1].strip(): x[0].strip() @@ -43,7 +42,7 @@ def get_bluetooth_battery( ) } - line = [] + resp = [] for device_name, device_id in device_name_id_map.items(): device_info = call_process_get_output( ["bluetoothctl", "info", device_id] @@ -81,6 +80,7 @@ def find_and_clean_up(info, to_find): if icon: icon = icon_name_symbol.get(icon) + color = None if is_connected and battery is not None: battery_text = f"{battery:.0f}%" if icon: @@ -89,19 +89,21 @@ def find_and_clean_up(info, to_find): battery_text = f"{device_name} {battery_text}" if battery_red <= battery <= battery_yellow: - if ignore_colors: - battery_text = f"{battery_text} 󰥄" - else: - battery_text = bcolors.WARNING.colored(battery_text) + color = "#FFFF00" + battery_text = f"{battery_text} 󰥄" elif battery < battery_red: - if ignore_colors: - battery_text = f"{battery_text} 󰤾" - else: - battery_text = bcolors.FAIL.colored(battery_text) - - line.append(battery_text) + color = "#FF0000" + battery_text = f"{battery_text} 󰤾" + + resp.append( + I3Module( + full_text=battery_text, + name=f"bluetooth_battery_{device_name}", + color=color, + ) + ) - return line + return resp def print_line(message): @@ -128,29 +130,21 @@ def read_line(): parser = argparse.ArgumentParser() parser.add_argument("--debug", "-d", help="Run in debug mode", action="store_true") parser.add_argument( - "--ignore-colors", help="Ignore printing using colors", action="store_true" + "--battery-red", help="Battery level to print red", type=int, default=20 ) parser.add_argument( - "--battery_red", help="Battery level to print red", type=int, default=20 - ) - parser.add_argument( - "--battery_yellow", help="Battery level to print red", type=int, default=50 + "--battery-yellow", help="Battery level to print red", type=int, default=50 ) args = parser.parse_args() - # FIXME: the colors are not working anymore in the status bar - if args.debug: - content = " ".join( - get_bluetooth_battery( - is_debug=args.debug, - battery_yellow=args.battery_yellow, - battery_red=args.battery_red, - ignore_colors=args.ignore_colors, - ) + i3_modules = get_bluetooth_battery( + is_debug=args.debug, + battery_yellow=args.battery_yellow, + battery_red=args.battery_red, ) print(f"args: {args}") - print(content) + print(i3_modules) else: # Skip the first line which contains the version header. print_line(read_line()) @@ -164,24 +158,17 @@ def read_line(): if line.startswith(","): line, prefix = line[1:], "," - content = " ".join( - get_bluetooth_battery( - is_debug=args.debug, - battery_yellow=args.battery_yellow, - battery_red=args.battery_red, - ignore_colors=args.ignore_colors, - ) + i3_modules = get_bluetooth_battery( + is_debug=args.debug, + battery_yellow=args.battery_yellow, + battery_red=args.battery_red, ) j = json.loads(line) # insert information into the start of the json, but could be anywhere # CHANGE THIS LINE TO INSERT SOMETHING ELSE - j.insert( - 0, - { - "full_text": "%s" % content, - "name": "bluetooth_battery", - }, - ) + + for i3_module in i3_modules: + j.insert(0, i3_module.__dict__) # and echo back new encoded json print_line(prefix + json.dumps(j))