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="{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,
                    }
                )
            ])
        # 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="%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.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"])