870 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			870 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import json
 | 
						|
import os
 | 
						|
import random
 | 
						|
import time
 | 
						|
import datetime
 | 
						|
import socket
 | 
						|
import subprocess
 | 
						|
from typing import Optional
 | 
						|
 | 
						|
from libqtile.backend.base import Window
 | 
						|
from libqtile.backend.wayland.layer import LayerStatic
 | 
						|
from libqtile.backend.wayland.xwindow import XWindow as WaylandXWindow, XStatic as WaylandXStatic
 | 
						|
from libqtile.backend.x11.window import XWindow as XorgXWindow
 | 
						|
# Initialize logging
 | 
						|
from libqtile.log_utils import logger
 | 
						|
 | 
						|
logger.warning("Importing qtile theme requirements...")
 | 
						|
 | 
						|
from libqtile.config import Key, Screen, Group, Drag, Click, Match
 | 
						|
from libqtile.lazy import lazy
 | 
						|
from libqtile import layout, bar, widget, qtile
 | 
						|
from qtile_extras import widget as extra_widget
 | 
						|
 | 
						|
logger.warning("Importing theme util functions...")
 | 
						|
 | 
						|
# Import theme util functions
 | 
						|
from xcffib.xproto import WindowError
 | 
						|
 | 
						|
logger.warning("Importing kuro utils...")
 | 
						|
 | 
						|
import kuro.utils.widgets
 | 
						|
from kuro.utils import general as 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 import layouts as kuro_layouts
 | 
						|
 | 
						|
logger.warning("Importing configuration...")
 | 
						|
 | 
						|
from kuro.utils import load_config_class
 | 
						|
Config = load_config_class()
 | 
						|
if Config is None:
 | 
						|
    raise ImportError("Could not load theme Config or BaseConfig! Error: {}".format(e))
 | 
						|
Config.initialize(qtile)
 | 
						|
 | 
						|
logger.warning("Imports done")
 | 
						|
 | 
						|
 | 
						|
class Kuro(BaseTheme):
 | 
						|
    # Shorthand for modifier key
 | 
						|
    mod = Config.get("modifier", "mod4")
 | 
						|
 | 
						|
    # Screen count
 | 
						|
    num_screens = 0
 | 
						|
 | 
						|
    # Static windows
 | 
						|
    static_windows = []
 | 
						|
 | 
						|
    # Current wallpaper path
 | 
						|
    current_wallpaper = None
 | 
						|
 | 
						|
    # Window manager name
 | 
						|
    wmname = "qtile"
 | 
						|
 | 
						|
    startup_completed = False
 | 
						|
    autostart_app_rules = {}
 | 
						|
 | 
						|
    # Floating layout override
 | 
						|
    floating_layout = kuro_layouts.KuroFloating(
 | 
						|
        border_width=0,
 | 
						|
        border_focus="#000000",
 | 
						|
        border_normal="#000000",
 | 
						|
        float_rules=[
 | 
						|
            # Run the utility of `xprop` to see the wm class and name of an X client.
 | 
						|
            *layout.Floating.default_float_rules,
 | 
						|
            Match(wm_class='confirmreset'),  # gitk
 | 
						|
            Match(wm_class='makebranch'),  # gitk
 | 
						|
            Match(wm_class='maketag'),  # gitk
 | 
						|
            Match(wm_class='ssh-askpass'),  # ssh-askpass
 | 
						|
            Match(title='branchdialog'),  # gitk
 | 
						|
            Match(title='pinentry'),  # GPG key password entry
 | 
						|
            # Extra rules from host-specific Config
 | 
						|
            *[Match(**rule) for rule in Config.get('extra_float_rules', [])]
 | 
						|
        ]
 | 
						|
    )
 | 
						|
 | 
						|
    def initialize(self):
 | 
						|
        # Update color scheme
 | 
						|
        logger.warning("Initializing colorscheme...")
 | 
						|
        self.initialize_colorscheme()
 | 
						|
 | 
						|
        logger.warning("Initializing superclass...")
 | 
						|
        super(Kuro, self).initialize()
 | 
						|
 | 
						|
        logger.warning("Updating keys for groups and layouts...")
 | 
						|
        self.update_keys()
 | 
						|
 | 
						|
    def init_keys(self):
 | 
						|
        logger.warning("Initializing keys")
 | 
						|
        keys = [
 | 
						|
            # Switch between windows in current stack pane
 | 
						|
            Key([self.mod], "k", lazy.layout.down()),
 | 
						|
            Key([self.mod], "j", lazy.layout.up()),
 | 
						|
 | 
						|
            # Move windows up or down in current stack
 | 
						|
            Key([self.mod, "control"], "k", lazy.layout.shuffle_down()),
 | 
						|
            Key([self.mod, "control"], "j", lazy.layout.shuffle_up()),
 | 
						|
 | 
						|
            # Switch window focus to other pane(s) of stack
 | 
						|
            Key([self.mod], "space", lazy.layout.next()),
 | 
						|
 | 
						|
            # Swap panes of split stack
 | 
						|
            Key([self.mod, "shift"], "space", lazy.layout.rotate()),
 | 
						|
 | 
						|
            # Fullscreen toggle
 | 
						|
            Key([self.mod], 'f', lazy.window.toggle_fullscreen()),
 | 
						|
 | 
						|
            # Floating toggle
 | 
						|
            Key([self.mod, "shift"], 'f', lazy.window.toggle_floating()),
 | 
						|
 | 
						|
            # Toggle between split and unsplit sides of stack.
 | 
						|
            # Split = all windows displayed
 | 
						|
            # Unsplit = 1 window displayed, like Max layout, but still with
 | 
						|
            # multiple stack panes
 | 
						|
            Key([self.mod, "shift"], "Return", lazy.layout.toggle_split()),
 | 
						|
 | 
						|
            # Super-Enter to start terminal
 | 
						|
            Key([self.mod], "Return", lazy.spawn(Config.get('app_terminal', "xterm"))),
 | 
						|
 | 
						|
            # Super-R to start dmenu_run
 | 
						|
            Key([self.mod], "r", lazy.spawn(Config.get('app_launcher', "dmenu_run"))),
 | 
						|
 | 
						|
            # Super-B to start webbrowser
 | 
						|
            Key([self.mod], "b", lazy.spawn(Config.get('web_browser', "xterm links"))),
 | 
						|
 | 
						|
            # Super-T to start file manager
 | 
						|
            Key([self.mod], "t", lazy.spawn(Config.get('file_manager', "thunar"))),
 | 
						|
 | 
						|
            # Super-Shift-R to start spawncmd
 | 
						|
            Key([self.mod, "shift"], "r", lazy.spawncmd()),
 | 
						|
 | 
						|
            # Lock shortcut
 | 
						|
            Key([self.mod], "l", lazy.spawn(Config.get('lock_command', "i3lock"))),
 | 
						|
 | 
						|
            # Media keys
 | 
						|
            Key([], "XF86AudioPlay", lazy.spawn(Config.get('cmd_media_play', 'true'))),
 | 
						|
            Key([], "XF86AudioNext", lazy.spawn(Config.get('cmd_media_next', 'true'))),
 | 
						|
            Key([], "XF86AudioMute", lazy.spawn(Config.get('cmd_media_mute', 'true'))),
 | 
						|
            Key([], "XF86AudioRaiseVolume", lazy.spawn(Config.get('cmd_media_volume_up', 'true'))),
 | 
						|
            Key([], "XF86AudioLowerVolume", lazy.spawn(Config.get('cmd_media_volume_down', 'true'))),
 | 
						|
 | 
						|
            # Sleep key
 | 
						|
            Key([], "XF86Sleep", lazy.spawn(Config.get('cmd_sleep', 'true'))),
 | 
						|
 | 
						|
            # Screenshot key
 | 
						|
            Key([], "Print", lazy.spawn(Config.get('cmd_screenshot', 'xfce4-screenshooter'))),
 | 
						|
 | 
						|
            # Alt screenshot
 | 
						|
            Key([self.mod], "Print", lazy.spawn(Config.get('cmd_alt_screenshot', 'xfce4-screenshooter'))),
 | 
						|
 | 
						|
            # Copy from clipboard history
 | 
						|
            Key([self.mod, "control"], "c", lazy.spawn(Config.get('cliphistory_command', 'true'))),
 | 
						|
 | 
						|
            # Toggle between different layouts as defined below
 | 
						|
            Key([self.mod], "Tab", lazy.next_layout()),
 | 
						|
 | 
						|
            # Kill the current window
 | 
						|
            Key([self.mod], "w", lazy.window.kill()),
 | 
						|
 | 
						|
            # Restart/reload QTile (Restart only available in X11 backend)
 | 
						|
            Key([self.mod, "control"], "r", lazy.restart() if qtile.core.name == "x11" else lazy.reload_config()),
 | 
						|
 | 
						|
            # Shutdown QTile
 | 
						|
            Key([self.mod, "control"], "q", lazy.shutdown()),
 | 
						|
 | 
						|
            # Update wallpaper
 | 
						|
            Key([self.mod, "control"], "w",
 | 
						|
                lazy.function(self.set_random_wallpaper), lazy.function(self.update_colorscheme)),
 | 
						|
 | 
						|
            # Reload screen configuration
 | 
						|
            Key([self.mod, "control"], "s", lazy.spawn(Config.get('cmd_reconfigure_screens', 'true'))),
 | 
						|
 | 
						|
            # Reload colorscheme
 | 
						|
            Key([self.mod, "control"], "t", lazy.function(self.update_colorscheme)),
 | 
						|
 | 
						|
            # Toggle static windows
 | 
						|
            Key([self.mod], "p", lazy.function(self.toggle_window_static)),
 | 
						|
 | 
						|
 | 
						|
            ##
 | 
						|
            # Debug keyboard shortcuts
 | 
						|
            ##
 | 
						|
            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)),
 | 
						|
 | 
						|
            # Spawn a popup, and despawn it after 3 seconds
 | 
						|
            Key([self.mod, "control"], "p", lazy.function(test_popups)),
 | 
						|
        ]
 | 
						|
        # Extra keybinds from host-specific Config
 | 
						|
        keys.extend([
 | 
						|
            Key([(self.mod if m == 'mod' else m) for m in key['modifiers']], key['key'], key['action'])
 | 
						|
            for key in Config.get('extra_keys', [])
 | 
						|
        ])
 | 
						|
        return keys
 | 
						|
 | 
						|
    def init_groups(self):
 | 
						|
        logger.warning("Initializing groups")
 | 
						|
        # http://fontawesome.io/cheatsheet
 | 
						|
        groups = []
 | 
						|
 | 
						|
        for group in Config.get('groups', [{'name': '1'}]):
 | 
						|
            if 'layout' in group:
 | 
						|
                if group['layout'] == "floating":
 | 
						|
                    groups.append(Group(
 | 
						|
                        group['name'],
 | 
						|
                        layout="floating",
 | 
						|
                        layouts=[
 | 
						|
                            layout.Floating(**group.get('options', {}))
 | 
						|
                        ]
 | 
						|
                    ))
 | 
						|
                else:
 | 
						|
                    logger.warning(f"Unknown group layout for group '{group['name']}': {group['layout']}")
 | 
						|
            else:
 | 
						|
                groups.append(Group(group['name']))
 | 
						|
        return groups
 | 
						|
 | 
						|
    def init_layouts(self):
 | 
						|
        logger.warning("Initializing layouts")
 | 
						|
        return [
 | 
						|
            kuro_layouts.KuroWmii(
 | 
						|
                theme=self,
 | 
						|
                border_focus=Config.get('colour_border_focus', "#ffffff"),
 | 
						|
                border_focus_stack=Config.get('colour_border_normal', "#777777"),
 | 
						|
                border_normal=Config.get('colour_border_normal', "#777777"),
 | 
						|
                border_normal_stack=Config.get('colour_border_normal', "#777777"),
 | 
						|
                border_width=Config.get('width_border', "1"),
 | 
						|
                grow_amount=Config.get('grow_amount', "5"),
 | 
						|
                margin=Config.get('margin_layout', "0"),
 | 
						|
            ),
 | 
						|
            layout.Max(),
 | 
						|
            layout.Zoomy(
 | 
						|
                columnwidth=Config.get('width_zoomy_column', 150),
 | 
						|
                margin=Config.get('margin_layout', "0"),
 | 
						|
            )
 | 
						|
        ]
 | 
						|
 | 
						|
    def init_widget_defaults(self):
 | 
						|
        logger.warning("Initializing widget_defaults")
 | 
						|
        return {
 | 
						|
            "font": Config.get('font_topbar', "Sans"),
 | 
						|
            "fontsize": Config.get('fontsize_topbar', 16),
 | 
						|
            "padding": 3,
 | 
						|
        }
 | 
						|
 | 
						|
    def init_screens(self):
 | 
						|
        logger.warning("Initializing screens")
 | 
						|
        self.reinit_screens()
 | 
						|
        return self.screens
 | 
						|
 | 
						|
    def init_mouse(self):
 | 
						|
        logger.warning("Initializing mouse")
 | 
						|
        # Drag floating layouts.
 | 
						|
        mouse = [
 | 
						|
            Drag([self.mod], "Button1", lazy.window.set_position_floating(),
 | 
						|
                 start=lazy.window.get_position()),
 | 
						|
            Drag([self.mod], "Button3", lazy.window.set_size_floating(),
 | 
						|
                 start=lazy.window.get_size()),
 | 
						|
            Click([self.mod], "Button2", lazy.window.bring_to_front())
 | 
						|
        ]
 | 
						|
        return mouse
 | 
						|
 | 
						|
    def initialize_colorscheme(self):
 | 
						|
        colors = None
 | 
						|
        if os.path.isfile(f"{Config.get('homedir', '~')}/.cache/wal/colors.json"):
 | 
						|
            with open(f"{Config.get('homedir', '~')}/.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):
 | 
						|
        if Config.get("use_fake_screens", False):
 | 
						|
            logger.warning(f"Using fake screens!")
 | 
						|
            self.num_screens = Config.get("fake_screen_count", 1)
 | 
						|
        else:
 | 
						|
            self.num_screens = max(1, utils.get_screen_count())
 | 
						|
        logger.warning(f"Detected {self.num_screens} screens.")
 | 
						|
 | 
						|
        screen_kwargs = Config.get("screen_kwargs", [])
 | 
						|
 | 
						|
        for x in range(self.num_screens):
 | 
						|
            logger.warning(f"Reconfiguring bars for screen {x}")
 | 
						|
 | 
						|
            try:
 | 
						|
                screen = self.screens[x]
 | 
						|
            except IndexError:
 | 
						|
                try:
 | 
						|
                    kwargs = screen_kwargs[x]
 | 
						|
                    logger.warning(f"Config for screen {x}: {kwargs}")
 | 
						|
                except IndexError:
 | 
						|
                    logger.warning(f"No kwarg config for screen {x}")
 | 
						|
                    kwargs = {}
 | 
						|
                screen = Screen(**kwargs)
 | 
						|
            
 | 
						|
            if screen.top is None:
 | 
						|
                screen.top = self.build_bar_for_screen(x)
 | 
						|
            topbar = screen.top
 | 
						|
 | 
						|
            self.screens.append(screen)
 | 
						|
 | 
						|
    def update_keys(self):
 | 
						|
        logger.warning("Updating keys")
 | 
						|
        for i, g in enumerate(self.groups):
 | 
						|
            if i == 9:
 | 
						|
                i = -1
 | 
						|
            elif i > 9:
 | 
						|
                continue
 | 
						|
            # mod1 + number = switch to group
 | 
						|
            self.keys.append(
 | 
						|
                Key([self.mod], str(i + 1), lazy.group[g.name].toscreen())
 | 
						|
            )
 | 
						|
 | 
						|
            # mod1 + shift + number = switch to & move focused window to group
 | 
						|
            self.keys.append(
 | 
						|
                Key([self.mod, "shift"], str(i + 1), lazy.window.togroup(g.name))
 | 
						|
            )
 | 
						|
 | 
						|
        # Keys for the Wmii layout
 | 
						|
        self.keys.extend([
 | 
						|
            Key([self.mod, "shift"], "j", lazy.layout.shuffle_down()),
 | 
						|
            Key([self.mod, "shift"], "k", lazy.layout.shuffle_up()),
 | 
						|
            Key([self.mod, "shift"], "h", lazy.layout.shuffle_left()),
 | 
						|
            Key([self.mod, "shift"], "l", lazy.layout.shuffle_right()),
 | 
						|
 | 
						|
            Key([self.mod, "shift", "control"], "j", lazy.layout.grow_down()),
 | 
						|
            Key([self.mod, "shift", "control"], "k", lazy.layout.grow_up()),
 | 
						|
            Key([self.mod, "shift", "control"], "h", lazy.layout.grow_left()),
 | 
						|
            Key([self.mod, "shift", "control"], "l", lazy.layout.grow_right()),
 | 
						|
 | 
						|
            Key([self.mod], "s", lazy.layout.toggle_split()),
 | 
						|
            Key([self.mod], "n", lazy.layout.normalize()),
 | 
						|
        ])
 | 
						|
 | 
						|
    def build_bar_for_screen(self, screen_num):
 | 
						|
        widgets = [
 | 
						|
            # Workspaces
 | 
						|
            kuro.utils.widgets.KuroGroupBox(
 | 
						|
                active=Config.get('colour_groupbox_icon_active', '#ffffff'),
 | 
						|
                borderwidth=Config.get('width_groupbox_border', 1),
 | 
						|
                disable_drag=Config.get('bool_groupbox_disable_drag', False),
 | 
						|
                font=Config.get('font_groupbox', 'Arial'),
 | 
						|
                fontsize=Config.get('fontsize_groupbox', 15),
 | 
						|
                highlight_color=Config.get("colour_groupbox_border_normal", '#444444'),
 | 
						|
                inactive=Config.get('colour_groupbox_icon_inactive', '#444444'),
 | 
						|
                rounded=Config.get('bool_groupbox_rounded_borders', True),
 | 
						|
                this_current_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'),
 | 
						|
                this_screen_border=Config.get('colour_groupbox_border_focus', '#ffffff'),
 | 
						|
                margin=Config.get('margin_groupbox', 0)
 | 
						|
            ),
 | 
						|
 | 
						|
            # Spawn prompt (only shown if activated)
 | 
						|
            widget.Prompt(**self.widget_defaults),
 | 
						|
 | 
						|
            # 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=" ",
 | 
						|
            )
 | 
						|
        ]
 | 
						|
 | 
						|
        # Media widget(s)
 | 
						|
        if Config.get('show_media_widget', False):
 | 
						|
            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="<b>{xesam:title}</b> - {xesam:artist} - <i>{xesam:album}</i>",
 | 
						|
                    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,
 | 
						|
                    }
 | 
						|
                )
 | 
						|
            ])
 | 
						|
 | 
						|
        # Sensor widgets
 | 
						|
        sensor_widgets = []
 | 
						|
 | 
						|
        # 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
 | 
						|
            ))
 | 
						|
 | 
						|
        # 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),
 | 
						|
        ])
 | 
						|
 | 
						|
        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="|")
 | 
						|
        ])
 | 
						|
 | 
						|
        # Battery level
 | 
						|
        if Config.get('show_battery_widget', False):
 | 
						|
            widgets.extend([
 | 
						|
                kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11),
 | 
						|
            ])
 | 
						|
 | 
						|
        # Volume widget(s)
 | 
						|
        for sink_name in Config.get('volume_pulse_sinks', []):
 | 
						|
            widgets.append(
 | 
						|
                kuro.utils.widgets.VolumeInfoWidget(
 | 
						|
                    pulse_sink=sink_name,
 | 
						|
                    fontsize_left=18,
 | 
						|
                    fontsize_right=11,
 | 
						|
                    font_left=Config.get('font_groupbox', None),
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
        # 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),
 | 
						|
                config_application=Config.get('network_config', None),
 | 
						|
            ),
 | 
						|
        ])
 | 
						|
 | 
						|
        # 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.CurrentLayout(mode="icon", custom_icon_paths=Config.get('custom_layout_icon_paths', [])),
 | 
						|
            widget.Clock(format="%a %d %b ", **self.widget_defaults),
 | 
						|
            widget.Clock(
 | 
						|
                format="<b>%H</b>:%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.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([*Config.get('cmd_wal', ['wallust', 'run']), "{}".format(self.current_wallpaper)])
 | 
						|
            if p:
 | 
						|
                p.wait()
 | 
						|
        else:
 | 
						|
            wallpaper = None
 | 
						|
            if os.path.isfile(f"{Config.get('homedir', '~')}/.cache/wal/colors.json"):
 | 
						|
                with open(f"{Config.get('homedir', '~')}/.cache/wal/colors.json", 'r') as f:
 | 
						|
                    try:
 | 
						|
                        wallpaper = json.load(f)['wallpaper']
 | 
						|
                    except KeyError:
 | 
						|
                        wallpaper = None
 | 
						|
            if wallpaper:
 | 
						|
                self.set_wallpaper(wallpaper)
 | 
						|
            else:
 | 
						|
                logger.warning("No wallpaper to restore.")
 | 
						|
 | 
						|
        # Update color scheme
 | 
						|
        self.update_colorscheme()
 | 
						|
 | 
						|
    def callback_startup_complete(self, *args, **kwargs):
 | 
						|
        logger.warning("Callback Startup Complete")
 | 
						|
        if not hasattr(qtile, 'theme_instance'):
 | 
						|
            # Save theme instance in qtile
 | 
						|
            qtile.theme_instance = self
 | 
						|
 | 
						|
        # Update color scheme
 | 
						|
        self.update_colorscheme()
 | 
						|
 | 
						|
        # Setup XDG Desktop Portal on Wayland
 | 
						|
        if qtile.core.name == "wayland":
 | 
						|
            self.setup_xdg_desktop_portal()
 | 
						|
 | 
						|
        # After first startup is complete, autostart configured apps
 | 
						|
        logger.warning("Autostarting apps...")
 | 
						|
        for category in Config.get("apps_autostart", {}).keys():
 | 
						|
            if qtile.core.name == category or category == "common":
 | 
						|
                logger.warning(f"Autostarting apps for {category}...")
 | 
						|
                for app in Config.get("apps_autostart", {}).get(category, []):
 | 
						|
                    logger.warning(f"Starting '{app}'...")
 | 
						|
                    utils.execute_once(app)
 | 
						|
            else:
 | 
						|
                logger.warning(f"Skipping autostart apps for {category}, because core is {qtile.core.name}...")
 | 
						|
 | 
						|
        for app in Config.get("apps_autostart_group", []):
 | 
						|
            if all(x in app.keys() for x in ["group", "command"]):
 | 
						|
                logger.warning(f"Starting '{app['command']}' in group {app['group']}...")
 | 
						|
                utils.start_in_group_once(theme=self, qtile=qtile, **app)
 | 
						|
            else:
 | 
						|
                logger.warning(f"Invalid app in 'apps_autostart_group', "
 | 
						|
                               f"must have 'group' and 'command' keys: {app}...")
 | 
						|
        logger.warning("Autostart complete")
 | 
						|
        cur_time = time.time()
 | 
						|
        logger.warning(f"QTile startup completed! Started up in {(cur_time - self.startup_time):.1f} seconds!")
 | 
						|
        self.startup_completed = True
 | 
						|
 | 
						|
    def callback_client_managed(self, *args, **kwargs):
 | 
						|
        client: Optional[Window] = args[0] if len(args) > 0 else None
 | 
						|
 | 
						|
        # TODO: Move get_pid to an utility function
 | 
						|
        w_pid = None
 | 
						|
        try:
 | 
						|
            w_pid = client.get_pid()
 | 
						|
        except AttributeError:  # Some windows might not have this .get_pid method. Try other ways
 | 
						|
            if isinstance(client, WaylandXWindow) or isinstance(client, WaylandXStatic):
 | 
						|
                w_pid = client.surface.pid
 | 
						|
            elif isinstance(client, XorgXWindow):
 | 
						|
                w_pid = client.get_net_wm_pid()
 | 
						|
            elif isinstance(client, LayerStatic):
 | 
						|
                pass  # Wayland background layer 'window'
 | 
						|
            else:
 | 
						|
                logger.error(f"Unknown window type {client.__class__.__name__}")
 | 
						|
 | 
						|
        if w_pid is not None and w_pid in self.autostart_app_rules.keys():
 | 
						|
            rule_id = self.autostart_app_rules[w_pid]
 | 
						|
            logger.warning(f"Removing rule {rule_id} for PID {w_pid}, client {client.name}")
 | 
						|
            lazy.remove_rule(rule_id)
 | 
						|
 | 
						|
    def callback_client_killed(self, *args, **kwargs):
 | 
						|
        client = args[0]
 | 
						|
        logger.warning("Client {} Killed".format(client))
 | 
						|
 | 
						|
        # If this window was static, remove it from the static window list
 | 
						|
        if hasattr(client, "is_static_window") and client.is_static_window:
 | 
						|
            logger.warning("Removing static window {}".format(client.name))
 | 
						|
            del client.is_static_window
 | 
						|
            self.static_windows.remove(client)
 | 
						|
 | 
						|
    def callback_screen_change(self, *args, **kwargs):
 | 
						|
        logger.warning(f"Screen configuration changed, reinitializing screens")
 | 
						|
        self.reinit_screens()
 | 
						|
        qtile.reconfigure_screens()
 | 
						|
        #qtile.reconfigure_screens()  # Twice, see: https://github.com/qtile/qtile/issues/4673#issuecomment-2196459114
 | 
						|
 | 
						|
    #def callback_screens_reconfigured(self, *args, **kwargs):
 | 
						|
        logger.warning(f"Screens were reconfgured, updating wallpapers and color scheme")
 | 
						|
        self.set_wallpaper(self.current_wallpaper)
 | 
						|
        self.update_colorscheme()
 | 
						|
 | 
						|
    def callback_setgroup(self, *args, **kwargs):
 | 
						|
        for window in self.static_windows:
 | 
						|
            # Only move if the window is not currently on any screen.
 | 
						|
            if window.group.screen is None:
 | 
						|
                try:
 | 
						|
                    window.togroup()
 | 
						|
                except WindowError as e:
 | 
						|
                    logger.warning("Could not move static window {}, removing from list: {}".format(window.name, e))
 | 
						|
                    del window.is_static_window
 | 
						|
                    self.static_windows.remove(window)
 | 
						|
 | 
						|
    def show_window_info(self, *args, **kwargs):
 | 
						|
        import pprint
 | 
						|
        window = qtile.current_window
 | 
						|
        if window:
 | 
						|
            logger.warning(f"Window properties {window.name}\n{pprint.pformat(vars(window))}")
 | 
						|
            if window.info():
 | 
						|
                logger.warning(f"Window info of {window.name}\n{pprint.pformat(window.info())}")
 | 
						|
 | 
						|
    def 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
 | 
						|
 | 
						|
        window.floating = True
 | 
						|
 | 
						|
    def set_random_wallpaper(self, *args, **kwargs):
 | 
						|
        wallpapers = []
 | 
						|
        wallpaper_dir = Config.get("desktop_bg_folder", "")
 | 
						|
 | 
						|
        # Use a wallpaper from the night folder after 9PM and before 6AM
 | 
						|
        wallpaper_night_dir = Config.get("desktop_bg_night_folder", "")
 | 
						|
        if wallpaper_night_dir and os.path.isdir(wallpaper_night_dir):
 | 
						|
            cur_time = datetime.datetime.now()
 | 
						|
            if cur_time.hour > 21 or cur_time.hour < 6:
 | 
						|
                wallpaper_dir = wallpaper_night_dir
 | 
						|
 | 
						|
        try:
 | 
						|
            wallpapers = [x for x in os.listdir(wallpaper_dir) if ".vertical." not in x]
 | 
						|
        except os.error as e:
 | 
						|
            logger.warning("Could not load wallpapers from directory: {}".format(e))
 | 
						|
 | 
						|
        if wallpapers:
 | 
						|
            if Config.get("desktop_bg_override", False):
 | 
						|
                wallpaper_file = Config.get("desktop_bg_override", "")
 | 
						|
            else:
 | 
						|
                wallpaper_file = os.path.join(wallpaper_dir, random.choice(wallpapers))
 | 
						|
                logger.warning(f"Selected new wallpaper: {wallpaper_file}")
 | 
						|
            self.set_wallpaper(wallpaper_file)
 | 
						|
        else:
 | 
						|
            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('x11_wallpaper_config_command', 'wal-nitrogen-noupdate')} {filename}")
 | 
						|
            if p:
 | 
						|
                p.wait()
 | 
						|
        else:
 | 
						|
            # Wayland can set wallpaper in qtile directly per screen
 | 
						|
            for screen_i, screen in enumerate(qtile.screens):
 | 
						|
                sinfo = screen.info()
 | 
						|
                sfilename = filename
 | 
						|
                if sinfo.get('width', 100) < sinfo.get('height', 10):
 | 
						|
                    # Vertical screen, see if there is a vertical alt wallpaper
 | 
						|
                    basename, ext = os.path.splitext(filename)
 | 
						|
                    new_filename = f"{basename}.vertical{ext}"
 | 
						|
                    if os.path.isfile(new_filename):
 | 
						|
                        sfilename = new_filename
 | 
						|
                logger.warning(f"Setting Screen#{screen_i} wallpaper to {sfilename}.")
 | 
						|
                screen.set_wallpaper(sfilename, "fill")
 | 
						|
        self.current_wallpaper = filename
 | 
						|
 | 
						|
    def update_colorscheme(self, *args, **kwargs):
 | 
						|
        if self.current_wallpaper:
 | 
						|
            logger.warning(f"Updating wal colors for wallpaper {self.current_wallpaper}")
 | 
						|
            p = utils.execute_once([*Config.get('cmd_wal', ['wallust', 'run']), "{}".format(self.current_wallpaper)])
 | 
						|
            if p:
 | 
						|
                p.wait()
 | 
						|
 | 
						|
        colors = None
 | 
						|
        if os.path.isfile(f"{Config.get('homedir', '~')}/.cache/wal/colors.json"):
 | 
						|
            with open(f"{Config.get('homedir', '~')}/.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']
 | 
						|
 | 
						|
            # Update border colors in layouts
 | 
						|
            for group in qtile.groups:
 | 
						|
                for layout in group.layouts:
 | 
						|
                    if isinstance(layout, kuro_layouts.KuroWmii):
 | 
						|
                        layout.border_focus = colors['color15']
 | 
						|
                        layout.border_focus_stack = colors['color1']
 | 
						|
                        layout.border_normal = colors['color1']
 | 
						|
                        layout.border_normal_stack = colors['color1']
 | 
						|
 | 
						|
            for screen_i, screen in enumerate(qtile.screens):
 | 
						|
                bar = screen.top
 | 
						|
                logger.warning(f"Updating colorscheme for screen {screen_i}")
 | 
						|
                if bar:
 | 
						|
                    bar.background = f"{colors['color1']}{Config.get('bar_rgba_opacity', 'AA')}"
 | 
						|
                    bar.drawer.clear(bar.background)
 | 
						|
 | 
						|
                    def update_widget(w):
 | 
						|
                        if hasattr(w, '_update_drawer'):
 | 
						|
                            try:
 | 
						|
                                w._update_drawer()
 | 
						|
                            except Exception as e:
 | 
						|
                                logger.error("Error while updating drawer for widget {}: {}".format(w, e))
 | 
						|
 | 
						|
                        if hasattr(w, 'foreground'):
 | 
						|
                            w.foreground = colors['color15']
 | 
						|
 | 
						|
                        if hasattr(w, 'foreground_normal'):
 | 
						|
                            w.foreground_normal = colors['color15']
 | 
						|
 | 
						|
                        if hasattr(w, 'foreground_alert'):
 | 
						|
                            w.foreground_alert = colors['color3']
 | 
						|
 | 
						|
                        if hasattr(w, 'border'):
 | 
						|
                            w.border = colors['color15']
 | 
						|
 | 
						|
                        if hasattr(w, 'active'):
 | 
						|
                            w.active = colors['color15']
 | 
						|
 | 
						|
                        if hasattr(w, 'highlight_color'):
 | 
						|
                            w.highlight_color = colors['color3']
 | 
						|
 | 
						|
                        if hasattr(w, 'inactive'):
 | 
						|
                            w.inactive = colors['color8']
 | 
						|
 | 
						|
                        if hasattr(w, 'this_current_screen_border'):
 | 
						|
                            w.this_current_screen_border = colors['color15']
 | 
						|
 | 
						|
                        if hasattr(w, 'this_screen_border'):
 | 
						|
                            w.this_screen_border = colors['color15']
 | 
						|
 | 
						|
                        if hasattr(w, 'other_current_screen_border'):
 | 
						|
                            w.other_current_screen_border = colors['color8']
 | 
						|
 | 
						|
                        if hasattr(w, 'other_screen_border'):
 | 
						|
                            w.other_screen_border = colors['color8']
 | 
						|
 | 
						|
                        if isinstance(w, widget.WidgetBox):
 | 
						|
                            for subw in w.widgets:
 | 
						|
                                update_widget(subw)
 | 
						|
 | 
						|
                    for w in bar.widgets:
 | 
						|
                        update_widget(w)
 | 
						|
 | 
						|
                    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"])
 | 
						|
                if p:
 | 
						|
                    p.wait()
 | 
						|
            except subprocess.SubprocessError as e:
 | 
						|
                logger.error(f"Error running 'pywalfox update': {e}")
 | 
						|
 | 
						|
            utils.notify(qtile,
 | 
						|
                         "Updated colorscheme!",
 | 
						|
                         f"active: {colors['color15']}, inactive: {colors['color1']}")
 | 
						|
 | 
						|
    def setup_xdg_desktop_portal(self):
 | 
						|
        # XDG Desktop portal is used for screensharing, screenshots and filepickers in wayland.
 | 
						|
        # To work correctly, it needs to have two env variables set in the systemd user session
 | 
						|
        logger.warning(f"Setting XDG_CURRENT_DESKTOP env and updating XDG Desktop Portal configuration...")
 | 
						|
        os.environ["XDG_CURRENT_DESKTOP"] = "qtile"
 | 
						|
        subprocess.Popen(["systemctl", "--user", "import-environment", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP"])
 | 
						|
        subprocess.Popen(["dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP=qtile"])
 | 
						|
        subprocess.Popen(["systemctl", "--user", "restart", "xdg-desktop-portal"])
 |