972 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			972 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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, execute
 | 
						|
 | 
						|
 | 
						|
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=None, fontsize_left=None, fontsize_right=None, fontshadow=None):
 | 
						|
        """
 | 
						|
            Change the font used by this widget. If font is None, the current
 | 
						|
            font is used.
 | 
						|
        """
 | 
						|
        if font is not None:
 | 
						|
            self.font = font
 | 
						|
        if fontsize_left is not None:
 | 
						|
            self.fontsize_left = fontsize_left
 | 
						|
        if fontsize_right is not None:
 | 
						|
            self.fontsize_right = fontsize_right
 | 
						|
        if fontshadow is not None:
 | 
						|
            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 <b>unknown</b>.\n\nAfter the next login it will be the <b>{}</b> 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 <b>{}</b> GPU. Press the middle mouse "
 | 
						|
                                     "button on this icon to switch GPUs.\n\nAfter the next login it will be the <b>{}</b> 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"),
 | 
						|
        ('config_application', None, "Application to launch when right/middle clicking"),
 | 
						|
    ]
 | 
						|
 | 
						|
    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
 | 
						|
        if self.wireless_interface:
 | 
						|
            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
 | 
						|
        if self.wired_interface:
 | 
						|
            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_interface:
 | 
						|
            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 = ""
 | 
						|
        else:
 | 
						|
            self.text_left = ""
 | 
						|
 | 
						|
        if self.wired_interface:
 | 
						|
            if self.wired_connected:
 | 
						|
                self.text_right = ""
 | 
						|
            else:
 | 
						|
                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 = "<b>Wired {}</b>" \
 | 
						|
                             "{}{}{}".format(self.wired_interface, wired_ipv4, wired_ipv6, wired_mac)
 | 
						|
            else:
 | 
						|
                wired_text = "<b>Wired: Not connected</b>"
 | 
						|
 | 
						|
            if wifi_text:
 | 
						|
                notify(None, title, "{}\n\n{}".format(wifi_text, wired_text))
 | 
						|
            else:
 | 
						|
                notify(None, title, "\n{}".format(wired_text))
 | 
						|
        if button == BUTTON_RIGHT or button == BUTTON_MIDDLE:
 | 
						|
            if self.config_application:
 | 
						|
                execute(self.config_application)
 | 
						|
 | 
						|
 | 
						|
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}")
 |