import math import os import re import subprocess import cairocffi import iwlib import netifaces import psutil import six import unicodedata from libqtile import bar, pangocffi 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.check_updates import CheckUpdates from libqtile.widget.currentlayout import CurrentLayoutIcon from libqtile.widget.graph import _Graph from libqtile.widget.tasklist import TaskList from libqtile.widget.wlan import get_status from libqtile.backend.x11.window import Window from kuro.utils.general import notify, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_DOWN, BUTTON_UP, BUTTON_MUTE, \ call_process class CheckUpdatesYay(CheckUpdates): def __init__(self, **config): super(CheckUpdatesYay, self).__init__(**config) # Override command and output with yay command self.cmd = "yay -Qu".split() self.status_cmd = "yay -Qu --color never".split() self.update_cmd = "sudo yay".split() self.subtr = 0 def _check_updates(self): #subprocess.check_output(self.update_cmd) res = super(CheckUpdatesYay, self)._check_updates() return res def button_press(self, x, y, button): if button == BUTTON_LEFT: output = subprocess.check_output(self.status_cmd).decode('utf-8').split('\n') num_updates = len(output)-1 msg = "{} updates available.".format(num_updates) if num_updates > 0: msg += "\n\n" for x in range(min(num_updates, 9)): msg += output[x] + "\n" if num_updates > 9: msg += "and {} more...".format(num_updates-9) notify( "System updates", msg ) elif button == BUTTON_MIDDLE and self.execute is not None: subprocess.Popen(self.execute, shell=True) 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 def cmd_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 MediaWidget(base.InLoopPollText): """Media Status Widget""" class Status: OFFLINE = 0 PLAYING = 1 PAUSED = 2 STOPPED = 3 orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('off_text', '', 'The pattern for the text if no players are found.'), ('on_text_play', ' {}', 'The pattern for the text if music is playing.'), ('on_text_pause', ' {}', 'The pattern for the text if music is paused.'), ('on_text_stop', ' {}', 'The pattern for the text if music is stopped.'), ('update_interval', 1, 'The update interval.'), ('max_chars_per_player', 50, 'Maximum characters of text per player.'), ('ignore_players', '', 'Comma-separated list of players to ignore.') ] player_icons = { 'spotify': '', 'vlc': '', 'firefox': '', 'mpv': '', } custom_player_data = { 'firefox': { 'showing': False, 'title': '', 'state': Status.STOPPED, } } image_urls = {} current_image_url = None player_to_control = None def __init__(self, **config): super(MediaWidget, self).__init__(**config) self.add_defaults(MediaWidget.defaults) self.surfaces = {} self.player_to_control = None def _player_to_control(self): info = self._get_info() players = {} for player in info.keys(): if player not in self.custom_player_data.keys(): if info[player][0] in [MediaWidget.Status.PLAYING, MediaWidget.Status.PAUSED]: players[player] = info[player] if self.player_to_control is not None and self.player_to_control not in players.keys(): self.player_to_control = None if self.player_to_control is not None: players = {self.player_to_control: players[self.player_to_control]} if len(players.keys()) == 1: player = list(players.keys())[0] self.player_to_control = player return player elif len(players) == 0: notify("MediaWidget", "Nothing to control!") else: notify("MediaWidget", "Multiple players to control, I don't know what you want to do!") return None def button_press(self, x, y, button): if button == BUTTON_LEFT: player = self._player_to_control() if player is not None: command = ["playerctl", "-i", self.ignore_players, "-p", player, "play-pause"] _ = self.call_process(command) notify("MediaWidget", "Toggled {}".format(player)) if button == BUTTON_RIGHT: player = self._player_to_control() if player is not None: command = ["playerctl", "-i", self.ignore_players, "-p", player, "next"] _ = self.call_process(command) if button == BUTTON_MIDDLE: # Jump to the screen that the player is on # clients = list(self.bar.qtile.windows_map.values()) # logger.warning("{}") pass def cmd_update_custom_player(self, player_name, data): # Update firefox player if player_name.startswith("firefox"): if data['playing'] and data['muted']: self.custom_player_data['firefox']['showing'] = True self.custom_player_data['firefox']['state'] = MediaWidget.Status.PAUSED self.custom_player_data['firefox']['title'] = data['title'][:50] elif data['playing'] and not data['muted']: self.custom_player_data['firefox']['showing'] = True self.custom_player_data['firefox']['state'] = MediaWidget.Status.PLAYING self.custom_player_data['firefox']['title'] = data['title'][:50] elif not data['playing'] and data['muted']: self.custom_player_data['firefox']['showing'] = True self.custom_player_data['firefox']['state'] = MediaWidget.Status.STOPPED self.custom_player_data['firefox']['title'] = data['title'][:50] elif not data['playing'] and not data['muted']: self.custom_player_data['firefox']['showing'] = False self.custom_player_data['firefox']['state'] = MediaWidget.Status.OFFLINE self.custom_player_data['firefox']['title'] = data['title'][:50] def _get_players(self): players = [] # Playerctl players try: result = self.call_process(["playerctl", "-i", self.ignore_players, "-l"]) except subprocess.CalledProcessError: result = None if result: players.extend([x for x in result.split("\n") if x]) # Custom players - Firefox if self.custom_player_data['firefox']['showing']: players.append('firefox') if players: return players else: return None def _get_info(self): players = self._get_players() if not players: return {} else: result = {} for player in players: if player in self.custom_player_data.keys(): # Custom player -- Firefox if player == "firefox": result[player] = [self.custom_player_data['firefox']['state'], self.custom_player_data['firefox']['title']] # Other custom players -- generic attempt with error catching else: try: result[player] = [self.custom_player_data[player]['state'], self.custom_player_data[player]['title']] except KeyError: pass else: # PlayerCtl player command = ["playerctl", "-i", self.ignore_players, "-p", player, "status"] cmd_result = self.call_process(command).strip() text = "Unknown" if cmd_result in ["Playing", "Paused"]: try: artist = self.call_process(['playerctl', "-i", self.ignore_players, '-p', player, 'metadata', 'artist']).strip() except subprocess.CalledProcessError: artist = None try: title = self.call_process(['playerctl', "-i", self.ignore_players, '-p', player, 'metadata', 'title']).strip() except subprocess.CalledProcessError: title = None if artist and title: text = "{} - {}".format(artist, title) elif artist: text = artist elif title: text = title if cmd_result == "Playing": result[player] = [MediaWidget.Status.PLAYING, text] elif cmd_result == "Paused": result[player] = [MediaWidget.Status.PAUSED, text] elif cmd_result == "Stopped": result[player] = [MediaWidget.Status.STOPPED, ""] return result def _get_formatted_text(self, status): if status[0] == MediaWidget.Status.PLAYING: res = self.on_text_play.format(status[1]) elif status[0] == MediaWidget.Status.PAUSED: res = self.on_text_pause.format(status[1]) elif status[0] == MediaWidget.Status.STOPPED: res = self.on_text_stop.format(status[1]) else: res = "Unknown" res = pangocffi.markup_escape_text(res) res = unicodedata.normalize('NFKD', res) if len(res) > self.max_chars_per_player: res = res[:self.max_chars_per_player] + "..." return res def draw(self): super(MediaWidget, self).draw() def poll(self): text = [] status = self._get_info() if not status: return self.off_text else: for player in status.keys(): # Shorten firefox.instance[0-9]+ to just firefox for icon finding if player.startswith("firefox"): player_icon = "firefox" else: player_icon = player icon = self.player_icons.get(player_icon, player_icon) text.append("{} {}".format(icon, self._get_formatted_text(status[player]))) return " | ".join(text) if text else self.off_text class AudioVisualizerWidget(_Graph): """Display Audio Visualization graph""" orientations = base.ORIENTATION_HORIZONTAL fixed_upper_bound = True defaults = [ ("graph_color", "FFFFFF.0", "Graph color"), ("fill_color", "FFFFFF.0", "Fill color for linefill graph"), ("border_color", "FFFFFF.0", "Widget border color"), ("border_width", 0, "Widget border width"), ("line_width", 0, "Line width"), ] def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(AudioVisualizerWidget.defaults) self.client = None self.screen = None self.old_position = None def set_client(self, c, s): self.client = c self.screen = s def update_graph(self): if self.client is not None: viz_info = self.info() pos_x = viz_info['offset'] + self.margin_x + self.screen.x pos_y = 0 + self.margin_y + self.screen.y if self.old_position != (pos_x, pos_y): self.old_position = (pos_x, pos_y) # Check if a window on this screen is full-screen fullscreen = False for window in self.screen.group.windows: if isinstance(window, Window): if window.fullscreen: fullscreen = True break logger.debug("Repositioning {} {} to {}x{}".format(self.client, self.client.window.wid, pos_x, pos_y)) self.client.reposition(pos_x, pos_y, above=not fullscreen) self.draw() def draw(self): self.drawer.clear(self.background or self.bar.background) self.drawer.draw(offsetx=self.offset, width=self.width) class KuroCurrentLayoutIcon(CurrentLayoutIcon): def _get_layout_names(self): names = super(KuroCurrentLayoutIcon, self)._get_layout_names() from kuro.utils import layouts as kuro_layouts from libqtile.layout.base import Layout klayouts = [ layout_class_name.lower() for layout_class, layout_class_name in map(lambda x: (getattr(kuro_layouts, x), x), dir(kuro_layouts)) if isinstance(layout_class, six.class_types) and issubclass(layout_class, Layout) ] names.extend(klayouts) return list(set(names)) class KuroTaskList(TaskList): defaults = [ ( 'txt_pinned', 'P ', 'Text representation of the pinned window state. ' 'e.g., "P " or "\U0001F5D7 "' ), ( 'markup_pinned', None, 'Text markup of the pinned window state. Supports pangomarkup with markup=True.' 'e.g., "{}" or "{}"' ), ] def __init__(self, *args, **kwargs): super(KuroTaskList, self).__init__(*args, **kwargs) self.add_defaults(KuroTaskList.defaults) def get_taskname(self, window): """ Get display name for given window. Depending on its state minimized, maximized and floating appropriate characters are prepended. """ state = '' markup_str = self.markup_normal # Enforce markup and new string format behaviour when # at least one markup_* option is used. # Mixing non markup and markup may cause problems. if self.markup_minimized or self.markup_maximized\ or self.markup_floating or self.markup_focused or self.markup_pinned: enforce_markup = True else: enforce_markup = False if window is None: pass elif hasattr(window, "is_static_window") and window.is_static_window: state = self.txt_pinned markup_str = self.markup_pinned elif window.minimized: state = self.txt_minimized markup_str = self.markup_minimized elif window.maximized: state = self.txt_maximized markup_str = self.markup_maximized elif window.floating: state = self.txt_floating markup_str = self.markup_floating elif window is window.group.current_window: markup_str = self.markup_focused window_name = window.name if window and window.name else "?" # Emulate default widget behavior if markup_str is None if enforce_markup and markup_str is None: markup_str = "%s{}" % (state) if markup_str is not None: self.markup = True window_name = pangocffi.markup_escape_text(window_name) return markup_str.format(window_name) return "%s%s" % (state, window_name) 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): output = self.call_process(self.check_command, shell=True) 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 IndexError: next_gpu = "Unknown" if self.current_status == "Unknown": notify("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("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"] output = self.call_process(command) if "nvidia" in output: notify("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("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("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 TextSpacerWidget(base._TextBox): """Displays a text separator""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('spacer', None, 'The character/text to use as separator. Default "|" if None.'), ('color', "#ffffff", "Color of the text."), ] def __init__(self, **config): super(TextSpacerWidget, self).__init__("Separator", bar.CALCULATED, **config) self.add_defaults(TextSpacerWidget.defaults) self.text = self.spacer or "|" def draw(self): base._TextBox.draw(self) 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): sensors_out = self.call_process(self.get_command()) 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("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("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("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("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 / 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'] eth_status = call_process(["ip", "link", "show", "{}".format(self.wired_interface)]) 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(title, "{}\n\n{}".format(wifi_text, wired_text)) else: notify(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("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: 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("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() def cmd_increase_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_UP) def cmd_decrease_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_DOWN) def cmd_mute(self): # Emulate button press. self.button_press(0, 0, BUTTON_MUTE) def cmd_run_app(self): # Emulate button press. self.button_press(0, 0, BUTTON_RIGHT)