From d7546970bd34c28ce3bcf4ec4448cdfbead24821 Mon Sep 17 00:00:00 2001 From: Frantisek Bohacek Date: Mon, 25 Mar 2024 21:20:18 +0100 Subject: [PATCH] refactor: split qtile configuration --- modules/desktop/qtile/config/.gitignore | 3 + modules/desktop/qtile/config/bars.py | 225 ++++++++ modules/desktop/qtile/config/bluetooth.py | 186 ------- modules/desktop/qtile/config/config.py | 509 ++----------------- modules/desktop/qtile/config/functions.py | 90 ++++ modules/desktop/qtile/config/mpris2widget.py | 448 ---------------- modules/desktop/qtile/config/screens.py | 93 ++++ modules/desktop/qtile/config/styling.py | 12 + modules/desktop/qtile/config/utils.py | 45 ++ modules/desktop/qtile/home.nix | 140 +---- modules/desktop/qtile/scripts/xephyr | 2 + modules/services/autorandr.nix | 99 ++++ modules/services/dunst.nix | 8 + modules/services/home.nix | 2 + modules/services/mpris-ctl.nix | 29 ++ pkgs/rutherther/mpris-ctl.nix | 33 ++ pkgs/rutherther/sequence-detector.nix | 37 ++ 17 files changed, 724 insertions(+), 1237 deletions(-) create mode 100644 modules/desktop/qtile/config/.gitignore create mode 100644 modules/desktop/qtile/config/bars.py delete mode 100644 modules/desktop/qtile/config/bluetooth.py create mode 100644 modules/desktop/qtile/config/functions.py delete mode 100644 modules/desktop/qtile/config/mpris2widget.py create mode 100644 modules/desktop/qtile/config/screens.py create mode 100644 modules/desktop/qtile/config/styling.py create mode 100644 modules/desktop/qtile/config/utils.py create mode 100644 modules/services/autorandr.nix create mode 100644 modules/services/mpris-ctl.nix create mode 100644 pkgs/rutherther/mpris-ctl.nix create mode 100644 pkgs/rutherther/sequence-detector.nix diff --git a/modules/desktop/qtile/config/.gitignore b/modules/desktop/qtile/config/.gitignore new file mode 100644 index 0000000..5a39fca --- /dev/null +++ b/modules/desktop/qtile/config/.gitignore @@ -0,0 +1,3 @@ +nixenvironment.py +__pycache__ +.mypy_cache diff --git a/modules/desktop/qtile/config/bars.py b/modules/desktop/qtile/config/bars.py new file mode 100644 index 0000000..262ce08 --- /dev/null +++ b/modules/desktop/qtile/config/bars.py @@ -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) diff --git a/modules/desktop/qtile/config/bluetooth.py b/modules/desktop/qtile/config/bluetooth.py deleted file mode 100644 index e05315b..0000000 --- a/modules/desktop/qtile/config/bluetooth.py +++ /dev/null @@ -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) diff --git a/modules/desktop/qtile/config/config.py b/modules/desktop/qtile/config/config.py index 894781f..ad81f3c 100644 --- a/modules/desktop/qtile/config/config.py +++ b/modules/desktop/qtile/config/config.py @@ -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 = ['', '', ''] + [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 diff --git a/modules/desktop/qtile/config/functions.py b/modules/desktop/qtile/config/functions.py new file mode 100644 index 0000000..6d96cbd --- /dev/null +++ b/modules/desktop/qtile/config/functions.py @@ -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() diff --git a/modules/desktop/qtile/config/mpris2widget.py b/modules/desktop/qtile/config/mpris2widget.py deleted file mode 100644 index bb628fd..0000000 --- a/modules/desktop/qtile/config/mpris2widget.py +++ /dev/null @@ -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 diff --git a/modules/desktop/qtile/config/screens.py b/modules/desktop/qtile/config/screens.py new file mode 100644 index 0000000..9813091 --- /dev/null +++ b/modules/desktop/qtile/config/screens.py @@ -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() diff --git a/modules/desktop/qtile/config/styling.py b/modules/desktop/qtile/config/styling.py new file mode 100644 index 0000000..80e1d71 --- /dev/null +++ b/modules/desktop/qtile/config/styling.py @@ -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', +} diff --git a/modules/desktop/qtile/config/utils.py b/modules/desktop/qtile/config/utils.py new file mode 100644 index 0000000..083a625 --- /dev/null +++ b/modules/desktop/qtile/config/utils.py @@ -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 = ['', '', ''] + [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 diff --git a/modules/desktop/qtile/home.nix b/modules/desktop/qtile/home.nix index 29a8857..03e20d9 100644 --- a/modules/desktop/qtile/home.nix +++ b/modules/desktop/qtile/home.nix @@ -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 " ''; } diff --git a/modules/desktop/qtile/scripts/xephyr b/modules/desktop/qtile/scripts/xephyr index 38b5401..e66e88f 100644 --- a/modules/desktop/qtile/scripts/xephyr +++ b/modules/desktop/qtile/scripts/xephyr @@ -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=$! ( diff --git a/modules/services/autorandr.nix b/modules/services/autorandr.nix new file mode 100644 index 0000000..74bbaf7 --- /dev/null +++ b/modules/services/autorandr.nix @@ -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"; + }; + }; + }; + }; + }; +} diff --git a/modules/services/dunst.nix b/modules/services/dunst.nix index ebf8de9..e691502 100644 --- a/modules/services/dunst.nix +++ b/modules/services/dunst.nix @@ -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" ]; }; diff --git a/modules/services/home.nix b/modules/services/home.nix index e907e0a..7c442f5 100644 --- a/modules/services/home.nix +++ b/modules/services/home.nix @@ -16,4 +16,6 @@ ./picom.nix ./udiskie.nix ./redshift.nix + ./mpris-ctl.nix + ./autorandr.nix ] diff --git a/modules/services/mpris-ctl.nix b/modules/services/mpris-ctl.nix new file mode 100644 index 0000000..a1476ac --- /dev/null +++ b/modules/services/mpris-ctl.nix @@ -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"; + }; + }; + }; + +} diff --git a/pkgs/rutherther/mpris-ctl.nix b/pkgs/rutherther/mpris-ctl.nix new file mode 100644 index 0000000..cbeed51 --- /dev/null +++ b/pkgs/rutherther/mpris-ctl.nix @@ -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; +} diff --git a/pkgs/rutherther/sequence-detector.nix b/pkgs/rutherther/sequence-detector.nix new file mode 100644 index 0000000..73ea2f5 --- /dev/null +++ b/pkgs/rutherther/sequence-detector.nix @@ -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 = []; + }; +} -- 2.48.1