From 705a2aaaa762493db7fbde2bdea77fad232f498e Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Wed, 14 Oct 2020 18:57:20 +0200 Subject: [PATCH] Laptop config changes --- config.py | 17 +- kuro/config.py | 19 +- kuro/theme.py | 170 +++---- kuro/utils/general.py | 12 +- kuro/utils/widgets.py | 1080 +++++++++++++++++++++++++++++------------ required_packages.txt | 1 + 6 files changed, 878 insertions(+), 421 deletions(-) diff --git a/config.py b/config.py index acf0e36..c3abfcd 100644 --- a/config.py +++ b/config.py @@ -53,12 +53,12 @@ except ImportError as e: try: - logger.error("Initializing theme...") + logger.info("Initializing theme...") # Initialize the Theme Theme.initialize() - logger.error("Initialize done") + logger.info("Initialize done") - logger.error("Hooking theme into callbacks...") + logger.info("Hooking theme into callbacks...") # Hook theme into all hooks we know of hook.subscribe.startup_once(Theme.callback_startup_once) hook.subscribe.startup(Theme.callback_startup) @@ -73,8 +73,6 @@ try: hook.subscribe.client_new(Theme.callback_client_new) hook.subscribe.client_managed(Theme.callback_client_managed) hook.subscribe.client_killed(Theme.callback_client_killed) - hook.subscribe.client_state_changed(Theme.callback_client_state_changed) - hook.subscribe.client_type_changed(Theme.callback_client_type_changed) hook.subscribe.client_focus(Theme.callback_client_focus) hook.subscribe.client_mouse_enter(Theme.callback_client_mouse_enter) hook.subscribe.client_name_updated(Theme.callback_client_name_updated) @@ -85,9 +83,9 @@ try: hook.subscribe.selection_change(Theme.callback_selection_change) hook.subscribe.screen_change(Theme.callback_screen_change) hook.subscribe.current_screen_change(Theme.callback_current_screen_change) - logger.error("Hooking done") + logger.info("Hooking done") - logger.error("Initializing theme variables") + logger.info("Initializing theme variables") # Initialize variables from theme keys = Theme.keys mouse = Theme.mouse @@ -104,8 +102,9 @@ try: floating_layout = Theme.floating_layout auto_fullscreen = Theme.auto_fullscreen focus_on_window_activation = Theme.focus_on_window_activation - extentions = Theme.extensions - logger.error("Variable initialization done") + extensions = Theme.extensions + wmname = Theme.wmname + logger.info("Variable initialization done") except Exception as e: Theme = None Config = None diff --git a/kuro/config.py b/kuro/config.py index 83d6f8b..05f2f87 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -19,7 +19,7 @@ class Config(BaseConfig): app_launcher = "/home/kevin/bin/dmenu_wal.sh" web_browser = "firefox-developer-edition" file_manager = "thunar" - app_chat = "franz" + app_chat = "/home/kevin/bin/ramboxpro" app_irc = "quasselclient" app_mail = "thunderbird" cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" @@ -27,6 +27,7 @@ class Config(BaseConfig): lock_command = "bash /home/kevin/bin/lock.sh" visualizer_app = "glava" cmd_screenshot = "xfce4-screenshooter -r -c -d 1" + cmd_alt_screenshot = "xfce4-screenshooter -w -c -d 0" # Commands wallpaper_config_command = "/home/kevin/bin/wal-nitrogen-noupdate" @@ -71,7 +72,7 @@ class Config(BaseConfig): bar_hover_opacity = 1 # Groupbox variables - font_groupbox = "FontAwesome" + font_groupbox = "Font Awesome 5 Pro" fontsize_groupbox = 15 width_groupbox_border = 1 height_groupbox = 24 @@ -119,10 +120,8 @@ class Config(BaseConfig): volume_font = "Noto Sans" volume_fontsize = 11 volume_theme_path = "/home/kevin/.config/qtile/kuro/resources/volume" - volume_get_command = "pamixer --get-volume".split() - volume_mute_command = "pamixer -t".split() - volume_up_command = "pamixer -i 2".split() - volume_down_command = "pamixer -d 2".split() + volume_pulse_sink = "alsa_output.pci-0000_00_1f.3.analog-stereo" + volume_is_bluetooth_icon = False volume_update_interval = 0.2 @@ -143,8 +142,8 @@ class Config(BaseConfig): updates_colour_available = '#f4d742' # Screen organization - laptop_screen_nvidia = "DP-2" - laptop_screen_intel = "DP-2" + laptop_screen_nvidia = "eDP-1-1" + laptop_screen_intel = "eDP1" # Keyboard colors do_keyboard_updates = False @@ -155,3 +154,7 @@ class Config(BaseConfig): # Show thermal widget show_temperature = True + + # Audio control applications + # apps_audio = ["pavucontrol"] + apps_audio_afterstart = "/home/kevin/bin/start_jack_audio_chain.sh" diff --git a/kuro/theme.py b/kuro/theme.py index c354d0b..ac322df 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -71,7 +71,7 @@ class Kuro(BaseTheme): do_keyboard_updates = True # Window manager name - wmname = "QTile" + wmname = "qtile" # Floating layout override floating_layout = kuro_layouts.KuroFloating(float_rules=[ @@ -180,6 +180,9 @@ class Kuro(BaseTheme): # Screenshot key Key([], "Print", lazy.spawn(Config.get('cmd_screenshot', 'xfce4-screenshooter'))), + # Alt Screenshot + Key([self.mod], "Print", lazy.spawn(Config.get('cmd_alt_screenshot', 'xfce4-screenshooter'))), + # Toggle between different layouts as defined below Key([self.mod], "Tab", lazy.next_layout()), @@ -239,6 +242,20 @@ class Kuro(BaseTheme): groups.append(Group("")) groups.append(Group("")) groups.append(Group("")) + groups.append(Group("", spawn=Config.get('apps_audio', "true"), layout='verticaltile', layouts=[ + layout.VerticalTile( + border_focus=Config.get('colour_border_focus', "#ffffff"), + border_normal=Config.get('colour_border_normal', "#777777"), + border_width=Config.get('width_border', "1"), + margin=Config.get('margin_layout', "0"), + ) + ])) + groups.append(Group("", layout='floating', layouts=[ + layout.Floating( + border_focus="#990000", + border_normal="#440000" + ) + ])) return groups @@ -314,25 +331,15 @@ class Kuro(BaseTheme): ]) if Config.get('show_audio_visualizer', False): - widgets.append(kuro.utils.widgets.AudioVisualizerWidget( - graph_color=Config.get('visualizer_graph_color', "#ffffff"), - fill_color=Config.get('visualizer_fill_color', "#ffffff"), - border_color=Config.get('visualizer_border_color', "#000000"), - border_width=Config.get('visualizer_graph_width', 0), - line_width=Config.get('visualizer_line_width', 0), - margin_x=0, - margin_y=0, - frequency=1 - )) + widgets.append(kuro.utils.widgets.AudioVisualizerWidget(margin_x=0, margin_y=0)) widgets.extend([ kuro.utils.widgets.MediaWidget(), - - kuro.utils.widgets.SeparatorWidget(), + kuro.utils.widgets.TextSpacerWidget(fontsize=14), ]) if Config.get('show_temperature', False): - widgets.append( + widgets.extend([ kuro.utils.widgets.ThermalSensorWidget( font=Config.get('font_topbar', 'Arial'), fontsize=Config.get('fontsize_topbar', 16), @@ -342,99 +349,28 @@ class Kuro(BaseTheme): chip=Config.get('thermal_chip', None), threshold=Config.get('thermal_threshold', 70), update_interval=5, - ) - ) + fontsize_left=18, fontsize_right=11 + ), + ]) widgets.extend([ - widget.CPUGraph( - width=Config.get('cpu_width', 25), - border_color=Config.get('cpu_border_colour', "#000000"), - graph_color=Config.get('cpu_graph_colour', "#00ffff"), - border_width=Config.get('cpu_graph_width', 0), - line_width=Config.get('cpu_line_width', 1), - samples=Config.get('cpu_samples', 10), - frequency=2, + kuro.utils.widgets.CPUInfoWidget(fontsize_left=16, fontsize_right=11), + kuro.utils.widgets.MemoryInfoWidget(fontsize_left=18, fontsize_right=11), + kuro.utils.widgets.DiskIOInfoWidget(fontsize_left=16, fontsize_right=11), + kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11), + kuro.utils.widgets.VolumeInfoWidget( + pulse_sink=Config.get('volume_pulse_sink', None), + fontsize_left=18, + fontsize_right=11, ), - - widget.MemoryGraph( - width=Config.get('mem_width', 25), - border_color=Config.get('mem_border_colour', "#000000"), - graph_color=Config.get('mem_graph_colour', "#00ffff"), - border_width=Config.get('mem_graph_width', 0), - line_width=Config.get('mem_line_width', 1), - samples=Config.get('mem_samples', 10), - frequency=2, + kuro.utils.widgets.TextSpacerWidget(fontsize=14), + kuro.utils.widgets.NetworkInfoWidget(fontsize_left=16, fontsize_right=14), + kuro.utils.widgets.GPUStatusWidget( + theme_path=Config.get('gpu_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' + '/checkouts/latest/libqtile/resources/battery-icons'), + padding=0, ), - - widget.HDDBusyGraph( - width=Config.get('hdd_width', 25), - border_color=Config.get('hdd_border_colour', "#000000"), - graph_color=Config.get('hdd_graph_colour', "#00ffff"), - border_width=Config.get('hdd_border_width', 0), - line_width=Config.get('hdd_line_width', 1), - samples=Config.get('hdd_samples', 10), - frequency=2, - ), - - widget.NetGraph( - width=Config.get('net_width', 25), - border_color=Config.get('net_border_colour', "#000000"), - graph_color=Config.get('net_graph_colour', "#00ffff"), - border_width=Config.get('net_border_width', 0), - line_width=Config.get('net_line_width', 1), - samples=Config.get('net_samples', 10), - frequency=2, - ), - - kuro.utils.widgets.KuroBatteryIcon( - battery_name=Config.get('battery_name', 'BAT0'), - energy_full_file=Config.get('battery_energy_full_file', 'charge_full'), - energy_now_file=Config.get('battery_energy_now_file', 'charge_now'), - theme_path=Config.get('battery_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' - '/checkouts/latest/libqtile/resources/battery-icons'), - update_delay=Config.get('battery_update_delay', 30) - ), - - kuro.utils.widgets.WifiIconWidget( - interface=Config.get('wifi_interface', 'wlp4s0'), - theme_path=Config.get('wifi_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' - '/checkouts/latest/libqtile/resources/battery-icons'), - update_interval=Config.get('wifi_update_interval', 30) - ), - - kuro.utils.widgets.PulseVolumeWidget( - cardid=Config.get('volume_cardid', None), - channel=Config.get('volume_channel', 'Master'), - device=Config.get('volume_device', None), - font=Config.get('volume_font', 'Arial'), - fontsize=Config.get('volume_fontsize', 15), - foreground=Config.get('volume_foreground', '#ffffff'), - get_volume_command=Config.get('volume_get_command', None), - mute_command=Config.get('volume_mute_command', None), - theme_path=Config.get('volume_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' - '/checkouts/latest/libqtile/resources/volume-icons'), - volume_down_command=Config.get('volume_down_command', None), - volume_up_command=Config.get('volume_up_command', None), - is_bluetooth_icon=Config.get('volume_is_bluetooth_icon', False), - update_interval=Config.get('volume_update_interval', 0.2) - ), - - kuro.utils.widgets.PulseVolumeWidget( - cardid=Config.get('bluevol_cardid', None), - channel=Config.get('bluevol_channel', 'Master'), - device=Config.get('bluevol_device', None), - font=Config.get('bluevol_font', 'Arial'), - fontsize=Config.get('bluevol_fontsize', 15), - foreground=Config.get('bluevol_foreground', '#ffffff'), - get_volume_command=Config.get('bluevol_get_command', None), - mute_command=Config.get('bluevol_mute_command', None), - theme_path=Config.get('bluevol_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile' - '/checkouts/latest/libqtile/resources/volume-icons'), - volume_down_command=Config.get('bluevol_down_command', None), - volume_up_command=Config.get('bluevol_up_command', None), - is_bluetooth_icon=Config.get('bluevol_is_bluetooth_icon', False), - update_interval=Config.get('bluevol_update_interval', 0.2) - ) + kuro.utils.widgets.TextSpacerWidget(fontsize=14), ]) # Systray can only be on one screen, so put it on the first @@ -505,6 +441,8 @@ class Kuro(BaseTheme): for i, g in enumerate(self.groups): if i == 9: i = -1 + elif i > 9: + continue # mod1 + number = switch to group self.keys.append( Key([self.mod], str(i + 1), lazy.group[g.name].toscreen()) @@ -604,11 +542,13 @@ class Kuro(BaseTheme): utils.notify(title="Window properties {}".format(name), content="{}".format(pprint.pformat(vars(window)))) + logger.warning("{}".format(pprint.pformat(vars(window)))) if info: info = pprint.pformat(info) utils.notify(title="Window info of {}".format(name), content="{}".format(info)) + logger.warning("{}".format(info)) # @staticmethod def toggle_window_static(self, qtile): @@ -628,6 +568,10 @@ class Kuro(BaseTheme): def callback_startup_once(self, *args, **kwargs): self.update_wallpaper(self.qtile) + # Setup audio + # p = utils.execute_once(["qjackctl"]) + # p.wait() + def callback_startup(self): if self.current_wallpaper: p = utils.execute_once(["wal", "-n", "-i", "{}".format(self.current_wallpaper)]) @@ -692,10 +636,17 @@ class Kuro(BaseTheme): self.callback_client_new() + # After first startup is complete, start the audio apps that can only be started after boot is complete + if not self.qtile.no_spawn: + for app in Config.get("apps_audio_afterstart", []): + utils.execute_once(app) + # Update color scheme Kuro.update_colorscheme(self.qtile) def callback_client_new(self, *args, **kwargs): + client = args[0] if len(args) > 0 else None + if len(self.initial_windows) > 0: init = self.initial_windows.copy() for pid, gname in init: @@ -703,6 +654,8 @@ class Kuro(BaseTheme): if len(group.windows) > 0: for window in group.windows: w_pid = window.window.get_net_wm_pid() + self.log_info("Comparing pid {} with window PID {}, window {}".format(pid, w_pid, + window.name)) if pid == w_pid: c = self.qtile.dgroups.rules_map.copy() for rid, r in c.items(): @@ -711,11 +664,10 @@ class Kuro(BaseTheme): self.initial_windows.remove((pid, gname)) self.log_info("Removed group rule for PID {}, window {}".format(pid, window.name)) - self.log_info(str(self.qtile.dgroups.rules_map)) + self.log_info(str(self.qtile.dgroups.rules_map)) # Check if it is a visualizer if Config.get("show_audio_visualizer", False): - client = args[0] if len(args) > 0 else None if client is not None and client.window.get_name() == "GLava": placed = False for screen in self.qtile.screens: @@ -742,6 +694,14 @@ class Kuro(BaseTheme): logger.warning("Not repositioning GLava {} because there is no widget where it can fit".format(client)) utils.notify("Glava", "Not repisitioning new GLava process because there is no screen without a visualizer") + # If this is Non-Mixer, move it to the audio group + logger.warning("Processing window {}".format(client)) + if client is not None and client.window.get_wm_class() == ('Non-Mixer', 'Non-Mixer'): + logger.warning("Moving to correct group!") + client.window.togroup("") + + + def callback_client_killed(self, *args, **kwargs): client = args[0] logger.warning("Client {} Killed".format(client)) diff --git a/kuro/utils/general.py b/kuro/utils/general.py index 81b4cb7..da8d96b 100644 --- a/kuro/utils/general.py +++ b/kuro/utils/general.py @@ -1,19 +1,25 @@ import re import subprocess +import traceback from time import sleep import notify2 import six +from dbus import DBusException from libqtile import widget from libqtile.window import Internal from libqtile.bar import Bar from notify2 import Notification, URGENCY_NORMAL +from libqtile.log_utils import logger notify2.init("QTileWM") BUTTON_LEFT = 1 BUTTON_MIDDLE = 2 BUTTON_RIGHT = 3 +BUTTON_UP = 4 +BUTTON_DOWN = 5 +BUTTON_MUTE = 1 BUTTON_SCROLL_UP = 4 BUTTON_SCROLL_DOWN = 5 @@ -92,7 +98,11 @@ def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): notification.set_timeout(timeout) notification.set_urgency(urgency) - return notification.show() + try: + return notification.show() + except DBusException as e: + logger.warning("Showing notification failed: {}".format(e)) + logger.warning(traceback.format_exc()) def spawn_popup(qtile, x, y, text): diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index da629cd..a122a50 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -1,43 +1,35 @@ +import math import os import re import subprocess import cairocffi +import iwlib +import netifaces +import psutil import six from libqtile import bar, pangocffi from libqtile.log_utils import logger -from libqtile.utils import catch_exception_and_warn, UnixCommandNotFound from libqtile.widget import base -from libqtile.widget.battery import BatteryIcon, default_icon_path +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.image import Image -from libqtile.widget.sensors import ThermalSensor from libqtile.widget.tasklist import TaskList -from libqtile.widget.volume import Volume from libqtile.widget.wlan import get_status from libqtile.window import Window -from kuro.utils.general import BUTTON_LEFT, execute, spawn_popup, notify, BUTTON_MIDDLE, bluetooth_audio_connected, \ - bluetooth_audio_sink, BUTTON_RIGHT - - -class AppLauncherIcon(Image): - def button_press(self, x, y, button): - if button == BUTTON_LEFT: - execute("dmenu_run -i -p '»' -nb '#000000' -fn 'Noto Sans-11' -nf '#777777' -sb '#1793d0' -sf '#ffffff'") - - def handle_hover(self, event): - spawn_popup(self.qtile, self.offsetx, self.offsety, "Hovered over AppLauncherIcon!") +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 -Qua".split() - self.status_cmd = "yay -Qua --color never".split() + self.cmd = "yay -Qu".split() + self.status_cmd = "yay -Qu --color never".split() self.update_cmd = "sudo yay".split() self.subtr = 0 @@ -69,296 +61,187 @@ class CheckUpdatesYay(CheckUpdates): subprocess.Popen(self.execute, shell=True) -class KuroBatteryIcon(BatteryIcon): - status_cmd = "acpi" - - 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 PulseVolumeWidget(Volume): - +class DualPaneTextboxBase(base._Widget): + """ + Base class for widgets that are two boxes next to each other both containing text. + """ + orientations = ORIENTATION_HORIZONTAL defaults = [ - ("cardid", None, "Card Id"), - ("device", "default", "Device Name"), - ("channel", "Master", "Channel"), - ("padding", 3, "Padding left and right. Calculated if None."), - ("theme_path", None, "Path of the icons"), - ("update_interval", 0.2, "Update time in seconds."), - ("emoji", False, "Use emoji to display volume states, only if ``theme_path`` is not set." - "The specified font needs to contain the correct unicode characters."), - ("mute_command", None, "Mute command"), - ("volume_up_command", None, "Volume up command"), - ("volume_down_command", None, "Volume down command"), - ("get_volume_command", None, "Command to get the current volume"), - ("is_bluetooth_icon", False, "Is this icon for a Bluetooth Audio device?"), + ("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 - _old_length = 0 + 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) - def __init__(self, **config): - super(PulseVolumeWidget, self).__init__(**config) - self._old_length = self._length + @property + def text_left(self): + return self._text_left - # Augment commands with bluetooth sink ID if this is a bluetooth icon - if self.is_bluetooth_icon and bluetooth_audio_connected(): - bsink = bluetooth_audio_sink() - self.mute_command = " ".join(self._user_config['mute_command']).format(bsink=bsink).split() - self.volume_up_command = " ".join(self._user_config['volume_up_command']).format(bsink=bsink).split() - self.volume_down_command = " ".join(self._user_config['volume_down_command']).format(bsink=bsink).split() - self.get_volume_command = " ".join(self._user_config['get_volume_command']).format(bsink=bsink).split() - logger.info("Updated bluetooth commands with bluetooth sink {}".format(bsink)) - self._length = self._old_length - self.commands_need_reset = False - elif self.is_bluetooth_icon: - self.commands_need_reset = True + @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 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: - self.commands_need_reset = False + return self.padding - self._old_length = self._length - - def reset_bluetooth_commands(self): - if self.is_bluetooth_icon and bluetooth_audio_connected(): - bsink = 0 if bluetooth_audio_sink() == -1 else bluetooth_audio_sink() - self.mute_command = " ".join(self._user_config['mute_command']).format(bsink=bsink).split() - self.volume_up_command = " ".join(self._user_config['volume_up_command']).format(bsink=bsink).split() - self.volume_down_command = " ".join(self._user_config['volume_down_command']).format(bsink=bsink).split() - self.get_volume_command = " ".join(self._user_config['get_volume_command']).format(bsink=bsink).split() - logger.info("Updated bluetooth commands with bluetooth sink {}".format(bsink)) - self._length = self._old_length - self.commands_need_reset = False - - def get_volume(self): - try: - get_volume_cmd = "echo 0".split() - - if self.get_volume_command: - if self.is_bluetooth_icon and bluetooth_audio_sink() == -1: - pass - else: - get_volume_cmd = self.get_volume_command - - mixer_out = self.call_process(get_volume_cmd) - except subprocess.CalledProcessError: - return -1 - - try: - return int(mixer_out.strip()) - except ValueError: - return -1 - - def _update_drawer(self): - if self.volume is not None: - super(PulseVolumeWidget, self)._update_drawer() - self.text = "" - if self.is_bluetooth_icon and not bluetooth_audio_connected(): - self._length = 0 - - def draw(self): - if self.is_bluetooth_icon and not bluetooth_audio_connected(): - if not self.commands_need_reset: - logger.info("Bluetooth device disconnected. Hiding bluetooth audio mixer") - self.commands_need_reset = True - base._TextBox.draw(self) + @property + def actual_padding_between(self): + if self.padding_between is None: + return max(self.fontsize_left, self.fontsize_right) / 4 else: - if self.commands_need_reset: - self.reset_bluetooth_commands() - if self.theme_path: - self.drawer.draw(offsetx=self.offset, width=self.length) - else: - base._TextBox.draw(self) - - def button_press(self, x, y, button): - if button == BUTTON_LEFT: - volume = self.get_volume() - - width = 15 - - if volume >= 0: - volume_amount = round((volume/100)*width) - else: - volume_amount = 0 - - msg = "[{}{}]".format( - "".join(["#" for x in range(volume_amount)]), - "".join(["-" for x in range(width-volume_amount)]) - ) - - notify( - "{}Volume : {}%".format("Bluetooth " if self.is_bluetooth_icon else "", volume), - msg - ) - else: - super(PulseVolumeWidget, self).button_press(x, y, button) - - -class WifiIconWidget(base._TextBox): - """WiFi connection strength indicator widget.""" - - orientations = base.ORIENTATION_HORIZONTAL - defaults = [ - ('interface', 'wlan0', 'The interface to monitor'), - ('update_interval', 1, 'The update interval.'), - ('theme_path', default_icon_path(), 'Path of the icons'), - ('custom_icons', {}, 'dict containing key->filename icon map'), - ('disconnected_message', {'error': False, 'essid': None, 'quality': 0, 'percent': 0}, 'Message to show when WiFi is disconnected'), - ] - - def __init__(self, **config): - super(WifiIconWidget, self).__init__("WLAN", bar.CALCULATED, **config) - self.add_defaults(WifiIconWidget.defaults) - - if self.theme_path: - self.length_type = bar.STATIC - self.length = 0 - self.surfaces = {} - self.current_icon = 'wireless-disconnected' - self.icons = dict([(x, '{0}.png'.format(x)) for x in ( - 'wireless-disconnected', - 'wireless-none', - 'wireless-low', - 'wireless-medium', - 'wireless-high', - 'wireless-full', - )]) - self.icons.update(self.custom_icons) - - def _get_info(self): - try: - essid, quality = get_status(self.interface) - disconnected = essid is None - if disconnected: - return self.disconnected_message - - return { - 'error': False, - 'essid': essid, - 'quality': quality, - 'percent': (quality / 70) - } - except EnvironmentError: - logger.error( - '%s: Probably your wlan device is switched off or ' - ' otherwise not present in your system.', - self.__class__.__name__) - - return {'error': True} - - def timer_setup(self): - self.update() - self.timeout_add(self.update_interval, self.timer_setup) + return self.padding_between def _configure(self, qtile, bar): - super(WifiIconWidget, self)._configure(qtile, bar) - self.setup_images() + 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 _get_icon_key(self): - key = 'wireless' - info = self._get_info() - if info is False or info.get('error'): - key += '-none' - elif info.get('essid') is None: - key += '-disconnected' - else: - percent = info['percent'] - if percent < 0.2: - key += '-low' - elif percent < 0.4: - key += '-medium' - elif percent < 0.8: - key += '-high' - else: - key += '-full' - - return key - - def update(self): - icon = self._get_icon_key() - if icon != self.current_icon: - self.current_icon = icon - self.draw() + 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 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) + # 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 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('Wireless 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 - - -class ThermalSensorWidget(ThermalSensor): - defaults = [ - ('metric', True, 'True to use metric/C, False to use imperial/F'), - ('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'), - ] - - @catch_exception_and_warn(warning=UnixCommandNotFound, excepts=OSError) - def get_temp_sensors(self): - """calls the unix `sensors` command with `-f` flag if user has specified that - the output should be read in Fahrenheit. + def cmd_set_font(self, font=base.UNSPECIFIED, fontsize_left=base.UNSPECIFIED, fontsize_right=base.UNSPECIFIED, fontshadow=base.UNSPECIFIED): """ - command = ["sensors", ] - if self.chip: - command.append(self.chip) - if not self.metric: - command.append("-f") - sensors_out = self.call_process(command) - return self._format_sensors_output(sensors_out) + 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() - -class SeparatorWidget(base._TextBox): - def __init__(self): - super(SeparatorWidget, self).__init__(text="|", width=bar.CALCULATED, fontsize=14) + 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): @@ -381,7 +264,7 @@ class MediaWidget(base.InLoopPollText): player_icons = { 'spotify': '', - 'vlc': '', + 'vlc': '', 'firefox': '', 'mpv': '', } @@ -442,6 +325,11 @@ class MediaWidget(base.InLoopPollText): if player is not None: command = ["playerctl", "-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 @@ -559,6 +447,13 @@ 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) @@ -589,11 +484,15 @@ class AudioVisualizerWidget(_Graph): fullscreen = True break - logger.warning("Repositioning {} {} to {}x{}".format(self.client, self.client.window.wid, pos_x, pos_y)) + 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): @@ -687,6 +586,7 @@ class GPUStatusWidget(base._TextBox): 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'), @@ -784,13 +684,18 @@ class GPUStatusWidget(base._TextBox): 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.", + 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.\n" - "Press the middle mouse button on this icon to switch GPUs.".format( - self.current_status + 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())) ) @@ -810,3 +715,582 @@ class GPUStatusWidget(base._TextBox): 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', 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"), + ("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) diff --git a/required_packages.txt b/required_packages.txt index 9a1cfa4..8b3391e 100644 --- a/required_packages.txt +++ b/required_packages.txt @@ -2,6 +2,7 @@ pamixer nitrogen notification-daemon otf-font-awesome +python-osc # /optional/ playerctl