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 = [];
+ };
+}