From 2860100089af727d90c0aaf22596c566b69a8188 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Thu, 8 Feb 2024 18:59:30 +0100 Subject: [PATCH] Meconopsis changes for Wayland and general refactoring/optimizations --- config.py | 14 +- kuro/base.py | 34 +- kuro/config.py | 64 ++- kuro/theme.py | 1063 ++++++++++++++++-------------------- kuro/utils/general.py | 187 +++---- kuro/utils/kb_backlight.py | 2 +- kuro/utils/layouts.py | 15 +- kuro/utils/widgets.py | 494 ++--------------- kuro/utils/windows.py | 9 +- 9 files changed, 697 insertions(+), 1185 deletions(-) diff --git a/config.py b/config.py index 7d5f49f..2959508 100644 --- a/config.py +++ b/config.py @@ -53,12 +53,12 @@ except ImportError as e: try: - logger.info("Initializing theme...") + logger.warning("Initializing theme...") # Initialize the Theme Theme.initialize() - logger.info("Initialize done") + logger.warning("Initialize done") - logger.info("Hooking theme into callbacks...") + logger.warning("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) @@ -82,10 +82,11 @@ 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.info("Hooking done") + logger.warning("Hooking done") - logger.info("Initializing theme variables") + logger.warning("Initializing theme variables") # Initialize variables from theme keys = Theme.keys mouse = Theme.mouse @@ -104,7 +105,8 @@ try: focus_on_window_activation = Theme.focus_on_window_activation extensions = Theme.extensions wmname = Theme.wmname - logger.info("Variable initialization done") + reconfigure_screens = Theme.reconfigure_screens + logger.warning("Variable initialization done") except Exception as e: Theme = None Config = None diff --git a/kuro/base.py b/kuro/base.py index 370c02f..9557f9e 100644 --- a/kuro/base.py +++ b/kuro/base.py @@ -2,6 +2,9 @@ from libqtile import layout as libqtile_layout, layout, bar, widget from libqtile.command import lazy from libqtile.config import Key, Group, Screen, Drag, Click, Match +# Initialize logging +from libqtile.log_utils import logger + class BaseConfig: @classmethod @@ -15,10 +18,11 @@ class BaseConfig: class BaseTheme: # Changing variables initialized by function keys = None + mouse = None groups = None layouts = None widget_defaults = None - screens = None + screens = [] qtile = None # 'Static' variables @@ -41,6 +45,7 @@ class BaseTheme: auto_fullscreen = True focus_on_window_activation = "smart" extensions = [] + reconfigure_screens = True # 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 @@ -56,11 +61,17 @@ class BaseTheme: wmname = "LG3D" 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): @@ -95,6 +106,15 @@ 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: @@ -138,15 +158,6 @@ 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 @@ -220,5 +231,8 @@ 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.py b/kuro/config.py index 6761564..f70166c 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -14,22 +14,37 @@ class Config(BaseConfig): inactive_light = "#777777" inactive_dark = "#333333" - # Default Applications - app_terminal = "terminator" - app_launcher = "/home/kevin/bin/dmenu_wal.sh" - web_browser = "firefox" - file_manager = "thunar" - app_chat = "/usr/bin/rambox" - app_irc = "quasselclient" - app_mail = "thunderbird" - app_music = "spotify" + # Predefined commands 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" + app_terminal = "terminator" + web_browser = "firefox" + file_manager = "thunar" + app_launcher = "ulauncher-toggle --no-window-shadow" + lock_command = "bash /home/kevin/bin/lock.sh" + + # 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"]}, + ] + 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 + ] + # Keyboard commands cmd_media_play = "playerctl -i kdeconnect play-pause" cmd_media_next = "playerctl -i kdeconnect next" @@ -43,9 +58,10 @@ 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 = "/home/kevin/bin/wal-nitrogen-noupdate" + wallpaper_config_command = "/bin/true" # Images desktop_bg = "/home/kevin/Pictures/wallpapers/desktop.png" @@ -83,7 +99,8 @@ class Config(BaseConfig): # Bar variables bar_background = background - bar_opacity = 0.8 + bar_rgba_opacity = "AA" + bar_opacity = 1.0 bar_hover_opacity = 1 # Groupbox variables @@ -104,11 +121,12 @@ 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 = "Tdie" - thermal_chip = "zenpower-pci-00c3" + thermal_sensor = "Package id 0" + thermal_chip = "coretemp-isa-0000" # CPU graph variables cpu_graph_colour = '#ff0000' @@ -123,10 +141,11 @@ class Config(BaseConfig): battery_theme_path = "/home/kevin/.config/qtile/kuro/resources/battery" battery_update_delay = 5 - # Wifi variables - wifi_interface = "wifi0" + # Network variables + wifi_interface = "wlp3s0" wifi_theme_path = "/home/kevin/.config/qtile/kuro/resources/wifi" wifi_update_interval = 5 + wired_interface = "enp4s0" # GPU variables gpu_theme_path = "/home/kevin/.config/qtile/kuro/resources/gpu" @@ -135,8 +154,8 @@ class Config(BaseConfig): volume_font = "Noto Sans" volume_fontsize = 11 volume_theme_path = "/home/kevin/.config/qtile/kuro/resources/volume" - 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_pulse_sink = "alsa_output.usb-CSCTEK_USB_Audio_and_HID_A34004801402-00.analog-stereo" + volume_pulse_sink2 = None volume_is_bluetooth_icon = False volume_update_interval = 0.2 @@ -161,9 +180,6 @@ 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 @@ -177,9 +193,5 @@ 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" diff --git a/kuro/theme.py b/kuro/theme.py index f85eb9e..b9d4e66 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -1,36 +1,42 @@ import json import os import random +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.error("Importing qtile theme requirements...") +logger.warning("Importing qtile theme requirements...") from libqtile.config import Key, Screen, Group, Drag, Click, Match from libqtile.command import lazy from libqtile import layout, bar, widget, qtile +from qtile_extras import widget as extra_widget -logger.error("Importing theme util functions...") +logger.warning("Importing theme util functions...") # Import theme util functions from xcffib.xproto import WindowError -logger.error("Importing kuro utils...") +logger.warning("Importing kuro utils...") import kuro.utils.widgets from kuro.utils import general as utils -logger.error("Importing variables and other utils...") +logger.warning("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.error("Importing configuration...") +logger.warning("Importing configuration...") try: from kuro.config import Config @@ -41,88 +47,62 @@ except ImportError: Config = None raise ImportError("Could not load theme Config or BaseConfig!") -logger.error("Imports done") +logger.warning("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(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 - ]) - - 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) + 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 + ] + ) def initialize(self): - logger.error("Initializing Kuro theme...") - self.log_debug("Initializing Kuro Theme...") - # Update color scheme + logger.warning("Initializing colorscheme...") self.initialize_colorscheme() - # Set settings - self.do_keyboard_updates = Config.get("do_keyboard_updates", True) - + logger.warning("Initializing superclass...") super(Kuro, self).initialize() - self.update() - - def update(self): - # Update keys with keys for groups and layouts + logger.warning("Updating keys for groups and layouts...") self.update_keys() def init_keys(self): - self.log_debug("Initializing keys") - + logger.warning("Initializing keys") return [ # Switch between windows in current stack pane Key([self.mod], "k", lazy.layout.down()), @@ -144,9 +124,6 @@ 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 @@ -162,8 +139,8 @@ class Kuro(BaseTheme): # Super-B to start webbrowser Key([self.mod], "b", lazy.spawn(Config.get('web_browser', "xterm links"))), - # Super-F to start file manager -# Key([self.mod], "f", lazy.spawn(Config.get('file_m4anager', "thunar"))), + # Super-T to start file manager + Key([self.mod], "t", lazy.spawn(Config.get('file_manager', "thunar"))), # Super-Shift-R to start spawncmd Key([self.mod, "shift"], "r", lazy.spawncmd()), @@ -171,10 +148,6 @@ 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'))), @@ -182,6 +155,10 @@ 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'))), @@ -204,21 +181,22 @@ class Kuro(BaseTheme): # Kill the current window Key([self.mod], "w", lazy.window.kill()), - # Restart QTile - Key([self.mod, "control"], "r", lazy.restart()), + # 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()), # Shutdown QTile Key([self.mod, "control"], "q", lazy.shutdown()), # Update wallpaper - Key([self.mod, "control"], "w", lazy.function(self.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.function(Config.get('cmd_reconfigure_screens', 'true'))), # 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)), @@ -228,12 +206,6 @@ class Kuro(BaseTheme): ## 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)), @@ -242,34 +214,29 @@ class Kuro(BaseTheme): ] def init_groups(self): - self.log_debug("Initializing groups") - - groups = [] - + logger.warning("Initializing groups") # http://fontawesome.io/cheatsheet - groups.append(Group("", spawn=Config.get('web_browser', "true"))) - groups.append(Group("", spawn=Config.get('app_terminal', "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" - ) - ])) - + groups = [ + Group(""), + Group(""), + Group(""), + Group(""), + Group(""), + Group(""), + Group(""), + Group(""), + Group(""), + Group("", layout='floating', layouts=[ + layout.Floating( + border_focus="#990000", + border_normal="#440000" + ) + ]) + ] return groups def init_layouts(self): - self.log_debug("Initializing layouts") - + logger.warning("Initializing layouts") return [ kuro_layouts.KuroWmii( theme=self, @@ -289,8 +256,7 @@ class Kuro(BaseTheme): ] def init_widget_defaults(self): - self.log_debug("Initializing widget_defaults") - + logger.warning("Initializing widget_defaults") return { "font": Config.get('font_topbar', "Sans"), "fontsize": Config.get('fontsize_topbar', 16), @@ -298,158 +264,12 @@ class Kuro(BaseTheme): } def init_screens(self): - 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 + logger.warning("Initializing screens") + self.reinit_screens() + return self.screens def init_mouse(self): - self.log_debug("Initializing mouse") - + logger.warning("Initializing mouse") # Drag floating layouts. mouse = [ Drag([self.mod], "Button1", lazy.window.set_position_floating(), @@ -458,12 +278,49 @@ class Kuro(BaseTheme): start=lazy.window.get_size()), Click([self.mod], "Button2", lazy.window.bring_to_front()) ] - return mouse - def update_keys(self): - self.log_debug("Updating keys") + 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): + # Re-initalize bars + self.topbars.clear() + + 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.") + + 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)) + + self.screens.clear() + for s in screens: + self.screens.append(s) + + def update_keys(self): + logger.warning("Updating keys") for i, g in enumerate(self.groups): if i == 9: i = -1 @@ -495,122 +352,237 @@ class Kuro(BaseTheme): Key([self.mod], "n", lazy.layout.normalize()), ]) - # Util functions - @staticmethod - def redraw_bar(qtile): - for s in qtile.screens: - s.top.draw() + def build_bar_for_screen(self, screen_num): + widgets = [ + # Workspaces + 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) + ), - @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) + # Spawn prompt (only shown if activated) + widget.Prompt(**self.widget_defaults), - # 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") + # 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=" ", + ) + ] - 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() + # 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 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() + # Sensor widgets + sensor_widgets = [] - def show_window_info(self, qtile): - window = qtile.current_window if qtile else None + # 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 + )) - import pprint - if window: - info = window.cmd_inspect() or None - name = window.name + # 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), + ]) - utils.notify(title="Window properties {}".format(name), - content="{}".format(pprint.pformat(vars(window)))) - logger.warning("{}".format(pprint.pformat(vars(window)))) + 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="|") + ]) - if info: - info = pprint.pformat(info) - utils.notify(title="Window info of {}".format(name), - content="{}".format(info)) - logger.warning("{}".format(info)) + # Battery level + if Config.get('show_battery_widget', False): + widgets.extend([ + kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11), + ]) - # @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 + # Volume widget(s) + widgets.append( + 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), + ) + ) + # Violet has multiple volume widgets + if socket.gethostname() in ["Violet"]: + widgets.append( + 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), + ) + ) - window.floating = 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) + ), + ]) - # Pinned toggle function - @staticmethod - def toggle_pinned(qtile): - windows = qtile.cmd_windows() - print(windows) + # 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) + ) # 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.update_wallpaper(qtile) - - # Setup audio - # p = utils.execute_once(["qjackctl"]) - # p.wait() + self.set_random_wallpaper() 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.wait() @@ -623,38 +595,74 @@ class Kuro(BaseTheme): except KeyError: wallpaper = None if wallpaper: - Kuro.set_wallpaper(qtile, wallpaper) + self.set_wallpaper(wallpaper) else: - 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") + logger.warning("No wallpaper to restore.") # Update color scheme - self.initialize_colorscheme() + self.update_colorscheme() + self.startup_completed = True - # def callback_screen_change(self, *args, **kwargs): - # for window in self.static_windows: - # window.togroup() + 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() + + # 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 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") + + def callback_client_managed(self, *args, **kwargs): + client: Optional[Window] = args[0] if len(args) > 0 else None + + 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_screens_reconfigured(self, *args, **kwargs): + logger.warning(f"Re-configuring screens!") + self.reinit_screens() + self.set_wallpaper(self.current_wallpaper) + self.update_colorscheme() def callback_setgroup(self, *args, **kwargs): for window in self.static_windows: @@ -667,121 +675,28 @@ class Kuro(BaseTheme): del window.is_static_window self.static_windows.remove(window) - def callback_focus_change(self, *args, **kwargs): - if self.do_keyboard_updates: - kb_handle_focus_change(self) + 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())}") - initial_windows = [] + 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 - 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)) + window.floating = True - 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): + def set_random_wallpaper(self, *args, **kwargs): wallpapers = [] wallpaper_dir = Config.get("desktop_bg_folder", "") try: @@ -791,54 +706,36 @@ class Kuro(BaseTheme): if wallpapers: if Config.get("desktop_bg_override", False): - qtile.theme_instance.current_wallpaper = Config.get("desktop_bg_override", "") + wallpaper_file = Config.get("desktop_bg_override", "") else: - qtile.theme_instance.current_wallpaper = os.path.join(wallpaper_dir, random.choice(wallpapers)) - Kuro.set_wallpaper(qtile, qtile.theme_instance.current_wallpaper) + wallpaper_file = os.path.join(wallpaper_dir, random.choice(wallpapers)) + self.set_wallpaper(wallpaper_file) else: - utils.execute_once("nitrogen --restore") + logger.warning("Random wallpaper requested but no wallpapers are available.") + 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 - @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)]) + def update_colorscheme(self, *args, **kwargs): + if self.current_wallpaper: + logger.warning(f"Updating wal colors for wallpaper {self.current_wallpaper}") + p = utils.execute(["wal", "-n", "-i", "{}".format(self.current_wallpaper)]) p.wait() colors = None @@ -867,62 +764,72 @@ class Kuro(BaseTheme): layout.border_normal = colors['color1'] layout.border_normal_stack = colors['color1'] - for screen in qtile.screens: + for screen_i, screen in enumerate(qtile.screens): bar = screen.top - 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)) + 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) - if hasattr(w, 'foreground'): - w.foreground = colors['color15'] + 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_normal'): - w.foreground_normal = colors['color15'] + if hasattr(w, 'foreground'): + w.foreground = colors['color15'] - if hasattr(w, 'foreground_alert'): - w.foreground_alert = colors['color3'] + if hasattr(w, 'foreground_normal'): + w.foreground_normal = colors['color15'] - if hasattr(w, 'border'): - w.border = colors['color15'] + if hasattr(w, 'foreground_alert'): + w.foreground_alert = colors['color3'] - if hasattr(w, 'active'): - w.active = colors['color15'] + if hasattr(w, 'border'): + w.border = colors['color15'] - if hasattr(w, 'highlight_color'): - w.highlight_color = colors['color3'] + if hasattr(w, 'active'): + w.active = colors['color15'] - if hasattr(w, 'inactive'): - w.inactive = colors['color8'] + if hasattr(w, 'highlight_color'): + w.highlight_color = colors['color3'] - if hasattr(w, 'this_current_screen_border'): - w.this_current_screen_border = colors['color15'] + if hasattr(w, 'inactive'): + w.inactive = colors['color8'] - if hasattr(w, 'this_screen_border'): - w.this_screen_border = colors['color15'] + if hasattr(w, 'this_current_screen_border'): + w.this_current_screen_border = colors['color15'] - if hasattr(w, 'other_current_screen_border'): - w.other_current_screen_border = colors['color8'] + if hasattr(w, 'this_screen_border'): + w.this_screen_border = colors['color15'] - if hasattr(w, 'other_screen_border'): - w.other_screen_border = colors['color8'] + if hasattr(w, 'other_current_screen_border'): + w.other_current_screen_border = colors['color8'] - if isinstance(w, kuro.utils.widgets.AudioVisualizerWidget): - w.graph_color = colors['color15'] - w.fill_color = colors['color8'] + if hasattr(w, 'other_screen_border'): + w.other_screen_border = colors['color8'] - bar.draw() + if isinstance(w, widget.WidgetBox): + for subw in w.widgets: + update_widget(subw) - # 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() + for w in bar.widgets: + update_widget(w) - utils.notify( - "Updated colorscheme!", - "active: {}, inactive: {}".format(colors['color15'], colors['color1']) - ) + 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']}") diff --git a/kuro/utils/general.py b/kuro/utils/general.py index f63e1ad..0a2b225 100644 --- a/kuro/utils/general.py +++ b/kuro/utils/general.py @@ -2,20 +2,14 @@ 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 - -try: - notify2.init("QTileWM") -except DBusException as e: - logger.error("Could not initialize notify2: {}".format(e)) +from libqtile import qtile BUTTON_LEFT = 1 BUTTON_MIDDLE = 2 @@ -38,40 +32,68 @@ def is_running(process): def execute(process): - 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): + try: if isinstance(process, list): return subprocess.Popen(process) elif isinstance(process, str): return subprocess.Popen(process.split()) else: - pass + 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}") def call_process(command, **kwargs): """ - This method uses `subprocess.check_output` to run the given command - and return the string from stdout, which is decoded when using - Python 3. + Run the given command and return the string from stdout. """ - output = subprocess.check_output(command, **kwargs) - if six.PY3: - output = output.decode() - return output + return subprocess.check_output(command, **kwargs).decode() def get_screen_count(): try: - output = subprocess.check_output("xrandr -q".split()).decode('utf-8') - output = [x for x in output.split("\n") if " connected" in x] + 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") except subprocess.CalledProcessError: return 1 @@ -87,8 +109,21 @@ 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(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): + +def notify(qtile, title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): if image is not None: notification = Notification( summary=title, message=content, @@ -101,13 +136,14 @@ def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): notification.set_timeout(timeout) notification.set_urgency(urgency) + init_notify(qtile) + try: - return notification.show() - except notify2.UninittedError: - logger.warning("Notify2 was uninitialized, initializing...") - notify2.init("qtile") - return notification.show() - except DBusException as e: + try: + return notification.show() + except notify2.UninittedError: + logger.warning("Notify2 is not initialized") + except Exception as e: logger.warning("Showing notification failed: {}".format(e)) logger.warning(traceback.format_exc()) @@ -125,6 +161,10 @@ 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 ) @@ -158,11 +198,11 @@ def display_wm_class(qtile): window = qtile.currentWindow if qtile else None if window: - wm_class = window.window.get_wm_class() or None + wm_class = window.get_wm_class() or None name = window.name if wm_class: - notify(title="WM_Class of {}".format(name), + notify(qtile=qtile, title="WM_Class of {}".format(name), content="{}".format(wm_class), urgency=notify2.URGENCY_CRITICAL) @@ -189,76 +229,3 @@ 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 b818ec4..e684735 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.window.get_wm_class() or None + wm_class = 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 e74dee6..719b526 100644 --- a/kuro/utils/layouts.py +++ b/kuro/utils/layouts.py @@ -32,29 +32,25 @@ 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.window.get_wm_class() or '' - is_java_dropdown = 'sun-awt-X11-XWindowPeer' in cls + cls = client.get_wm_class() or "" + is_java_dropdown = "sun-awt-X11-XWindowPeer" in cls if is_java_dropdown: client.paint_borders(bc, bw) - client.cmd_bring_to_front() + client.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.cmd_bring_to_front() + client.bring_to_front() else: above = False # We definitely have a screen here, so let's be sure we'll float on screen - try: - client.float_x - client.float_y - except AttributeError: + if client.float_x is None or client.float_y is None: # 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, @@ -63,5 +59,6 @@ class KuroFloating(Floating): bw, bc, above, + respect_hints=True, ) client.unhide() diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index d03c0da..e7aadce 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -7,61 +7,18 @@ import cairocffi import iwlib import netifaces import psutil -import six -import unicodedata -from libqtile import bar, pangocffi +from libqtile import bar, qtile from libqtile.log_utils import logger from libqtile.widget import base from libqtile.widget.base import ORIENTATION_HORIZONTAL from libqtile.widget.battery import default_icon_path, load_battery, BatteryState -from libqtile.widget.check_updates import CheckUpdates -from libqtile.widget.currentlayout import CurrentLayoutIcon -from libqtile.widget.graph import _Graph -from libqtile.widget.tasklist import TaskList from libqtile.widget.wlan import get_status -from libqtile.backend.x11.window import Window +from libqtile.command.base import expose_command from kuro.utils.general import notify, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_DOWN, BUTTON_UP, BUTTON_MUTE, \ call_process -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. @@ -242,7 +199,8 @@ class DualPaneTextboxBase(base._Widget): self.bar.draw() self.changed = False - def cmd_set_font(self, font=base.UNSPECIFIED, fontsize_left=base.UNSPECIFIED, fontsize_right=base.UNSPECIFIED, fontshadow=base.UNSPECIFIED): + @expose_command() + def set_font(self, font=base.UNSPECIFIED, fontsize_left=base.UNSPECIFIED, fontsize_right=base.UNSPECIFIED, fontshadow=base.UNSPECIFIED): """ Change the font used by this widget. If font is None, the current font is used. @@ -265,363 +223,6 @@ 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 - - def cmd_update_custom_player(self, player_name, data): - # Update firefox player - if player_name.startswith("firefox"): - if data['playing'] and data['muted']: - self.custom_player_data['firefox']['showing'] = True - self.custom_player_data['firefox']['state'] = MediaWidget.Status.PAUSED - self.custom_player_data['firefox']['title'] = data['title'] - 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 = super(KuroCurrentLayoutIcon, self)._get_layout_names() - - from kuro.utils import layouts as kuro_layouts - from libqtile.layout.base import Layout - klayouts = [ - layout_class_name.lower() - for layout_class, layout_class_name - in map(lambda x: (getattr(kuro_layouts, x), x), dir(kuro_layouts)) - if isinstance(layout_class, six.class_types) and issubclass(layout_class, Layout) - ] - names.extend(klayouts) - - return list(set(names)) - - -class KuroTaskList(TaskList): - defaults = [ - ( - 'txt_pinned', - 'P ', - 'Text representation of the pinned window state. ' - 'e.g., "P " or "\U0001F5D7 "' - ), - ( - 'markup_pinned', - None, - 'Text markup of the pinned window state. Supports pangomarkup with markup=True.' - 'e.g., "{}" or "{}"' - ), - ] - - def __init__(self, *args, **kwargs): - super(KuroTaskList, self).__init__(*args, **kwargs) - self.add_defaults(KuroTaskList.defaults) - - def get_taskname(self, window): - """ - Get display name for given window. - Depending on its state minimized, maximized and floating - appropriate characters are prepended. - """ - state = '' - markup_str = self.markup_normal - - # Enforce markup and new string format behaviour when - # at least one markup_* option is used. - # Mixing non markup and markup may cause problems. - if self.markup_minimized or self.markup_maximized\ - or self.markup_floating or self.markup_focused or self.markup_pinned: - enforce_markup = True - else: - enforce_markup = False - - if window is None: - pass - elif hasattr(window, "is_static_window") and window.is_static_window: - state = self.txt_pinned - markup_str = self.markup_pinned - elif window.minimized: - state = self.txt_minimized - markup_str = self.markup_minimized - elif window.maximized: - state = self.txt_maximized - markup_str = self.markup_maximized - elif window.floating: - state = self.txt_floating - markup_str = self.markup_floating - elif window is window.group.current_window: - markup_str = self.markup_focused - - window_name = window.name if window and window.name else "?" - - # Emulate default widget behavior if markup_str is None - if enforce_markup and markup_str is None: - markup_str = "%s{}" % (state) - - if markup_str is not None: - self.markup = True - window_name = pangocffi.markup_escape_text(window_name) - return markup_str.format(window_name) - - return "%s%s" % (state, window_name) - - class GPUStatusWidget(base._TextBox): """Displays the currently used GPU.""" @@ -652,7 +253,11 @@ class GPUStatusWidget(base._TextBox): self.icons.update(self.custom_icons) def _get_info(self): - output = self.call_process(self.check_command, shell=True) + try: + output = self.call_process(self.check_command, shell=True) + except subprocess.CalledProcessError as e: + logger.error(f"Error while calling {self.check_command} - {e}") + output = None mode = "nvidia" if "nvidia" in output else "intel" if "intel" in output else "unknown" return {'error': False, 'mode': mode} @@ -728,14 +333,16 @@ 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("GPU Status", "The currently used GPU is unknown.\n\nAfter the next login it will be the {} GPU.".format(next_gpu), + notify(None, "GPU Status", "The currently used GPU is unknown.\n\nAfter the next login it will be the {} GPU.".format(next_gpu), image=os.path.join(self.theme_path, "gpu-unknown.png")) else: - notify("GPU Status", "The system is currently running on the {} GPU. Press the middle mouse " + notify(None, "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 ), @@ -744,38 +351,25 @@ class GPUStatusWidget(base._TextBox): if button == BUTTON_MIDDLE: command = ["optimus-manager", "--no-confirm", "--switch", "auto"] - output = self.call_process(command) + try: + output = self.call_process(command) + except subprocess.CalledProcessError as e: + logger.error(f"Error while calling {command} - {e}") + output = "" if "nvidia" in output: - notify("GPU Switched", "The GPU has been switched from Intel to NVidia.\n" + notify(None, "GPU Switched", "The GPU has been switched from Intel to NVidia.\n" "Please log out and log back in to apply the changes to the session.", image=os.path.join(self.theme_path, "gpu-nvidia.png")) elif "intel" in output: - notify("GPU Switched", "The GPU has been switched from NVidia to Intel.\n" + notify(None, "GPU Switched", "The GPU has been switched from NVidia to Intel.\n" "Please log out and log back in to apply the changes to the session.", image=os.path.join(self.theme_path, "gpu-intel.png")) else: - notify("GPU Switch Error", "I could not determine if the GPU was switched successfully.\n" + notify(None, "GPU Switch Error", "I could not determine if the GPU was switched successfully.\n" "Please log out and log back in to clear up the inconsistency.", image=os.path.join(self.theme_path, "gpu-unknown.png")) -class 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'), @@ -813,7 +407,11 @@ class ThermalSensorWidget(DualPaneTextboxBase): self.timeout_add(self.update_interval, self.timer_setup) def _update_values(self): - sensors_out = self.call_process(self.get_command()) + try: + sensors_out = self.call_process(self.get_command()) + except subprocess.CalledProcessError as e: + logger.error(f"Error while calling {self.get_command()} - {e}") + return temperature_values = {} for name, temp, symbol in self.sensors_temp.findall(sensors_out): name = name.strip() @@ -843,7 +441,7 @@ class ThermalSensorWidget(DualPaneTextboxBase): def button_press(self, x, y, button): if button == BUTTON_LEFT: - notify("Temperature Information", "\n".join( + notify(None, "Temperature Information", "\n".join( "{}: {}{}".format(name, *values) for name, values in self.values.items() )) @@ -896,7 +494,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("CPU Information", "user: {} %\nnice: {} %\nsys: {} %\nidle: {} %\ntotal: {} %".format( + notify(None, "CPU Information", "user: {} %\nnice: {} %\nsys: {} %\nidle: {} %\ntotal: {} %".format( math.ceil((self.cpu_old[0] / total) * 100), math.ceil((self.cpu_old[1] / total) * 100), math.ceil((self.cpu_old[2] / total) * 100), @@ -949,7 +547,7 @@ class MemoryInfoWidget(DualPaneTextboxBase): val['SwapUsed'] = swap.used // 1024 // 1024 if button == BUTTON_LEFT: - notify("Memory Information", "Memory: {}MB / {}MB\n {}%\nSwap: {}MB / {}MB\n {}%".format( + notify(None, "Memory Information", "Memory: {}MB / {}MB\n {}%\nSwap: {}MB / {}MB\n {}%".format( val['MemUsed'], val['MemTotal'], math.ceil((mem.used / mem.total) * 100), val['SwapUsed'], val['SwapTotal'], @@ -1005,7 +603,7 @@ class DiskIOInfoWidget(DualPaneTextboxBase): def button_press(self, x, y, button): if button == BUTTON_LEFT: - notify("Disk IO Information", + notify(None, "Disk IO Information", "Time that there were IO requests queued for /dev/{}: {} ms".format(self.hdd_device, self.io)) @@ -1055,7 +653,7 @@ class NetworkInfoWidget(DualPaneTextboxBase): 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) + percent = math.ceil(((quality or 0) / 70) * 100) self.wireless_quality = quality self.wireless_signal = percent self.wireless_name = essid @@ -1074,7 +672,12 @@ class NetworkInfoWidget(DualPaneTextboxBase): 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)]) + 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) @@ -1132,9 +735,9 @@ class NetworkInfoWidget(DualPaneTextboxBase): wired_text = "Wired: Not connected" if wifi_text: - notify(title, "{}\n\n{}".format(wifi_text, wired_text)) + notify(None, title, "{}\n\n{}".format(wifi_text, wired_text)) else: - notify(title, "\n{}".format(wired_text)) + notify(None, title, "\n{}".format(wired_text)) class BatteryInfoWidget(DualPaneTextboxBase): @@ -1210,7 +813,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("Battery Status", output) + notify(None, "Battery Status", output) class VolumeInfoWidget(DualPaneTextboxBase): @@ -1245,7 +848,8 @@ class VolumeInfoWidget(DualPaneTextboxBase): else: cmd = self.status_cmd mixer_out = self.call_process(cmd.split(" ")) - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as e: + logger.error(f"Error while calling {cmd} - {e}") return -1 try: return int(mixer_out) @@ -1289,7 +893,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("Volume Status", sink+output) + notify(None, "Volume Status", sink+output) elif button == BUTTON_RIGHT: if "{sink}" in self.volume_app: @@ -1321,18 +925,22 @@ class VolumeInfoWidget(DualPaneTextboxBase): self.update() - def cmd_increase_vol(self): + @expose_command() + def increase_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_UP) - def cmd_decrease_vol(self): + @expose_command() + def decrease_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_DOWN) - def cmd_mute(self): + @expose_command() + def mute(self): # Emulate button press. self.button_press(0, 0, BUTTON_MUTE) - def cmd_run_app(self): + @expose_command() + def run_app(self): # Emulate button press. self.button_press(0, 0, BUTTON_RIGHT) diff --git a/kuro/utils/windows.py b/kuro/utils/windows.py index 6bfd997..7370274 100644 --- a/kuro/utils/windows.py +++ b/kuro/utils/windows.py @@ -1,6 +1,11 @@ from cairocffi.test_xcb import xcffib -from libqtile import hook -from libqtile.backend.x11.window import Window, Static +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 + class KuroStatic(Static):