From 6dd362247e0b7337278598d27f8129efc710857e Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Fri, 25 Jul 2025 17:39:36 +0200 Subject: [PATCH] Unify configs so we can get rid of all those branches per machine. Also Wayland changes for Violet --- config.py | 18 +++-- kuro/base.py | 17 ++++- kuro/{config.py => config/__init__.py} | 53 +++++++++----- kuro/config/aria.py | 45 ++++++++++++ kuro/config/meconopsis.py | 22 ++++++ kuro/config/violet.py | 26 +++++++ kuro/theme.py | 70 ++++++++++--------- kuro/utils/__init__.py | 30 ++++++++ kuro/utils/widgets.py | 96 ++++++++++++++------------ required_packages.txt | 2 + 10 files changed, 274 insertions(+), 105 deletions(-) rename kuro/{config.py => config/__init__.py} (81%) create mode 100644 kuro/config/aria.py create mode 100644 kuro/config/meconopsis.py create mode 100644 kuro/config/violet.py diff --git a/config.py b/config.py index 2959508..2749ee2 100644 --- a/config.py +++ b/config.py @@ -27,6 +27,7 @@ # Import Theme from libqtile import hook from libqtile.log_utils import logger +from kuro.utils import load_config_class try: from kuro.theme import Kuro @@ -40,20 +41,15 @@ except ImportError as e: Kuro = None raise ImportError("Could not load theme Config or BaseTheme! Error: {}".format(e)) -# Import theme configuration -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)) +# Import theme configuration +Config = load_config_class() +if Config is 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', '????')}'") # Initialize the Theme Theme.initialize() logger.warning("Initialize done") @@ -114,6 +110,8 @@ except Exception as 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 15ce41d..8f02d65 100644 --- a/kuro/base.py +++ b/kuro/base.py @@ -1,3 +1,5 @@ +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 @@ -7,13 +9,21 @@ from libqtile.log_utils import logger class BaseConfig: + config_name = "KuroBase" + @classmethod def get(cls, key, default): if hasattr(cls, key): - return cls.__dict__[key] + return getattr(cls, 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 @@ -45,7 +55,7 @@ class BaseTheme: auto_fullscreen = True focus_on_window_activation = "smart" extensions = [] - reconfigure_screens = True + 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 @@ -60,6 +70,9 @@ 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() diff --git a/kuro/config.py b/kuro/config/__init__.py similarity index 81% rename from kuro/config.py rename to kuro/config/__init__.py index 43ff6e5..00ff7ea 100644 --- a/kuro/config.py +++ b/kuro/config/__init__.py @@ -1,8 +1,11 @@ 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 @@ -24,7 +27,7 @@ class Config(BaseConfig): # Default Applications app_terminal = "ghostty" - app_launcher = "wofi --show drun,run" + app_launcher = "wofi --show run,drun" file_manager = "thunar" visualizer_app = "glava" web_browser = "firefox" @@ -38,15 +41,24 @@ class Config(BaseConfig): {'group': "", 'command': ["thunderbird"]}, {'group': "", 'command': ["spotify"]}, ] - apps_autostart = [ - # ["ulauncher", "--hide-window", "--no-window-shadow"], # App launcher background daemon - ["mako"], # Notification daemon - ["kanshi"], # Display hotplug - ["wl-paste", "--watch", "cliphist", "store"], # Clipboard manager - ["/usr/lib/kdeconnectd"], # KDE Connect daemon - ["kdeconnect-indicator"], # KDE Connect tray - ["vorta"], # Vorta backup scheduler - ] + 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 + ] + } # Keyboard commands cmd_media_play = "playerctl -i kdeconnect play-pause" @@ -129,8 +141,8 @@ class Config(BaseConfig): # Thermal indicator variables thermal_threshold = 75 - thermal_sensor = "Tdie" - thermal_chip = "zenpower-pci-00c3" + thermal_sensor = "Package id 0" + thermal_chip = "coretemp-isa-0000" # CPU graph variables cpu_graph_colour = '#ff0000' @@ -149,7 +161,7 @@ class Config(BaseConfig): wifi_interface = "wifi0" wifi_theme_path = "/home/kevin/.config/qtile/kuro/resources/wifi" wifi_update_interval = 5 - wired_interface = "br1" + wired_interface = "eth0" # GPU variables gpu_theme_path = "/home/kevin/.config/qtile/kuro/resources/gpu" @@ -158,10 +170,7 @@ class Config(BaseConfig): volume_font = "Noto Sans" volume_fontsize = 11 volume_theme_path = "/home/kevin/.config/qtile/kuro/resources/volume" - volume_pulse_sinks = [ - "alsa_output.usb-Burr-Brown_from_TI_USB_Audio_CODEC-00.analog-stereo-output", - "alsa_output.pci-0000_0d_00.4.analog-stereo", - ] + volume_pulse_sinks = [] volume_is_bluetooth_icon = False volume_update_interval = 0.2 @@ -200,3 +209,13 @@ class Config(BaseConfig): # 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 new file mode 100644 index 0000000..ebb87bf --- /dev/null +++ b/kuro/config/aria.py @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..e580c92 --- /dev/null +++ b/kuro/config/meconopsis.py @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..e6dd5a6 --- /dev/null +++ b/kuro/config/violet.py @@ -0,0 +1,26 @@ +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 8bafb1d..7311976 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -1,6 +1,7 @@ import json import os import random +import time import datetime import socket import subprocess @@ -39,14 +40,11 @@ from kuro.utils import layouts as kuro_layouts logger.warning("Importing configuration...") -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!") +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") @@ -58,9 +56,6 @@ class Kuro(BaseTheme): # Screen count num_screens = 0 - # Top bars - topbars = [] - # Static windows static_windows = [] @@ -310,9 +305,6 @@ class Kuro(BaseTheme): Config.bar_background = colors['color1'] def reinit_screens(self): - # Re-initalize bars - self.topbars.clear() - # TODO: Move backend check into utils method if qtile.core.name == "x11": self.num_screens = max(1, utils.get_screen_count()) @@ -320,18 +312,19 @@ class Kuro(BaseTheme): self.num_screens = max(1, len(qtile.core.get_screen_info())) logger.warning(f"Detected {self.num_screens} screens.") - # TODO: If i get the double topbar issue, this might be the - # cause; creating new screens on reinit... - screens = [] for x in range(self.num_screens): - logger.warning("Initializing bars for screen {}".format(x)) - topbar = self.build_bar_for_screen(x) - self.topbars.append(topbar) - screens.append(Screen(top=topbar)) + logger.warning("Reconfiguring bars for screen {}".format(x)) - self.screens.clear() - for s in screens: - self.screens.append(s) + 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") @@ -606,7 +599,6 @@ class Kuro(BaseTheme): # Update color scheme self.update_colorscheme() - self.startup_completed = True def callback_startup_complete(self, *args, **kwargs): logger.warning("Callback Startup Complete") @@ -617,14 +609,20 @@ class Kuro(BaseTheme): # Update color scheme self.update_colorscheme() - # Setup XDG Desktop Portal - self.setup_xdg_desktop_portal() + # 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 app in Config.get("apps_autostart", []): - logger.warning(f"Starting '{app}'...") - utils.execute_once(app) + 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"]): @@ -634,6 +632,9 @@ class Kuro(BaseTheme): 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 @@ -667,9 +668,14 @@ class Kuro(BaseTheme): del client.is_static_window self.static_windows.remove(client) - def callback_screens_reconfigured(self, *args, **kwargs): - logger.warning(f"Re-configuring screens!") + 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() diff --git a/kuro/utils/__init__.py b/kuro/utils/__init__.py index e69de29..758cba3 100644 --- a/kuro/utils/__init__.py +++ b/kuro/utils/__init__.py @@ -0,0 +1,30 @@ +import socket +import importlib +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: + pass + 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: + pass + return None diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index ff95975..aff7f56 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -649,62 +649,70 @@ class NetworkInfoWidget(DualPaneTextboxBase): def _update_values(self): # Wifi - try: - essid, quality = get_status(self.wireless_interface) - status = iwlib.get_iwconfig(self.wireless_interface) - self.wireless_ips = netifaces.ifaddresses(self.wireless_interface) - disconnected = essid is None - percent = math.ceil(((quality or 0) / 70) * 100) - self.wireless_quality = quality - self.wireless_signal = percent - self.wireless_name = essid - self.wireless_connected = not disconnected - self.wireless_accesspoint = status.get('Access Point', b'Unknown').decode() - self.wireless_frequency = status.get('Frequency', b'Unknown').decode() - self.wireless_ipv4 = self.wireless_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr'] - self.wireless_ipv6 = self.wireless_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr'] - self.wireless_mac = self.wireless_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr'] - except EnvironmentError: - pass + if self.wireless_interface: + try: + essid, quality = get_status(self.wireless_interface) + status = iwlib.get_iwconfig(self.wireless_interface) + self.wireless_ips = netifaces.ifaddresses(self.wireless_interface) + disconnected = essid is None + percent = math.ceil(((quality or 0) / 70) * 100) + self.wireless_quality = quality + self.wireless_signal = percent + self.wireless_name = essid + self.wireless_connected = not disconnected + self.wireless_accesspoint = status.get('Access Point', b'Unknown').decode() + self.wireless_frequency = status.get('Frequency', b'Unknown').decode() + self.wireless_ipv4 = self.wireless_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr'] + self.wireless_ipv6 = self.wireless_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr'] + self.wireless_mac = self.wireless_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr'] + except EnvironmentError: + pass # Wired - 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)] + if 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 + self.wired_ips = netifaces.ifaddresses(self.wired_interface) + self.wired_ipv4 = self.wired_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr'] + self.wired_ipv6 = self.wired_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr'] + self.wired_mac = self.wired_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr'] + command = ["ip", "link", "show", "{}".format(self.wired_interface)] + try: + eth_status = call_process(command) + except subprocess.CalledProcessError as e: + logger.error(f"Error while calling {command} - {e}") + return + m = self.wired_up_regex.search(eth_status) + if m: + self.wired_connected = "UP" in m.group(1) + else: + self.wired_connected = False - except (EnvironmentError, ValueError): - pass + except (EnvironmentError, ValueError): + pass def update(self): self._update_values() self.draw() def draw(self): - if self.wireless_connected: - strength = "" - if self.wireless_signal < 66: - strength = "" - if self.wireless_signal < 33: - strength = "" - self.text_left = strength + if self.wireless_interface: + if self.wireless_connected: + strength = "" + if self.wireless_signal < 66: + strength = "" + if self.wireless_signal < 33: + strength = "" + self.text_left = strength + else: + self.text_left = "" else: - self.text_left = "" + self.text_left = "" - if self.wired_connected: - self.text_right = "" + if self.wired_interface: + if self.wired_connected: + self.text_right = "" + else: + self.text_right = "" else: self.text_right = "" diff --git a/required_packages.txt b/required_packages.txt index cf0478e..3462d93 100644 --- a/required_packages.txt +++ b/required_packages.txt @@ -4,6 +4,8 @@ notification-daemon otf-font-awesome python-osc +qtile-extras + # /optional/ playerctl