From 2dfb5381ae390b82d671de724357343ecb0901bb Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Thu, 15 Apr 2021 17:18:04 +0200 Subject: [PATCH 01/10] Changes to Aria --- kuro/utils/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index a122a50..a32d571 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -337,19 +337,19 @@ class MediaWidget(base.InLoopPollText): 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'] + self.custom_player_data['firefox']['title'] = data['title'][:50] elif data['playing'] and not data['muted']: self.custom_player_data['firefox']['showing'] = True self.custom_player_data['firefox']['state'] = MediaWidget.Status.PLAYING - self.custom_player_data['firefox']['title'] = data['title'] + self.custom_player_data['firefox']['title'] = data['title'][:50] elif not data['playing'] and data['muted']: self.custom_player_data['firefox']['showing'] = True self.custom_player_data['firefox']['state'] = MediaWidget.Status.STOPPED - self.custom_player_data['firefox']['title'] = data['title'] + self.custom_player_data['firefox']['title'] = data['title'][:50] elif not data['playing'] and not data['muted']: self.custom_player_data['firefox']['showing'] = False self.custom_player_data['firefox']['state'] = MediaWidget.Status.OFFLINE - self.custom_player_data['firefox']['title'] = data['title'] + self.custom_player_data['firefox']['title'] = data['title'][:50] def _get_players(self): players = [] @@ -399,7 +399,7 @@ class MediaWidget(base.InLoopPollText): text = "Unknown" if cmd_result in ["Playing", "Paused"]: artist = self.call_process(['playerctl', '-p', player, 'metadata', 'artist']).strip() - title = self.call_process(['playerctl', '-p', player, 'metadata', 'title']).strip() + title = self.call_process(['playerctl', '-p', player, 'metadata', 'title']).strip()[:50] if artist and title: text = "{} - {}".format(artist, title) From f775ce8b03267643866a363eac1758bf597c9749 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Thu, 15 Apr 2021 17:35:02 +0200 Subject: [PATCH 02/10] Update Aria config to latest qtile version --- kuro/base.py | 24 +++++------- kuro/theme.py | 45 +++++++++-------------- kuro/utils/general.py | 9 ++++- kuro/utils/layouts.py | 85 ++++++++++++++++++------------------------- 4 files changed, 70 insertions(+), 93 deletions(-) diff --git a/kuro/base.py b/kuro/base.py index fd7bc84..370c02f 100644 --- a/kuro/base.py +++ b/kuro/base.py @@ -1,6 +1,6 @@ from libqtile import layout as libqtile_layout, layout, bar, widget from libqtile.command import lazy -from libqtile.config import Key, Group, Screen, Drag, Click +from libqtile.config import Key, Group, Screen, Drag, Click, Match class BaseConfig: @@ -29,20 +29,14 @@ class BaseTheme: bring_front_click = False cursor_warp = False floating_layout = libqtile_layout.Floating(float_rules=[ - {'wmclass': 'confirm'}, - {'wmclass': 'dialog'}, - {'wmclass': 'download'}, - {'wmclass': 'error'}, - {'wmclass': 'file_progress'}, - {'wmclass': 'notification'}, - {'wmclass': 'splash'}, - {'wmclass': 'toolbar'}, - {'wmclass': 'confirmreset'}, # gitk - {'wmclass': 'makebranch'}, # gitk - {'wmclass': 'maketag'}, # gitk - {'wname': 'branchdialog'}, # gitk - {'wname': 'pinentry'}, # GPG key password entry - {'wmclass': 'ssh-askpass'}, # ssh-askpass + # 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 ]) auto_fullscreen = True focus_on_window_activation = "smart" diff --git a/kuro/theme.py b/kuro/theme.py index ac322df..9e14eaa 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -7,7 +7,7 @@ from libqtile.log_utils import logger logger.error("Importing qtile theme requirements...") -from libqtile.config import Key, Screen, Group, Drag, Click +from libqtile.config import Key, Screen, Group, Drag, Click, Match from libqtile.command import lazy from libqtile import layout, bar, widget @@ -75,20 +75,14 @@ class Kuro(BaseTheme): # Floating layout override floating_layout = kuro_layouts.KuroFloating(float_rules=[ - {'wmclass': 'confirm'}, - {'wmclass': 'dialog'}, - {'wmclass': 'download'}, - {'wmclass': 'error'}, - {'wmclass': 'file_progress'}, - {'wmclass': 'notification'}, - {'wmclass': 'splash'}, - {'wmclass': 'toolbar'}, - {'wmclass': 'confirmreset'}, # gitk - {'wmclass': 'makebranch'}, # gitk - {'wmclass': 'maketag'}, # gitk - {'wname': 'branchdialog'}, # gitk - {'wname': 'pinentry'}, # GPG key password entry - {'wmclass': 'ssh-askpass'}, # ssh-askpass + # 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 ]) def set_debug_text(self, text): @@ -242,14 +236,7 @@ class Kuro(BaseTheme): groups.append(Group("")) groups.append(Group("")) groups.append(Group("")) - groups.append(Group("", spawn=Config.get('apps_audio', "true"), layout='verticaltile', layouts=[ - layout.VerticalTile( - border_focus=Config.get('colour_border_focus', "#ffffff"), - border_normal=Config.get('colour_border_normal', "#777777"), - border_width=Config.get('width_border', "1"), - margin=Config.get('margin_layout', "0"), - ) - ])) + groups.append(Group("", spawn=Config.get('apps_audio', "true"))) groups.append(Group("", layout='floating', layouts=[ layout.Floating( border_focus="#990000", @@ -385,6 +372,7 @@ class Kuro(BaseTheme): 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), @@ -591,11 +579,14 @@ class Kuro(BaseTheme): p.wait() self.log_info("Starting compositor...") - utils.execute_once("compton -b") + 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") + # Update color scheme self.initialize_colorscheme() @@ -627,9 +618,9 @@ class Kuro(BaseTheme): for r in dg.rules: pid = -1 # noinspection PyProtectedMember - for r2 in r.match._rules: - if r2[0] == "net_wm_pid": - pid = r2[1] + 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)) diff --git a/kuro/utils/general.py b/kuro/utils/general.py index da8d96b..ff5893f 100644 --- a/kuro/utils/general.py +++ b/kuro/utils/general.py @@ -12,7 +12,10 @@ from libqtile.bar import Bar from notify2 import Notification, URGENCY_NORMAL from libqtile.log_utils import logger -notify2.init("QTileWM") +try: + notify2.init("QTileWM") +except DBusException as e: + logger.error("Could not initialize notify2: {}".format(e)) BUTTON_LEFT = 1 BUTTON_MIDDLE = 2 @@ -100,6 +103,10 @@ def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None): try: return notification.show() + except notify2.UninittedError: + logger.warning("Notify2 was uninitialized, initializing...") + notify2.init("qtile") + return notification.show() except DBusException as e: logger.warning("Showing notification failed: {}".format(e)) logger.warning(traceback.format_exc()) diff --git a/kuro/utils/layouts.py b/kuro/utils/layouts.py index c12007c..e74dee6 100644 --- a/kuro/utils/layouts.py +++ b/kuro/utils/layouts.py @@ -15,68 +15,53 @@ class KuroFloating(Floating): super(KuroFloating, self).__init__(*args, **kwargs) self.add_defaults(KuroFloating.defaults) - def configure(self, client, screen): - # '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 - if is_java_dropdown: - return - + def configure(self, client, screen_rect): if hasattr(client, "is_static_window") and client.is_static_window: - bc = client.group.qtile.color_pixel(self.border_static) + bc = self.border_static elif client.has_focus: - bc = client.group.qtile.color_pixel(self.border_focus) + bc = self.border_focus else: - bc = client.group.qtile.color_pixel(self.border_normal) + bc = self.border_normal + if client.maximized: bw = self.max_border_width elif client.fullscreen: bw = self.fullscreen_border_width else: bw = self.border_width - 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: - # this window hasn't been placed before, let's put it in a sensible spot - transient_for = client.window.get_wm_transient_for() - win = client.group.qtile.windows_map.get(transient_for) - if win is not None: - # if transient for a window, place in the center of the window - center_x = win.x + win.width / 2 - center_y = win.y + win.height / 2 - else: - center_x = screen.x + screen.width / 2 - center_y = screen.y + screen.height / 2 - above = True + # '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 + if is_java_dropdown: + client.paint_borders(bc, bw) + client.cmd_bring_to_front() - x = center_x - client.width / 2 - y = center_y - client.height / 2 + # 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() - # don't go off the right... - x = min(x, screen.x + screen.width) - # or left... - x = max(x, screen.x) - # or bottom... - y = min(y, screen.y + screen.height) - # or top - y = max(y, screen.y) + else: + above = False - if not (self.no_reposition_match and self.no_reposition_match.compare(client)): - client.x = int(round(x)) - client.y = int(round(y)) + # 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: + # 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, - client.width, - client.height, - bw, - bc, - above, - ) + + client.place( + client.x, + client.y, + client.width, + client.height, + bw, + bc, + above, + ) client.unhide() From 8f4f08e3bf058844ae56c4722ff9c1ca3fbb991e Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 13 Sep 2021 19:56:40 +0200 Subject: [PATCH 03/10] Update Aria config to latest qtile version and newest config --- config.py | 6 ++++++ kuro/config.py | 2 +- kuro/utils/general.py | 2 +- kuro/utils/widgets.py | 2 +- kuro/utils/windows.py | 3 +-- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/config.py b/config.py index c3abfcd..46c84be 100644 --- a/config.py +++ b/config.py @@ -27,28 +27,33 @@ # Import Theme from libqtile import hook from libqtile.log_utils import logger +import traceback try: from kuro.theme import Kuro Theme = Kuro() except ImportError as e: + logger.error(traceback.format_exc()) logger.error("Could not load Kuro Theme. Trying to load BaseTheme. Error: {}".format(e)) try: from kuro.base import BaseTheme as Kuro Theme = Kuro() except ImportError as e: Kuro = None + logger.error(traceback.format_exc()) raise ImportError("Could not load theme Config or BaseTheme! Error: {}".format(e)) # Import theme configuration try: from kuro.config import Config except ImportError as e: + logger.error(traceback.format_exc()) 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 + logger.error(traceback.format_exc()) raise ImportError("Could not load theme Config or BaseConfig! Error: {}".format(e)) @@ -108,6 +113,7 @@ try: except Exception as e: Theme = None Config = None + logger.error(traceback.format_exc()) raise AttributeError("Could not configure theme! Error: {}".format(e)) diff --git a/kuro/config.py b/kuro/config.py index 05f2f87..e178ee6 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -19,7 +19,7 @@ class Config(BaseConfig): app_launcher = "/home/kevin/bin/dmenu_wal.sh" web_browser = "firefox-developer-edition" file_manager = "thunar" - app_chat = "/home/kevin/bin/ramboxpro" + app_chat = "ramboxpro" app_irc = "quasselclient" app_mail = "thunderbird" cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" diff --git a/kuro/utils/general.py b/kuro/utils/general.py index ff5893f..8a432b0 100644 --- a/kuro/utils/general.py +++ b/kuro/utils/general.py @@ -7,7 +7,7 @@ import notify2 import six from dbus import DBusException from libqtile import widget -from libqtile.window import Internal +from libqtile.backend.x11.window import Internal from libqtile.bar import Bar from notify2 import Notification, URGENCY_NORMAL from libqtile.log_utils import logger diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index a32d571..7473717 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -18,7 +18,7 @@ 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.window import Window +from libqtile.backend.x11.window import Window from kuro.utils.general import notify, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_DOWN, BUTTON_UP, BUTTON_MUTE, \ call_process diff --git a/kuro/utils/windows.py b/kuro/utils/windows.py index edaf7ac..6bfd997 100644 --- a/kuro/utils/windows.py +++ b/kuro/utils/windows.py @@ -1,7 +1,6 @@ from cairocffi.test_xcb import xcffib from libqtile import hook -from libqtile.window import Window, Static - +from libqtile.backend.x11.window import Window, Static class KuroStatic(Static): From 0f4ef9190a657e8a060c660dd85d0e04cb46164a Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 22 Nov 2021 20:47:16 +0100 Subject: [PATCH 04/10] Refactor usage of qtile object and improvements to media widget --- config.py | 3 --- kuro/config.py | 2 +- kuro/theme.py | 54 +++++++++++++++++++++++++------------------ kuro/utils/widgets.py | 39 +++++++++++++++++++++++-------- 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/config.py b/config.py index 46c84be..75af0da 100644 --- a/config.py +++ b/config.py @@ -127,8 +127,5 @@ def main(qtile): else: qtile.cmd_warning() - # Save qtile instance in theme - Theme.qtile = qtile - # Save theme instance in qtile qtile.theme_instance = Theme diff --git a/kuro/config.py b/kuro/config.py index e178ee6..5d06eb3 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -19,7 +19,7 @@ class Config(BaseConfig): app_launcher = "/home/kevin/bin/dmenu_wal.sh" web_browser = "firefox-developer-edition" file_manager = "thunar" - app_chat = "ramboxpro" + app_chat = "/usr/bin/ramboxpro" app_irc = "quasselclient" app_mail = "thunderbird" cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" diff --git a/kuro/theme.py b/kuro/theme.py index 9e14eaa..2226c5e 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -9,7 +9,7 @@ logger.error("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 +from libqtile import layout, bar, widget, qtile logger.error("Importing theme util functions...") @@ -89,7 +89,7 @@ class Kuro(BaseTheme): for field in self.debug_textfields: field.text = text for bar in self.debug_bars: - if self.qtile is not None: + if qtile is not None: bar.draw() def log_debug(self, text): @@ -496,22 +496,22 @@ class Kuro(BaseTheme): else: utils.execute("arandr") - def reinitialize_visualizers(self, qtile=None): + def reinitialize_visualizers(self): if Config.get("show_audio_visualizer", False): logger.warning("Reinitializing visualizers...") - for screen in self.qtile.screens: + 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(qtile=qtile) + self.update_visualizers() - def update_visualizers(self, qtile=None): + def update_visualizers(self): if Config.get("show_audio_visualizer", False): logger.warning("Updating visualizers..") - for screen in self.qtile.screens: + for screen in qtile.screens: for widget in screen.top.widgets: if isinstance(widget, kuro.utils.widgets.AudioVisualizerWidget): if widget.client is None: @@ -554,13 +554,20 @@ class Kuro(BaseTheme): # QTile base callbacks def callback_startup_once(self, *args, **kwargs): - self.update_wallpaper(self.qtile) + 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() def callback_startup(self): + if not hasattr(qtile, 'theme_instance'): + # Save theme instance in qtile + qtile.theme_instance = self + if self.current_wallpaper: p = utils.execute_once(["wal", "-n", "-i", "{}".format(self.current_wallpaper)]) p.wait() @@ -573,7 +580,7 @@ class Kuro(BaseTheme): except KeyError: wallpaper = None if wallpaper: - Kuro.set_wallpaper(self.qtile, wallpaper) + Kuro.set_wallpaper(qtile, wallpaper) else: p = utils.execute_once("nitrogen --restore") p.wait() @@ -612,9 +619,13 @@ class Kuro(BaseTheme): initial_windows = [] 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 self.qtile.no_spawn: - dg = self.qtile.dgroups + if not qtile.no_spawn: + dg = qtile.dgroups for r in dg.rules: pid = -1 # noinspection PyProtectedMember @@ -628,12 +639,12 @@ class Kuro(BaseTheme): self.callback_client_new() # After first startup is complete, start the audio apps that can only be started after boot is complete - if not self.qtile.no_spawn: + if not qtile.no_spawn: for app in Config.get("apps_audio_afterstart", []): utils.execute_once(app) # Update color scheme - Kuro.update_colorscheme(self.qtile) + Kuro.update_colorscheme(qtile) def callback_client_new(self, *args, **kwargs): client = args[0] if len(args) > 0 else None @@ -641,27 +652,27 @@ class Kuro(BaseTheme): if len(self.initial_windows) > 0: init = self.initial_windows.copy() for pid, gname in init: - for group in self.qtile.groups: + 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 = self.qtile.dgroups.rules_map.copy() + c = qtile.dgroups.rules_map.copy() for rid, r in c.items(): if r.matches(window): - self.qtile.dgroups.remove_rule(rid) + 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(self.qtile.dgroups.rules_map)) + 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 self.qtile.screens: + 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: @@ -670,10 +681,10 @@ class Kuro(BaseTheme): pos_y = 0 + widget.margin_y width = viz_info['width'] - (2 * widget.margin_x) height = viz_info['height'] - (2 * widget.margin_y) - screen_index = self.qtile.screens.index(screen) + 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.set_opacity(Config.get("bar_opacity", 1.0)) + c.opacity = Config.get("bar_opacity", 1.0) widget.set_client(c, screen) placed = True if not placed: @@ -692,7 +703,6 @@ class Kuro(BaseTheme): client.window.togroup("") - def callback_client_killed(self, *args, **kwargs): client = args[0] logger.warning("Client {} Killed".format(client)) @@ -853,7 +863,7 @@ class Kuro(BaseTheme): # 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 {}".format(colors['color15'], colors['color1'][1:])) + f.write("#define COLOR {}\n#request setbg {}00".format(colors['color15'], colors['color1'][1:])) qtile.theme_instance.reinitialize_visualizers() utils.notify( diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index 7473717..c32532c 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -260,6 +260,7 @@ class MediaWidget(base.InLoopPollText): ('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.'), ] player_icons = { @@ -333,7 +334,7 @@ class MediaWidget(base.InLoopPollText): def cmd_update_custom_player(self, player_name, data): # Update firefox player - if player_name == "firefox": + 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 @@ -355,8 +356,11 @@ class MediaWidget(base.InLoopPollText): players = [] # Playerctl players - command = ["playerctl", "-l"] - result = self.call_process(command) + try: + result = self.call_process(["playerctl", "-l"]) + except subprocess.CalledProcessError: + result = None + if result: players.extend([x for x in result.split("\n") if x]) @@ -398,8 +402,14 @@ class MediaWidget(base.InLoopPollText): text = "Unknown" if cmd_result in ["Playing", "Paused"]: - artist = self.call_process(['playerctl', '-p', player, 'metadata', 'artist']).strip() - title = self.call_process(['playerctl', '-p', player, 'metadata', 'title']).strip()[:50] + try: + artist = self.call_process(['playerctl', '-p', player, 'metadata', 'artist']).strip() + except subprocess.CalledProcessError: + artist = None + try: + title = self.call_process(['playerctl', '-p', player, 'metadata', 'title']).strip() + except subprocess.CalledProcessError: + title = None if artist and title: text = "{} - {}".format(artist, title) @@ -419,13 +429,17 @@ class MediaWidget(base.InLoopPollText): def _get_formatted_text(self, status): if status[0] == MediaWidget.Status.PLAYING: - return self.on_text_play.format(status[1]) + res = self.on_text_play.format(status[1]) elif status[0] == MediaWidget.Status.PAUSED: - return self.on_text_pause.format(status[1]) + res = self.on_text_pause.format(status[1]) elif status[0] == MediaWidget.Status.STOPPED: - return self.on_text_stop.format(status[1]) + res = self.on_text_stop.format(status[1]) else: - return "Unknown" + res = "Unknown" + res = pangocffi.markup_escape_text(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() @@ -437,7 +451,12 @@ class MediaWidget(base.InLoopPollText): return self.off_text else: for player in status.keys(): - icon = self.player_icons.get(player, player) + # 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 From 3b6ee3d20ddeb51457424cbeffc570569b61e5b4 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Sat, 28 May 2022 14:01:07 +0200 Subject: [PATCH 05/10] Fixes for crash and updates --- kuro/config.py | 2 +- kuro/utils/general.py | 2 +- kuro/utils/widgets.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/kuro/config.py b/kuro/config.py index 5d06eb3..b942998 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -19,7 +19,7 @@ class Config(BaseConfig): app_launcher = "/home/kevin/bin/dmenu_wal.sh" web_browser = "firefox-developer-edition" file_manager = "thunar" - app_chat = "/usr/bin/ramboxpro" + app_chat = "/usr/bin/rambox" app_irc = "quasselclient" app_mail = "thunderbird" cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" diff --git a/kuro/utils/general.py b/kuro/utils/general.py index 8a432b0..5bc12bc 100644 --- a/kuro/utils/general.py +++ b/kuro/utils/general.py @@ -196,7 +196,7 @@ class KuroTopBar(Bar): self.theme = theme super(KuroTopBar, self).__init__(widgets, size, **config) - def _configure(self, qtile, screen): + 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 diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index c32532c..36e1283 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -8,6 +8,7 @@ import iwlib import netifaces import psutil import six +import unicodedata from libqtile import bar, pangocffi from libqtile.log_utils import logger from libqtile.widget import base @@ -437,6 +438,7 @@ class MediaWidget(base.InLoopPollText): 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 From 2860100089af727d90c0aaf22596c566b69a8188 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Thu, 8 Feb 2024 18:59:30 +0100 Subject: [PATCH 06/10] 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): From 5501824875902b2019e940e2ffe71cb963099af9 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Tue, 20 Feb 2024 16:03:00 +0100 Subject: [PATCH 07/10] Small fixes and app changeS --- kuro/config.py | 9 +++++---- kuro/theme.py | 16 +++++++++++++--- kuro/utils/general.py | 2 +- kuro/utils/widgets.py | 14 ++++++++++++++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/kuro/config.py b/kuro/config.py index f70166c..9939081 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -17,14 +17,15 @@ class Config(BaseConfig): # Predefined commands cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10" cmd_brightness_down = "sudo /usr/bin/xbacklight -dec 10" - cmd_screenshot = "xfce4-screenshooter -r -c -d 1" - cmd_alt_screenshot = "xfce4-screenshooter -w -c -d 0" + cmd_screenshot = "/home/kevin/bin/screenshot.sh" + cmd_alt_screenshot = "/home/kevin/bin/screenshot.sh" app_terminal = "terminator" web_browser = "firefox" file_manager = "thunar" - app_launcher = "ulauncher-toggle --no-window-shadow" + app_launcher = "wofi --show drun,run" lock_command = "bash /home/kevin/bin/lock.sh" + cliphistory_command = "/home/kevin/bin/cliphistory.sh" # Autostart applications apps_autostart_group = [ @@ -36,7 +37,7 @@ class Config(BaseConfig): {'group': "", 'command': ["spotify"]}, ] apps_autostart = [ - ["ulauncher", "--hide-window", "--no-window-shadow"], # App launcher background daemon + # ["ulauncher", "--hide-window", "--no-window-shadow"], # App launcher background daemon ["mako"], # Notification daemon ["kanshi"], # Display hotplug ["wl-paste", "--watch", "cliphist", "store"], # Clipboard manager diff --git a/kuro/theme.py b/kuro/theme.py index b9d4e66..25c5356 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -87,6 +87,13 @@ class Kuro(BaseTheme): Match(title='branchdialog'), # gitk Match(title='pinentry'), # GPG key password entry Match(title='origin.exe', wm_class='Wine'), # Wine Origin game launcher + + # Homebank popups + Match(title='Add transaction', wm_class='homebank'), + Match(title='Edit transaction', wm_class='homebank'), + Match(title='Inherit transaction', wm_class='homebank'), + Match(title='Multiple edit transactions', wm_class='homebank'), + Match(title='Transaction splits', wm_class='homebank'), ] ) @@ -175,6 +182,9 @@ class Kuro(BaseTheme): # Alt screenshot Key([self.mod], "Print", lazy.spawn(Config.get('cmd_alt_screenshot', 'xfce4-screenshooter'))), + # Copy from clipboard history + Key([self.mod, "control"], "c", lazy.spawn(Config.get('cliphistory_command', 'true'))), + # Toggle between different layouts as defined below Key([self.mod], "Tab", lazy.next_layout()), @@ -192,7 +202,7 @@ class Kuro(BaseTheme): 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'))), + Key([self.mod, "control"], "s", lazy.spawn(Config.get('cmd_reconfigure_screens', 'true'))), # Reload colorscheme Key([self.mod, "control"], "t", lazy.function(self.update_colorscheme)), @@ -204,7 +214,7 @@ class Kuro(BaseTheme): ## # Debug keyboard shortcuts ## - Key([self.mod, "control"], "c", lazy.function(display_wm_class)), + Key([self.mod, "control"], "w", lazy.function(display_wm_class)), # Show extensive window info Key([self.mod, "shift", "control"], "i", lazy.function(self.show_window_info)), @@ -355,7 +365,7 @@ class Kuro(BaseTheme): def build_bar_for_screen(self, screen_num): widgets = [ # Workspaces - widget.GroupBox( + kuro.utils.widgets.KuroGroupBox( active=Config.get('colour_groupbox_icon_active', '#ffffff'), borderwidth=Config.get('width_groupbox_border', 1), disable_drag=Config.get('bool_groupbox_disable_drag', False), diff --git a/kuro/utils/general.py b/kuro/utils/general.py index 0a2b225..c8d6a8c 100644 --- a/kuro/utils/general.py +++ b/kuro/utils/general.py @@ -195,7 +195,7 @@ def test_popups(qtile): def display_wm_class(qtile): - window = qtile.currentWindow if qtile else None + window = qtile.current_window if qtile else None if window: wm_class = window.get_wm_class() or None diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index e7aadce..a27249a 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -13,6 +13,7 @@ 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.wlan import get_status +from libqtile.widget.groupbox import GroupBox 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, \ @@ -944,3 +945,16 @@ class VolumeInfoWidget(DualPaneTextboxBase): def run_app(self): # Emulate button press. self.button_press(0, 0, BUTTON_RIGHT) + + +class KuroGroupBox(GroupBox): + @property + def length(self): + try: + return super(KuroGroupBox, self).length + except AttributeError: + return 1 + + @length.setter + def length(self, length): + logger.warning(f"Setting groupbox length to {length}") From a043d8d452df81d6e3d90ab55cd266b8a75015fc Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 4 Mar 2024 14:44:38 +0100 Subject: [PATCH 08/10] Default apps changes --- kuro/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kuro/config.py b/kuro/config.py index b942998..dc21c57 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -17,7 +17,7 @@ class Config(BaseConfig): # Default Applications app_terminal = "terminator" app_launcher = "/home/kevin/bin/dmenu_wal.sh" - web_browser = "firefox-developer-edition" + web_browser = "firefox" file_manager = "thunar" app_chat = "/usr/bin/rambox" app_irc = "quasselclient" @@ -149,7 +149,7 @@ class Config(BaseConfig): do_keyboard_updates = False # Show audio visualizer - show_audio_visualizer = True + show_audio_visualizer = False kill_unnecessary_glava_processes = True # Show thermal widget From 737800806d1c1e38f1e95197b8cee30ff5f7a160 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Fri, 25 Jul 2025 12:36:49 +0200 Subject: [PATCH 09/10] Various wayland fixes, ghostty, updates etcetera. --- config_debug.py | 2 +- kuro/base.py | 2 +- kuro/config.py | 5 ++--- kuro/theme.py | 20 ++++++++++++++++---- kuro/utils/widgets.py | 10 +++++----- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/config_debug.py b/config_debug.py index 8dbfaff..3aa6617 100644 --- a/config_debug.py +++ b/config_debug.py @@ -25,7 +25,7 @@ # SOFTWARE. from libqtile.config import Key, Screen, Group, Drag, Click -from libqtile.command import lazy +from libqtile.lazy import lazy from libqtile import layout, bar, widget diff --git a/kuro/base.py b/kuro/base.py index 9557f9e..15ce41d 100644 --- a/kuro/base.py +++ b/kuro/base.py @@ -1,5 +1,5 @@ from libqtile import layout as libqtile_layout, layout, bar, widget -from libqtile.command import lazy +from libqtile.lazy import lazy from libqtile.config import Key, Group, Screen, Drag, Click, Match # Initialize logging diff --git a/kuro/config.py b/kuro/config.py index 9939081..ecfb2a4 100644 --- a/kuro/config.py +++ b/kuro/config.py @@ -19,8 +19,7 @@ class Config(BaseConfig): cmd_brightness_down = "sudo /usr/bin/xbacklight -dec 10" cmd_screenshot = "/home/kevin/bin/screenshot.sh" cmd_alt_screenshot = "/home/kevin/bin/screenshot.sh" - - app_terminal = "terminator" + app_terminal = "ghostty" web_browser = "firefox" file_manager = "thunar" app_launcher = "wofi --show drun,run" @@ -30,7 +29,7 @@ class Config(BaseConfig): # Autostart applications apps_autostart_group = [ {'group': "", 'command': ["firefox"]}, - {'group': "", 'command': ["terminator"]}, + {'group': "", 'command': ["ghostty", "--gtk-single-instance=true", "--quit-after-last-window-close=false", "--initial-window=true"]}, {'group': "", 'command': ["/usr/bin/rambox"]}, {'group': "", 'command': ["thunar"]}, {'group': "", 'command': ["thunderbird"]}, diff --git a/kuro/theme.py b/kuro/theme.py index 25c5356..88a0dca 100644 --- a/kuro/theme.py +++ b/kuro/theme.py @@ -15,7 +15,7 @@ from libqtile.log_utils import logger logger.warning("Importing qtile theme requirements...") from libqtile.config import Key, Screen, Group, Drag, Click, Match -from libqtile.command import lazy +from libqtile.lazy import lazy from libqtile import layout, bar, widget, qtile from qtile_extras import widget as extra_widget @@ -214,7 +214,7 @@ class Kuro(BaseTheme): ## # Debug keyboard shortcuts ## - Key([self.mod, "control"], "w", lazy.function(display_wm_class)), + Key([self.mod, "shift", "control"], "w", lazy.function(display_wm_class)), # Show extensive window info Key([self.mod, "shift", "control"], "i", lazy.function(self.show_window_info)), @@ -594,7 +594,7 @@ class Kuro(BaseTheme): logger.warning("Restoring wallpaper...") if self.current_wallpaper: - p = utils.execute_once(["wal", "-n", "-i", "{}".format(self.current_wallpaper)]) + p = utils.execute_once(["wallust", "run", "{}".format(self.current_wallpaper)]) p.wait() else: wallpaper = None @@ -622,6 +622,9 @@ class Kuro(BaseTheme): # Update color scheme self.update_colorscheme() + # Setup XDG Desktop Portal + self.setup_xdg_desktop_portal() + # After first startup is complete, autostart configured apps logger.warning("Autostarting apps...") for app in Config.get("apps_autostart", []): @@ -745,7 +748,7 @@ class Kuro(BaseTheme): 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 = utils.execute_once(["wallust", "run", "{}".format(self.current_wallpaper)]) p.wait() colors = None @@ -843,3 +846,12 @@ class Kuro(BaseTheme): utils.notify(qtile, "Updated colorscheme!", f"active: {colors['color15']}, inactive: {colors['color1']}") + + def setup_xdg_desktop_portal(self): + # XDG Desktop portal is used for screensharing, screenshots and filepickers in wayland. + # To work correctly, it needs to have two env variables set in the systemd user session + logger.warning(f"Setting XDG_CURRENT_DESKTOP env and updating XDG Desktop Portal configuration...") + os.environ["XDG_CURRENT_DESKTOP"] = "qtile" + subprocess.Popen(["systemctl", "--user", "import-environment", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP"]) + subprocess.Popen(["dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP=qtile"]) + subprocess.Popen(["systemctl", "--user", "restart", "xdg-desktop-portal"]) diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py index a27249a..f1b51fd 100644 --- a/kuro/utils/widgets.py +++ b/kuro/utils/widgets.py @@ -201,18 +201,18 @@ class DualPaneTextboxBase(base._Widget): self.changed = False @expose_command() - def set_font(self, font=base.UNSPECIFIED, fontsize_left=base.UNSPECIFIED, fontsize_right=base.UNSPECIFIED, fontshadow=base.UNSPECIFIED): + def set_font(self, font=None, fontsize_left=0, fontsize_right=0, fontshadow=""): """ Change the font used by this widget. If font is None, the current font is used. """ - if font is not base.UNSPECIFIED: + if font is not None: self.font = font - if fontsize_left is not base.UNSPECIFIED: + if fontsize_left != 0: self.fontsize_left = fontsize_left - if fontsize_right is not base.UNSPECIFIED: + if fontsize_right != 0: self.fontsize_right = fontsize_right - if fontshadow is not base.UNSPECIFIED: + if fontshadow != "": self.fontshadow = fontshadow self.bar.draw() From 6dd362247e0b7337278598d27f8129efc710857e Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Fri, 25 Jul 2025 17:39:36 +0200 Subject: [PATCH 10/10] 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