diff --git a/config.py b/config.py index e4e63a5..7d5f49f 100644 --- a/config.py +++ b/config.py @@ -27,36 +27,38 @@ # Import Theme from libqtile import hook from libqtile.log_utils import logger -from kuro.utils import load_config_class -import traceback try: from kuro.theme import Kuro Theme = Kuro() except ImportError as e: - logger.error(traceback.format_exc()) logger.error("Could not load Kuro Theme. Trying to load BaseTheme. Error: {}".format(e)) try: from kuro.base import BaseTheme as Kuro Theme = Kuro() except ImportError as e: Kuro = None - logger.error(traceback.format_exc()) raise ImportError("Could not load theme Config or BaseTheme! Error: {}".format(e)) # Import theme configuration -Config = load_config_class() -if Config is None: - raise ImportError("Could not load theme Config or BaseConfig! Error: {}".format(e)) +try: + from kuro.config import Config +except ImportError as e: + logger.error("Could not load Kuro Config. Trying to load BaseConfig. Error: {}".format(e)) + try: + from kuro.base import BaseConfig as Config + except ImportError as e: + Config = None + raise ImportError("Could not load theme Config or BaseConfig! Error: {}".format(e)) + try: - logger.warning("Initializing theme...") - logger.warning(f"Using config variables for '{Config.get('config_name', '????')}'") + logger.info("Initializing theme...") # Initialize the Theme Theme.initialize() - logger.warning("Initialize done") + logger.info("Initialize done") - logger.warning("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) @@ -80,11 +82,10 @@ try: hook.subscribe.selection_notify(Theme.callback_selection_notify) hook.subscribe.selection_change(Theme.callback_selection_change) hook.subscribe.screen_change(Theme.callback_screen_change) - hook.subscribe.screens_reconfigured(Theme.callback_screens_reconfigured) hook.subscribe.current_screen_change(Theme.callback_current_screen_change) - logger.warning("Hooking done") + logger.info("Hooking done") - logger.warning("Initializing theme variables") + logger.info("Initializing theme variables") # Initialize variables from theme keys = Theme.keys mouse = Theme.mouse @@ -103,18 +104,14 @@ try: focus_on_window_activation = Theme.focus_on_window_activation extensions = Theme.extensions wmname = Theme.wmname - reconfigure_screens = Theme.reconfigure_screens - logger.warning("Variable initialization done") + logger.info("Variable initialization done") except Exception as e: Theme = None Config = None - logger.error(traceback.format_exc()) raise AttributeError("Could not configure theme! Error: {}".format(e)) def main(qtile): - Config.initialize(qtile) - # set logging level if Config.get('debug', False): if Config.get('verbose', False): diff --git a/kuro/base.py b/kuro/base.py index 8f02d65..221d1fd 100644 --- a/kuro/base.py +++ b/kuro/base.py @@ -1,38 +1,24 @@ -import time - from libqtile import layout as libqtile_layout, layout, bar, widget from libqtile.lazy import lazy from libqtile.config import Key, Group, Screen, Drag, Click, Match -# Initialize logging -from libqtile.log_utils import logger - class BaseConfig: - config_name = "KuroBase" - @classmethod def get(cls, key, default): if hasattr(cls, key): - return getattr(cls, key) - #return cls.__dict__[key] + return cls.__dict__[key] else: return default - @classmethod - def initialize(cls, qtile): - # Can do extra initialization based on qtile instance here - pass - class BaseTheme: # Changing variables initialized by function keys = None - mouse = None groups = None layouts = None widget_defaults = None - screens = [] + screens = None qtile = None # 'Static' variables @@ -55,7 +41,6 @@ class BaseTheme: auto_fullscreen = True focus_on_window_activation = "smart" extensions = [] - reconfigure_screens = False # XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this # string besides java UI toolkits; you can see several discussions on the @@ -70,21 +55,12 @@ class BaseTheme: # 'export _JAVA_AWT_WM_NONREPARENTING=1' wmname = "LG3D" - def __init__(self): - self.startup_time = time.time() - def initialize(self): - logger.info("Initializing widget defaults...") self.widget_defaults = self.init_widget_defaults() - logger.info("Initializing keys...") self.keys = self.init_keys() - logger.info("Initializing groups...") self.groups = self.init_groups() - logger.info("Initializing layouts...") self.layouts = self.init_layouts() - logger.info("Initializing screens...") self.screens = self.init_screens() - logger.info("Initializing mouse...") self.mouse = self.init_mouse() def init_keys(self): @@ -119,15 +95,6 @@ class BaseTheme: Key(["mod4"], "r", lazy.spawncmd()), ] - def init_mouse(self): - return [ - Drag(["mod4"], "Button1", lazy.window.set_position_floating(), - start=lazy.window.get_position()), - Drag(["mod4"], "Button3", lazy.window.set_size_floating(), - start=lazy.window.get_size()), - Click(["mod4"], "Button2", lazy.window.bring_to_front()) - ] - def init_groups(self): groups = [Group(i) for i in "asdfuiop"] for i in groups: @@ -171,6 +138,15 @@ class BaseTheme: ), ] + def init_mouse(self): + return [ + Drag(["mod4"], "Button1", lazy.window.set_position_floating(), + start=lazy.window.get_position()), + Drag(["mod4"], "Button3", lazy.window.set_size_floating(), + start=lazy.window.get_size()), + Click(["mod4"], "Button2", lazy.window.bring_to_front()) + ] + # Callbacks def callback_startup_once(self, *args, **kwargs): pass @@ -244,8 +220,5 @@ class BaseTheme: def callback_screen_change(self, *args, **kwargs): pass - def callback_screens_reconfigured(self, *args, **kwargs): - pass - def callback_current_screen_change(self, *args, **kwargs): pass diff --git a/kuro/config/__init__.py b/kuro/config.py similarity index 69% rename from kuro/config/__init__.py rename to kuro/config.py index 00ff7ea..5bc2ce7 100644 --- a/kuro/config/__init__.py +++ b/kuro/config.py @@ -1,11 +1,8 @@ from kuro.base import BaseConfig -from libqtile.log_utils import logger # Config variables used in the main configuration class Config(BaseConfig): - config_name = "KuroGeneral" - # Show debug bar and messages debug = False verbose = False @@ -17,48 +14,22 @@ class Config(BaseConfig): inactive_light = "#777777" inactive_dark = "#333333" - # Predefined commands - cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" - cmd_brightness_down = "sudo /usr/bin/xbacklight -dec 10" - cmd_screenshot = "/home/kevin/bin/screenshot.sh" - cmd_alt_screenshot = "/home/kevin/bin/screenshot.sh" - lock_command = "bash /home/kevin/bin/lock.sh" - cliphistory_command = "/home/kevin/bin/cliphistory.sh" - # Default Applications app_terminal = "ghostty" - app_launcher = "wofi --show run,drun" - file_manager = "thunar" - visualizer_app = "glava" + app_terminal_init = "ghostty --gtk-single-instance=true --quit-after-last-window-close=false --initial-window=true" + app_launcher = "/home/kevin/bin/dmenu_wal.sh" web_browser = "firefox" - - # Autostart applications - apps_autostart_group = [ - {'group': "", 'command': ["firefox"]}, - {'group': "", 'command': ["ghostty", "--gtk-single-instance=true", "--quit-after-last-window-close=false", "--initial-window=true"]}, - {'group': "", 'command': ["/usr/bin/rambox"]}, - {'group': "", 'command': ["thunar"]}, - {'group': "", 'command': ["thunderbird"]}, - {'group': "", 'command': ["spotify"]}, - ] - apps_autostart = { - 'common': [ - ["/usr/lib/kdeconnectd"], # KDE Connect daemon - ["kdeconnect-indicator"], # KDE Connect tray - ["vorta"], # Vorta backup scheduler - ], - 'x11': [ - ["dunst"], # Notification daemon - ["picom", "-b"], # Compositor - ["xfce4-clipman"], # Clipboard manager - ["xiccd"], # Color profile manager - ], - 'wayland': [ - ["mako"], # Notification daemon - ["wl-paste", "--watch", "cliphist", "store"], # Clipboard manager - ["kanshi"], # Display hotplugging - ] - } + file_manager = "thunar" + app_chat = "/usr/bin/rambox" + app_irc = "" + app_mail = "thunderbird" + app_music = "spotify" + cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" + cmd_brightness_down = "sudo /usr/bin/xbacklight -dec 10" + 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" # Keyboard commands cmd_media_play = "playerctl -i kdeconnect play-pause" @@ -73,10 +44,9 @@ class Config(BaseConfig): cmd_monitor_mode_day = "bash /home/kevin/bin/monitor_day.sh" cmd_monitor_mode_night = "bash /home/kevin/bin/monitor_night.sh" cmd_monitor_mode_alt = "bash /home/kevin/bin/monitor_gamenight.sh" - cmd_reconfigure_screens = "kanshictl reload" # Commands - wallpaper_config_command = "/bin/true" # TODO: Remove + wallpaper_config_command = "/home/kevin/bin/wal-nitrogen-noupdate" # Images desktop_bg = "/home/kevin/Pictures/wallpapers/desktop.png" @@ -115,8 +85,7 @@ class Config(BaseConfig): # Bar variables bar_background = background - bar_rgba_opacity = "AA" - bar_opacity = 1.0 + bar_opacity = 0.8 bar_hover_opacity = 1 # Groupbox variables @@ -137,12 +106,11 @@ class Config(BaseConfig): tasklist_urgent_border = highlight tasklist_font = "Noto Sans" tasklist_fontsize = 11 - tasklist_rounded = False # Thermal indicator variables thermal_threshold = 75 - thermal_sensor = "Package id 0" - thermal_chip = "coretemp-isa-0000" + thermal_sensor = "Tdie" + thermal_chip = "zenpower-pci-00c3" # CPU graph variables cpu_graph_colour = '#ff0000' @@ -157,11 +125,10 @@ class Config(BaseConfig): battery_theme_path = "/home/kevin/.config/qtile/kuro/resources/battery" battery_update_delay = 5 - # Network variables + # Wifi variables wifi_interface = "wifi0" wifi_theme_path = "/home/kevin/.config/qtile/kuro/resources/wifi" wifi_update_interval = 5 - wired_interface = "eth0" # GPU variables gpu_theme_path = "/home/kevin/.config/qtile/kuro/resources/gpu" @@ -170,7 +137,9 @@ class Config(BaseConfig): volume_font = "Noto Sans" volume_fontsize = 11 volume_theme_path = "/home/kevin/.config/qtile/kuro/resources/volume" - volume_pulse_sinks = [] + volume_pulse_sink = "alsa_output.usb-Burr-Brown_from_TI_USB_Audio_CODEC-00.analog-stereo-output" + volume_pulse_sink2 = "alsa_output.pci-0000_0d_00.4.analog-stereo" + volume_is_bluetooth_icon = False volume_update_interval = 0.2 @@ -194,6 +163,9 @@ class Config(BaseConfig): laptop_screen_nvidia = "eDP-1-1" laptop_screen_intel = "eDP1" + # Keyboard colors + do_keyboard_updates = False + # Show audio visualizer show_audio_visualizer = False kill_unnecessary_glava_processes = True @@ -207,15 +179,9 @@ class Config(BaseConfig): # Show battery widget show_battery_widget = False + # Audio control applications + # apps_audio = ["pavucontrol"] + apps_audio_afterstart = [] + # Comma-separated list of ignored players in the media widget media_ignore_players = "kdeconnect" - - @classmethod - def initialize(cls, qtile): - # Can do extra initialization based on qtile instance here - super(Config, cls).initialize(qtile=qtile) - - # Replace some apps if launched in X11 mode - if qtile.core.name == "x11": - logger.warning("Launched in X11 mode, overriding some apps in Config to xorg-variants.") - cls.app_launcher = "/home/kevin/bin/dmenu_wal.sh" diff --git a/kuro/config/aria.py b/kuro/config/aria.py deleted file mode 100644 index ebb87bf..0000000 --- a/kuro/config/aria.py +++ /dev/null @@ -1,45 +0,0 @@ -from kuro.config import Config as GeneralConfig - - -class Config(GeneralConfig): - """ - Kuro QTile configuration overrides for Aria - """ - config_name = "Aria" - - # Default Applications - app_terminal = "terminator" - app_launcher = "/home/kevin/bin/dmenu_wal.sh" - cmd_brightness_up = "true" - cmd_brightness_down = "true" - cmd_screenshot = "xfce4-screenshooter -r -c -d 1" - cmd_alt_screenshot = "xfce4-screenshooter -w -c -d 0" - lock_command = "bash /home/kevin/bin/lock.sh" - cliphistory_command = "true" - - # Autostart applications - apps_autostart_group = [ - {'group': "", 'command': ["firefox"]}, - {'group': "", 'command': ["terminator"]}, - {'group': "", 'command': ["/usr/bin/rambox"]}, - {'group': "", 'command': ["thunar"]}, - {'group': "", 'command': ["thunderbird"]}, - {'group': "", 'command': ["spotify"]}, - ] - - # Thermal indicator variables - thermal_sensor = "Package id 0" - thermal_chip = "coretemp-isa-0000" - - # Network variables - wifi_interface = "wifi0" - wired_interface = "enp7s0" - - # Volume widget variables - volume_pulse_sinks = [ - "alsa_output.pci-0000_00_1f.3.analog-stereo", - ] - - # Screen organization - laptop_screen_nvidia = "eDP-1-1" - laptop_screen_intel = "eDP1" diff --git a/kuro/config/meconopsis.py b/kuro/config/meconopsis.py deleted file mode 100644 index e580c92..0000000 --- a/kuro/config/meconopsis.py +++ /dev/null @@ -1,22 +0,0 @@ -from kuro.config import Config as GeneralConfig - - -class Config(GeneralConfig): - """ - Kuro QTile configuration overrides for Meconopsis - """ - config_name = "Meconopsis" - - # Thermal indicator variables - thermal_sensor = "Package id 0" - thermal_chip = "coretemp-isa-0000" - - # Network variables - wifi_interface = "wlp3s0" - wired_interface = "enp4s0" - - # Volume widget variables - volume_pulse_sinks = [ - # Analog jack - "alsa_output.usb-CSCTEK_USB_Audio_and_HID_A34004801402-00.analog-stereo", - ] diff --git a/kuro/config/violet.py b/kuro/config/violet.py deleted file mode 100644 index e6dd5a6..0000000 --- a/kuro/config/violet.py +++ /dev/null @@ -1,26 +0,0 @@ -from kuro.config import Config as GeneralConfig - - -class Config(GeneralConfig): - """ - Kuro QTile configuration overrides for Violet - """ - config_name = "Violet" - - # Thermal indicator variables - thermal_sensor = "Tdie" - thermal_chip = "zenpower-pci-00c3" - - # Network variables - wifi_interface = None - wired_interface = "br1" - - # Volume widget variables - volume_pulse_sinks = [ - # Behringer USB mixer - "alsa_output.usb-Burr-Brown_from_TI_USB_Audio_CODEC-00.analog-stereo-output", - # Motherboard output (Starship/Matisse) - "alsa_output.pci-0000_0e_00.4.iec958-stereo", - # PCIe card output (CMI8738/CMI8768 PCI Audio) - "alsa_output.pci-0000_08_00.0.analog-stereo", - ] diff --git a/kuro/theme.py b/kuro/theme.py index 7311976..b83e0d6 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -1,111 +1,130 @@ import json import os import random -import time import datetime -import socket -import subprocess -from typing import Optional -from libqtile.backend.base import Window -from libqtile.backend.wayland.layer import LayerStatic -from libqtile.backend.wayland.xwindow import XWindow as WaylandXWindow, XStatic as WaylandXStatic -from libqtile.backend.x11.window import XWindow as XorgXWindow # Initialize logging from libqtile.log_utils import logger -logger.warning("Importing qtile theme requirements...") +logger.error("Importing qtile theme requirements...") from libqtile.config import Key, Screen, Group, Drag, Click, Match +from libqtile.command.base import expose_command from libqtile.lazy import lazy from libqtile import layout, bar, widget, qtile -from qtile_extras import widget as extra_widget -logger.warning("Importing theme util functions...") +logger.error("Importing theme util functions...") # Import theme util functions from xcffib.xproto import WindowError -logger.warning("Importing kuro utils...") +logger.error("Importing kuro utils...") import kuro.utils.widgets from kuro.utils import general as utils -logger.warning("Importing variables and other utils...") +logger.error("Importing variables and other utils...") # Import variables from kuro.base import BaseTheme from kuro.utils.general import display_wm_class, test_popups +from kuro.utils.kb_backlight import handle_focus_change as kb_handle_focus_change from kuro.utils import layouts as kuro_layouts +from kuro.utils.windows import KuroStatic -logger.warning("Importing configuration...") +logger.error("Importing configuration...") -from kuro.utils import load_config_class -Config = load_config_class() -if Config is None: - raise ImportError("Could not load theme Config or BaseConfig! Error: {}".format(e)) -Config.initialize(qtile) - -logger.warning("Imports done") +try: + from kuro.config import Config +except ImportError: + try: + from kuro.baseconfig import BaseConfig as Config + except ImportError: + Config = None + raise ImportError("Could not load theme Config or BaseConfig!") +logger.error("Imports done") class Kuro(BaseTheme): # Shorthand for modifier key mod = Config.get("modifier", "mod4") + # Show debug messages + debug = Config.get('debug', False) + debug_textfields = [] + debug_bars = [] + # Screen count num_screens = 0 + # Top bars + topbars = [] + + # Visualizers + audio_visualizers = [] + # Static windows static_windows = [] # Current wallpaper path current_wallpaper = None + # Whether or not to perform keyboard backlight updates + do_keyboard_updates = True + # Window manager name wmname = "qtile" - startup_completed = False - autostart_app_rules = {} - # Floating layout override - floating_layout = kuro_layouts.KuroFloating( - border_width=0, - border_focus="#000000", - border_normal="#000000", - float_rules=[ - # Run the utility of `xprop` to see the wm class and name of an X client. - *layout.Floating.default_float_rules, - Match(wm_class='confirmreset'), # gitk - Match(wm_class='makebranch'), # gitk - Match(wm_class='maketag'), # gitk - Match(wm_class='ssh-askpass'), # ssh-askpass - Match(title='branchdialog'), # gitk - Match(title='pinentry'), # GPG key password entry - Match(title='origin.exe', wm_class='Wine'), # Wine Origin game launcher + floating_layout = kuro_layouts.KuroFloating(float_rules=[ + # Run the utility of `xprop` to see the wm class and name of an X client. + *layout.Floating.default_float_rules, + Match(wm_class='confirmreset'), # gitk + Match(wm_class='makebranch'), # gitk + Match(wm_class='maketag'), # gitk + Match(wm_class='ssh-askpass'), # ssh-askpass + Match(title='branchdialog'), # gitk + Match(title='pinentry'), # GPG key password entry + Match(title='origin.exe', wm_class='Wine'), # Wine Origin game launcher + ]) - # Homebank popups - Match(title='Add transaction', wm_class='homebank'), - Match(title='Edit transaction', wm_class='homebank'), - Match(title='Inherit transaction', wm_class='homebank'), - Match(title='Multiple edit transactions', wm_class='homebank'), - Match(title='Transaction splits', wm_class='homebank'), - ] - ) + def set_debug_text(self, text): + for field in self.debug_textfields: + field.text = text + for bar in self.debug_bars: + if qtile is not None: + bar.draw() + + def log_debug(self, text): + if Config.get('verbose', False): + self.set_debug_text(text) + logger.debug(text) + + def log_info(self, text): + self.set_debug_text(text) + logger.info(text) def initialize(self): + logger.error("Initializing Kuro theme...") + self.log_debug("Initializing Kuro Theme...") + # Update color scheme - logger.warning("Initializing colorscheme...") self.initialize_colorscheme() - logger.warning("Initializing superclass...") + # Set settings + self.do_keyboard_updates = Config.get("do_keyboard_updates", True) + super(Kuro, self).initialize() - logger.warning("Updating keys for groups and layouts...") + self.update() + + def update(self): + # Update keys with keys for groups and layouts self.update_keys() def init_keys(self): - logger.warning("Initializing keys") + self.log_debug("Initializing keys") + return [ # Switch between windows in current stack pane Key([self.mod], "k", lazy.layout.down()), @@ -127,6 +146,9 @@ class Kuro(BaseTheme): # Floating toggle Key([self.mod, "shift"], 'f', lazy.window.toggle_floating()), + # Pinned toggle + Key([self.mod, "shift"], 'p', lazy.function(self.toggle_pinned)), + # Toggle between split and unsplit sides of stack. # Split = all windows displayed # Unsplit = 1 window displayed, like Max layout, but still with @@ -142,8 +164,8 @@ class Kuro(BaseTheme): # Super-B to start webbrowser Key([self.mod], "b", lazy.spawn(Config.get('web_browser', "xterm links"))), - # Super-T to start file manager - Key([self.mod], "t", lazy.spawn(Config.get('file_manager', "thunar"))), + # Super-F to start file manager +# Key([self.mod], "f", lazy.spawn(Config.get('file_m4anager', "thunar"))), # Super-Shift-R to start spawncmd Key([self.mod, "shift"], "r", lazy.spawncmd()), @@ -151,6 +173,10 @@ class Kuro(BaseTheme): # Lock shortcut Key([self.mod], "l", lazy.spawn(Config.get('lock_command', "i3lock"))), + # Backlight keys + Key([], "XF86MonBrightnessUp", lazy.spawn(Config.get('cmd_brightness_up', 'xbacklight -inc 10'))), + Key([], "XF86MonBrightnessDown", lazy.spawn(Config.get('cmd_brightness_down', 'xbacklight -dec 10'))), + # Display modes Key([self.mod], "Prior", lazy.spawn(Config.get('cmd_monitor_mode_3s144', 'true'))), Key([self.mod], "Next", lazy.spawn(Config.get('cmd_monitor_mode_3s60', 'true'))), @@ -158,10 +184,6 @@ class Kuro(BaseTheme): Key([self.mod], "End", lazy.spawn(Config.get('cmd_monitor_mode_night', 'true'))), Key([self.mod], "Insert", lazy.spawn(Config.get('cmd_monitor_mode_alt', 'true'))), - # Backlight keys - Key([], "XF86MonBrightnessUp", lazy.spawn(Config.get('cmd_brightness_up', 'xbacklight -inc 10'))), - Key([], "XF86MonBrightnessDown", lazy.spawn(Config.get('cmd_brightness_down', 'xbacklight -dec 10'))), - # Media keys Key([], "XF86AudioPlay", lazy.spawn(Config.get('cmd_media_play', 'true'))), Key([], "XF86AudioNext", lazy.spawn(Config.get('cmd_media_next', 'true'))), @@ -178,31 +200,27 @@ class Kuro(BaseTheme): # Alt screenshot Key([self.mod], "Print", lazy.spawn(Config.get('cmd_alt_screenshot', 'xfce4-screenshooter'))), - # Copy from clipboard history - Key([self.mod, "control"], "c", lazy.spawn(Config.get('cliphistory_command', 'true'))), - # Toggle between different layouts as defined below Key([self.mod], "Tab", lazy.next_layout()), # Kill the current window Key([self.mod], "w", lazy.window.kill()), - # Restart/reload QTile (Restart only available in X11 backend) - Key([self.mod, "control"], "r", lazy.restart() if qtile.core.name == "x11" else lazy.reload_config()), + # Restart QTile + Key([self.mod, "control"], "r", lazy.restart()), # Shutdown QTile Key([self.mod, "control"], "q", lazy.shutdown()), # Update wallpaper - Key([self.mod, "control"], "w", - lazy.function(self.set_random_wallpaper), lazy.function(self.update_colorscheme)), - - # Reload screen configuration - Key([self.mod, "control"], "s", lazy.spawn(Config.get('cmd_reconfigure_screens', 'true'))), + Key([self.mod, "control"], "w", lazy.function(self.update_wallpaper)), # Reload colorscheme Key([self.mod, "control"], "t", lazy.function(self.update_colorscheme)), + # Reorganize screens + Key([self.mod, "control"], "s", lazy.function(self.update_screens)), + # Toggle static windows Key([self.mod], "p", lazy.function(self.toggle_window_static)), @@ -210,7 +228,13 @@ class Kuro(BaseTheme): ## # Debug keyboard shortcuts ## - Key([self.mod, "shift", "control"], "w", lazy.function(display_wm_class)), + Key([self.mod, "control"], "c", lazy.function(display_wm_class)), + + # Redraw the top bar + Key([self.mod, "shift", "control"], "r", lazy.function(self.redraw_bar)), + + # Update visualizer widgets + Key([self.mod, "shift", "control"], "v", lazy.function(self.reinitialize_visualizers)), # Show extensive window info Key([self.mod, "shift", "control"], "i", lazy.function(self.show_window_info)), @@ -220,29 +244,34 @@ class Kuro(BaseTheme): ] def init_groups(self): - logger.warning("Initializing groups") + self.log_debug("Initializing groups") + + groups = [] + # http://fontawesome.io/cheatsheet - groups = [ - Group(""), - Group(""), - Group(""), - Group(""), - Group(""), - Group(""), - Group(""), - Group(""), - Group(""), - Group("", layout='floating', layouts=[ - layout.Floating( - border_focus="#990000", - border_normal="#440000" - ) - ]) - ] + groups.append(Group("", spawn=Config.get('web_browser', "true"))) + groups.append(Group("", spawn=Config.get('app_terminal_init', "true"))) + groups.append(Group("")) + groups.append(Group("", spawn=Config.get('app_chat', "true"))) + groups.append(Group("", spawn=Config.get('app_irc', "true"))) + groups.append(Group("", spawn=Config.get('file_manager', "true"))) + groups.append(Group("", spawn=Config.get('app_mail', "true"))) + groups.append(Group("")) + groups.append(Group("", spawn=Config.get('app_music', "true"))) + groups.append(Group("")) + groups.append(Group("", spawn=Config.get('apps_audio', "true"))) + groups.append(Group("", layout='floating', layouts=[ + layout.Floating( + border_focus="#990000", + border_normal="#440000" + ) + ])) + return groups def init_layouts(self): - logger.warning("Initializing layouts") + self.log_debug("Initializing layouts") + return [ kuro_layouts.KuroWmii( theme=self, @@ -262,7 +291,8 @@ class Kuro(BaseTheme): ] def init_widget_defaults(self): - logger.warning("Initializing widget_defaults") + self.log_debug("Initializing widget_defaults") + return { "font": Config.get('font_topbar', "Sans"), "fontsize": Config.get('fontsize_topbar', 16), @@ -270,12 +300,158 @@ class Kuro(BaseTheme): } def init_screens(self): - logger.warning("Initializing screens") - self.reinit_screens() - return self.screens + self.log_debug("Initializing screens") + + self.num_screens = utils.get_screen_count() + if self.num_screens <= 0: + self.num_screens = 1 + + screens = [] + for x in range(self.num_screens): + self.log_info("Initializing bars for screen {}".format(x)) + widgets = [] + widgets.extend([ + widget.GroupBox( + active=Config.get('colour_groupbox_icon_active', '#ffffff'), + borderwidth=Config.get('width_groupbox_border', 1), + disable_drag=Config.get('bool_groupbox_disable_drag', False), + font=Config.get('font_groupbox', 'Arial'), + fontsize=Config.get('fontsize_groupbox', 15), + highlight_color=Config.get("colour_groupbox_border_normal", '#444444'), + inactive=Config.get('colour_groupbox_icon_inactive', '#444444'), + rounded=Config.get('bool_groupbox_rounded_borders', True), + this_current_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'), + this_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'), + margin=Config.get('margin_groupbox', 0) + ), + widget.Prompt(**self.widget_defaults), + + kuro.utils.widgets.KuroTaskList( + border=Config.get('tasklist_border', '#ffffff'), + borderwidth=Config.get('tasklist_borderwidth', 1), + font=Config.get('tasklist_font', 'Arial'), + fontsize=Config.get('tasklist_fontsize', 15), + highlight_method=Config.get('tasklist_highlight_method', 'border'), + max_title_width=Config.get('tasklist_max_title_width', 200), + rounded=Config.get('tasklist_rounded', True), + urgent_alert_method=Config.get('tasklist_urgent_alert_method', 'border'), + urgent_border=Config.get('tasklist_urgent_border', '#ff0000'), + margin=Config.get('margin_groupbox', 0) + ) + ]) + + if Config.get('show_audio_visualizer', False): + widgets.append(kuro.utils.widgets.AudioVisualizerWidget(margin_x=0, margin_y=0)) + + widgets.extend([ + kuro.utils.widgets.MediaWidget(ignore_players=Config.get('media_ignore_players', '')), + kuro.utils.widgets.TextSpacerWidget(fontsize=14), + ]) + + if Config.get('show_temperature', False): + widgets.extend([ + kuro.utils.widgets.ThermalSensorWidget( + font=Config.get('font_topbar', 'Arial'), + fontsize=Config.get('fontsize_topbar', 16), + foreground=Config.get('thermal_colour', '#ffffff'), + foreground_alert=Config.get('thermal_colour_alert', '#ff0000'), + tag_sensor=Config.get('thermal_sensor', 'temp1'), + chip=Config.get('thermal_chip', None), + threshold=Config.get('thermal_threshold', 70), + update_interval=5, + fontsize_left=18, fontsize_right=11 + ), + ]) + + widgets.extend([ + 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), + ]) + if Config.get('show_battery_widget', False): + widgets.extend([ + kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11), + ]) + widgets.extend([ + kuro.utils.widgets.VolumeInfoWidget( + pulse_sink=Config.get('volume_pulse_sink', None), + fontsize_left=18, + fontsize_right=11, + font_left=Config.get('font_groupbox', None), + ), + kuro.utils.widgets.VolumeInfoWidget( + pulse_sink=Config.get('volume_pulse_sink2', None), + fontsize_left=18, + fontsize_right=11, + font_left=Config.get('font_groupbox', None), + ), + kuro.utils.widgets.TextSpacerWidget(fontsize=14), + kuro.utils.widgets.NetworkInfoWidget(fontsize_left=16, fontsize_right=14), + ]) + if Config.get('show_gpu_widget', False): + widgets.extend([ + 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, + ) + ]) + widgets.extend([ + kuro.utils.widgets.TextSpacerWidget(fontsize=14), + ]) + + # Systray can only be on one screen, so put it on the first + if x == 0: + widgets.append(widget.Systray(**self.widget_defaults)) + + widgets.extend([ + kuro.utils.widgets.KuroCurrentLayoutIcon(custom_icon_paths=Config.get('custom_layout_icon_paths', [])), + widget.Clock(format="%a %d %b, %H:%M", **self.widget_defaults), + kuro.utils.widgets.CheckUpdatesYay( + colour_no_updates=Config.get('updates_colour_none', '#ffffff'), + colour_have_updates=Config.get('updates_colour_available', '#ff0000'), + display_format=Config.get('updates_display_format', 'Updates: {updates}'), + execute=Config.get('updates_execute_command', None), + update_interval=Config.get('updates_interval', 600), + **self.widget_defaults + ), + widget.TextBox("#{}".format(x), name="default", **self.widget_defaults), + ]) + + topbar = utils.KuroTopBar( + theme=self, + background=Config.get('bar_background', '#000000'), + opacity=Config.get('bar_opacity', 1.0), + widgets=widgets, + size=Config.get('height_groupbox', 30) + ) + + self.topbars.append(topbar) + + screens.append(Screen(top=topbar)) + + # Add debug bars on each window if debugging is enabled + if Config.get('debug', False): + self.debug_textfields = [] + for x in range(self.num_screens): + textfield = widget.TextBox("...", name="debugtext", **self.widget_defaults) + self.debug_textfields.append(textfield) + widgets = [] + widgets.extend([ + widget.TextBox(" Debugging bar ", name="default", **self.widget_defaults), + textfield, + ]) + screens[x].bottom = bar.Bar( + widgets=widgets, + size=Config.get('height_debugbar', 30) + ) + self.debug_bars.append(screens[x].bottom) + + return screens def init_mouse(self): - logger.warning("Initializing mouse") + self.log_debug("Initializing mouse") + # Drag floating layouts. mouse = [ Drag([self.mod], "Button1", lazy.window.set_position_floating(), @@ -284,50 +460,12 @@ class Kuro(BaseTheme): start=lazy.window.get_size()), Click([self.mod], "Button2", lazy.window.bring_to_front()) ] + return mouse - def initialize_colorscheme(self): - colors = None - if os.path.isfile("/home/kevin/.cache/wal/colors.json"): - with open("/home/kevin/.cache/wal/colors.json", 'r') as f: - try: - colors = json.load(f)['colors'] - except KeyError: - colors = None - - if colors: - # Update Config - Config.foreground = colors['color15'] - Config.background = colors['color0'] - Config.highlight = colors['color3'] - Config.inactive_light = colors['color4'] - Config.inactive_dark = colors['color5'] - Config.bar_background = colors['color1'] - - def reinit_screens(self): - # TODO: Move backend check into utils method - if qtile.core.name == "x11": - self.num_screens = max(1, utils.get_screen_count()) - else: - self.num_screens = max(1, len(qtile.core.get_screen_info())) - logger.warning(f"Detected {self.num_screens} screens.") - - for x in range(self.num_screens): - logger.warning("Reconfiguring bars for screen {}".format(x)) - - try: - screen = self.screens[x] - except IndexError: - screen = Screen() - - if screen.top is None: - screen.top = self.build_bar_for_screen(x) - topbar = screen.top - - self.screens.append(Screen(top=topbar)) - def update_keys(self): - logger.warning("Updating keys") + self.log_debug("Updating keys") + for i, g in enumerate(self.groups): if i == 9: i = -1 @@ -359,229 +497,124 @@ class Kuro(BaseTheme): Key([self.mod], "n", lazy.layout.normalize()), ]) - def build_bar_for_screen(self, screen_num): - widgets = [ - # Workspaces - kuro.utils.widgets.KuroGroupBox( - active=Config.get('colour_groupbox_icon_active', '#ffffff'), - borderwidth=Config.get('width_groupbox_border', 1), - disable_drag=Config.get('bool_groupbox_disable_drag', False), - font=Config.get('font_groupbox', 'Arial'), - fontsize=Config.get('fontsize_groupbox', 15), - highlight_color=Config.get("colour_groupbox_border_normal", '#444444'), - inactive=Config.get('colour_groupbox_icon_inactive', '#444444'), - rounded=Config.get('bool_groupbox_rounded_borders', True), - this_current_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'), - this_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'), - margin=Config.get('margin_groupbox', 0) - ), + # Util functions + @staticmethod + def redraw_bar(qtile): + for s in qtile.screens: + s.top.draw() - # Spawn prompt (only shown if activated) - widget.Prompt(**self.widget_defaults), + @staticmethod + def update_screens(qtile): + out = utils.call_process(["xrandr", "--current"]) + video_mode = "nvidia" + #if "nvidia" in mode_out: + # video_mode = "nvidia" + #elif "intel" in mode_out: + # video_mode = "intel" + laptop_screen = None + screens = [] + for x in out.split("\n"): + if " connected " in x: + if Config.get("laptop_screen_nvidia", None) is not None \ + and Config.get("laptop_screen_intel", None) is not None: + if video_mode == "nvidia" and Config.get("laptop_screen_nvidia", None) in x: + laptop_screen = x + elif video_mode == "intel" and Config.get("laptop_screen_intel", None) in x: + laptop_screen = x + else: + screens.append(x) + else: + screens.append(x) - # Open window list - widget.TaskList( - border=Config.get('tasklist_border', '#ffffff'), - borderwidth=Config.get('tasklist_borderwidth', 1), - font=Config.get('tasklist_font', 'Arial'), - fontsize=Config.get('tasklist_fontsize', 15), - highlight_method=Config.get('tasklist_highlight_method', 'border'), - max_title_width=Config.get('tasklist_max_title_width', 200), - rounded=Config.get('tasklist_rounded', True), - urgent_alert_method=Config.get('tasklist_urgent_alert_method', 'border'), - urgent_border=Config.get('tasklist_urgent_border', '#ff0000'), - margin=Config.get('margin_groupbox', 0), - icon_size=Config.get('tasklist_iconsize', 17), - theme_mode=Config.get('tasklist_thememode', 'preferred'), - theme_path=Config.get('tasklist_themepath', '/usr/share/icons/Papirus-Dark'), - txt_floating=" ", txt_maximized=" ", txt_minimized=" ", - ) - ] + # Only configure two screens. Open arandr if more screens are present. + if laptop_screen is not None and len(screens) == 1: + laptop = laptop_screen.split()[0] + other = screens[0].split()[0] + utils.call_process(["xrandr", "--output", laptop, "--below", other]) + qtile.cmd_restart() + else: + utils.execute("arandr") - # Media widget(s) - widgets.extend([ - # An MPRIS widget that shows the media play status as an icon. - widget.Mpris2( - font=Config.get('font_groupbox', 'Arial'), - fontsize=Config.get('fontsize_groupbox', 15), - format="", - scroll=False, - playing_text="", - paused_text="", - stopped_text="", - no_metadata_text="", - name=f"media_icon{screen_num}", - mouse_callbacks={ - "Button1": lazy.widget[f"media_icon{screen_num}"].play_pause(), - "Button3": lazy.widget[f"media_icon{screen_num}"].next(), - "Button4": lambda: None, - "Button5": lambda: None, - } - ), - # An MPRIS widget that shows the currently playing song information in a nice format. - widget.Mpris2( - font=Config.get('font_topbar', 'Arial'), - fontsize=Config.get('fontsize_topbar', 15), - format="{xesam:title} - {xesam:artist} - {xesam:album}", - scroll=True, - width=300, # Maximum width before widget starts scrolling - playing_text="{track}", - paused_text="{track}", - stopped_text="", - no_metadata_text="No metadata available", - name=f"media_text{screen_num}", - mouse_callbacks={ - "Button1": lazy.widget[f"media_icon{screen_num}"].play_pause(), - "Button3": lazy.widget[f"media_icon{screen_num}"].next(), - "Button4": lambda: None, - "Button5": lambda: None, - } - ), - # An MPRIS widget masquerading as a text widget, that only shows "|" when media is playing or paused. - widget.Mpris2( - fontsize=14, - format="", - scroll=False, - playing_text="|", - paused_text="|", - stopped_text="", - no_metadata_text="", - mouse_callbacks={ - "Button1": lambda: None, - "Button4": lambda: None, - "Button5": lambda: None, - } - ) - ]) + def reinitialize_visualizers(self): + if Config.get("show_audio_visualizer", False): + logger.warning("Reinitializing visualizers...") + for screen in qtile.screens: + for widget in screen.top.widgets: + if isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget): + if widget.client is not None: + widget.client.kill() + widget.client = None + widget.screen = None + self.update_visualizers() - # Sensor widgets - sensor_widgets = [] + def update_visualizers(self): + if Config.get("show_audio_visualizer", False): + logger.warning("Updating visualizers..") + for screen in qtile.screens: + for widget in screen.top.widgets: + if isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget): + if widget.client is None: + logger.warning("Spawning for screen {}".format(screen)) + utils.execute(Config.get('visualizer_app', "glava")) + else: + widget.update_graph() - # Temperature sensor - if Config.get('show_temperature', False): - sensor_widgets.append(kuro.utils.widgets.ThermalSensorWidget( - font=Config.get('font_topbar', 'Arial'), - fontsize=Config.get('fontsize_topbar', 16), - foreground=Config.get('thermal_colour', '#ffffff'), - foreground_alert=Config.get('thermal_colour_alert', '#ff0000'), - tag_sensor=Config.get('thermal_sensor', 'temp1'), - chip=Config.get('thermal_chip', None), - threshold=Config.get('thermal_threshold', 70), - update_interval=5, - fontsize_left=18, fontsize_right=11 - )) + def show_window_info(self, qtile): + window = qtile.current_window if qtile else None - # CPU/Memory/Disk sensors - sensor_widgets.extend([ - 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), - ]) + import pprint + if window: + info = window.cmd_inspect() or None + name = window.name - widgets.extend([ - widget.WidgetBox( - font=Config.get('font_groupbox', 'Arial'), - fontsize=Config.get('fontsize_groupbox', 15), - text_open="[>]", - text_closed="[]", - widgets=sensor_widgets - ), - widget.TextBox(fontsize=14, text="|") - ]) + utils.notify(title="Window properties {}".format(name), + content="{}".format(pprint.pformat(vars(window)))) + logger.warning("{}".format(pprint.pformat(vars(window)))) - # Battery level - if Config.get('show_battery_widget', False): - widgets.extend([ - kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11), - ]) + if info: + info = pprint.pformat(info) + utils.notify(title="Window info of {}".format(name), + content="{}".format(info)) + logger.warning("{}".format(info)) - # Volume widget(s) - for sink_name in Config.get('volume_pulse_sinks', []): - widgets.append( - kuro.utils.widgets.VolumeInfoWidget( - pulse_sink=sink_name, - fontsize_left=18, - fontsize_right=11, - font_left=Config.get('font_groupbox', None), - ) - ) + # @staticmethod + def toggle_window_static(self, qtile): + window = qtile.current_window + if window in self.static_windows: + utils.notify("Unpinned {}".format(window.name), "{} has been unpinned".format(window.name)) + self.static_windows.remove(window) + del window.is_static_window + else: + utils.notify("Pinned {}".format(window.name), "{} has been pinned".format(window.name)) + self.static_windows.append(window) + window.is_static_window = True - # Network information - widgets.extend([ - widget.TextBox(fontsize=14, text="|"), - kuro.utils.widgets.NetworkInfoWidget( - fontsize_left=16, fontsize_right=14, - wireless_interface=Config.get('wifi_interface', None), - wired_interface=Config.get('wired_interface', None) - ), - ]) + window.floating = True - # GPU widget - if Config.get('show_gpu_widget', False): - widgets.extend([ - kuro.utils.widgets.GPUStatusWidget( - theme_path=Config.get( - 'gpu_theme_path', - '/usr/lib/python3.11/site-packages/libqtile/resources/battery-icons'), - padding=0, - ) - ]) - widgets.append(widget.TextBox(fontsize=14, text="|")) - - # Determine systray to use (X or Wayland) - # X system tray can only be on one screen, so only put it on the first - if qtile.core.name == "x11" and screen_num == 0: - widgets.append(widget.Systray(**self.widget_defaults)) - elif qtile.core.name != "x11": - widgets.append(extra_widget.StatusNotifier(**self.widget_defaults)) - - # Layout switcher, clock and Screen ID - widgets.extend([ - widget.CurrentLayoutIcon(custom_icon_paths=Config.get('custom_layout_icon_paths', [])), - widget.Clock(format="%a %d %b ", **self.widget_defaults), - widget.Clock( - format="%H:%M", - font=Config.get('font_topbar', "Sans"), - fontsize=16, - padding=0, - ), - widget.Clock( - format=":%S ", - font=Config.get('font_topbar', "Sans"), - fontsize=11, - padding=0, - ), - widget.CurrentScreen( - active_color="66FF66", inactive_color="FFFFFF", - active_text=f"#{screen_num}", inactive_text=f"#{screen_num}", - **self.widget_defaults - ) - ]) - - # Build the bar - return bar.Bar( - background=f"{Config.get('bar_background', '#000000')}{Config.get('bar_rgba_opacity', 'AA')}", - opacity=Config.get('bar_opacity', 1.0), - widgets=widgets, - size=Config.get('height_groupbox', 30) - ) + # Pinned toggle function + @staticmethod + def toggle_pinned(qtile): + windows = qtile.cmd_windows() + print(windows) # QTile base callbacks def callback_startup_once(self, *args, **kwargs): - logger.warning("Callback Startup Once") if not hasattr(qtile, 'theme_instance'): # Save theme instance in qtile qtile.theme_instance = self - self.set_random_wallpaper() + self.update_wallpaper(qtile) + + # Setup audio + # p = utils.execute_once(["qjackctl"]) + # p.wait() def callback_startup(self): - logger.warning("Callback Startup") if not hasattr(qtile, 'theme_instance'): # Save theme instance in qtile qtile.theme_instance = self - logger.warning("Restoring wallpaper...") if self.current_wallpaper: + #p = utils.execute_once(["wal", "-n", "-i", "{}".format(self.current_wallpaper)]) p = utils.execute_once(["wallust", "run", "{}".format(self.current_wallpaper)]) p.wait() else: @@ -593,91 +626,38 @@ class Kuro(BaseTheme): except KeyError: wallpaper = None if wallpaper: - self.set_wallpaper(wallpaper) + Kuro.set_wallpaper(qtile, wallpaper) else: - logger.warning("No wallpaper to restore.") + p = utils.execute_once("nitrogen --restore") + p.wait() + + self.log_info("Starting compositor...") + utils.execute_once("picom -b") + + self.log_info("Starting clipboard manager...") + utils.execute_once("xfce4-clipman") + + self.log_info("Starting notification daemon...") + utils.execute_once("dunst") + + self.log_info("Starting xiccd color profile manager...") + utils.execute_once("xiccd") + + #self.log_info("Starting KDE connect daemon...") + #utils.execute_once("/usr/lib/kdeconnectd") + + self.log_info("Starting KDE connect indicator...") + utils.execute_once("/usr/bin/kdeconnect-indicator") + + self.log_info("Starting automatic backup scheduler...") + utils.execute_once("/usr/bin/vorta") # Update color scheme - self.update_colorscheme() + self.initialize_colorscheme() - def callback_startup_complete(self, *args, **kwargs): - logger.warning("Callback Startup Complete") - if not hasattr(qtile, 'theme_instance'): - # Save theme instance in qtile - qtile.theme_instance = self - - # Update color scheme - self.update_colorscheme() - - # Setup XDG Desktop Portal on Wayland - if qtile.core.name == "wayland": - self.setup_xdg_desktop_portal() - - # After first startup is complete, autostart configured apps - logger.warning("Autostarting apps...") - for category in Config.get("apps_autostart", {}).keys(): - if qtile.core.name == category or category == "common": - logger.warning(f"Autostarting apps for {category}...") - for app in Config.get("apps_autostart", {}).get(category, []): - logger.warning(f"Starting '{app}'...") - utils.execute_once(app) - else: - logger.warning(f"Skipping autostart apps for {category}, because core is {qtile.core.name}...") - - for app in Config.get("apps_autostart_group", []): - if all(x in app.keys() for x in ["group", "command"]): - logger.warning(f"Starting '{app['command']}' in group {app['group']}...") - utils.start_in_group_once(theme=self, qtile=qtile, **app) - else: - logger.warning(f"Invalid app in 'apps_autostart_group', " - f"must have 'group' and 'command' keys: {app}...") - logger.warning("Autostart complete") - cur_time = time.time() - logger.warning(f"QTile startup completed! Started up in {(cur_time - self.startup_time):.1f} seconds!") - self.startup_completed = True - - def callback_client_managed(self, *args, **kwargs): - client: Optional[Window] = args[0] if len(args) > 0 else None - - # TODO: Move get_pid to an utility function - w_pid = None - try: - w_pid = client.get_pid() - except AttributeError: # Some windows might not have this .get_pid method. Try other ways - if isinstance(client, WaylandXWindow) or isinstance(client, WaylandXStatic): - w_pid = client.surface.pid - elif isinstance(client, XorgXWindow): - w_pid = client.get_net_wm_pid() - elif isinstance(client, LayerStatic): - pass # Wayland background layer 'window' - else: - logger.error(f"Unknown window type {client.__class__.__name__}") - - if w_pid is not None and w_pid in self.autostart_app_rules.keys(): - rule_id = self.autostart_app_rules[w_pid] - logger.warning(f"Removing rule {rule_id} for PID {w_pid}, client {client.name}") - lazy.remove_rule(rule_id) - - def callback_client_killed(self, *args, **kwargs): - client = args[0] - logger.warning("Client {} Killed".format(client)) - - # If this window was static, remove it from the static window list - if hasattr(client, "is_static_window") and client.is_static_window: - logger.warning("Removing static window {}".format(client.name)) - del client.is_static_window - self.static_windows.remove(client) - - def callback_screen_change(self, *args, **kwargs): - logger.warning(f"Screen configuration changed, reinitializing screens") - self.reinit_screens() - qtile.reconfigure_screens() - #qtile.reconfigure_screens() # Twice, see: https://github.com/qtile/qtile/issues/4673#issuecomment-2196459114 - - #def callback_screens_reconfigured(self, *args, **kwargs): - logger.warning(f"Screens were reconfgured, updating wallpapers and color scheme") - self.set_wallpaper(self.current_wallpaper) - self.update_colorscheme() + # def callback_screen_change(self, *args, **kwargs): + # for window in self.static_windows: + # window.togroup() def callback_setgroup(self, *args, **kwargs): for window in self.static_windows: @@ -690,28 +670,121 @@ class Kuro(BaseTheme): del window.is_static_window self.static_windows.remove(window) - def show_window_info(self, *args, **kwargs): - import pprint - window = qtile.current_window - if window: - logger.warning(f"Window properties {window.name}\n{pprint.pformat(vars(window))}") - if window.info(): - logger.warning(f"Window info of {window.name}\n{pprint.pformat(window.info())}") + def callback_focus_change(self, *args, **kwargs): + if self.do_keyboard_updates: + kb_handle_focus_change(self) - def toggle_window_static(self, *args, **kwargs): - window = qtile.current_window - if window in self.static_windows: - utils.notify(qtile, "Unpinned {}".format(window.name), "{} has been unpinned".format(window.name)) - self.static_windows.remove(window) - del window.is_static_window - else: - utils.notify(qtile, "Pinned {}".format(window.name), "{} has been pinned".format(window.name)) - self.static_windows.append(window) - window.is_static_window = True + initial_windows = [] - window.floating = True + def callback_startup_complete(self, *args, **kwargs): + if not hasattr(qtile, 'theme_instance'): + # Save theme instance in qtile + qtile.theme_instance = self + + # Only run on first startup + if not qtile.no_spawn: + dg = qtile.dgroups + for r in dg.rules: + pid = -1 + # noinspection PyProtectedMember + for m in r.matchlist: + if m._rules.get('net_wm_pid', None) is not None: + pid = m._rules.get('net_wm_pid') + break + if pid != -1: + self.initial_windows.append((pid, r.group)) - def set_random_wallpaper(self, *args, **kwargs): + self.callback_client_new() + + # After first startup is complete, start the audio apps that can only be started after boot is complete + if not qtile.no_spawn: + for app in Config.get("apps_audio_afterstart", []): + utils.execute_once(app) + + # Update color scheme + Kuro.update_colorscheme(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: + for group in qtile.groups: + 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 = qtile.dgroups.rules_map.copy() + for rid, r in c.items(): + if r.matches(window): + qtile.dgroups.remove_rule(rid) + self.initial_windows.remove((pid, gname)) + self.log_info("Removed group rule for PID {}, window {}".format(pid, + window.name)) + self.log_info(str(qtile.dgroups.rules_map)) + + # Check if it is a visualizer + if Config.get("show_audio_visualizer", False): + if client is not None and client.window.get_name() == "GLava": + placed = False + for screen in qtile.screens: + for widget in screen.top.widgets: + if not placed and isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget): + if widget.client is None: + viz_info = widget.info() + pos_x = viz_info['offset'] + widget.margin_x + pos_y = 0 + widget.margin_y + width = viz_info['width'] - (2 * widget.margin_x) + height = viz_info['height'] - (2 * widget.margin_y) + screen_index = qtile.screens.index(screen) + logger.warning("Attaching {} {} to {} on screen {}".format(client, client.window.wid, type(widget).__name__, screen_index)) + c = KuroStatic.create(client, screen, x=pos_x, y=pos_y, width=width, height=height) + c.opacity = Config.get("bar_opacity", 1.0) + widget.set_client(c, screen) + placed = True + if not placed: + if Config.get("kill_unnecessary_glava_processes", False): + logger.warning("Killing GLava {} because there is no widget where it can fit".format(client)) + utils.notify("Glava", "Killing new GLava process because there is no screen without a visualizer") + client.kill() + else: + 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)) + + # Detach visualizer from widget if it was a visualizer window + if isinstance(client, KuroStatic): + for screen in qtile.screens: + for widget in screen.top.widgets: + if isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget): + if widget.client == client: + screen_index = qtile.screens.index(screen) + logger.warning("Detaching {} {} from widget {} on screen {}".format(client, client.window.wid, type(widget).__name__, screen_index)) + widget.client = None + widget.screen = None + + # If this window was static, remove it from the static window list + if hasattr(client, "is_static_window") and client.is_static_window: + logger.warning("Removing static window {}".format(client.name)) + del client.is_static_window + self.static_windows.remove(client) + + @staticmethod + def update_wallpaper(qtile): wallpapers = [] wallpaper_dir = Config.get("desktop_bg_folder", "") @@ -727,39 +800,59 @@ class Kuro(BaseTheme): except os.error as e: logger.warning("Could not load wallpapers from directory: {}".format(e)) + if wallpapers: if Config.get("desktop_bg_override", False): - wallpaper_file = Config.get("desktop_bg_override", "") + qtile.theme_instance.current_wallpaper = Config.get("desktop_bg_override", "") else: - wallpaper_file = os.path.join(wallpaper_dir, random.choice(wallpapers)) - logger.warning(f"Selected new wallpaper: {wallpaper_file}") - self.set_wallpaper(wallpaper_file) + qtile.theme_instance.current_wallpaper = os.path.join(wallpaper_dir, random.choice(wallpapers)) + logger.warning("Selected new wallpaper: {}".format(qtile.theme_instance.current_wallpaper)) + Kuro.set_wallpaper(qtile, qtile.theme_instance.current_wallpaper) else: - logger.warning("Random wallpaper requested but no wallpapers are available.") + utils.execute_once("nitrogen --restore") - def set_wallpaper(self, filename): - if qtile.core.name == "x11": - p = utils.execute_once(f"{Config.get('wallpaper_config_command', 'wal-nitrogen-noupdate')} {filename}") - p.wait() - else: - # Wayland can set wallpaper in qtile directly per screen - for screen_i, screen in enumerate(qtile.screens): - sinfo = screen.info() - sfilename = filename - if sinfo.get('width', 100) < sinfo.get('height', 10): - # Vertical screen, see if there is a vertical alt wallpaper - basename, ext = os.path.splitext(filename) - new_filename = f"{basename}.vertical{ext}" - if os.path.isfile(new_filename): - sfilename = new_filename - logger.warning(f"Setting Screen#{screen_i} wallpaper to {sfilename}.") - screen.set_wallpaper(sfilename, "fill") - self.current_wallpaper = filename - def update_colorscheme(self, *args, **kwargs): - if self.current_wallpaper: - logger.warning(f"Updating wal colors for wallpaper {self.current_wallpaper}") - p = utils.execute_once(["wallust", "run", "{}".format(self.current_wallpaper)]) + @staticmethod + def set_wallpaper(qtile, filename): + p = utils.execute_once("{} {}".format(Config.get('wallpaper_config_command', 'wal-nitrogen-noupdate'), + filename)) + p.wait() + qtile.theme_instance.current_wallpaper = filename + Kuro.update_colorscheme(qtile) + + + @staticmethod + def update_mediaclients(*args, **kwargs): + return str(str(args) + " " + str(kwargs)) + + + @staticmethod + def initialize_colorscheme(): + colors = None + if os.path.isfile("/home/kevin/.cache/wal/colors.json"): + with open("/home/kevin/.cache/wal/colors.json", 'r') as f: + try: + colors = json.load(f)['colors'] + except KeyError: + colors = None + + if colors: + # Update Config + Config.foreground = colors['color15'] + Config.background = colors['color0'] + Config.highlight = colors['color3'] + Config.inactive_light = colors['color4'] + Config.inactive_dark = colors['color5'] + Config.bar_background = colors['color1'] + + @staticmethod + def update_colorscheme(qtile): + """ + :type qtile: libqtile.manager.Qtile + """ + if qtile.theme_instance.current_wallpaper: + #p = utils.execute(["wal", "-n", "-i", "{}".format(qtile.theme_instance.current_wallpaper)]) + p = utils.execute(["wallust", "run", "{}".format(qtile.theme_instance.current_wallpaper)]) p.wait() colors = None @@ -788,81 +881,62 @@ class Kuro(BaseTheme): layout.border_normal = colors['color1'] layout.border_normal_stack = colors['color1'] - for screen_i, screen in enumerate(qtile.screens): + for screen in qtile.screens: bar = screen.top - logger.warning(f"Updating colorscheme for screen {screen_i}") - if bar: - bar.background = f"{colors['color1']}{Config.get('bar_rgba_opacity', 'AA')}" - bar.drawer.clear(bar.background) + bar.background = colors['color1'] + bar.drawer.clear(bar.background) + for w in bar.widgets: + if hasattr(w, '_update_drawer'): + try: + w._update_drawer() + except Exception as e: + logger.error("Error while updating drawer for widget {}: {}".format(w, e)) - def update_widget(w): - if hasattr(w, '_update_drawer'): - try: - w._update_drawer() - except Exception as e: - logger.error("Error while updating drawer for widget {}: {}".format(w, e)) + if hasattr(w, 'foreground'): + w.foreground = colors['color15'] - if hasattr(w, 'foreground'): - w.foreground = colors['color15'] + if hasattr(w, 'foreground_normal'): + w.foreground_normal = colors['color15'] - if hasattr(w, 'foreground_normal'): - w.foreground_normal = colors['color15'] + if hasattr(w, 'foreground_alert'): + w.foreground_alert = colors['color3'] - if hasattr(w, 'foreground_alert'): - w.foreground_alert = colors['color3'] + if hasattr(w, 'border'): + w.border = colors['color15'] - if hasattr(w, 'border'): - w.border = colors['color15'] + if hasattr(w, 'active'): + w.active = colors['color15'] - if hasattr(w, 'active'): - w.active = colors['color15'] + if hasattr(w, 'highlight_color'): + w.highlight_color = colors['color3'] - if hasattr(w, 'highlight_color'): - w.highlight_color = colors['color3'] + if hasattr(w, 'inactive'): + w.inactive = colors['color8'] - if hasattr(w, 'inactive'): - w.inactive = colors['color8'] + if hasattr(w, 'this_current_screen_border'): + w.this_current_screen_border = colors['color15'] - if hasattr(w, 'this_current_screen_border'): - w.this_current_screen_border = colors['color15'] + if hasattr(w, 'this_screen_border'): + w.this_screen_border = colors['color15'] - if hasattr(w, 'this_screen_border'): - w.this_screen_border = colors['color15'] + if hasattr(w, 'other_current_screen_border'): + w.other_current_screen_border = colors['color8'] - if hasattr(w, 'other_current_screen_border'): - w.other_current_screen_border = colors['color8'] + if hasattr(w, 'other_screen_border'): + w.other_screen_border = colors['color8'] - if hasattr(w, 'other_screen_border'): - w.other_screen_border = colors['color8'] + if isinstance(w, kuro.utils.widgets.AudioVisualizerWidget): + w.graph_color = colors['color15'] + w.fill_color = colors['color8'] - if isinstance(w, widget.WidgetBox): - for subw in w.widgets: - update_widget(subw) + bar.draw() - for w in bar.widgets: - update_widget(w) + # Update colors in visualizers and restart visualizers + with open(Config.get("glava_color_file_path", "~/.config/glava/kurobars_color.glsl"), 'w') as f: + f.write("#define COLOR {}\n#request setbg {}00".format(colors['color15'], colors['color1'][1:])) + qtile.theme_instance.reinitialize_visualizers() - bar.draw() - else: - logger.warning(f"Screen {screen_i} has no bar?") - - # Attempt to call pywalfox to update firefox/thunderbird colors - try: - logger.warning(f"Calling 'pywalfox update'...") - p = utils.execute(["pywalfox", "update"]) - p.wait() - except subprocess.SubprocessError as e: - logger.error(f"Error running 'pywalfox update': {e}") - - utils.notify(qtile, - "Updated colorscheme!", - f"active: {colors['color15']}, inactive: {colors['color1']}") - - def setup_xdg_desktop_portal(self): - # XDG Desktop portal is used for screensharing, screenshots and filepickers in wayland. - # To work correctly, it needs to have two env variables set in the systemd user session - logger.warning(f"Setting XDG_CURRENT_DESKTOP env and updating XDG Desktop Portal configuration...") - os.environ["XDG_CURRENT_DESKTOP"] = "qtile" - subprocess.Popen(["systemctl", "--user", "import-environment", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP"]) - subprocess.Popen(["dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP=qtile"]) - subprocess.Popen(["systemctl", "--user", "restart", "xdg-desktop-portal"]) + utils.notify( + "Updated colorscheme!", + "active: {}, inactive: {}".format(colors['color15'], colors['color1']) + ) diff --git a/kuro/utils/__init__.py b/kuro/utils/__init__.py index 7d8a838..e69de29 100644 --- a/kuro/utils/__init__.py +++ b/kuro/utils/__init__.py @@ -1,31 +0,0 @@ -import importlib -import socket -import traceback -from libqtile.log_utils import logger - -def load_config_class(): - # Try to import host-specific configuration first - hostname = socket.gethostname().lower() - if hostname: - try: - host_module = importlib.import_module(f"kuro.config.{hostname}") - return getattr(host_module, "Config") - except ImportError: - pass - logger.warning(f"No host-specific configuration available for {hostname}. Loading general config...") - - # If no config yet, load general Kuro Config object - try: - conf_module = importlib.import_module("kuro.config") - return getattr(conf_module, "Config") - except ImportError as e: - logger.error(traceback.format_exc()) - logger.error("Could not load Kuro Config. Trying to load BaseConfig. Error: {}".format(e)) - - # If no config yet, load fallback BaseConfig - try: - base_module = importlib.import_module("kuro.base") - return getattr(base_module, "BaseConfig") - except ImportError as e: - logger.error(traceback.format_exc()) - return None diff --git a/kuro/utils/general.py b/kuro/utils/general.py index c8d6a8c..f63e1ad 100644 --- a/kuro/utils/general.py +++ b/kuro/utils/general.py @@ -2,14 +2,20 @@ import re import subprocess import traceback from time import sleep -from typing import List import notify2 +import six from dbus import DBusException from libqtile import widget +from libqtile.backend.x11.window import Internal +from libqtile.bar import Bar from notify2 import Notification, URGENCY_NORMAL from libqtile.log_utils import logger -from libqtile import qtile + +try: + notify2.init("QTileWM") +except DBusException as e: + logger.error("Could not initialize notify2: {}".format(e)) BUTTON_LEFT = 1 BUTTON_MIDDLE = 2 @@ -32,68 +38,40 @@ def is_running(process): def execute(process): - try: + if isinstance(process, list): + return subprocess.Popen(process) + elif isinstance(process, str): + return subprocess.Popen(process.split()) + else: + pass + + +def execute_once(process): + if not is_running(process): if isinstance(process, list): return subprocess.Popen(process) elif isinstance(process, str): return subprocess.Popen(process.split()) else: - logger.info(f"Failed to execute_once") - except FileNotFoundError as e: - logger.error(f"Could not execute {process}, FileNotFoundError - {e}") - - -def execute_once(process): - logger.info(f"Attempting to execute_once: {process}") - if not is_running(process): - return execute(process) - logger.info(f"Process was already running: {process}") - - -def start_in_group(theme, qtile, group: str, command: List[str], floating: bool = False, - intrusive: bool = False, dont_break: bool = False): - try: - proc = subprocess.Popen(command) - match_args = {"net_wm_pid": proc.pid} - rule_args = { - "float": floating, - "intrusive": intrusive, - "group": group, - "break_on_match": not dont_break, - } - rule_id = qtile.add_rule(match_args, rule_args) - theme.autostart_app_rules[proc.pid] = rule_id - return proc - except FileNotFoundError as e: - logger.error(f"Could not execute {process}, FileNotFoundError - {e}") - - -def start_in_group_once(theme, qtile, group: str, command: List[str], floating: bool = False, - intrusive: bool = False, dont_break: bool = False): - logger.info(f"Attempting to start_in_group_once: {command}") - if not is_running(command): - return start_in_group(theme=theme, qtile=qtile, group=group, command=command, - floating=floating, intrusive=intrusive, dont_break=dont_break) - logger.info(f"Process was already running: {command}") + pass def call_process(command, **kwargs): """ - Run the given command and return the string from stdout. + This method uses `subprocess.check_output` to run the given command + and return the string from stdout, which is decoded when using + Python 3. """ - return subprocess.check_output(command, **kwargs).decode() + output = subprocess.check_output(command, **kwargs) + if six.PY3: + output = output.decode() + return output def get_screen_count(): try: - if qtile.core.name == "x11": - logger.info("Using xrandr to detect screen count") - output = subprocess.check_output("xrandr -q".split()).decode('utf-8') - output = [x for x in output.split("\n") if " connected" in x] - else: - logger.info("Using lsmon (wallutils) to detect screen count") - output = subprocess.check_output(["lsmon"]).decode('utf-8') - output = output.split("\n") + output = subprocess.check_output("xrandr -q".split()).decode('utf-8') + output = [x for x in output.split("\n") if " connected" in x] except subprocess.CalledProcessError: return 1 @@ -109,21 +87,8 @@ def bar_separator(config): padding=config.get('padding_spacer', 4), ) -def init_notify(qtile): - if qtile and qtile.theme_instance and qtile.theme_instance.startup_completed: - try: - if not notify2.is_initted(): - logger.warning("Initializing Notify2") - notify2.init("QTileWM") - except DBusException: - logger.error(f"Failed to initialize Notify2 (DBus error), retrying later.") - except Exception: - logger.error(f"Failed to initialize Notify2 (Generic error), retrying later.") - else: - logger.warning(f"Not initializing Notify2 yet, QTile startup not completed.") - -def notify(qtile, title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): +def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): if image is not None: notification = Notification( summary=title, message=content, @@ -136,14 +101,13 @@ def notify(qtile, title, content, urgency=URGENCY_NORMAL, timeout=5000, image=No notification.set_timeout(timeout) notification.set_urgency(urgency) - init_notify(qtile) - try: - try: - return notification.show() - except notify2.UninittedError: - logger.warning("Notify2 is not initialized") - except Exception as e: + return notification.show() + except notify2.UninittedError: + logger.warning("Notify2 was uninitialized, initializing...") + notify2.init("qtile") + return notification.show() + except DBusException as e: logger.warning("Showing notification failed: {}".format(e)) logger.warning(traceback.format_exc()) @@ -161,10 +125,6 @@ def spawn_popup(qtile, x, y, text): :return: The popup instance :rtype: Internal """ - if qtile.core.name == "x11": - from libqtile.backend.x11.window import Internal - else: - from libqtile.backend.wayland.window import Internal popup = Internal.create( qtile, x, y, 100, 100, opacity=1 ) @@ -195,14 +155,14 @@ def test_popups(qtile): def display_wm_class(qtile): - window = qtile.current_window if qtile else None + window = qtile.currentWindow if qtile else None if window: - wm_class = window.get_wm_class() or None + wm_class = window.window.get_wm_class() or None name = window.name if wm_class: - notify(qtile=qtile, title="WM_Class of {}".format(name), + notify(title="WM_Class of {}".format(name), content="{}".format(wm_class), urgency=notify2.URGENCY_CRITICAL) @@ -229,3 +189,76 @@ def bluetooth_audio_sink(): def bluetooth_audio_connected(): return bluetooth_audio_sink() != -1 + + +class KuroTopBar(Bar): + def __init__(self, theme, widgets, size, **config): + self.theme = theme + super(KuroTopBar, self).__init__(widgets, size, **config) + + def _configure(self, qtile, screen, *args, **kwargs): + super(KuroTopBar, self)._configure(qtile, screen) + self.window.handle_EnterNotify = self.handle_enter_notify + self.window.handle_LeaveNotify = self.handle_leave_notify + self.window.window.set_property("_NET_WM_NAME", "KuroTopBar") + self.window.update_name() + + def draw(self): + if not self.widgets: + return + if not self._draw_queued: + self.future = self.qtile.call_soon(self._actual_draw) + self._draw_queued = True + + def _actual_draw(self): + self._draw_queued = False + self._resize(self._length, self.widgets) + for i in self.widgets: + i.draw() + if self.widgets: + end = i.offset + i.length + if end < self._length: + if self.horizontal: + self.drawer.draw(offsetx=end, width=self._length - end) + else: + self.drawer.draw(offsety=end, height=self._length - end) + + self.theme.update_visualizers() + + def handle_enter_notify(self, e): + # self.theme.log_debug("Bar HandleEnterNotify") + # + # self.window.opacity = Config.get('bar_hover_opacity', 1.0) + # print("Bar Hover Enter") + # + # try: + # hovered_widget = [x for x in self.widgets if (x.offsetx + x.width) >= e.event_x][0] + # except IndexError: + # hovered_widget = None + # + # self.theme.log_debug("Hovered over {}".format(hovered_widget)) + # + # if hasattr(hovered_widget, "handle_hover_enter"): + # hovered_widget.handle_hover_enter(e) + + self.draw() + + def handle_leave_notify(self, e): + # self.theme.log_debug("Bar HandleLeaveNotify") + # + # self.window.opacity = Config.get('bar_opacity', 1.0) + # print("Bar Hover Leave") + # + # try: + # hovered_widget = [x for x in self.widgets if (x.offsetx + x.width) >= e.event_x][0] + # except IndexError: + # hovered_widget = None + # + # self.theme.log_debug("Hovered over {}".format(hovered_widget)) + # + # if hasattr(hovered_widget, "handle_hover_leave"): + # hovered_widget.handle_hover_leave(e) + + self.draw() + + diff --git a/kuro/utils/kb_backlight.py b/kuro/utils/kb_backlight.py index e684735..b818ec4 100644 --- a/kuro/utils/kb_backlight.py +++ b/kuro/utils/kb_backlight.py @@ -55,7 +55,7 @@ def handle_focus_change(theme): window = qtile.currentWindow if qtile else None if window: - wm_class = window.get_wm_class() or None + wm_class = window.window.get_wm_class() or None name = window.name if wm_class: diff --git a/kuro/utils/layouts.py b/kuro/utils/layouts.py index 719b526..e74dee6 100644 --- a/kuro/utils/layouts.py +++ b/kuro/utils/layouts.py @@ -32,25 +32,29 @@ class KuroFloating(Floating): # 'sun-awt-X11-XWindowPeer' is a dropdown used in Java application, # don't reposition it anywhere, let Java app to control it - cls = client.get_wm_class() or "" - is_java_dropdown = "sun-awt-X11-XWindowPeer" in cls + cls = client.window.get_wm_class() or '' + is_java_dropdown = 'sun-awt-X11-XWindowPeer' in cls if is_java_dropdown: client.paint_borders(bc, bw) - client.bring_to_front() + client.cmd_bring_to_front() # alternatively, users may have asked us explicitly to leave the client alone elif any(m.compare(client) for m in self.no_reposition_rules): client.paint_borders(bc, bw) - client.bring_to_front() + client.cmd_bring_to_front() else: above = False # We definitely have a screen here, so let's be sure we'll float on screen - if client.float_x is None or client.float_y is None: + try: + client.float_x + client.float_y + except AttributeError: # this window hasn't been placed before, let's put it in a sensible spot above = self.compute_client_position(client, screen_rect) + client.place( client.x, client.y, @@ -59,6 +63,5 @@ class KuroFloating(Floating): bw, bc, above, - respect_hints=True, ) client.unhide() diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index aff7f56..442595c 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -7,19 +7,62 @@ import cairocffi import iwlib import netifaces import psutil -from libqtile import bar, qtile +import six +import unicodedata +from libqtile import bar, pangocffi from libqtile.log_utils import logger +from libqtile.command.base import expose_command 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.widget.groupbox import GroupBox -from libqtile.command.base import expose_command +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. @@ -224,6 +267,364 @@ class DualPaneTextboxBase(base._Widget): 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 + + @expose_command() + def 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'] + 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'] + 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'] + 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'] + + 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 = list(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 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.""" @@ -254,11 +655,7 @@ class GPUStatusWidget(base._TextBox): self.icons.update(self.custom_icons) def _get_info(self): - try: - output = self.call_process(self.check_command, shell=True) - except subprocess.CalledProcessError as e: - logger.error(f"Error while calling {self.check_command} - {e}") - output = None + 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} @@ -334,16 +731,14 @@ class GPUStatusWidget(base._TextBox): if button == BUTTON_LEFT: try: next_gpu = self.call_process(self.next_command, shell=True).split(":")[1].strip() - except subprocess.CalledProcessError as e: - logger.error(f"Error while calling {self.next_command} - {e}") except IndexError: next_gpu = "Unknown" if self.current_status == "Unknown": - notify(None, "GPU Status", "The currently used GPU is unknown.\n\nAfter the next login it will be the {} GPU.".format(next_gpu), + 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(None, "GPU Status", "The system is currently running on the {} GPU. Press the middle mouse " + 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 ), @@ -352,25 +747,38 @@ class GPUStatusWidget(base._TextBox): if button == BUTTON_MIDDLE: command = ["optimus-manager", "--no-confirm", "--switch", "auto"] - try: - output = self.call_process(command) - except subprocess.CalledProcessError as e: - logger.error(f"Error while calling {command} - {e}") - output = "" + output = self.call_process(command) if "nvidia" in output: - notify(None, "GPU Switched", "The GPU has been switched from Intel to NVidia.\n" + 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(None, "GPU Switched", "The GPU has been switched from NVidia to Intel.\n" + 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(None, "GPU Switch Error", "I could not determine if the GPU was switched successfully.\n" + 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'), @@ -408,11 +816,7 @@ class ThermalSensorWidget(DualPaneTextboxBase): self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): - try: - sensors_out = self.call_process(self.get_command()) - except subprocess.CalledProcessError as e: - logger.error(f"Error while calling {self.get_command()} - {e}") - return + sensors_out = self.call_process(self.get_command()) temperature_values = {} for name, temp, symbol in self.sensors_temp.findall(sensors_out): name = name.strip() @@ -442,7 +846,7 @@ class ThermalSensorWidget(DualPaneTextboxBase): def button_press(self, x, y, button): if button == BUTTON_LEFT: - notify(None, "Temperature Information", "\n".join( + notify("Temperature Information", "\n".join( "{}: {}{}".format(name, *values) for name, values in self.values.items() )) @@ -495,7 +899,7 @@ class CPUInfoWidget(DualPaneTextboxBase): def button_press(self, x, y, button): if button == BUTTON_LEFT: total = sum([self.cpu_old[0], self.cpu_old[1], self.cpu_old[2], self.cpu_old[3]]) - notify(None, "CPU Information", "user: {} %\nnice: {} %\nsys: {} %\nidle: {} %\ntotal: {} %".format( + 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), @@ -548,7 +952,7 @@ class MemoryInfoWidget(DualPaneTextboxBase): val['SwapUsed'] = swap.used // 1024 // 1024 if button == BUTTON_LEFT: - notify(None, "Memory Information", "Memory: {}MB / {}MB\n {}%\nSwap: {}MB / {}MB\n {}%".format( + 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'], @@ -604,7 +1008,7 @@ class DiskIOInfoWidget(DualPaneTextboxBase): def button_press(self, x, y, button): if button == BUTTON_LEFT: - notify(None, "Disk IO Information", + notify("Disk IO Information", "Time that there were IO requests queued for /dev/{}: {} ms".format(self.hdd_device, self.io)) @@ -649,70 +1053,57 @@ class NetworkInfoWidget(DualPaneTextboxBase): def _update_values(self): # Wifi - if self.wireless_interface: - try: - essid, quality = get_status(self.wireless_interface) - status = iwlib.get_iwconfig(self.wireless_interface) - self.wireless_ips = netifaces.ifaddresses(self.wireless_interface) - disconnected = essid is None - percent = math.ceil(((quality or 0) / 70) * 100) - self.wireless_quality = quality - self.wireless_signal = percent - self.wireless_name = essid - self.wireless_connected = not disconnected - self.wireless_accesspoint = status.get('Access Point', b'Unknown').decode() - self.wireless_frequency = status.get('Frequency', b'Unknown').decode() - self.wireless_ipv4 = self.wireless_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr'] - self.wireless_ipv6 = self.wireless_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr'] - self.wireless_mac = self.wireless_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr'] - except EnvironmentError: - pass + 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 - if self.wired_interface: - try: - self.wired_ips = netifaces.ifaddresses(self.wired_interface) - self.wired_ipv4 = self.wired_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr'] - self.wired_ipv6 = self.wired_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr'] - self.wired_mac = self.wired_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr'] - command = ["ip", "link", "show", "{}".format(self.wired_interface)] - try: - eth_status = call_process(command) - except subprocess.CalledProcessError as e: - logger.error(f"Error while calling {command} - {e}") - return - m = self.wired_up_regex.search(eth_status) - if m: - self.wired_connected = "UP" in m.group(1) - else: - self.wired_connected = False + 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 + except (EnvironmentError, ValueError): + pass def update(self): self._update_values() self.draw() def draw(self): - if self.wireless_interface: - if self.wireless_connected: - strength = "" - if self.wireless_signal < 66: - strength = "" - if self.wireless_signal < 33: - strength = "" - self.text_left = strength - else: - self.text_left = "" + 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 = "" + self.text_left = "" - if self.wired_interface: - if self.wired_connected: - self.text_right = "" - else: - self.text_right = "" + if self.wired_connected: + self.text_right = "" else: self.text_right = "" @@ -744,9 +1135,9 @@ class NetworkInfoWidget(DualPaneTextboxBase): wired_text = "Wired: Not connected" if wifi_text: - notify(None, title, "{}\n\n{}".format(wifi_text, wired_text)) + notify(title, "{}\n\n{}".format(wifi_text, wired_text)) else: - notify(None, title, "\n{}".format(wired_text)) + notify(title, "\n{}".format(wired_text)) class BatteryInfoWidget(DualPaneTextboxBase): @@ -822,7 +1213,7 @@ class BatteryInfoWidget(DualPaneTextboxBase): def button_press(self, x, y, button): if button == BUTTON_LEFT: output = subprocess.check_output(self.status_cmd).decode('utf-8') - notify(None, "Battery Status", output) + notify("Battery Status", output) class VolumeInfoWidget(DualPaneTextboxBase): @@ -857,8 +1248,7 @@ class VolumeInfoWidget(DualPaneTextboxBase): else: cmd = self.status_cmd mixer_out = self.call_process(cmd.split(" ")) - except subprocess.CalledProcessError as e: - logger.error(f"Error while calling {cmd} - {e}") + except subprocess.CalledProcessError: return -1 try: return int(mixer_out) @@ -902,7 +1292,7 @@ class VolumeInfoWidget(DualPaneTextboxBase): output = subprocess.check_output(cmd.split(" ")).decode('utf-8') sink = "Sink {}\n".format(self.pulse_sink) if self.pulse_sink else "" - notify(None, "Volume Status", sink+output) + notify("Volume Status", sink+output) elif button == BUTTON_RIGHT: if "{sink}" in self.volume_app: @@ -953,16 +1343,3 @@ class VolumeInfoWidget(DualPaneTextboxBase): def run_app(self): # Emulate button press. self.button_press(0, 0, BUTTON_RIGHT) - - -class KuroGroupBox(GroupBox): - @property - def length(self): - try: - return super(KuroGroupBox, self).length - except AttributeError: - return 1 - - @length.setter - def length(self, length): - logger.warning(f"Setting groupbox length to {length}") diff --git a/kuro/utils/windows.py b/kuro/utils/windows.py index 7370274..6bfd997 100644 --- a/kuro/utils/windows.py +++ b/kuro/utils/windows.py @@ -1,11 +1,6 @@ from cairocffi.test_xcb import xcffib -from libqtile import hook, qtile - -if qtile.core.name == "x11": - from libqtile.backend.x11.window import Window, Static -else: - from libqtile.backend.wayland.window import Window, Static - +from libqtile import hook +from libqtile.backend.x11.window import Window, Static class KuroStatic(Static): diff --git a/required_packages.txt b/required_packages.txt index 3462d93..ff3bd88 100644 --- a/required_packages.txt +++ b/required_packages.txt @@ -4,34 +4,9 @@ notification-daemon otf-font-awesome python-osc -qtile-extras - # /optional/ playerctl xfce4-screenshooter xfce4-clipman-plugin wireless_tools - -# Utilities -kdeconnect # KDE Connect -vorta # Backup scheduler - -# Xorg-only -picom # Compositor -xfce4-clipman # Clipboard manager -dunst # Notification daemon -xiccd # Color profile manager - -# Wayland-only -xorg-xwayland -python-pywlroots -wofi # dmenu replacement -grim # Screenshot utility -swappy # Screenshot editor -slurp # Region selector -cliphist # Clipboard history -mako # Notifications daemon -kanshi # Display hotplugging -wallutils # Display/wallpaper utilities (lsmon) -papirus-icon-theme # Icon theme