import math import os import re import subprocess import cairocffi import iwlib import netifaces import psutil from libqtile import bar, qtile from libqtile.log_utils import logger from libqtile.widget import base from libqtile.widget.base import ORIENTATION_HORIZONTAL from libqtile.widget.battery import default_icon_path, load_battery, BatteryState from libqtile.widget.wlan import get_status from libqtile.widget.groupbox import GroupBox from libqtile.command.base import expose_command from kuro.utils.general import notify, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_DOWN, BUTTON_UP, BUTTON_MUTE, \ call_process class DualPaneTextboxBase(base._Widget): """ Base class for widgets that are two boxes next to each other both containing text. """ orientations = ORIENTATION_HORIZONTAL defaults = [ ("font", "sans", "Default font"), ("fontsize_left", None, "Font size of left text field. Calculated if None."), ("fontsize_right", None, "Font size of right text field. Calculated if None."), ("padding", None, "Padding. Calculated if None."), ("padding_between", None, "Padding between left and right. Calculated if None."), ("foreground", "ffffff", "Foreground colour"), ("fontshadow", None, "font shadow color, default is None(no shadow)"), ("markup", False, "Whether or not to use pango markup"), ] changed = False def __init__(self, text_left=" ", text_right=" ", width=bar.CALCULATED, **config): self.layout_left = None self.layout_right = None base._Widget.__init__(self, width, **config) self._text_left = None self._text_right = None self.text_left = text_left self.text_right = text_right self.changed = False self.add_defaults(DualPaneTextboxBase.defaults) @property def text_left(self): return self._text_left @text_left.setter def text_left(self, value): assert value is None or isinstance(value, str) if value != self._text_left: self.changed = True self._text_left = value if self.layout_left: self.layout_left.text = value @property def text_right(self): return self._text_right @text_right.setter def text_right(self, value): assert value is None or isinstance(value, str) if value != self._text_right: self.changed = True self._text_right = value if self.layout_right: self.layout_right.text = value @property def foreground(self): return self._foreground @foreground.setter def foreground(self, fg): self._foreground = fg if self.layout_left: self.layout_left.colour = fg if self.layout_right: self.layout_right.colour = fg @property def font(self): return self._font @font.setter def font(self, value): self._font = value if self.layout_left: self.layout_left.font = value if self.layout_right: self.layout_right.font = value @property def font_left(self): return self._font_left @font_left.setter def font_left(self, value): self._font_left = value if self.layout_left: self.layout_left.font = value @property def font_right(self): return self._font_right @font_right.setter def font_right(self, value): self._font_right = value if self.layout_right: self.layout_right.font = value @property def fontshadow(self): return self._fontshadow @fontshadow.setter def fontshadow(self, value): self._fontshadow = value if self.layout_left: self.layout_left.font_shadow = value if self.layout_right: self.layout_right.font_shadow = value @property def actual_padding(self): if self.padding is None: return min(self.fontsize_left, self.fontsize_right) / 2 else: return self.padding @property def actual_padding_between(self): if self.padding_between is None: return max(self.fontsize_left, self.fontsize_right) / 4 else: return self.padding_between def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) if self.fontsize_left is None: self.fontsize_left = self.bar.height - self.bar.height / 5 if self.fontsize_right is None: self.fontsize_right = self.bar.height - self.bar.height / 5 self.layout_left = self.drawer.textlayout( self.text_left, self.foreground, self.font, self.fontsize_left, self.fontshadow, markup=self.markup, ) self.layout_right = self.drawer.textlayout( self.text_right, self.foreground, self.font, self.fontsize_right, self.fontshadow, markup=self.markup, ) def calculate_length(self): length = 0 if self.text_left: length += min( self.layout_left.width, self.bar.width ) + self.actual_padding * 2 if self.text_right: length += min( self.layout_right.width, self.bar.width ) + self.actual_padding * 2 return length def draw(self): # if the bar hasn't placed us yet if self.offsetx is None: return self.drawer.clear(self.background or self.bar.background) self.layout_left.draw( self.actual_padding or 0, int(self.bar.height / 2.0 - self.layout_left.height / 2.0) + 1 ) left_width = self.layout_left.width self.layout_right.draw( (self.actual_padding or 0) + left_width + (self.actual_padding_between or 0), int(self.bar.height / 2.0 - self.layout_right.height / 2.0) + 1 ) self.drawer.draw(offsetx=self.offsetx, width=self.width) if self.changed: # Text changed, update the bar to propagate any changes in width self.bar.draw() self.changed = False @expose_command() def set_font(self, font=base.UNSPECIFIED, fontsize_left=base.UNSPECIFIED, fontsize_right=base.UNSPECIFIED, fontshadow=base.UNSPECIFIED): """ Change the font used by this widget. If font is None, the current font is used. """ if font is not base.UNSPECIFIED: self.font = font if fontsize_left is not base.UNSPECIFIED: self.fontsize_left = fontsize_left if fontsize_right is not base.UNSPECIFIED: self.fontsize_right = fontsize_right if fontshadow is not base.UNSPECIFIED: self.fontshadow = fontshadow self.bar.draw() def info(self): d = base._Widget.info(self) d['foreground'] = self.foreground d['text_left'] = self.text_left d['text_right'] = self.text_right return d class GPUStatusWidget(base._TextBox): """Displays the currently used GPU.""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('check_command', 'optimus-manager --print-mode', 'The command that shows the current mode.'), ('next_command', 'optimus-manager --print-next-mode', 'The command that shows the mode after reboot.'), ('update_interval', 60, 'The update interval in seconds.'), ('theme_path', default_icon_path(), 'Path of the icons'), ('custom_icons', {}, 'dict containing key->filename icon map'), ] def __init__(self, **config): super(GPUStatusWidget, self).__init__("GPU", bar.CALCULATED, **config) self.add_defaults(GPUStatusWidget.defaults) if self.theme_path: self.length_type = bar.STATIC self.length = 0 self.surfaces = {} self.current_icon = 'gpu-unknown' self.icons = dict([(x, '{0}.png'.format(x)) for x in ( 'gpu-intel', 'gpu-nvidia', 'gpu-unknown', )]) self.current_status = "Unknown" self.icons.update(self.custom_icons) def _get_info(self): try: output = self.call_process(self.check_command, shell=True) except subprocess.CalledProcessError as e: logger.error(f"Error while calling {self.check_command} - {e}") output = None mode = "nvidia" if "nvidia" in output else "intel" if "intel" in output else "unknown" return {'error': False, 'mode': mode} def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _configure(self, qtile, bar): super(GPUStatusWidget, self)._configure(qtile, bar) self.setup_images() def _get_icon_key(self): key = 'gpu' info = self._get_info() if info.get('mode') == "intel": key += '-intel' self.current_status = "Intel" elif info.get('mode') == "nvidia": key += '-nvidia' self.current_status = "NVidia" else: key += '-unknown' self.current_status = "Unknown" return key def update(self): icon = self._get_icon_key() if icon != self.current_icon: self.current_icon = icon self.draw() def draw(self): if self.theme_path: self.drawer.clear(self.background or self.bar.background) self.drawer.ctx.set_source(self.surfaces[self.current_icon]) self.drawer.ctx.paint() self.drawer.draw(offsetx=self.offset, width=self.length) else: self.text = self.current_icon[8:] base._TextBox.draw(self) def setup_images(self): for key, name in self.icons.items(): try: path = os.path.join(self.theme_path, name) img = cairocffi.ImageSurface.create_from_png(path) except cairocffi.Error: self.theme_path = None logger.warning('GPU Status Icon switching to text mode') return input_width = img.get_width() input_height = img.get_height() sp = input_height / (self.bar.height - 1) width = input_width / sp if width > self.length: # cast to `int` only after handling all potentially-float values self.length = int(width + self.actual_padding * 2) imgpat = cairocffi.SurfacePattern(img) scaler = cairocffi.Matrix() scaler.scale(sp, sp) scaler.translate(self.actual_padding * -1, 0) imgpat.set_matrix(scaler) imgpat.set_filter(cairocffi.FILTER_BEST) self.surfaces[key] = imgpat def button_press(self, x, y, button): if button == BUTTON_LEFT: try: next_gpu = self.call_process(self.next_command, shell=True).split(":")[1].strip() except subprocess.CalledProcessError as e: logger.error(f"Error while calling {self.next_command} - {e}") except IndexError: next_gpu = "Unknown" if self.current_status == "Unknown": notify(None, "GPU Status", "The currently used GPU is unknown.\n\nAfter the next login it will be the {} GPU.".format(next_gpu), image=os.path.join(self.theme_path, "gpu-unknown.png")) else: notify(None, "GPU Status", "The system is currently running on the {} GPU. Press the middle mouse " "button on this icon to switch GPUs.\n\nAfter the next login it will be the {} GPU.".format( self.current_status, next_gpu ), image=os.path.join(self.theme_path, "gpu-{}.png".format(self.current_status.lower())) ) if button == BUTTON_MIDDLE: command = ["optimus-manager", "--no-confirm", "--switch", "auto"] try: output = self.call_process(command) except subprocess.CalledProcessError as e: logger.error(f"Error while calling {command} - {e}") output = "" if "nvidia" in output: notify(None, "GPU Switched", "The GPU has been switched from Intel to NVidia.\n" "Please log out and log back in to apply the changes to the session.", image=os.path.join(self.theme_path, "gpu-nvidia.png")) elif "intel" in output: notify(None, "GPU Switched", "The GPU has been switched from NVidia to Intel.\n" "Please log out and log back in to apply the changes to the session.", image=os.path.join(self.theme_path, "gpu-intel.png")) else: notify(None, "GPU Switch Error", "I could not determine if the GPU was switched successfully.\n" "Please log out and log back in to clear up the inconsistency.", image=os.path.join(self.theme_path, "gpu-unknown.png")) class ThermalSensorWidget(DualPaneTextboxBase): defaults = [ ('show_tag', False, 'Show tag sensor'), ('update_interval', 2, 'Update interval in seconds'), ('tag_sensor', None, 'Tag of the temperature sensor. For example: "temp1" or "Core 0"'), ('chip', None, 'Chip argument for sensors command'), ('threshold', 70, 'If the current temperature value is above, then change to foreground_alert colour'), ('foreground_alert', 'ff0000', 'Foreground colour alert'), ] def __init__(self, **config): super(ThermalSensorWidget, self).__init__("TEMP", "", bar.CALCULATED, **config) self.add_defaults(ThermalSensorWidget.defaults) self.sensors_temp = re.compile( (r"\n([\w ]+):" # Sensor tag name r"\s+[+|-]" # temp signed r"(\d+\.\d+)" # temp value "({degrees}" # degree symbol match "[C|F])" # Celsius or Fahrenheit ).format(degrees=u"\xb0"), re.UNICODE | re.VERBOSE ) self.value_temp = re.compile(r"\d+\.\d+") self.foreground_normal = self.foreground self.values = None def get_command(self): command = ["sensors"] if self.chip: command.append(self.chip) return command def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): try: sensors_out = self.call_process(self.get_command()) except subprocess.CalledProcessError as e: logger.error(f"Error while calling {self.get_command()} - {e}") return temperature_values = {} for name, temp, symbol in self.sensors_temp.findall(sensors_out): name = name.strip() temperature_values[name] = temp, symbol self.values = temperature_values def update(self): self._update_values() self.draw() def draw(self): self.text_left = "" if self.values is None: self.text_right = None else: text = "" if self.show_tag and self.tag_sensor is not None: text = self.tag_sensor + ": " text += "".join(self.values.get(self.tag_sensor, ['N/A'])) temp_value = float(self.values.get(self.tag_sensor, [0])[0]) if temp_value > self.threshold: self.layout_right.colour = self.foreground_alert else: self.layout_right.colour = self.foreground_normal self.text_right = text super(ThermalSensorWidget, self).draw() def button_press(self, x, y, button): if button == BUTTON_LEFT: notify(None, "Temperature Information", "\n".join( "{}: {}{}".format(name, *values) for name, values in self.values.items() )) class CPUInfoWidget(DualPaneTextboxBase): """Displays information about the CPU usage""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('update_interval', 3, 'The update interval in seconds.'), ('text_pattern', "{cpu}%", 'The pattern for the text that is displayed.'), ('normal_color', "#ffffff", "Color when value is normal"), ('warning_color', "#ffffff", "Color when value is warning"), ('critical_color', "#ffffff", "Color when value is critical"), ] def __init__(self, **config): super(CPUInfoWidget, self).__init__("CPU", "", bar.CALCULATED, **config) self.add_defaults(CPUInfoWidget.defaults) self.text = "..." self.cpu = 0 self.cpu_old = [0, 0, 0, 0] def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): cpu = psutil.cpu_times() user = cpu.user * 100 nice = cpu.nice * 100 sys = cpu.system * 100 idle = cpu.idle * 100 nval = (int(user), int(nice), int(sys), int(idle)) oval = self.cpu_old busy = nval[0] + nval[1] + nval[2] - oval[0] - oval[1] - oval[2] total = busy + nval[3] - oval[3] if total: self.cpu = math.ceil(busy * 100.0 / total) self.cpu_old = nval def update(self): self._update_values() self.draw() def draw(self): self.text_left = "" self.text_right = self.text_pattern.format(cpu=self.cpu) super(CPUInfoWidget, self).draw() def button_press(self, x, y, button): if button == BUTTON_LEFT: total = sum([self.cpu_old[0], self.cpu_old[1], self.cpu_old[2], self.cpu_old[3]]) notify(None, "CPU Information", "user: {} %\nnice: {} %\nsys: {} %\nidle: {} %\ntotal: {} %".format( math.ceil((self.cpu_old[0] / total) * 100), math.ceil((self.cpu_old[1] / total) * 100), math.ceil((self.cpu_old[2] / total) * 100), math.ceil((self.cpu_old[3] / total) * 100), self.cpu )) class MemoryInfoWidget(DualPaneTextboxBase): """Displays information about the Memory usage""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('update_interval', 3, 'The update interval in seconds.'), ('text_pattern', "{mem}%", 'The pattern for the text that is displayed.'), ('normal_color', "#ffffff", "Color when value is normal"), ('warning_color', "#ffffff", "Color when value is warning"), ('critical_color', "#ffffff", "Color when value is critical"), ] def __init__(self, **config): super(MemoryInfoWidget, self).__init__("MEM", "", bar.CALCULATED, **config) self.add_defaults(MemoryInfoWidget.defaults) self.text = "..." self.memory = 0 def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): mem = psutil.virtual_memory() self.memory = math.ceil((mem.used / mem.total) * 100) def update(self): self._update_values() self.draw() def draw(self): self.text_left = "" self.text_right = self.text_pattern.format(mem=self.memory) super(MemoryInfoWidget, self).draw() def button_press(self, x, y, button): mem = psutil.virtual_memory() swap = psutil.swap_memory() val = {} val['MemUsed'] = mem.used // 1024 // 1024 val['MemTotal'] = mem.total // 1024 // 1024 val['SwapTotal'] = swap.total // 1024 // 1024 val['SwapUsed'] = swap.used // 1024 // 1024 if button == BUTTON_LEFT: notify(None, "Memory Information", "Memory: {}MB / {}MB\n {}%\nSwap: {}MB / {}MB\n {}%".format( val['MemUsed'], val['MemTotal'], math.ceil((mem.used / mem.total) * 100), val['SwapUsed'], val['SwapTotal'], math.ceil((swap.used / swap.total) * 100), )) class DiskIOInfoWidget(DualPaneTextboxBase): """Displays information about the DiskIO usage""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('update_interval', 3, 'The update interval in seconds.'), ('text_pattern', "{io}ms", 'The pattern for the text that is displayed.'), ('hdd_device', 'nvme0n1', 'The device name of the disk to use for IO stats'), ('normal_color', "#ffffff", "Color when value is normal"), ('warning_color', "#ffffff", "Color when value is warning"), ('critical_color', "#ffffff", "Color when value is critical"), ] def __init__(self, **config): super(DiskIOInfoWidget, self).__init__("DiskIO", "", bar.CALCULATED, **config) self.add_defaults(DiskIOInfoWidget.defaults) self.text = "..." self.io = 0 self.io_old = 0 self.hdd_path = '/sys/block/{dev}/stat'.format( dev=self.hdd_device ) def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): try: # io_ticks is field number 9 with open(self.hdd_path) as f: io_ticks = int(f.read().split()[9]) except IOError: return 0 activity = io_ticks - self.io_old self.io_old = io_ticks self.io = math.ceil(activity) def update(self): self._update_values() self.draw() def draw(self): self.text_left = "" self.text_right = self.text_pattern.format(io=self.io) super(DiskIOInfoWidget, self).draw() def button_press(self, x, y, button): if button == BUTTON_LEFT: notify(None, "Disk IO Information", "Time that there were IO requests queued for /dev/{}: {} ms".format(self.hdd_device, self.io)) class NetworkInfoWidget(DualPaneTextboxBase): """Displays information about the (wireless) network that we are connected to""" orientations = base.ORIENTATION_HORIZONTAL wired_up_regex = re.compile(" state (DOWN|UP) ") defaults = [ ('update_interval', 10, 'The update interval in seconds.'), ('text_pattern', "{name}", 'The pattern for the text that is displayed.'), ('text_disconnected', "", "The text to show when not connected using WiFi"), ('normal_color', "#ffffff", "Color when value is normal"), ('warning_color', "#ffffff", "Color when value is warning"), ('critical_color', "#ffffff", "Color when value is critical"), ('wireless_interface', "wifi0", "Wireless interface device name"), ('wired_interface', "enp7s0", "Wired interface device name"), ] def __init__(self, **config): super(NetworkInfoWidget, self).__init__("NET", "", bar.CALCULATED, **config) self.add_defaults(NetworkInfoWidget.defaults) self.wireless_text = "..." self.wireless_quality = 0 self.wireless_signal = 0 self.wireless_name = 0 self.wireless_connected = False self.wireless_accesspoint = "" self.wireless_frequency = "" self.wireless_ipv4 = "" self.wireless_ipv6 = "" self.wireless_mac = "" self.wired_name = 0 self.wired_connected = False self.wired_ipv4 = "" self.wired_ipv6 = "" self.wired_mac = "" def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): # Wifi try: essid, quality = get_status(self.wireless_interface) status = iwlib.get_iwconfig(self.wireless_interface) self.wireless_ips = netifaces.ifaddresses(self.wireless_interface) disconnected = essid is None percent = math.ceil(((quality or 0) / 70) * 100) self.wireless_quality = quality self.wireless_signal = percent self.wireless_name = essid self.wireless_connected = not disconnected self.wireless_accesspoint = status.get('Access Point', b'Unknown').decode() self.wireless_frequency = status.get('Frequency', b'Unknown').decode() self.wireless_ipv4 = self.wireless_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr'] self.wireless_ipv6 = self.wireless_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr'] self.wireless_mac = self.wireless_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr'] except EnvironmentError: pass # Wired try: self.wired_ips = netifaces.ifaddresses(self.wired_interface) self.wired_ipv4 = self.wired_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr'] self.wired_ipv6 = self.wired_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr'] self.wired_mac = self.wired_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr'] command = ["ip", "link", "show", "{}".format(self.wired_interface)] try: eth_status = call_process(command) except subprocess.CalledProcessError as e: logger.error(f"Error while calling {command} - {e}") return m = self.wired_up_regex.search(eth_status) if m: self.wired_connected = "UP" in m.group(1) else: self.wired_connected = False except (EnvironmentError, ValueError): pass def update(self): self._update_values() self.draw() def draw(self): if self.wireless_connected: strength = "" if self.wireless_signal < 66: strength = "" if self.wireless_signal < 33: strength = "" self.text_left = strength else: self.text_left = "" if self.wired_connected: self.text_right = "" else: self.text_right = "" super(NetworkInfoWidget, self).draw() def button_press(self, x, y, button): if button == BUTTON_LEFT: if self.wireless_connected: title = "Wireless {}".format(self.wireless_interface) wireless_ipv4 = "\nIPv4: {}".format(self.wireless_ipv4) if self.wireless_ipv4 else "" wireless_ipv6 = "\nIPv6: {}".format(self.wireless_ipv6) if self.wireless_ipv6 else "" wireless_mac = "\nMAC: {}".format(self.wireless_mac) if self.wireless_mac else "" wifi_text = "SSID: {}\n" \ "Quality: {} / 70\n"\ "Strength: {}%" \ "{}{}{}".format(self.wireless_name, self.wireless_quality, self.wireless_signal, wireless_ipv4, wireless_ipv6, wireless_mac) else: title = "Wireless: Not connected" wifi_text = "" if self.wired_connected: wired_ipv4 = "\nIPv4: {}".format(self.wired_ipv4) if self.wired_ipv4 else "" wired_ipv6 = "\nIPv6: {}".format(self.wired_ipv6) if self.wired_ipv6 else "" wired_mac = "\nMAC: {}".format(self.wired_mac) if self.wired_mac else "" wired_text = "Wired {}" \ "{}{}{}".format(self.wired_interface, wired_ipv4, wired_ipv6, wired_mac) else: wired_text = "Wired: Not connected" if wifi_text: notify(None, title, "{}\n\n{}".format(wifi_text, wired_text)) else: notify(None, title, "\n{}".format(wired_text)) class BatteryInfoWidget(DualPaneTextboxBase): """Displays information about the battery""" orientations = base.ORIENTATION_HORIZONTAL status_cmd = "acpi" defaults = [ ('update_interval', 10, 'The update interval in seconds.'), ('text_pattern', "{percentage}%", 'The pattern for the text that is displayed.'), ('charging_color', "#ffffff", "Color when battery is charging"), ('normal_color', "#ffffff", "Color when value is normal"), ('warning_color', "#ffffff", "Color when value is warning"), ('critical_color', "#ffffff", "Color when value is critical"), ('battery', 0, 'Which battery should be monitored'), ] def __init__(self, **config): super(BatteryInfoWidget, self).__init__("BAT", "", bar.CALCULATED, **config) self.add_defaults(BatteryInfoWidget.defaults) self.text = "..." self.state = 0 self.percentage = 0 self.power = 0 self.time = 0 self._battery = self._load_battery(**config) @staticmethod def _load_battery(**config): return load_battery(**config) def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): status = self._battery.update_status() self.state = status.state self.percentage = math.ceil(status.percent * 100) self.power = status.power self.time = status.time def update(self): self._update_values() self.draw() def draw(self): if self.state == BatteryState.EMPTY: self.text_left = "" self.text_right = self.text_pattern.format(percentage=0) elif self.state == BatteryState.CHARGING: self.text_left = "" self.text_right = None elif self.state == BatteryState.DISCHARGING: capacity = "" if self.percentage < 80: capacity = "" if self.percentage < 60: capacity = "" if self.percentage < 40: capacity = "" if self.percentage < 20: capacity = "" self.text_left = capacity self.text_right = self.text_pattern.format(percentage=self.percentage) elif self.state == BatteryState.FULL: self.text_left = "" self.text_right = self.text_pattern.format(percentage=100) else: self.text_left = "" self.text_right = self.text_pattern.format(percentage=self.percentage) super(BatteryInfoWidget, self).draw() def button_press(self, x, y, button): if button == BUTTON_LEFT: output = subprocess.check_output(self.status_cmd).decode('utf-8') notify(None, "Battery Status", output) class VolumeInfoWidget(DualPaneTextboxBase): """Displays information about the volume""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('update_interval', 5, 'The update interval in seconds.'), ('text_pattern', "{percentage}%", 'The pattern for the text that is displayed.'), ('charging_color', "#ffffff", "Color when battery is charging"), ('normal_color', "#ffffff", "Color when value is normal"), ('warning_color', "#ffffff", "Color when value is warning"), ('critical_color', "#ffffff", "Color when value is critical"), ("pulse_sink", None, "PulseAudio sink name to control"), ("status_cmd", "pamixer{sink} --get-volume", "Command to get current volume"), ("mute_cmd", "pamixer{sink} -t", "Command to mute volume"), ("up_cmd", "pamixer{sink} -i 2", "Command to turn volume up"), ("down_cmd", "pamixer{sink} -d 2", "Command to turn volume down"), ("volume_app", "pavucontrol", "Volume mixer app to open on middle click"), ] def __init__(self, **config): super(VolumeInfoWidget, self).__init__("VOL", "", bar.CALCULATED, **config) self.add_defaults(VolumeInfoWidget.defaults) self.text = "..." self.percentage = 0 self.volume = 0 def get_volume(self): try: if "{sink}" in self.status_cmd: cmd = self.status_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "") else: cmd = self.status_cmd mixer_out = self.call_process(cmd.split(" ")) except subprocess.CalledProcessError as e: logger.error(f"Error while calling {cmd} - {e}") return -1 try: return int(mixer_out) except ValueError: return -1 def timer_setup(self): self.update() self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): self.volume = self.get_volume() def update(self): self._update_values() self.draw() def draw(self): mute = "" volume = "" if self.volume < 75: volume = "" if self.volume < 50: volume = "" if self.volume < 25: volume = "" if self.volume < 0: self.text_left = mute self.text_right = "" else: self.text_left = volume self.text_right = self.text_pattern.format(percentage=self.volume) super(VolumeInfoWidget, self).draw() def button_press(self, x, y, button): if button == BUTTON_LEFT: if "{sink}" in self.status_cmd: cmd = self.status_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "") else: cmd = self.status_cmd output = subprocess.check_output(cmd.split(" ")).decode('utf-8') sink = "Sink {}\n".format(self.pulse_sink) if self.pulse_sink else "" notify(None, "Volume Status", sink+output) elif button == BUTTON_RIGHT: if "{sink}" in self.volume_app: cmd = self.volume_app.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "") else: cmd = self.volume_app subprocess.Popen(cmd.split(" ")) elif button == BUTTON_DOWN: if "{sink}" in self.down_cmd: cmd = self.down_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "") else: cmd = self.down_cmd subprocess.call(cmd.split(" ")) elif button == BUTTON_UP: if "{sink}" in self.up_cmd: cmd = self.up_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "") else: cmd = self.up_cmd subprocess.call(cmd.split(" ")) elif button == BUTTON_MUTE: if "{sink}" in self.mute_cmd: cmd = self.mute_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "") else: cmd = self.mute_cmd subprocess.call(cmd.split(" ")) self.update() @expose_command() def increase_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_UP) @expose_command() def decrease_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_DOWN) @expose_command() def mute(self): # Emulate button press. self.button_press(0, 0, BUTTON_MUTE) @expose_command() def run_app(self): # Emulate button press. self.button_press(0, 0, BUTTON_RIGHT) class KuroGroupBox(GroupBox): @property def length(self): try: return super(KuroGroupBox, self).length except AttributeError: return 1 @length.setter def length(self, length): logger.warning(f"Setting groupbox length to {length}")