~ruther/nixos-config

d7546970bd34c28ce3bcf4ec4448cdfbead24821 — Frantisek Bohacek 1 year, 13 days ago 438f048
refactor: split qtile configuration
A modules/desktop/qtile/config/.gitignore => modules/desktop/qtile/config/.gitignore +3 -0
@@ 0,0 1,3 @@
nixenvironment.py
__pycache__
.mypy_cache

A modules/desktop/qtile/config/bars.py => modules/desktop/qtile/config/bars.py +225 -0
@@ 0,0 1,225 @@
import subprocess
import re
from libqtile import layout, bar, qtile
from qtile_extras.widget.decorations import BorderDecoration, PowerLineDecoration, RectDecoration
import qtile_extras.widget as widget
from styling import colors
from libqtile.widget import Mpris2, Bluetooth
from tasklist import TaskList

def create_top_bar(systray = False):
    powerline = {
        'decorations': [
            PowerLineDecoration(path = 'forward_slash')
        ]
    }

    widgets = [
        widget.Sep(padding = 5, size_percent = 0, background = colors['background_secondary']),
        widget.CurrentScreen(
            active_text = 'I',
            active_color = colors['active'],
            padding = 3,
            fontsize = 16,
            background = colors['background_secondary'],
        ),
        widget.GroupBox(
            markup = False,
            highlight_method = 'line',
            rounded = False,
            margin_x = 2,
            disable_drag = True,
            use_mouse_wheel = True,
            active = colors['white'],
            inactive = colors['grey'],
            urgent_alert_method = 'line',
            urgent_border = colors['urgent'],
            this_current_screen_border = colors['active'],
            this_screen_border = colors['secondary'],
            other_screen_border = colors['inactive'],
            other_current_screen_border = '6989c0',
            background = colors['background_secondary'],
        ),
        widget.CurrentScreen(
            active_text = 'I',
            active_color = colors['active'],
            padding = 3,
            fontsize = 16,
            background = colors['background_secondary'],
            decorations = [
                PowerLineDecoration(path = 'forward_slash'),
            ],
        ),
        widget.Sep(
            linewidth=2,
            size_percent=0,
            padding=5,
        ),
        widget.Prompt(),
        widget.WindowName(
            foreground = colors['primary'],
            width = bar.CALCULATED,
            padding = 10,
            empty_group_string = 'Desktop',
            max_chars = 160,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 0,
                    filled = True,
                    clip = True,
                ),
            ],
        ),
        widget.Spacer(),
        widget.Chord(
            padding = 15,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 6,
                    filled = True,
                    clip = True,
                ),
            ]
        ),
        # widget.Net(
        #     interface = 'enp24s0',
        #     prefix='M',
        #     format = '{down:6.2f} {down_suffix:<2}↓↑{up:6.2f} {up_suffix:<2}',
        #     background = colors['background_secondary'],
        #     **powerline,
        # ),
        widget.Memory(
            format = '{MemFree: .0f}{mm}',
            fmt = '{} free',
            **powerline,
        ),
        widget.CPU(
            format = '{load_percent} %',
            fmt = '   {}',
            background = colors['background_secondary'],
            **powerline,
        ),
        widget.DF(
            update_interval = 60,
            partition = '/',
            #format = '[{p}] {uf}{m} ({r:.0f}%)',
            format = '{uf}{m} free',
            fmt = '   {}',
            visible_on_warn = False,
            **powerline,
        ),
        widget.GenPollText(
            func = lambda: subprocess.check_output(['xkblayout-state', 'print', '%s']).decode('utf-8').upper(),
            fmt = '⌨ {}',
            update_interval = 0.5,
            **powerline,
        ),
        widget.Clock(
            timezone='Europe/Prague',
            foreground = colors['primary'],
            format='%A, %B %d - %H:%M:%S',
            background = colors['background_secondary'],
            **powerline
        ),
        widget.Volume(
            fmt = '🕫  {}',
        ),
        widget.Sep(
            foreground = colors['background_secondary'],
            size_percent = 70,
            linewidth = 3,
        ),
        Bluetooth(
            device = '/dev_88_C9_E8_49_93_16',
            symbol_connected = '',
            symbol_paired = ' DC\'d',
            symbol_unknown = '',
            symbol_powered = ('', ''),
            device_format = '{battery_level}{symbol}',
            device_battery_format = ' {battery} %',
            format_unpowered = '',
        ),
    ]

    if systray:
        widgets.append(widget.Sep(
            foreground = colors['background_secondary'],
            size_percent = 70,
            linewidth = 2,
        ))
        widgets.append(widget.Systray())
        widgets.append(widget.Sep(padding = 5, size_percent = 0))

    return bar.Bar(widgets, 30)

def create_bottom_bar():
    powerline = {
        'decorations': [
            PowerLineDecoration(path = 'forward_slash')
        ]
    }

    return bar.Bar([
        TaskList(
            parse_text = lambda text : re.split(' [–—-] ', text)[-1],
            highlight_method = 'line',
            txt_floating = '🗗 ',
            txt_maximized = '🗖 ',
            txt_minimized = '🗕 ',
            borderwidth = 3,
        ),
        widget.Spacer(),
        Mpris2(
            format = '{xesam:title}',
            playerfilter = '.*Firefox.*',
            scroll = False,
            paused_text = '', #'   {track}',
            playing_text = '    {track}',
            padding = 10,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 5,
                    filled = True,
                    clip = True,
                ),
            ],
        ),
        Mpris2(
            format = '{xesam:title} - {xesam:artist}',
            objname = 'org.mpris.MediaPlayer2.spotify',
            scroll = False,
            paused_text = '', #'   {track}',
            playing_text = '  {track}', # '   {track}',
            padding = 10,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 5,
                    filled = True,
                    clip = True,
                ),
            ],
        ),
        widget.Sep(
            size_percent = 0,
            padding = 5,
            **powerline,
        ),
        widget.Wttr(
            location = {'Odolena_Voda': ''},
            format = '%t %c',
            background = colors['background_secondary'],
            **powerline,
        ),
    ], 30)

D modules/desktop/qtile/config/bluetooth.py => modules/desktop/qtile/config/bluetooth.py +0 -186
@@ 1,186 0,0 @@
# Copyright (c) 2021 Graeme Holliday
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from dbus_next.aio import MessageBus
from dbus_next.constants import BusType

from libqtile.widget import base
from libqtile.log_utils import logger

import asyncio

BLUEZ = "org.bluez"
BLUEZ_PATH = "/org/bluez/hci0"
BLUEZ_ADAPTER = "org.bluez.Adapter1"
BLUEZ_DEVICE = "org.bluez.Device1"
BLUEZ_BATTERY = "org.bluez.Battery1"
BLUEZ_PROPERTIES = "org.freedesktop.DBus.Properties"

def synchronize_async_helper(to_await):
    async_response = []

    async def run_and_capture_result():
        r = await to_await
        async_response.append(r)

    loop = asyncio.get_event_loop()
    coroutine = run_and_capture_result()
    loop.run_until_complete(coroutine)
    return async_response[0]

class Bluetooth(base._TextBox):
    """
    Displays bluetooth status for a particular connected device.

    (For example your bluetooth headphones.)

    Uses dbus-next to communicate with the system bus.

    Widget requirements: dbus-next_.

    .. _dbus-next: https://pypi.org/project/dbus-next/
    """

    defaults = [
        (
            "hci",
            "/dev_XX_XX_XX_XX_XX_XX",
            "hci0 device path, can be found with d-feet or similar dbus explorer.",
        ),
        (
            "format_connected",
            "{name}: {battery}%",
            "format of the string to show"
        ),
        (
            "format_disconnected",
            "not connected",
            "what to show when not connected"
        ),
        (
            "format_unpowered",
            "adapter off",
            "what to show when the adapter is off"
        ),
    ]

    def __init__(self, **config):
        base._TextBox.__init__(self, "", **config)
        self._update_battery_task = None
        self.add_defaults(Bluetooth.defaults)

    async def _config_async(self):
        # set initial values
        self.powered = await self._init_adapter()
        self.connected, self.device, self.battery = await self._init_device()
        self.update_text()

    async def _init_adapter(self):
        # set up interface to adapter properties using high-level api
        bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
        introspect = await bus.introspect(BLUEZ, BLUEZ_PATH)
        obj = bus.get_proxy_object(BLUEZ, BLUEZ_PATH, introspect)
        iface = obj.get_interface(BLUEZ_ADAPTER)
        props = obj.get_interface(BLUEZ_PROPERTIES)

        powered = await iface.get_powered()
        # subscribe receiver to property changed
        props.on_properties_changed(self._adapter_signal_received)
        return powered

    async def _init_device(self):
        # set up interface to device properties using high-level api
        bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
        introspect = await bus.introspect(BLUEZ, BLUEZ_PATH + self.hci)
        obj = bus.get_proxy_object(BLUEZ, BLUEZ_PATH + self.hci, introspect)
        device_iface = obj.get_interface(BLUEZ_DEVICE)
        props = obj.get_interface(BLUEZ_PROPERTIES)

        battery = None
        try:
            battery_iface = obj.get_interface(BLUEZ_BATTERY)
            battery = await battery_iface.get_percentage()
        except:
            pass

        connected = await device_iface.get_connected()
        name = await device_iface.get_name()
        # subscribe receiver to property changed
        props.on_properties_changed(self._device_signal_received)
        return connected, name, battery

    def _adapter_signal_received(
        self, interface_name, changed_properties, _invalidated_properties
    ):
        powered = changed_properties.get("Powered", None)
        if powered is not None:
            self.powered = powered.value
            self.update_text()

    def _device_signal_received(
        self, interface_name, changed_properties, _invalidated_properties
    ):
        connected = changed_properties.get("Connected", None)
        if connected is not None:
            self.connected = connected.value
            self.update_text()
            if self.connected == True:
                self.on_connected()

        device = changed_properties.get("Name", None)
        if device is not None:
            self.device = device.value
            self.update_text()

        battery = changed_properties.get("Percentage", None)
        if battery is not None:
            self.battery = battery.value
            self.update_text()

    def on_connected(self):
        if self._update_battery_task != None:
            self._update_battery_task.cancel()
        self._update_battery_task = self.timeout_add(1, self.update_battery_percentage)

    async def update_battery_percentage(self):
        self._update_battery_task = None
        battery = None
        bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
        try:
            introspect = await bus.introspect(BLUEZ, BLUEZ_PATH + self.hci)
            obj = bus.get_proxy_object(BLUEZ, BLUEZ_PATH + self.hci, introspect)
            battery_iface = obj.get_interface(BLUEZ_BATTERY)
            self.battery = await battery_iface.get_percentage()
            bus.disconnect()
            self.update_text()
        except Exception as e:
            logger.warning(e)
            bus.disconnect()

    def update_text(self):
        text = ""
        if not self.powered:
            text = self.format_unpowered
        else:
            if not self.connected:
                text = self.format_disconnected
            else:
                text = self.format_connected.format(battery = self.battery, name = self.device)
        self.update(text)

M modules/desktop/qtile/config/config.py => modules/desktop/qtile/config/config.py +32 -477
@@ 1,6 1,3 @@
import re
import os
import subprocess
import psutil
import libqtile
from libqtile import layout, bar, qtile


@@ 14,141 11,14 @@ from libqtile.log_utils import logger
from libqtile.backend.wayland import InputConfig
import qtile_extras.widget as widget
from qtile_extras.widget.decorations import BorderDecoration, PowerLineDecoration, RectDecoration
from tasklist import TaskList
from mpris2widget import Mpris2
from bluetooth import Bluetooth
import xmonadcustom
from nixenvironment import setupLocation, configLocation, sequenceDetectorExec
import functions
import utils
from screens import init_screens, observe_monitors, init_navigation_keys
from styling import colors
import time

import screeninfo

colors = {
    'primary': '51afef',
    'active': '8babf0',
    'inactive': '555e60',
    'secondary': '55eddc',
    'background_primary': '222223',
    'background_secondary': '444444',
    'urgent': 'c45500',
    'white': 'd5d5d5',
    'grey': '737373',
    'black': '121212',
}

# #####################################
# Utility functions
@lazy.function
def focus_window_by_class(qtile: Qtile, wmclass: str):
    match = Match(wm_class=wmclass)
    windows = [w for w in qtile.windows_map.values() if isinstance(w, Window) and match.compare(w)]
    if len(windows) == 0:
        return

    window = windows[0]
    group = window.group
    group.toscreen()
    group.focus(window)

@lazy.function
def warp_cursor_to_focused_window(qtile: Qtile):
    current_window = qtile.current_window
    win_size = current_window.get_size()
    win_pos = current_window.get_position()

    x = win_pos[0] + win_size[0] // 2
    y = win_pos[1] + win_size[1] // 2

    qtile.core.warp_pointer(x, y)

@lazy.function
def go_to_screen(qtile: Qtile, index: int):
    current_screen = qtile.current_screen
    screen = qtile.screens[index]

    logger.warning(screen)
    logger.warning(current_screen)
    if current_screen == screen:
        x = screen.x + screen.width // 2
        y = screen.y + screen.height // 2
        qtile.core.warp_pointer(x, y)
    else:
        qtile.to_screen(index)

    qtile.current_window.focus()


@lazy.function
def go_to_group(qtile: Qtile, group_name: str, switch_monitor: bool = False):
    found = False
    current_group = qtile.current_group
    if group_name == current_group.name:
        warp_cursor_to_focused_window()
        return

    current_screen = qtile.current_screen
    target_screen = current_screen

    for screen in qtile.screens:
        if screen.group.name == group_name:
            target_screen = screen
            if switch_monitor:
                qtile.focus_screen(screen.index)
            found = True
            break

    current_bar = current_screen.top
    target_bar = target_screen.top

    if found and current_bar != target_bar and isinstance(target_bar, libqtile.bar.Bar) and isinstance(current_bar, libqtile.bar.Bar):
        # found on other monitor, so switch bars
        target_bar_show = target_bar.is_show()
        current_bar_show = current_bar.is_show()

        current_bar.show(target_bar_show)
        target_bar.show(current_bar_show)

    qtile.groups_map[group_name].toscreen()

    for window in current_group.windows:
        if window.fullscreen:
            window.toggle_fullscreen()
            # time.sleep(0.1)
            window.toggle_fullscreen()

    if not switch_monitor or not found:
        window: Window
        for window in qtile.groups_map[group_name].windows:
            if window.fullscreen:
                window.toggle_fullscreen()
                # time.sleep(0.1)
                window.toggle_fullscreen()

# expands list of keys with the rest of regular keys,
# mainly usable for KeyChords, where you want any other key
# to exit the key chord instead.
def expand_with_rest_keys(keys: list[EzKey], global_prefix: str) -> list[EzKey]:
    all_keys = ['<semicolon>', '<return>', '<space>'] + [chr(c) for c in range(ord('a'), ord('z') + 1)]
    prefixes = ['', 'M-', 'M-S-', 'M-C-', 'C-', 'S-']

    for prefix in prefixes:
        for potentially_add_key in all_keys:
            potentially_add_key = prefix + potentially_add_key
            if potentially_add_key == global_prefix:
                continue

            found = False
            for existing_key in keys:
                if existing_key.key.lower() == potentially_add_key:
                    found = True
                    break

            if not found:
                keys.append(EzKey(potentially_add_key, lazy.spawn(f'notify-send "Not registered key {global_prefix} {potentially_add_key}"')))

    return keys


# #####################################
# Environment
mod = 'mod4'


@@ 176,244 46,6 @@ widget_defaults = dict(
)
extension_defaults = widget_defaults.copy()

def create_top_bar(systray = False):
    powerline = {
        'decorations': [
            PowerLineDecoration(path = 'forward_slash')
        ]
    }

    widgets = [
        widget.Sep(padding = 5, size_percent = 0, background = colors['background_secondary']),
        widget.CurrentScreen(
            active_text = 'I',
            active_color = colors['active'],
            padding = 3,
            fontsize = 16,
            background = colors['background_secondary'],
        ),
        widget.GroupBox(
            markup = False,
            highlight_method = 'line',
            rounded = False,
            margin_x = 2,
            disable_drag = True,
            use_mouse_wheel = True,
            active = colors['white'],
            inactive = colors['grey'],
            urgent_alert_method = 'line',
            urgent_border = colors['urgent'],
            this_current_screen_border = colors['active'],
            this_screen_border = colors['secondary'],
            other_screen_border = colors['inactive'],
            other_current_screen_border = '6989c0',
            background = colors['background_secondary'],
        ),
        widget.CurrentScreen(
            active_text = 'I',
            active_color = colors['active'],
            padding = 3,
            fontsize = 16,
            background = colors['background_secondary'],
            decorations = [
                PowerLineDecoration(path = 'forward_slash'),
            ],
        ),
        widget.Sep(
            linewidth=2,
            size_percent=0,
            padding=5,
        ),
        widget.Prompt(),
        widget.WindowName(
            foreground = colors['primary'],
            width = bar.CALCULATED,
            padding = 10,
            empty_group_string = 'Desktop',
            max_chars = 160,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 0,
                    filled = True,
                    clip = True,
                ),
            ],
        ),
        widget.Spacer(),
        widget.Chord(
            padding = 15,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 6,
                    filled = True,
                    clip = True,
                ),
            ]
        ),
        # widget.Net(
        #     interface = 'enp24s0',
        #     prefix='M',
        #     format = '{down:6.2f} {down_suffix:<2}↓↑{up:6.2f} {up_suffix:<2}',
        #     background = colors['background_secondary'],
        #     **powerline,
        # ),
        widget.Memory(
            format = '{MemFree: .0f}{mm}',
            fmt = '{} free',
            **powerline,
        ),
        widget.CPU(
            format = '{load_percent} %',
            fmt = '   {}',
            background = colors['background_secondary'],
            **powerline,
        ),
        widget.DF(
            update_interval = 60,
            partition = '/',
            #format = '[{p}] {uf}{m} ({r:.0f}%)',
            format = '{uf}{m} free',
            fmt = '   {}',
            visible_on_warn = False,
            **powerline,
        ),
        widget.GenPollText(
            func = lambda: subprocess.check_output(['xkblayout-state', 'print', '%s']).decode('utf-8').upper(),
            fmt = '⌨ {}',
            update_interval = 0.5,
            **powerline,
        ),
        widget.Clock(
            timezone='Europe/Prague',
            foreground = colors['primary'],
            format='%A, %B %d - %H:%M:%S',
            background = colors['background_secondary'],
            **powerline
        ),
        widget.Volume(
            fmt = '🕫  {}',
        ),
        widget.Sep(
            foreground = colors['background_secondary'],
            size_percent = 70,
            linewidth = 3,
        ),
        Bluetooth(
            hci = '/dev_88_C9_E8_49_93_16',
            format_connected = '  {battery} %',
            format_disconnected = '  Disconnected',
            format_unpowered = ''
        ),
    ]

    if systray:
        widgets.append(widget.Sep(
            foreground = colors['background_secondary'],
            size_percent = 70,
            linewidth = 2,
        ))
        widgets.append(widget.Systray())
        widgets.append(widget.Sep(padding = 5, size_percent = 0))

    return bar.Bar(widgets, 30)

def create_bottom_bar():
    powerline = {
        'decorations': [
            PowerLineDecoration(path = 'forward_slash')
        ]
    }

    return bar.Bar([
        TaskList(
            parse_text = lambda text : re.split(' [–—-] ', text)[-1],
            highlight_method = 'line',
            txt_floating = '🗗 ',
            txt_maximized = '🗖 ',
            txt_minimized = '🗕 ',
            borderwidth = 3,
        ),
        widget.Spacer(),
        Mpris2(
            format = '{xesam:title}',
            playerfilter = '.*Firefox.*',
            scroll = False,
            paused_text = '', #'   {track}',
            playing_text = '    {track}',
            padding = 10,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 5,
                    filled = True,
                    clip = True,
                ),
            ],
        ),
        Mpris2(
            format = '{xesam:title} - {xesam:artist}',
            objname = 'org.mpris.MediaPlayer2.spotify',
            scroll = False,
            paused_text = '', #'   {track}',
            playing_text = '  {track}', # '   {track}',
            padding = 10,
            decorations = [
                RectDecoration(
                    colour = colors['black'],
                    radius = 0,
                    padding_y = 4,
                    padding_x = 5,
                    filled = True,
                    clip = True,
                ),
            ],
        ),
        widget.Sep(
            size_percent = 0,
            padding = 5,
            **powerline,
        ),
        widget.Wttr(
            location = {'Odolena_Voda': ''},
            format = '%t %c',
            background = colors['background_secondary'],
            **powerline,
        ),
    ], 30)

def init_screens():
    wallpaper = f'{setupLocation}/wall.png'

    screens_info = screeninfo.get_monitors()
    screens_count = len(screens_info)
    screens = [None] * screens_count

    logger.warning(f'setting up {screens_count} screens')

    for i in range(0, screens_count):
        screen_info = screens_info[i]
        systray = False
        if screens_count <= 2 and i == 0:
            systray = True
            print(f'Putting systray on {i}')
        elif i == 1:
            systray  = True
            print(f'Putting systray on {i}')

        top_bar = create_top_bar(systray = systray)

        screens[i] = Screen(top=top_bar, bottom=create_bottom_bar(), wallpaper=f'{setupLocation}/wall.png', width=screen_info.width, height=screen_info.height)

    return screens

screens = init_screens()

# Keys


@@ 454,10 86,10 @@ keys.extend([

keys.extend([
    # social navigation
    EzKeyChord('M-a', expand_with_rest_keys([
       EzKey('b', focus_window_by_class('discord')),
       EzKey('n', focus_window_by_class('element')),
       EzKey('m', focus_window_by_class('element')),
    EzKeyChord('M-a', utils.expand_with_other_keys([
       EzKey('b', functions.focus_window_by_class('discord')),
       EzKey('n', functions.focus_window_by_class('element')),
       EzKey('m', functions.focus_window_by_class('element')),

       # notifications
       EzKey('l', lazy.spawn(f'{setupLocation}/scripts/notifications/clear-popups.sh')),


@@ 470,7 102,7 @@ keys.extend([
])

keys.extend([
    EzKeyChord('M-s', expand_with_rest_keys([
    EzKeyChord('M-s', utils.expand_with_other_keys([
       EzKey('e', lazy.spawn('emacsclient -c')),
       EzKey('c', lazy.group['scratchpad'].dropdown_toggle('ipcam')),
       EzKey('s', lazy.group['scratchpad'].dropdown_toggle('spotify')),


@@ 517,16 149,7 @@ keys.extend([
    EzKey('M-C-q', lazy.shutdown()),
])

if len(screens) >= 4:
    monitor_navigation_keys = ['q', 'w', 'e', 'r']
else:
    monitor_navigation_keys = ['w', 'e', 'r']

for i, key in enumerate(monitor_navigation_keys):
    keys.extend([
        EzKey(f'M-{key}', go_to_screen(i), desc = f'Move focus to screen {i}'),
        EzKey(f'M-S-{key}', lazy.window.toscreen(i), desc = f'Move window to screen {i}'),
    ])
init_navigation_keys(keys, screens)

if qtile.core.name == 'x11':
    keys.append(EzKey('M-S-z', lazy.spawn('clipmenu')))


@@ 543,7 166,7 @@ group_defaults = {
        'layout': 'max'
    }
}
logger.info(group_defaults.get('9'))

groups = [Group(i) if not (i in group_defaults.keys()) else Group(i, **group_defaults.get(i)) for i in '123456789']

for i in groups:


@@ 551,7 174,7 @@ for i in groups:
        [
            EzKey(
                f'M-{i.name}',
                go_to_group(i.name),
                functions.go_to_group(i.name),
                desc='Switch to group {}'.format(i.name),
            ),
            Key(


@@ 639,75 262,6 @@ bring_front_click = False
wl_input_rules = {}
wmname = 'LG3D'

from threading import Timer

def debounce(wait):
    """ Decorator that will postpone a functions
        execution until after wait seconds
        have elapsed since the last time it was invoked. """
    def decorator(fn):
        def debounced(*args, **kwargs):
            def call_it():
                fn(*args, **kwargs)
            try:
                debounced.t.cancel()
            except(AttributeError):
                pass
            debounced.t = Timer(wait, call_it)
            debounced.t.start()
        return debounced
    return decorator


# Monitors changing connected displays and the lid.
# Calls autorandr to change the outputs, and QTile
# restart
async def _observe_monitors():
    from pydbus import SystemBus
    from gi.repository import GLib
    from libqtile.utils import add_signal_receiver
    from dbus_next.message import Message
    import pyudev

    @debounce(0.2)
    def call_autorandr():
        subprocess.call(['autorandr', '--change', '--default', 'horizontal'])
        time.sleep(0.3)
        subprocess.call(['qtile', 'cmd-obj', '-o', 'cmd', '-f', 'restart'])

    def on_upower_event(message: Message):
        args = message.body
        properties = args[1]
        logger.info(message.body)
        if 'LidIsClosed' in properties:
            call_autorandr()

    def on_drm_event(action=None, device=None):
        if action == "change":
            call_autorandr()

    context = pyudev.Context()
    monitor = pyudev.Monitor.from_netlink(context)
    monitor.filter_by(subsystem = 'drm')
    monitor.enable_receiving()

    # bus = SystemBus()
    # upower = bus.get('org.freedesktop.UPower', '/org/freedesktop/UPower')
    # upower.PropertiesChanged.connect(on_upower_event)

    logger.warning("Adding signal receiver")
    subscribe = await add_signal_receiver(
        on_upower_event,
        session_bus = False,
        signal_name = "PropertiesChanged",
        path = '/org/freedesktop/UPower',
        dbus_interface = 'org.freedesktop.DBus.Properties',
    )
    logger.warning(f"Add signal receiver: {subscribe}")

    observer = pyudev.MonitorObserver(monitor, on_drm_event)
    observer.start()

# Swallow windows,
# when a process with window spawns
# another process with a window as a child, minimize the first


@@ 715,36 269,37 @@ async def _observe_monitors():
# is done.
# @hook.subscribe.client_new
#   I don't like this much :( hence I commented it out
def _swallow(window):
    pid = window.window.get_net_wm_pid()
    ppid = psutil.Process(pid).ppid()
    cpids = {c.window.get_net_wm_pid(): wid for wid, c in window.qtile.windows_map.items()}
    for i in range(5):
        if not ppid:
            return
        if ppid in cpids:
            parent = window.qtile.windows_map.get(cpids[ppid])
            parent.minimized = True
            window.parent = parent
            return
        ppid = psutil.Process(ppid).ppid()

# @hook.subscribe.client_killed
def _unswallow(window):
    if hasattr(window, 'parent'):
        window.parent.minimized = False
# def _swallow(window):
#     pid = window.window.get_net_wm_pid()
#     ppid = psutil.Process(pid).ppid()
#     cpids = {c.window.get_net_wm_pid(): wid for wid, c in window.qtile.windows_map.items()}
#     for i in range(5):
#         if not ppid:
#             return
#         if ppid in cpids:
#             parent = window.qtile.windows_map.get(cpids[ppid])
#             parent.minimized = True
#             window.parent = parent
#             return
#         ppid = psutil.Process(ppid).ppid()

# # @hook.subscribe.client_killed
# def _unswallow(window):
#     if hasattr(window, 'parent'):
#         window.parent.minimized = False

# Startup setup,
# windows to correct workspaces,
# start autostart.sh
@hook.subscribe.startup_once
def autostart():
    import subprocess
    subprocess.call([f'{configLocation}/autostart.sh'])


@hook.subscribe.startup
async def observer_start():
    await _observe_monitors()
    await observe_monitors()

firefoxInstance = 0
@hook.subscribe.client_new

A modules/desktop/qtile/config/functions.py => modules/desktop/qtile/config/functions.py +90 -0
@@ 0,0 1,90 @@
import libqtile
from libqtile.lazy import lazy
from libqtile.core.manager import Qtile
from libqtile.backend.base import Window
from libqtile.config import Click, Drag, Group, KeyChord, EzKey, EzKeyChord, Match, Screen, ScratchPad, DropDown, Key

# #####################################
# Utility functions
@lazy.function
def focus_window_by_class(qtile: Qtile, wmclass: str):
    match = Match(wm_class=wmclass)
    windows = [w for w in qtile.windows_map.values() if isinstance(w, Window) and match.compare(w)]
    if len(windows) == 0:
        return

    window = windows[0]
    group = window.group
    group.toscreen()
    group.focus(window)

@lazy.function
def warp_cursor_to_focused_window(qtile: Qtile):
    current_window = qtile.current_window
    win_size = current_window.get_size()
    win_pos = current_window.get_position()

    x = win_pos[0] + win_size[0] // 2
    y = win_pos[1] + win_size[1] // 2

    qtile.core.warp_pointer(x, y)

@lazy.function
def go_to_screen(qtile: Qtile, index: int):
    current_screen = qtile.current_screen
    screen = qtile.screens[index]

    if current_screen == screen:
        x = screen.x + screen.width // 2
        y = screen.y + screen.height // 2
        qtile.core.warp_pointer(x, y)

    qtile.to_screen(index)
    qtile.current_group.focus(qtile.current_group.current_window)
    qtile.current_window.focus()

@lazy.function
def go_to_group(qtile: Qtile, group_name: str, switch_monitor: bool = False):
    found = False
    current_group = qtile.current_group
    if group_name == current_group.name:
        warp_cursor_to_focused_window()
        return

    current_screen = qtile.current_screen
    target_screen = current_screen

    for screen in qtile.screens:
        if screen.group.name == group_name:
            target_screen = screen
            if switch_monitor:
                qtile.focus_screen(screen.index)
            found = True
            break

    current_bar = current_screen.top
    target_bar = target_screen.top

    if found and current_bar != target_bar and isinstance(target_bar, libqtile.bar.Bar) and isinstance(current_bar, libqtile.bar.Bar):
        # found on other monitor, so switch bars
        target_bar_show = target_bar.is_show()
        current_bar_show = current_bar.is_show()

        current_bar.show(target_bar_show)
        target_bar.show(current_bar_show)

    qtile.groups_map[group_name].toscreen()

    for window in current_group.windows:
        if window.fullscreen:
            window.toggle_fullscreen()
            # time.sleep(0.1)
            window.toggle_fullscreen()

    if not switch_monitor or not found:
        window: Window
        for window in qtile.groups_map[group_name].windows:
            if window.fullscreen:
                window.toggle_fullscreen()
                # time.sleep(0.1)
                window.toggle_fullscreen()

D modules/desktop/qtile/config/mpris2widget.py => modules/desktop/qtile/config/mpris2widget.py +0 -448
@@ 1,448 0,0 @@
# Copyright (c) 2014 Sebastian Kricner
# Copyright (c) 2014 Sean Vig
# Copyright (c) 2014 Adi Sieker
# Copyright (c) 2014 Tycho Andersen
# Copyright (c) 2020 elParaguayo
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations

import asyncio
import re
import string
from typing import TYPE_CHECKING

from dbus_next import Message, Variant
from dbus_next.constants import MessageType

from libqtile.command.base import expose_command
from libqtile.log_utils import logger
from libqtile.utils import _send_dbus_message, add_signal_receiver, create_task
from libqtile.widget import base

if TYPE_CHECKING:
    from typing import Any

MPRIS_PATH = "/org/mpris/MediaPlayer2"
MPRIS_OBJECT = "org.mpris.MediaPlayer2"
MPRIS_PLAYER = "org.mpris.MediaPlayer2.Player"
PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
MPRIS_REGEX = re.compile(r"(\{(.*?):(.*?)(:.*?)?\})")


class Mpris2Formatter(string.Formatter):
    """
    Custom string formatter for MPRIS2 metadata.

    Keys have a colon (e.g. "xesam:title") which causes issues with python's string
    formatting as the colon splits the identifier from the format specification.

    This formatter handles this issue by changing the first colon to an underscore and
    then formatting the incoming kwargs to match.

    Additionally, a default value is returned when an identifier is not provided by the
    kwarg data.
    """

    def __init__(self, default=""):
        string.Formatter.__init__(self)
        self._default = default

    def get_value(self, key, args, kwargs):
        """
        Replaces colon in kwarg keys with an underscore before getting value.

        Missing identifiers are replaced with the default value.
        """
        kwargs = {k.replace(":", "_"): v for k, v in kwargs.items()}
        try:
            return string.Formatter.get_value(self, key, args, kwargs)
        except (IndexError, KeyError):
            return self._default

    def parse(self, format_string):
        """
        Replaces first colon in format string with an underscore.

        This will cause issues if any identifier is provided that does not
        contain a colon. This should not happen according to the MPRIS2
        specification!
        """
        format_string = MPRIS_REGEX.sub(r"{\2_\3\4}", format_string)
        return string.Formatter.parse(self, format_string)


class Mpris2(base._TextBox):
    """An MPRIS 2 widget

    A widget which displays the current track/artist of your favorite MPRIS
    player. This widget scrolls the text if neccessary and information that
    is displayed is configurable.

    The widget relies on players broadcasting signals when the metadata or playback
    status changes. If you are getting inconsistent results then you can enable background
    polling of the player by setting the `poll_interval` parameter. This is disabled by
    default.

    Basic mouse controls are also available: button 1 = play/pause,
    scroll up = next track, scroll down = previous track.

    Widget requirements: dbus-next_.

    .. _dbus-next: https://pypi.org/project/dbus-next/
    """

    defaults = [
        ("name", "audacious", "Name of the MPRIS widget."),
        (
            "objname",
            None,
            "DBUS MPRIS 2 compatible player identifier"
            "- Find it out with dbus-monitor - "
            "Also see: http://specifications.freedesktop.org/"
            "mpris-spec/latest/#Bus-Name-Policy. "
            "``None`` will listen for notifications from all MPRIS2 compatible players.",
        ),

        (
            "playerfilter",
            None,
            "Filter the player identifier based on this regex",
            "usable with objname = None"
        ),
        (
            "format",
            "{xesam:title} - {xesam:album} - {xesam:artist}",
            "Format string for displaying metadata. "
            "See http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/#index5h3 "
            "for available values",
        ),
        ("separator", ", ", "Separator for metadata fields that are a list."),
        (
            "display_metadata",
            ["xesam:title", "xesam:album", "xesam:artist"],
            "(Deprecated) Which metadata identifiers to display. ",
        ),
        ("scroll", True, "Whether text should scroll."),
        ("playing_text", "{track}", "Text to show when playing"),
        ("paused_text", "Paused: {track}", "Text to show when paused"),
        ("stopped_text", "", "Text to show when stopped"),
        (
            "stop_pause_text",
            None,
            "(Deprecated) Optional text to display when in the stopped/paused state",
        ),
        (
            "no_metadata_text",
            "No metadata for current track",
            "Text to show when track has no metadata",
        ),
        (
            "poll_interval",
            0,
            "Periodic background polling interval of player (0 to disable polling).",
        ),
    ]

    def __init__(self, **config):
        base._TextBox.__init__(self, "", **config)
        self.add_defaults(Mpris2.defaults)
        self.is_playing = False
        self.count = 0
        self.displaytext = ""
        self.track_info = ""
        self.status = "{track}"
        self.add_callbacks(
            {
                "Button1": self.play_pause,
                "Button4": self.next,
                "Button5": self.previous,
            }
        )
        paused = ""
        stopped = ""
        if "stop_pause_text" in config:
            logger.warning(
                "The use of 'stop_pause_text' is deprecated. Please use 'paused_text' and 'stopped_text' instead."
            )
            if "paused_text" not in config:
                paused = self.stop_pause_text

            if "stopped_text" not in config:
                stopped = self.stop_pause_text

        if "display_metadata" in config:
            logger.warning(
                "The use of `display_metadata is deprecated. Please use `format` instead."
            )
            self.format = " - ".join(f"{{{s}}}" for s in config["display_metadata"])

        self._formatter = Mpris2Formatter()

        self.prefixes = {
            "Playing": self.playing_text,
            "Paused": paused or self.paused_text,
            "Stopped": stopped or self.stopped_text,
        }

        self._current_player: str | None = None
        self.player_names: dict[str, str] = {}
        self._background_poll: asyncio.TimerHandle | None = None

    @property
    def player(self) -> str:
        if self._current_player is None:
            return "None"
        else:
            return self.player_names.get(self._current_player, "Unknown")

    async def _config_async(self):
        # Set up a listener for NameOwner changes so we can remove players when they close
        await add_signal_receiver(
            self._name_owner_changed,
            session_bus=True,
            signal_name="NameOwnerChanged",
            dbus_interface="org.freedesktop.DBus",
        )

        # Listen out for signals from any Mpris2 compatible player
        subscribe = await add_signal_receiver(
            self.message,
            session_bus=True,
            signal_name="PropertiesChanged",
            bus_name=self.objname,
            path="/org/mpris/MediaPlayer2",
            dbus_interface="org.freedesktop.DBus.Properties",
        )

        if not subscribe:
            logger.warning("Unable to add signal receiver for Mpris2 players")

        # If the user has specified a player to be monitored, we can poll it now.
        if self.objname is not None:
            await self._check_player()

    def _name_owner_changed(self, message):
        # We need to track when an interface has been removed from the bus
        # We use the NameOwnerChanged signal and check if the new owner is
        # empty.
        name, _, new_owner = message.body

        # Check if the current player has closed
        if new_owner == "" and name == self._current_player:
            self._current_player = None
            self.update("")

            # Cancel any scheduled background poll
            self._set_background_poll(False)

    def message(self, message):
        if message.message_type != MessageType.SIGNAL:
            return

        create_task(self.process_message(message))

    async def process_message(self, message):
        current_player = message.sender

        name = await self.get_player_name(current_player)
        if current_player not in self.player_names:
            self.player_names[current_player] = name

        if self.playerfilter != None and not re.match(self.playerfilter, name):
            return

        self._current_player = current_player

        self.parse_message(*message.body)

    async def _check_player(self):
        """Check for player at startup and retrieve metadata."""
        if not (self.objname or self._current_player):
            return

        bus, message = await _send_dbus_message(
            True,
            MessageType.METHOD_CALL,
            self.objname if self.objname else self._current_player,
            PROPERTIES_INTERFACE,
            MPRIS_PATH,
            "GetAll",
            "s",
            [MPRIS_PLAYER],
        )

        if bus:
            bus.disconnect()

        # If we get an error here it will be because the player object doesn't exist
        if message.message_type != MessageType.METHOD_RETURN:
            self._current_player = None
            self.update("")
            return

        if message.body:
            self._current_player = message.sender
            self.parse_message(self.objname, message.body[0], [])

    def _set_background_poll(self, poll=True):
        if self._background_poll is not None:
            self._background_poll.cancel()

        if poll:
            self._background_poll = self.timeout_add(self.poll_interval, self._check_player)

    async def get_player_name(self, player):
        bus, message = await _send_dbus_message(
            True,
            MessageType.METHOD_CALL,
            player,
            PROPERTIES_INTERFACE,
            MPRIS_PATH,
            "Get",
            "ss",
            [MPRIS_OBJECT, "Identity"],
        )

        if bus:
            bus.disconnect()

        if message.message_type != MessageType.METHOD_RETURN:
            logger.warning("Could not retrieve identity of player on %s.", player)
            return ""

        return message.body[0].value

    def parse_message(
        self,
        _interface_name: str,
        changed_properties: dict[str, Any],
        _invalidated_properties: list[str],
    ) -> None:
        """
        http://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Mapping:Metadata_Map
        """
        if not self.configured:
            return

        if "Metadata" not in changed_properties and "PlaybackStatus" not in changed_properties:
            return

        self.displaytext = ""

        metadata = changed_properties.get("Metadata")
        if metadata:
            self.track_info = self.get_track_info(metadata.value)

        playbackstatus = getattr(changed_properties.get("PlaybackStatus"), "value", None)
        if playbackstatus:
            self.is_playing = playbackstatus == "Playing"
            self.status = self.prefixes.get(playbackstatus, "{track}")

        if not self.track_info:
            self.track_info = self.no_metadata_text

        self.displaytext = self.status.format(track=self.track_info)

        if self.text != self.displaytext:
            self.update(self.displaytext)

        if self.poll_interval:
            self._set_background_poll()

    def get_track_info(self, metadata: dict[str, Variant]) -> str:
        self.metadata = {}
        for key in metadata:
            new_key = key
            val = getattr(metadata.get(key), "value", None)
            if isinstance(val, str):
                self.metadata[new_key] = val
            elif isinstance(val, list):
                self.metadata[new_key] = self.separator.join(
                    (y for y in val if isinstance(y, str))
                )

        return self._formatter.format(self.format, **self.metadata).replace("\n", "")

    def _player_cmd(self, cmd: str) -> None:
        if self._current_player is None:
            return

        task = create_task(self._send_player_cmd(cmd))
        assert task
        task.add_done_callback(self._task_callback)

    async def _send_player_cmd(self, cmd: str) -> Message | None:
        bus, message = await _send_dbus_message(
            True,
            MessageType.METHOD_CALL,
            self._current_player,
            MPRIS_PLAYER,
            MPRIS_PATH,
            cmd,
            "",
            [],
        )

        if bus:
            bus.disconnect()

        return message

    def _task_callback(self, task: asyncio.Task) -> None:
        message = task.result()

        # This happens if we can't connect to dbus. Logger call is made
        # elsewhere so we don't need to do any more here.
        if message is None:
            return

        if message.message_type != MessageType.METHOD_RETURN:
            logger.warning("Unable to send command to player.")

    @expose_command()
    def play_pause(self) -> None:
        """Toggle the playback status."""
        self._player_cmd("PlayPause")


    @expose_command()
    def next(self) -> None:
        """Play the next track."""
        self._player_cmd("Next")


    @expose_command()
    def previous(self) -> None:
        """Play the previous track."""
        self._player_cmd("Previous")


    @expose_command()
    def stop(self) -> None:
        """Stop playback."""
        self._player_cmd("Stop")


    @expose_command()
    def info(self):
        """What's the current state of the widget?"""
        d = base._TextBox.info(self)
        d.update(dict(isplaying=self.is_playing, player=self.player))
        return d

A modules/desktop/qtile/config/screens.py => modules/desktop/qtile/config/screens.py +93 -0
@@ 0,0 1,93 @@
import time
import subprocess
from libqtile.lazy import lazy
from libqtile.config import Click, Drag, Group, KeyChord, EzKey, EzKeyChord, Match, Screen, ScratchPad, DropDown, Key
import screeninfo
import utils
import bars
from functions import focus_window_by_class, warp_cursor_to_focused_window, go_to_screen, go_to_group
from nixenvironment import setupLocation, configLocation, sequenceDetectorExec
from libqtile.log_utils import logger

def init_screens():
    wallpaper = f'{setupLocation}/wall.png'

    screens_info = screeninfo.get_monitors()
    screens_count = len(screens_info)
    screens = [None] * screens_count

    logger.warning(f'setting up {screens_count} screens')

    for i in range(0, screens_count):
        screen_info = screens_info[i]
        systray = False
        if screens_count <= 2 and i == 0:
            systray = True
            print(f'Putting systray on {i}')
        elif i == 1:
            systray  = True
            print(f'Putting systray on {i}')

        top_bar = bars.create_top_bar(systray = systray)

        screens[i] = Screen(top=top_bar, bottom=bars.create_bottom_bar(), wallpaper=f'{setupLocation}/wall.png', width=screen_info.width, height=screen_info.height)

    return screens

def init_navigation_keys(keys, screens):
    if len(screens) >= 4:
        monitor_navigation_keys = ['q', 'w', 'e', 'r']
    else:
        monitor_navigation_keys = ['w', 'e', 'r']

    for i, key in enumerate(monitor_navigation_keys):
        keys.extend([
            EzKey(f'M-{key}', go_to_screen(i), desc = f'Move focus to screen {i}'),
            EzKey(f'M-S-{key}', lazy.window.toscreen(i), desc = f'Move window to screen {i}'),
        ])

# Monitors changing connected displays and the lid.
# Calls autorandr to change the outputs, and QTile
# restart
async def observe_monitors():
    from pydbus import SystemBus
    from gi.repository import GLib
    from libqtile.utils import add_signal_receiver
    from dbus_next.message import Message
    import pyudev

    @utils.debounce(0.2)
    def call_autorandr():
        # Autorandr restarts QTile automatically
        subprocess.call(['autorandr', '--change', '--default', 'horizontal'])
        # time.sleep(0.3)
        # subprocess.call(['qtile', 'cmd-obj', '-o', 'cmd', '-f', 'restart'])

    def on_upower_event(message: Message):
        args = message.body
        properties = args[1]
        logger.info(message.body)
        if 'LidIsClosed' in properties:
            call_autorandr()

    def on_drm_event(action=None, device=None):
        if action == "change":
            call_autorandr()

    context = pyudev.Context()
    monitor = pyudev.Monitor.from_netlink(context)
    monitor.filter_by(subsystem = 'drm')
    monitor.enable_receiving()

    logger.warning("Adding signal receiver")
    subscribe = await add_signal_receiver(
        on_upower_event,
        session_bus = False,
        signal_name = "PropertiesChanged",
        path = '/org/freedesktop/UPower',
        dbus_interface = 'org.freedesktop.DBus.Properties',
    )
    logger.warning(f"Add signal receiver: {subscribe}")

    observer = pyudev.MonitorObserver(monitor, on_drm_event)
    observer.start()

A modules/desktop/qtile/config/styling.py => modules/desktop/qtile/config/styling.py +12 -0
@@ 0,0 1,12 @@
colors = {
    'primary': '51afef',
    'active': '8babf0',
    'inactive': '555e60',
    'secondary': '55eddc',
    'background_primary': '222223',
    'background_secondary': '444444',
    'urgent': 'c45500',
    'white': 'd5d5d5',
    'grey': '737373',
    'black': '121212',
}

A modules/desktop/qtile/config/utils.py => modules/desktop/qtile/config/utils.py +45 -0
@@ 0,0 1,45 @@
from threading import Timer
from libqtile.lazy import lazy
from libqtile.config import Click, Drag, Group, KeyChord, EzKey, Match, Screen, ScratchPad, DropDown, Key

def debounce(wait):
    """ Decorator that will postpone a functions
        execution until after wait seconds
        have elapsed since the last time it was invoked. """
    def decorator(fn):
        def debounced(*args, **kwargs):
            def call_it():
                fn(*args, **kwargs)
            try:
                debounced.t.cancel()
            except(AttributeError):
                pass
            debounced.t = Timer(wait, call_it)
            debounced.t.start()
        return debounced
    return decorator


# expands list of keys with the rest of regular keys,
# mainly usable for KeyChords, where you want any other key
# to exit the key chord instead.
def expand_with_other_keys(keys: list[EzKey], global_prefix: str) -> list[EzKey]:
    all_keys = ['<semicolon>', '<return>', '<space>'] + [chr(c) for c in range(ord('a'), ord('z') + 1)]
    prefixes = ['', 'M-', 'M-S-', 'M-C-', 'C-', 'S-']

    for prefix in prefixes:
        for potentially_add_key in all_keys:
            potentially_add_key = prefix + potentially_add_key
            if potentially_add_key == global_prefix:
                continue

            found = False
            for existing_key in keys:
                if existing_key.key.lower() == potentially_add_key:
                    found = True
                    break

            if not found:
                keys.append(EzKey(potentially_add_key, lazy.spawn(f'notify-send "Not registered key {global_prefix} {potentially_add_key}"')))

    return keys

M modules/desktop/qtile/home.nix => modules/desktop/qtile/home.nix +14 -126
@@ 1,40 1,6 @@
{ config, lib, pkgs, user, location, ... }:

let
  nur = config.nur.repos;
in {
  # services.udev.extraRules =
  #     ''ACTION=="change", SUBSYSTEM=="drm", RUN+="${pkgs.autorandr}/bin/autorandr -c"'';
  services.autorandr = {
    enable = true;
  };
  systemd.user.services.autorandr = lib.mkIf config.services.autorandr.enable {
    Unit.PartOf = lib.mkForce [ "qtile-services.target" ];
    Install.WantedBy = lib.mkForce [ "qtile-services.target" ];
  };

  home.packages = with pkgs; [
    nur.rutherther.rutherther-mpris-ctl
    nur.rutherther.rutherther-sequence-detector
  ];

  systemd.user.services = {
    mpris-ctld = {
      Unit = {
        Description = "Daemon for mpris-ctl cli, that will keep track of last playing media";
        PartOf = [ "qtile-services.target" ];
      };

      Install = {
        WantedBy = [ "qtile-services.target" ];
      };

      Service = {
        ExecStart = "${nur.rutherther.rutherther-mpris-ctl}/bin/mpris-ctld";
      };
    };
  };

{
  systemd.user.targets.qtile-services = {
    Unit = {
      Description = "A target that is enabled when starting Qtile";


@@ 42,105 8,27 @@ in {
    };
  };

  programs.autorandr = {
    enable = true;
    profiles = {
      "home-docked" = {
        fingerprint = {
          "DP-7" = "00ffffffffffff0009d1e77801010101261e0104a5351e783a05f5a557529c270b5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff00455442394c3033373432534c30000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a2001e002031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-8" = "00ffffffffffff0009d1e778455400000d1c0104a5351e783a0565a756529c270f5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff0058334a30303131303031510a20000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a20017d02031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-9" = "00ffffffffffff0009d1d978455400002b1a010380351e782e6c40a755519f27145054a56b80d1c081c081008180a9c0b30001010101023a801871382d40582c45000f282100001e000000ff0054414730333931303031390a20000000fd00324c1e5311000a202020202020000000fc0042656e51204757323437300a200161020322f14f901f05140413031207161501061102230907078301000065030c001000023a801871382d40582c45000f282100001e011d8018711c1620582c25000f282100009e011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f282100001800000000000000000000000000000000000000000005";
        };
        config = {
          DP-9 = {
            enable = true;
            position = "0x0";
            mode = "1920x1080";
          };
          DP-7 = {
            enable = true;
            primary = true;
            position = "1920x0";
            mode = "1920x1080";
          };
          DP-8 = {
            enable = true;
            position = "3840x0";
            mode = "1920x1080";
          };
          eDP-1 = {
            enable = false;
          };
        };
      };
      "home-internal" = {
        fingerprint = {
          "DP-7" = "00ffffffffffff0009d1e77801010101261e0104a5351e783a05f5a557529c270b5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff00455442394c3033373432534c30000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a2001e002031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-8" = "00ffffffffffff0009d1e778455400000d1c0104a5351e783a0565a756529c270f5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff0058334a30303131303031510a20000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a20017d02031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-9" = "00ffffffffffff0009d1d978455400002b1a010380351e782e6c40a755519f27145054a56b80d1c081c081008180a9c0b30001010101023a801871382d40582c45000f282100001e000000ff0054414730333931303031390a20000000fd00324c1e5311000a202020202020000000fc0042656e51204757323437300a200161020322f14f901f05140413031207161501061102230907078301000065030c001000023a801871382d40582c45000f282100001e011d8018711c1620582c25000f282100009e011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f282100001800000000000000000000000000000000000000000005";
          "eDP-1" = "00ffffffffffff0009e5660b000000001a200104a51e137807e957a7544c9a26115457000000010101010101010101010101010101019c3e80c870b03c40302036002ebc1000001a163280c870b03c40302036002ebc1000001a000000fd001e3c4c4c10010a202020202020000000fe004e4531343057554d2d4e36470a00f7";
        };

        config = {
          DP-9 = {
            enable = true;
            position = "0x0";
            mode = "1920x1080";
          };
          DP-7 = {
            enable = true;
            position = "1920x0";
            mode = "1920x1080";
          };
          DP-8 = {
            enable = true;
            position = "3840x0";
            mode = "1920x1080";
          };
          eDP-1 = {
            enable = true;
            primary = true;
            position = "1920x1080";
            mode = "1920x1200";
          };
        };
      };
      "notebook" = {
        fingerprint = {
          "eDP-1" = "00ffffffffffff0009e5660b000000001a200104a51e137807e957a7544c9a26115457000000010101010101010101010101010101019c3e80c870b03c40302036002ebc1000001a163280c870b03c40302036002ebc1000001a000000fd001e3c4c4c10010a202020202020000000fe004e4531343057554d2d4e36470a00f7";
        };
        config = {
          eDP-1 = {
            enable = true;
            # crtc = 0;
            primary = true;
            position = "0x0";
            mode = "1920x1200";
            # gamma = "1.0:0.909:0.833";
            # rate = "60.00";
          };
        };
      };
    };
  };

  xdg.configFile."qtile/config.py".source = ./config/config.py;
  xdg.configFile."qtile/bluetooth.py".source = ./config/bluetooth.py;
  xdg.configFile."qtile/mpris2widget.py".source = ./config/mpris2widget.py;
  xdg.configFile."qtile/utils.py".source = ./config/utils.py;
  xdg.configFile."qtile/functions.py".source = ./config/functions.py;
  xdg.configFile."qtile/bars.py".source = ./config/bars.py;
  xdg.configFile."qtile/screens.py".source = ./config/screens.py;
  xdg.configFile."qtile/styling.py".source = ./config/styling.py;

  xdg.configFile."qtile/tasklist.py".source = ./config/tasklist.py;
  xdg.configFile."qtile/xmonadcustom.py".source = ./config/xmonadcustom.py;
  xdg.configFile."qtile/sequence-detector.config.json".source = ./config/sequence-detector.config.json;

  xdg.configFile."qtile/nixenvironment.py".text = ''
from string import Template
import os
    from string import Template
    import os

setupLocationRef = Template("${location}")
configLocationRef = Template("${location}/modules/desktop/qtile/config")
    setupLocationRef = Template("${location}")
    configLocationRef = Template("${location}/modules/desktop/qtile/config")

setupLocation = setupLocationRef.substitute(os.environ)
configLocation = configLocationRef.substitute(os.environ)
    setupLocation = setupLocationRef.substitute(os.environ)
    configLocation = configLocationRef.substitute(os.environ)

sequenceDetectorExec = "sequence_detector -c /home/${user}/.config/qtile/sequence-detector.config.json "
    sequenceDetectorExec = "sequence_detector -c /home/${user}/.config/qtile/sequence-detector.config.json "
  '';
}

M modules/desktop/qtile/scripts/xephyr => modules/desktop/qtile/scripts/xephyr +2 -0
@@ 6,6 6,8 @@ XDISPLAY=${XDISPLAY:-:1}
LOG_LEVEL=${LOG_LEVEL:-INFO}
APP=${APP:-$(python -c "from libqtile.utils import guess_terminal; print(guess_terminal())")}

cp ~/.config/qtile/nixenvironment.py "$HERE"/../config

Xephyr +extension RANDR -screen ${SCREEN_SIZE} ${XDISPLAY} -ac &
XEPHYR_PID=$!
(

A modules/services/autorandr.nix => modules/services/autorandr.nix +99 -0
@@ 0,0 1,99 @@
{ lib, config, ... }:

{
  services.autorandr = {
    enable = true;
  };

  systemd.user.services.autorandr = lib.mkIf config.services.autorandr.enable {
    Unit.PartOf = lib.mkForce [ "qtile-services.target" ];
    Install.WantedBy = lib.mkForce [ "qtile-services.target" ];
  };

  programs.autorandr = {
    enable = true;
    hooks = {
      postswitch = {
        "notify-qtile" = "qtile cmd-obj -o cmd -f restart";
        # Looks to me like after restarting QTile,
        # the system becomes slower until picom is restarted
        "restart-picom" = "systemctl restart --user picom";
      };
    };
    profiles = {
      "home-docked" = {
        fingerprint = {
          "DP-7" = "00ffffffffffff0009d1e77801010101261e0104a5351e783a05f5a557529c270b5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff00455442394c3033373432534c30000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a2001e002031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-8" = "00ffffffffffff0009d1e778455400000d1c0104a5351e783a0565a756529c270f5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff0058334a30303131303031510a20000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a20017d02031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-9" = "00ffffffffffff0009d1d978455400002b1a010380351e782e6c40a755519f27145054a56b80d1c081c081008180a9c0b30001010101023a801871382d40582c45000f282100001e000000ff0054414730333931303031390a20000000fd00324c1e5311000a202020202020000000fc0042656e51204757323437300a200161020322f14f901f05140413031207161501061102230907078301000065030c001000023a801871382d40582c45000f282100001e011d8018711c1620582c25000f282100009e011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f282100001800000000000000000000000000000000000000000005";
        };
        config = {
          DP-9 = {
            enable = true;
            position = "0x0";
            mode = "1920x1080";
          };
          DP-7 = {
            enable = true;
            primary = true;
            position = "1920x0";
            mode = "1920x1080";
          };
          DP-8 = {
            enable = true;
            position = "3840x0";
            mode = "1920x1080";
          };
          eDP-1 = {
            enable = false;
          };
        };
      };
      "home-internal" = {
        fingerprint = {
          "DP-7" = "00ffffffffffff0009d1e77801010101261e0104a5351e783a05f5a557529c270b5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff00455442394c3033373432534c30000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a2001e002031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-8" = "00ffffffffffff0009d1e778455400000d1c0104a5351e783a0565a756529c270f5054a56b80d1c0b300a9c08180810081c001010101023a801871382d40582c45000f282100001e000000ff0058334a30303131303031510a20000000fd00324c1e5311010a202020202020000000fc0042656e51204757323438300a20017d02031cf14f901f041303120211011406071516052309070783010000023a801871382d40582c45000f282100001f011d8018711c1620582c25000f282100009f011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f28210000180000000000000000000000000000000000000000000000000000008d";
          "DP-9" = "00ffffffffffff0009d1d978455400002b1a010380351e782e6c40a755519f27145054a56b80d1c081c081008180a9c0b30001010101023a801871382d40582c45000f282100001e000000ff0054414730333931303031390a20000000fd00324c1e5311000a202020202020000000fc0042656e51204757323437300a200161020322f14f901f05140413031207161501061102230907078301000065030c001000023a801871382d40582c45000f282100001e011d8018711c1620582c25000f282100009e011d007251d01e206e2855000f282100001e8c0ad08a20e02d10103e96000f282100001800000000000000000000000000000000000000000005";
          "eDP-1" = "00ffffffffffff0009e5660b000000001a200104a51e137807e957a7544c9a26115457000000010101010101010101010101010101019c3e80c870b03c40302036002ebc1000001a163280c870b03c40302036002ebc1000001a000000fd001e3c4c4c10010a202020202020000000fe004e4531343057554d2d4e36470a00f7";
        };

        config = {
          DP-9 = {
            enable = true;
            position = "0x0";
            mode = "1920x1080";
          };
          DP-7 = {
            enable = true;
            position = "1920x0";
            mode = "1920x1080";
          };
          DP-8 = {
            enable = true;
            position = "3840x0";
            mode = "1920x1080";
          };
          eDP-1 = {
            enable = true;
            primary = true;
            position = "1920x1080";
            mode = "1920x1200";
          };
        };
      };
      "notebook" = {
        fingerprint = {
          "eDP-1" = "00ffffffffffff0009e5660b000000001a200104a51e137807e957a7544c9a26115457000000010101010101010101010101010101019c3e80c870b03c40302036002ebc1000001a163280c870b03c40302036002ebc1000001a000000fd001e3c4c4c10010a202020202020000000fe004e4531343057554d2d4e36470a00f7";
        };
        config = {
          eDP-1 = {
            enable = true;
            primary = true;
            position = "0x0";
            mode = "1920x1200";
          };
        };
      };
    };
  };
}

M modules/services/dunst.nix => modules/services/dunst.nix +8 -0
@@ 10,11 10,19 @@ in
{
  home.packages = lib.mkIf config.services.dunst.enable [ pkgs.libnotify ];                   # Dependency

  # Remove dunst dbus Notification link so it's not started under Gnome!
  xdg.dataFile."dbus-1/services/org.knopwob.dunst.service".enable = false;

  systemd.user.services.dunst = lib.mkIf config.services.dunst.enable {
    Unit = {
      PartOf = lib.mkForce [ "qtile-services.target" ];
      After = lib.mkForce [];
    };
    Service = {
      # Remove reference to BusName so dunst is not started under Gnome!
      Type = lib.mkForce "simple";
      BusName = lib.mkForce "empty.dbus.name.placeholder";
    };
    Install = {
      WantedBy = lib.mkForce [ "qtile-services.target" ];
    };

M modules/services/home.nix => modules/services/home.nix +2 -0
@@ 16,4 16,6 @@
  ./picom.nix
  ./udiskie.nix
  ./redshift.nix
  ./mpris-ctl.nix
  ./autorandr.nix
]

A modules/services/mpris-ctl.nix => modules/services/mpris-ctl.nix +29 -0
@@ 0,0 1,29 @@
{ pkgs, ... }:

let
  mpris-ctl = pkgs.callPackage ../../pkgs/rutherther/mpris-ctl.nix {};
  sequence-detector = pkgs.callPackage ../../pkgs/rutherther/sequence-detector.nix {};
in {
  home.packages = [
    sequence-detector
    mpris-ctl
  ];

  systemd.user.services = {
    mpris-ctld = {
      Unit = {
        Description = "Daemon for mpris-ctl cli, that will keep track of last playing media";
        PartOf = [ "qtile-services.target" ];
      };

      Install = {
        WantedBy = [ "qtile-services.target" ];
      };

      Service = {
        ExecStart = "${mpris-ctl}/bin/mpris-ctld";
      };
    };
  };

}

A pkgs/rutherther/mpris-ctl.nix => pkgs/rutherther/mpris-ctl.nix +33 -0
@@ 0,0 1,33 @@
{ pkgs
, rustPlatform
, pkg-config
, dbus
, rustfmt
, cargo
, rustc
}:

rustPlatform.buildRustPackage rec {
  name = "mpris-ctl";
  version = "0.1.0";

  src = pkgs.fetchFromGitHub {
    owner = "Rutherther";
    repo = "mpris-ctl";
    rev = "c5731a17d99553d79810791e5a5aff61344669d5";
    hash = "sha256-vxNpZ6VsGxqFoxl1IpWTqU4iS2g4rfepLXuzPrpvbko=";
  };

  cargoHash = "sha256-QvnaySHqErWuwPBzd1l/asfBbt86c53TKwIyFBvBwQ0=";

  nativeBuildInputs = [
    rustfmt
    pkg-config
    cargo
    rustc
    dbus
  ];

  checkInputs = [ cargo rustc dbus ];
  doCheck = true;
}

A pkgs/rutherther/sequence-detector.nix => pkgs/rutherther/sequence-detector.nix +37 -0
@@ 0,0 1,37 @@
{ lib
, pkgs
, rustPlatform
, pkg-config
, rustfmt
, cargo
, rustc
}:

rustPlatform.buildRustPackage rec {
  src = pkgs.fetchFromGitHub {
    owner = "Rutherther";
    repo = "sequence-detector";
    rev = "c447c0d83877907c3ade8a2e9b4f659d4ef92904";
    hash = "sha256-Bo+IE3aBEHFsnKPrcSVe9x1QNmB8BgsavVmh7UBP4Rg=";
  };

  cargoHash = "sha256-JWg99wgauaoo6Jbt+MARWuHrjgnfTDGWBla56l97o+A=";

  name = "sequence_detector";
  version = "0.1.0";

  nativeBuildInputs = [
    rustfmt
    pkg-config
    cargo
    rustc
  ];

  checkInputs = [ cargo rustc ];
  doCheck = true;

  meta = {
    license = [ lib.licenses.mit ];
    maintainers = [];
  };
}

Do not follow this link