M hosts/home.nix => hosts/home.nix +1 -2
@@ 47,6 47,7 @@
# File Management
zathura # PDF Viewer
evince # PDF Viewer
+ foliate # Ebook viewer
rsync # Syncer - $ rsync -r dir1/ dir2/
unzip # Zip Files
unrar # Rar Files
@@ 71,8 72,6 @@
#xorg.xrandr # Screen Settings
#
# Xorg home-manager
- flameshot # Screenshot
- picom # Compositer
#
# Desktop
discord # Chat
A modules/desktop/qtile/config/autostart.sh => modules/desktop/qtile/config/autostart.sh +10 -0
@@ 0,0 1,10 @@
+#!/usr/bin/env sh
+
+# Browser
+firefox &
+
+# Comms
+discord &
+telegram-desktop &
+
+# aw-qt &
A modules/desktop/qtile/config/bluetooth.py => modules/desktop/qtile/config/bluetooth.py +188 -0
@@ 0,0 1,188 @@
+# 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
+ ):
+ logger.warning(f"{changed_properties.keys()}")
+ 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()
+ logger.warning(f"Updating text with battery")
+ 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)
A modules/desktop/qtile/config/config.py => modules/desktop/qtile/config/config.py +605 -0
@@ 0,0 1,605 @@
+import re
+import os
+import subprocess
+import psutil
+from libqtile import layout, bar, qtile
+from libqtile.backend.base import Window
+from libqtile.core.manager import Qtile
+from libqtile.config import Click, Drag, Group, KeyChord, EzKey, EzKeyChord, Match, Screen, ScratchPad, DropDown, Key
+from libqtile.lazy import lazy
+from libqtile.utils import guess_terminal
+from libqtile import hook
+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
+from nixenvironment import setupLocation, configLocation
+
+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 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:
+ return
+
+ if switch_monitor:
+ for screen in qtile.screens:
+ if screen.group.name == group_name:
+ qtile.focus_screen(screen.index)
+ found = True
+ break
+
+ qtile.groups_map[group_name].toscreen()
+
+ for window in current_group.windows:
+ if window.fullscreen:
+ 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()
+
+# 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'
+terminal = 'alacritty'
+
+layout_theme = {
+ 'border_focus': colors['active'],
+ 'border_normal': colors['inactive'],
+ 'border_width': 1,
+ 'margin': 3,
+}
+
+layouts = [
+ layout.MonadTall(**layout_theme),
+ layout.Max(**layout_theme),
+ layout.MonadWide(**layout_theme),
+]
+
+widget_defaults = dict(
+ font = 'Roboto Bold',
+ fontsize = 13,
+ padding = 3,
+ background = colors['background_primary'],
+ foreground = colors['white'],
+)
+extension_defaults = widget_defaults.copy()
+
+powerline = {
+ 'decorations': [
+ PowerLineDecoration(path = 'forward_slash')
+ ]
+}
+
+def create_top_bar(systray = False):
+ 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():
+ 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(
+ display_metadata = ['xesam:title'],
+ playerfilter = '.*Firefox.*',
+ 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(
+ display_metadata = ['xesam:title', 'xesam:artist'],
+ objname = 'org.mpris.MediaPlayer2.spotify',
+ 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_screen(top_bar, wallpaper, index):
+ return Screen(top=top_bar, bottom=create_bottom_bar(), wallpaper=wallpaper, x=1920*index, y=0, width=1920, height=1080)
+
+screens = [
+ init_screen(create_top_bar(), '/usr/share/backgrounds/dark futuristic city 3.png', 0),
+ init_screen(create_top_bar(systray = True), '/usr/share/backgrounds/dark futuristic city č.png', 1),
+ init_screen(create_top_bar(), '/usr/share/backgrounds/synthwave futuristic city.png', 2),
+]
+
+# Keys
+keys = []
+
+# up, down, main navigation
+keys.extend([
+ EzKey('M-j', lazy.layout.down(), desc = 'Move focus down'),
+ EzKey('M-k', lazy.layout.up(), desc = 'Move focus up'),
+ EzKey('M-S-j', lazy.layout.shuffle_down(), desc = 'Move focus down'),
+ EzKey('M-S-k', lazy.layout.shuffle_up(), desc = 'Move focus up'),
+ EzKey('M-C-h', lazy.layout.shrink_main(), desc = 'Move focus down'),
+ EzKey('M-C-l', lazy.layout.grow_main(), desc = 'Move focus up'),
+ EzKey('M-m', lazy.layout.focus_first(), desc = 'Focus main window'),
+ EzKey('M-u', lazy.next_urgent(), desc = 'Focus urgent window'),
+])
+
+keys.extend([
+ EzKey('M-n', lazy.layout.normalize()),
+ EzKey('M-t', lazy.window.disable_floating()),
+ EzKey('M-f', lazy.window.toggle_fullscreen()),
+ EzKey('M-<Return>', lazy.layout.swap_main()),
+ EzKey('M-<Space>', lazy.next_layout()),
+ EzKey('M-S-<Space>', lazy.to_layout_index(0), desc = 'Default layout'),
+])
+
+# Spwning programs
+keys.extend([
+ EzKey('M-<semicolon>', lazy.spawn('rofi -show drun')),
+ EzKey('A-<semicolon>', lazy.spawn('rofi -show windowcd -modi window,windowcd')),
+ EzKey('M-S-<semicolon>', lazy.spawn('rofi -show window -modi window,windowcd')),
+ EzKey('M-S-<Return>', lazy.spawn(terminal)),
+])
+
+keys.extend([
+ # social navigation
+ EzKeyChord('M-a', expand_with_rest_keys([
+ EzKey('b', focus_window_by_class('discord')),
+ EzKey('n', focus_window_by_class('Cinny')),
+ EzKey('m', focus_window_by_class('Cinny')),
+
+ # notifications
+ EzKey('l', lazy.spawn(f'{setupLocation}/scripts/notifications/clear-popups.sh')),
+ EzKey('p', lazy.spawn(f'{setupLocation}/scripts/notifications/pause.sh')),
+ EzKey('u', lazy.spawn(f'{setupLocation}/scripts/notifications/unpause.sh')),
+ EzKey('t', lazy.spawn(f'{setupLocation}/scripts/notifications/show-center.sh')),
+
+ EzKey('e', lazy.spawn('emacsclient -c')),
+ ], 'M-a'), name = 'a')
+])
+
+keys.extend([
+ EzKeyChord('M-s', expand_with_rest_keys([
+ EzKey('e', lazy.spawn('emacsclient -c')),
+ EzKey('c', lazy.group['scratchpad'].dropdown_toggle('ipcam')),
+ EzKey('s', lazy.group['scratchpad'].dropdown_toggle('spotify')),
+ EzKey('b', lazy.group['scratchpad'].dropdown_toggle('bluetooth')),
+ EzKey('a', lazy.group['scratchpad'].dropdown_toggle('audio')),
+ EzKey('m', lazy.group['scratchpad'].dropdown_toggle('mail')),
+ EzKey('p', lazy.group['scratchpad'].dropdown_toggle('proton')),
+ ], 'M-s'), name = 's')
+])
+
+# bars
+keys.extend([
+ EzKey('M-b', lazy.hide_show_bar('all')),
+ EzKey('M-v', lazy.hide_show_bar('bottom')),
+])
+
+# media keys
+keys.extend([
+ EzKey('<XF86AudioPlay>', lazy.spawn('playerctl play')),
+ EzKey('<XF86AudioPause>', lazy.spawn('playerctl pause')),
+ EzKey('<XF86AudioStop>', lazy.spawn('playerctl stop')),
+ EzKey('<XF86AudioNext>', lazy.spawn('playerctl next')),
+ EzKey('<XF86AudioPrev>', lazy.spawn('playerctl previous')),
+ EzKey('<XF86AudioMute>', lazy.spawn('amixer -D pulse set Master 1+ toggle')),
+ EzKey('<XF86MonBrightnessUp>', lazy.spawn('xbacklight -inc 5')),
+ EzKey('<XF86MonBrightnessDown>', lazy.spawn('xbacklight -dec 5')),
+])
+
+# Printscreen
+keys.extend([
+ EzKey('<Print>', lazy.spawn('flameshot gui')),
+])
+
+# Qtile control
+keys.extend([
+ EzKey('M-S-c', lazy.window.kill()),
+ EzKey('M-C-r', lazy.reload_config()),
+ EzKey('M-C-q', lazy.shutdown()),
+])
+
+# Monitor navigation
+monitor_navigation_keys = ['w', 'e', 'r']
+monitor_index_map = [ 1, 2, 0 ]
+for i, key in enumerate(monitor_navigation_keys):
+ keys.extend([
+ EzKey(f'M-{key}', lazy.to_screen(monitor_index_map[i]), desc = f'Move focus to screen {i}'),
+ EzKey(f'M-S-{key}', lazy.window.toscreen(monitor_index_map[i]), desc = f'Move focus to screen {i}'),
+ ])
+
+if qtile.core.name == 'x11':
+ keys.append(EzKey('M-S-z', lazy.spawn('clipmenu')))
+elif qtile.core.name == 'wayland':
+ keys.append(
+ EzKey(
+ 'M-S-z',
+ lazy.spawn('sh -c "cliphist list | dmenu -l 10 | cliphist decode | wl-copy"')
+ )
+ )
+
+group_defaults = {
+ '9': {
+ '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:
+ keys.extend(
+ [
+ EzKey(
+ f'M-{i.name}',
+ go_to_group(i.name),
+ desc='Switch to group {}'.format(i.name),
+ ),
+ Key(
+ [mod, 'shift'],
+ i.name,
+ lazy.window.togroup(i.name, switch_group=False),
+ desc='Switch to & move focused window to group {}'.format(i.name),
+ ),
+ ]
+ )
+
+scratch_pad_middle = {
+ 'x': 0.2, 'y': 0.2,
+ 'height': 0.6, 'width': 0.6,
+}
+
+groups.append(
+ ScratchPad('scratchpad', [
+ DropDown(
+ 'spotify',
+ ['spotify'],
+ on_focus_lost_hide = True,
+ **scratch_pad_middle
+ ),
+ DropDown(
+ 'bluetooth',
+ ['blueman-manager'],
+ on_focus_lost_hide = True,
+ **scratch_pad_middle
+ ),
+ DropDown(
+ 'audio',
+ ['pavucontrol'],
+ on_focus_lost_hide = True,
+ **scratch_pad_middle
+ ),
+ DropDown(
+ 'mail',
+ ['thunderbird'],
+ on_focus_lost_hide = True,
+ x = 0.025, y = 0.025,
+ width = 0.95, height = 0.95,
+ opacity = 1,
+ ),
+ ])
+)
+
+# Drag floating layouts.
+mouse = [
+ Drag([mod], 'Button1', lazy.window.set_position_floating(), start=lazy.window.get_position()),
+ Drag([mod], 'Button3', lazy.window.set_size_floating(), start=lazy.window.get_size()),
+ Click([mod], 'Button2', lazy.window.bring_to_front()),
+]
+
+dgroups_key_binder = None
+dgroups_app_rules = [] # type: list
+follow_mouse_focus = False
+cursor_warp = True
+floating_layout = layout.Floating(
+ float_rules=[
+ *layout.Floating.default_float_rules,
+ ]
+)
+auto_fullscreen = True
+focus_on_window_activation = 'urgent'
+reconfigure_screens = False
+auto_minimize = True
+bring_front_click = False
+
+wl_input_rules = {}
+wmname = 'LG3D'
+
+# Swallow windows,
+# when a process with window spawns
+# another process with a window as a child, minimize the first
+# winddow. Turn off the minimization after the child process
+# is done.
+@hook.subscribe.client_new
+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():
+ subprocess.call([f'{configLocation}/autostart.sh'])
+
+firefoxInstance = 0
+@hook.subscribe.client_new
+def startup_applications(client: Window):
+ global firefoxInstance
+
+ if not isinstance(client, Window):
+ return
+
+ if client.match(Match(wm_class = 'firefox')) and firefoxInstance <= 1:
+ client.togroup(groups[firefoxInstance].name)
+ firefoxInstance += 1
+ elif client.match(Match(wm_class = 'discord')) or client.match(Match(wm_class = 'cinny')):
+ client.togroup(groups[8].name)
+
+# Turn off fullscreen on unfocus
+@hook.subscribe.client_focus
+def exit_fullscreen_on_focus_changed(client: Window):
+ windows = client.group.windows
+ window: Window
+ for window in windows:
+ if window != client and window.fullscreen:
+ window.toggle_fullscreen()
+
+# Start scratchpads
+@hook.subscribe.startup_complete
+def scratchpad_startup():
+ scratchpad: ScratchPad = qtile.groups_map['scratchpad']
+ for dropdown_name, dropdown_config in scratchpad._dropdownconfig.items():
+ scratchpad._spawn(dropdown_config)
+ def wrapper(name):
+ def hide_dropdown(_):
+ dropdown = scratchpad.dropdowns.get(name)
+ if dropdown:
+ dropdown.hide()
+ hook.unsubscribe.client_managed(hide_dropdown)
+ return hide_dropdown
+
+ hook.subscribe.client_managed(wrapper(dropdown_name))
A modules/desktop/qtile/config/mpris2widget.py => modules/desktop/qtile/config/mpris2widget.py +448 -0
@@ 0,0 1,448 @@
+# 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/tasklist.py => modules/desktop/qtile/config/tasklist.py +618 -0
@@ 0,0 1,618 @@
+# Copyright (c) 2012-2014 roger
+# Copyright (c) 2012-2015 Tycho Andersen
+# Copyright (c) 2013 dequis
+# Copyright (c) 2013 Tao Sauvage
+# Copyright (c) 2013 Craig Barnes
+# Copyright (c) 2014 Sean Vig
+# Copyright (c) 2018 Piotr Przymus
+#
+# 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.
+import re
+
+import cairocffi
+
+try:
+ from xdg.IconTheme import getIconPath
+
+ has_xdg = True
+except ImportError:
+ has_xdg = False
+
+from libqtile import bar, hook, pangocffi
+from libqtile.images import Img
+from libqtile.log_utils import logger
+from libqtile.widget import base
+
+
+class TaskList(base._Widget, base.PaddingMixin, base.MarginMixin):
+ """Displays the icon and name of each window in the current group
+
+ Contrary to WindowTabs this is an interactive widget. The window that
+ currently has focus is highlighted.
+
+ Optional requirements: `pyxdg <https://pypi.org/project/pyxdg/>`__ is needed
+ to use theme icons and to display icons on Wayland.
+ """
+
+ orientations = base.ORIENTATION_HORIZONTAL
+ defaults = [
+ ("font", "sans", "Default font"),
+ ("fontsize", None, "Font size. Calculated if None."),
+ ("foreground", "ffffff", "Foreground colour"),
+ ("fontshadow", None, "font shadow color, default is None(no shadow)"),
+ ("borderwidth", 2, "Current group border width"),
+ ("border", "215578", "Border colour"),
+ ("rounded", True, "To round or not to round borders"),
+ (
+ "highlight_method",
+ "border",
+ "Method of highlighting (one of 'border' or 'block') "
+ "Uses `*_border` color settings",
+ ),
+ ("urgent_border", "FF0000", "Urgent border color"),
+ (
+ "urgent_alert_method",
+ "border",
+ "Method for alerting you of WM urgent " "hints (one of 'border' or 'text')",
+ ),
+ ("highlight_color", "000000", "The color to highlight with in line mode"),
+ (
+ "unfocused_border",
+ None,
+ "Border color for unfocused windows. "
+ "Affects only hightlight_method 'border' and 'block'. "
+ "Defaults to None, which means no special color.",
+ ),
+ (
+ "max_title_width",
+ None,
+ "Max size in pixels of task title." "(if set to None, as much as available.)",
+ ),
+ (
+ "title_width_method",
+ None,
+ "Method to compute the width of task title. (None, 'uniform'.)"
+ "Defaults to None, the normal behaviour.",
+ ),
+ (
+ "parse_text",
+ None,
+ "Function to parse and modify window names. "
+ "e.g. function in config that removes excess "
+ "strings from window name: "
+ "def my_func(text)"
+ ' for string in [" - Chromium", " - Firefox"]:'
+ ' text = text.replace(string, "")'
+ " return text"
+ "then set option parse_text=my_func",
+ ),
+ ("spacing", None, "Spacing between tasks." "(if set to None, will be equal to margin_x)"),
+ (
+ "txt_minimized",
+ "_ ",
+ "Text representation of the minimized window state. " 'e.g., "_ " or "\U0001F5D5 "',
+ ),
+ (
+ "txt_maximized",
+ "[] ",
+ "Text representation of the maximized window state. " 'e.g., "[] " or "\U0001F5D6 "',
+ ),
+ (
+ "txt_floating",
+ "V ",
+ "Text representation of the floating window state. " 'e.g., "V " or "\U0001F5D7 "',
+ ),
+ (
+ "markup_normal",
+ None,
+ "Text markup of the normal window state. Supports pangomarkup with markup=True."
+ 'e.g., "{}" or "<span underline="low">{}</span>"',
+ ),
+ (
+ "markup_minimized",
+ None,
+ "Text markup of the minimized window state. Supports pangomarkup with markup=True."
+ 'e.g., "{}" or "<span underline="low">{}</span>"',
+ ),
+ (
+ "markup_maximized",
+ None,
+ "Text markup of the maximized window state. Supports pangomarkup with markup=True."
+ 'e.g., "{}" or "<span underline="low">{}</span>"',
+ ),
+ (
+ "markup_floating",
+ None,
+ "Text markup of the floating window state. Supports pangomarkup with markup=True."
+ 'e.g., "{}" or "<span underline="low">{}</span>"',
+ ),
+ (
+ "markup_focused",
+ None,
+ "Text markup of the focused window state. Supports pangomarkup with markup=True."
+ 'e.g., "{}" or "<span underline="low">{}</span>"',
+ ),
+ (
+ "icon_size",
+ None,
+ "Icon size. " "(Calculated if set to None. Icons are hidden if set to 0.)",
+ ),
+ (
+ "theme_mode",
+ None,
+ "When to use theme icons. `None` = never, `preferred` = use if available, "
+ "`fallback` = use if app does not provide icon directly. "
+ "`preferred` and `fallback` have identical behaviour on Wayland.",
+ ),
+ (
+ "theme_path",
+ None,
+ "Path to icon theme to be used by pyxdg for icons. ``None`` will use default icon theme.",
+ ),
+ (
+ "window_name_location",
+ False,
+ "Whether to show the location of the window in the title.",
+ ),
+ (
+ "window_name_location_offset",
+ 0,
+ "The offset given to the window location",
+ ),
+ ]
+
+ def __init__(self, **config):
+ base._Widget.__init__(self, bar.STRETCH, **config)
+ self.add_defaults(TaskList.defaults)
+ self.add_defaults(base.PaddingMixin.defaults)
+ self.add_defaults(base.MarginMixin.defaults)
+ self._icons_cache = {}
+ self._box_end_positions = []
+ self.markup = False
+ self.clicked = None
+ if self.spacing is None:
+ self.spacing = self.margin_x
+
+ self.add_callbacks({"Button1": self.select_window})
+
+ def box_width(self, text):
+ """
+ calculate box width for given text.
+ If max_title_width is given, the returned width is limited to it.
+ """
+ if self.markup:
+ text = re.sub("<[^<]+?>", "", text)
+ width, _ = self.drawer.max_layout_size([text], self.font, self.fontsize)
+ width = width + 2 * (self.padding_x + self.borderwidth)
+ return width
+
+ def get_taskname(self, window):
+ """
+ Get display name for given window.
+ Depending on its state minimized, maximized and floating
+ appropriate characters are prepended.
+ """
+ state = ""
+ markup_str = self.markup_normal
+
+ # Enforce markup and new string format behaviour when
+ # at least one markup_* option is used.
+ # Mixing non markup and markup may cause problems.
+ if (
+ self.markup_minimized
+ or self.markup_maximized
+ or self.markup_floating
+ or self.markup_focused
+ ):
+ enforce_markup = True
+ else:
+ enforce_markup = False
+
+ if window is None:
+ pass
+ elif window.minimized:
+ state = self.txt_minimized
+ markup_str = self.markup_minimized
+ elif window.maximized:
+ state = self.txt_maximized
+ markup_str = self.markup_maximized
+ elif window.floating:
+ state = self.txt_floating
+ markup_str = self.markup_floating
+ elif window is window.group.current_window:
+ markup_str = self.markup_focused
+
+ window_location = (
+ f"[{window.group.windows.index(window) + self.window_name_location_offset}] "
+ if self.window_name_location
+ else ""
+ )
+ window_name = window_location + window.name if window and window.name else "?"
+
+ if callable(self.parse_text):
+ try:
+ window_name = self.parse_text(window_name)
+ except: # noqa: E722
+ logger.exception("parse_text function failed:")
+
+ # Emulate default widget behavior if markup_str is None
+ if enforce_markup and markup_str is None:
+ markup_str = "%s{}" % (state)
+
+ if markup_str is not None:
+ self.markup = True
+ window_name = pangocffi.markup_escape_text(window_name)
+ return markup_str.format(window_name)
+
+ return "%s%s" % (state, window_name)
+
+ @property
+ def windows(self):
+ if self.qtile.core.name == "x11":
+ return [
+ w
+ for w in self.bar.screen.group.windows
+ if w.window.get_wm_type() in ("normal", None)
+ ]
+ return self.bar.screen.group.windows
+
+ def calc_box_widths(self):
+ """
+ Calculate box width for each window in current group.
+ If the available space is less than overall size of boxes,
+ the boxes are shrunk by percentage if greater than average.
+ """
+ windows = self.windows
+ window_count = len(windows)
+
+ # if no windows present for current group just return empty list
+ if not window_count:
+ return []
+
+ # Determine available and max average width for task name boxes.
+ width_total = self.width - 2 * self.margin_x - (window_count - 1) * self.spacing
+ width_avg = width_total / window_count
+
+ names = [self.get_taskname(w) for w in windows]
+
+ if self.icon_size == 0:
+ icons = len(windows) * [None]
+ else:
+ icons = [self.get_window_icon(w) for w in windows]
+
+ # Obey title_width_method if specified
+ if self.title_width_method == "uniform":
+ width_uniform = width_total // window_count
+ width_boxes = [width_uniform for w in range(window_count)]
+ else:
+ # Default behaviour: calculated width for each task according to
+ # icon and task name consisting
+ # of state abbreviation and window name
+ width_boxes = [
+ (
+ self.box_width(names[idx])
+ + ((self.icon_size + self.padding_x) if icons[idx] else 0)
+ )
+ for idx in range(window_count)
+ ]
+
+ # Obey max_title_width if specified
+ if self.max_title_width:
+ width_boxes = [min(w, self.max_title_width) for w in width_boxes]
+
+ width_sum = sum(width_boxes)
+
+ # calculated box width are to wide for available widget space:
+ if width_sum > width_total:
+ # sum the width of tasks shorter than calculated average
+ # and calculate a ratio to shrink boxes greater than width_avg
+ width_shorter_sum = sum([w for w in width_boxes if w < width_avg])
+
+ ratio = (width_total - width_shorter_sum) / (width_sum - width_shorter_sum)
+ # determine new box widths by shrinking boxes greater than avg
+ width_boxes = [(w if w < width_avg else w * ratio) for w in width_boxes]
+
+ return zip(windows, icons, names, width_boxes)
+
+ def _configure(self, qtile, bar):
+ base._Widget._configure(self, qtile, bar)
+
+ if not has_xdg and self.theme_mode is not None:
+ logger.warning("You must install pyxdg to use theme icons.")
+ self.theme_mode = None
+
+ if self.theme_mode and self.theme_mode not in ["preferred", "fallback"]:
+ logger.warning(
+ "Unexpected theme_mode (%s). Theme icons will be disabled.", self.theme_mode
+ )
+ self.theme_mode = None
+
+ if qtile.core.name == "wayland" and self.theme_mode is None and self.icon_size != 0:
+ # Disable icons
+ self.icon_size = 0
+
+ if self.icon_size is None:
+ self.icon_size = self.bar.height - 2 * (self.borderwidth + self.margin_y)
+
+ if self.fontsize is None:
+ calc = self.bar.height - self.margin_y * 2 - self.borderwidth * 2 - self.padding_y * 2
+ self.fontsize = max(calc, 1)
+ self.layout = self.drawer.textlayout(
+ "", "ffffff", self.font, self.fontsize, self.fontshadow, wrap=False
+ )
+ self.setup_hooks()
+
+ def update(self, window=None):
+ if not window or window in self.windows:
+ self.bar.draw()
+
+ def remove_icon_cache(self, window):
+ wid = window.wid
+ if wid in self._icons_cache:
+ self._icons_cache.pop(wid)
+
+ def invalidate_cache(self, window):
+ self.remove_icon_cache(window)
+ self.update(window)
+
+ def setup_hooks(self):
+ hook.subscribe.client_name_updated(self.update)
+ hook.subscribe.focus_change(self.update)
+ hook.subscribe.float_change(self.update)
+ hook.subscribe.client_urgent_hint_changed(self.update)
+
+ hook.subscribe.net_wm_icon_change(self.invalidate_cache)
+ hook.subscribe.client_killed(self.remove_icon_cache)
+
+ def drawtext(self, text, textcolor, width):
+ if self.markup:
+ self.layout.markup = self.markup
+
+ self.layout.text = text
+
+ self.layout.font_family = self.font
+ self.layout.font_size = self.fontsize
+ self.layout.colour = textcolor
+ if width is not None:
+ self.layout.width = width
+
+ def drawbox(
+ self,
+ offset,
+ text,
+ bordercolor,
+ textcolor,
+ width=None,
+ rounded=False,
+ block=False,
+ line=False,
+ icon=None,
+ highlight_color=None,
+ ):
+ self.drawtext(text, textcolor, width)
+
+ icon_padding = (self.icon_size + self.padding_x) if icon else 0
+ padding_x = [self.padding_x + icon_padding, self.padding_x]
+
+ if bordercolor is None:
+ # border colour is set to None when we don't want to draw a border at all
+ # Rather than dealing with alpha blending issues, we just set border width
+ # to 0.
+ border_width = 0
+ framecolor = self.background or self.bar.background
+ else:
+ border_width = self.borderwidth
+ framecolor = bordercolor
+
+ framed = self.layout.framed(border_width, framecolor, padding_x, self.padding_y)
+ if block and bordercolor is not None:
+ framed.draw_fill(offset, self.margin_y, rounded)
+ elif line:
+ pad_y = [
+ (self.bar.height - self.layout.height - self.borderwidth) / 2,
+ (self.bar.height - self.layout.height + self.borderwidth) / 2,
+ ]
+ framed = self.layout.framed(border_width, framecolor, padding_x, pad_y, highlight_color)
+
+ framed.drawer.set_source_rgb(framed.border_color)
+
+ opts = [
+ offset,
+ self.margin_y,
+ framed.layout.width + framed.pad_left + framed.pad_right,
+ framed.layout.height + framed.pad_top + framed.pad_bottom,
+ framed.border_width,
+ ]
+
+ if bordercolor != None:
+ framed.drawer.set_source_rgb(framed.highlight_color)
+ framed.drawer.fillrect(*opts)
+ framed.drawer.set_source_rgb(framed.border_color)
+
+ opts = [
+ offset,
+ 0,# framed.height - framed.border_width,
+ framed.layout.width + framed.pad_left + framed.pad_right,
+ framed.border_width,
+ framed.border_width,
+ ]
+
+ framed.drawer.fillrect(*opts)
+ framed.drawer.ctx.stroke()
+ framed.layout.draw(offset + framed.pad_left, 0 + framed.pad_top)
+ else:
+ framed.draw(offset, self.margin_y, rounded)
+
+ if icon:
+ self.draw_icon(icon, offset)
+
+ def get_clicked(self, x, y):
+ box_start = self.margin_x
+ for box_end, win in zip(self._box_end_positions, self.windows):
+ if box_start <= x <= box_end:
+ return win
+ else:
+ box_start = box_end + self.spacing
+ # not found any , return None
+ return None
+
+ def button_press(self, x, y, button):
+ self.clicked = self.get_clicked(x, y)
+ base._Widget.button_press(self, x, y, button)
+
+ def select_window(self):
+ if self.clicked:
+ current_win = self.bar.screen.group.current_window
+ window = self.clicked
+ if window is not current_win:
+ window.group.focus(window, False)
+ if window.floating:
+ window.bring_to_front()
+ else:
+ window.toggle_minimize()
+
+ def _get_class_icon(self, window):
+ if not getattr(window, "icons", False):
+ return None
+
+ icons = sorted(
+ iter(window.icons.items()),
+ key=lambda x: abs(self.icon_size - int(x[0].split("x")[0])),
+ )
+ icon = icons[0]
+ width, height = map(int, icon[0].split("x"))
+
+ img = cairocffi.ImageSurface.create_for_data(
+ icon[1], cairocffi.FORMAT_ARGB32, width, height
+ )
+
+ return img
+
+ def _get_theme_icon(self, window):
+ classes = window.get_wm_class()
+
+ if not classes:
+ return None
+
+ icon = None
+
+ for cl in classes:
+ for app in set([cl, cl.lower()]):
+ icon = getIconPath(app, theme=self.theme_path)
+ if icon is not None:
+ break
+ else:
+ continue
+ break
+
+ if not icon:
+ return None
+
+ img = Img.from_path(icon)
+
+ return img.surface
+
+ def get_window_icon(self, window):
+ if not getattr(window, "icons", False) and self.theme_mode is None:
+ return None
+
+ cache = self._icons_cache.get(window.wid)
+ if cache:
+ return cache
+
+ surface = None
+ img = None
+
+ if self.qtile.core.name == "x11":
+ img = self._get_class_icon(window)
+
+ if self.theme_mode == "preferred" or (self.theme_mode == "fallback" and img is None):
+ xdg_img = self._get_theme_icon(window)
+ if xdg_img:
+ img = xdg_img
+
+ if img is not None:
+ surface = cairocffi.SurfacePattern(img)
+ height = img.get_height()
+ width = img.get_width()
+ scaler = cairocffi.Matrix()
+ if height != self.icon_size:
+ sp = height / self.icon_size
+ height = self.icon_size
+ width /= sp
+ scaler.scale(sp, sp)
+ surface.set_matrix(scaler)
+
+ self._icons_cache[window.wid] = surface
+ return surface
+
+ def draw_icon(self, surface, offset):
+ if not surface:
+ return
+
+ x = offset + self.borderwidth + self.padding_x
+ y = (self.height - self.icon_size) // 2
+
+ self.drawer.ctx.save()
+ self.drawer.ctx.translate(x, y)
+ self.drawer.ctx.set_source(surface)
+ self.drawer.ctx.paint()
+ self.drawer.ctx.restore()
+
+ def draw(self):
+ self.drawer.clear(self.background or self.bar.background)
+ offset = self.margin_x
+
+ self._box_end_positions = []
+ for w, icon, task, bw in self.calc_box_widths():
+ self._box_end_positions.append(offset + bw)
+
+ if w.urgent:
+ border = self.urgent_border
+ text_color = border
+ elif w is w.group.current_window:
+ border = self.border
+ text_color = border
+ else:
+ border = self.unfocused_border or None
+ text_color = self.foreground
+
+ if self.highlight_method == "text":
+ border = None
+ else:
+ text_color = self.foreground
+
+ textwidth = (
+ bw - 2 * self.padding_x - ((self.icon_size + self.padding_x) if icon else 0)
+ )
+ self.drawbox(
+ offset,
+ task,
+ border,
+ text_color,
+ rounded=self.rounded,
+ block=(self.highlight_method == "block"),
+ line=(self.highlight_method == "line"),
+ width=textwidth,
+ highlight_color=self.highlight_color,
+ icon=icon,
+ )
+ offset += bw + self.spacing
+
+ self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width)
M modules/desktop/qtile/default.nix => modules/desktop/qtile/default.nix +12 -8
@@ 1,17 1,21 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, nixpkgs, ... }:
{
- environment = {
- systemPackages = with pkgs; [
- qtile
- python310Packages.qtile-extras
- ];
- };
+ imports = [(import ./python-overlay.nix)];
+
+ environment.systemPackages = with pkgs; [
+ playerctl
+ ];
services = {
xserver = {
enable = true;
- windowManager.qtile.enable = true;
+ windowManager.qtile = {
+ enable = true;
+ extraPackages = ppkgs: [
+ ppkgs.qtile-extras
+ ];
+ };
};
};
}
M modules/desktop/qtile/home.nix => modules/desktop/qtile/home.nix +16 -2
@@ 1,5 1,19 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, location, ... }:
{
- xdg.configFile."qtile".source = ./qtile;
+ 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/tasklist.py".source = ./config/tasklist.py;
+
+ xdg.configFile."qtile/nixenvironment.py".text = ''
+from string import Template
+import os
+
+setupLocationRef = Template("${location}")
+configLocationRef = Template("${location}/modules/desktop/qtile/config")
+
+setupLocation = setupLocationRef.substitute(os.environ)
+configLocation = configLocationRef.substitute(os.environ)
+ '';
}
A modules/desktop/qtile/python-overlay.nix => modules/desktop/qtile/python-overlay.nix +113 -0
@@ 0,0 1,113 @@
+{nigpkgs, ...}:
+
+{
+
+ nixpkgs = {
+ overlays = [
+ (final: super: {
+ pythonPackagesOverlays =
+ (super.pythonPackagesOverlays or [])
+ ++ [
+ (_: pprev: {
+ cairocffi = pprev.cairocffi.overridePythonAttrs (_: rec {
+ pname = "cairocffi";
+ version = "1.6.1";
+ src = super.fetchPypi {
+ inherit pname version;
+ hash = "sha256-eOa75HNXZAxFPQvpKfpJzQXM4uEobz0qHKnL2n79uLc=";
+ };
+ format = "pyproject";
+ postPatch = "";
+ propagatedNativeBuildInputs = with super.python3Packages; [cffi flit-core];
+ });
+ pywlroots = pprev.pywlroots.overridePythonAttrs (_: rec {
+ version = "0.16.4";
+ src = super.fetchPypi {
+ inherit version;
+ pname = "pywlroots";
+ hash = "sha256-+1PILk14XoA/dINfoOQeeMSGBrfYX3pLA6eNdwtJkZE=";
+ };
+ buildInputs = with super; [libinput libxkbcommon pixman xorg.libxcb xorg.xcbutilwm udev wayland wlroots_0_16];
+ });
+ xcffib = pprev.xcffib.overridePythonAttrs (_: rec {
+ pname = "xcffib";
+ version = "1.4.0";
+ src = super.fetchPypi {
+ inherit pname version;
+ hash = "sha256-uXfADf7TjHWuGIq8EkLvmDwCJSqGd6gMSwKVMz9StiQ=";
+ };
+ patches = [];
+ });
+ qtile = pprev.qtile.overridePythonAttrs (_: {
+ version = ''2023.08.23''; # qtile
+ src = super.fetchFromGitHub {
+ owner = "qtile";
+ repo = "qtile";
+ rev = ''9b2aff3b3d4607f3e782afda2ec2a061d7eba9f1''; # qtile
+ hash = ''sha256-20MO9eo2itF4zGLe9efEtE6c5UtAyQWKJBgwOSWBqAM=''; # qtile
+ };
+ prePatch = ''
+ substituteInPlace libqtile/backend/wayland/cffi/build.py \
+ --replace /usr/include/pixman-1 ${super.pixman.outPath}/include \
+ --replace /usr/include/libdrm ${super.libdrm.dev.outPath}/include/libdrm
+ '';
+ buildInputs = with super;
+ [
+ libinput
+ wayland
+ libxkbcommon
+ xorg.xcbutilwm
+ wlroots_0_16
+ ];
+ });
+ qtile-extras = pprev.qtile-extras.overridePythonAttrs (old: {
+ version = ''2023.08.14''; # extras
+ src = super.fetchFromGitHub {
+ owner = "elParaguayo";
+ repo = "qtile-extras";
+ rev = ''ed01fd8b94997b2a87eecb9bf48e424be91baf76''; # extras
+ hash = ''sha256-pIfaFIzM+skT/vZir7+fWWNvYcVnUnfXT3mzctqYvUs=''; # extras
+ };
+ checkInputs = (old.checkInputs or []) ++ [super.python3Packages.pytest-lazy-fixture];
+ pytestFlagsArray = ["--disable-pytest-warnings"];
+ disabledTests =
+ (old.disabledTests or [])
+ ++ [
+ "1-x11-currentlayout_manager1-55"
+ "test_githubnotifications_colours"
+ "test_githubnotifications_logging"
+ "test_githubnotifications_icon"
+ "test_githubnotifications_reload_token"
+ "test_image_size_horizontal"
+ "test_image_size_vertical"
+ "test_image_size_mask"
+ "test_widget_init_config"
+ "test_mpris2_popup"
+ "test_snapcast_icon"
+ "test_snapcast_icon_colour"
+ "test_snapcast_http_error"
+ "test_syncthing_not_syncing"
+ "test_syncthing_is_syncing"
+ "test_syncthing_http_error"
+ "test_syncthing_no_api_key"
+ "test_visualiser"
+ "test_no_icons"
+ "test_currentlayouticon_missing_icon"
+ "test_no_filename"
+ "test_no_image"
+ ];
+ });
+ })
+ ];
+ python3 = let
+ self = super.python3.override {
+ inherit self;
+ packageOverrides = super.lib.composeManyExtensions final.pythonPackagesOverlays;
+ };
+ in
+ self;
+ python3Packages = final.python3.pkgs;
+ })
+ ];
+ };
+}
M modules/editors/nvim/home.nix => modules/editors/nvim/home.nix +2 -0
@@ 30,6 30,8 @@
nvim-surround
vim-easymotion
vim-sneak
+
+ vim-commentary
];
extraConfig = ''
M modules/services/flameshot.nix => modules/services/flameshot.nix +10 -12
@@ 5,18 5,16 @@
{ config, lib, pkgs, user, ... }:
{
- config = lib.mkIf (config.xsession.enable) { # Only evaluate code if using X11
- services = { # sxhkd shortcut = Printscreen button (Print)
- flameshot = {
- enable = true;
- settings = {
- General = { # Settings
- savePath = "/home/${user}/screens";
- saveAsFileExtension = ".png";
- uiColor = "#2d0096";
- showHelp = "false";
- disabledTrayIcon = "true"; # Hide from systray
- };
+ services = { # sxhkd shortcut = Printscreen button (Print)
+ flameshot = {
+ enable = true;
+ settings = {
+ General = { # Settings
+ savePath = "/home/${user}/screens";
+ saveAsFileExtension = ".png";
+ uiColor = "#2d0096";
+ showHelp = "false";
+ disabledTrayIcon = "true"; # Hide from systray
};
};
};
M modules/services/home.nix => modules/services/home.nix +0 -2
@@ 17,5 17,3 @@
./udiskie.nix
./redshift.nix
]
-
-# redshift and media temporarely disables
M modules/services/picom.nix => modules/services/picom.nix +64 -66
@@ 5,78 5,76 @@
{ config, lib, pkgs, ... }:
{
- config = lib.mkIf (config.xsession.enable) { # Only evaluate code if using X11
- services.picom = {
- enable = true;
- package = pkgs.picom.overrideAttrs(o: {
- src = pkgs.fetchFromGitHub {
- #repo = "picom";
- #owner = "pijulius";
- #rev = "982bb43e5d4116f1a37a0bde01c9bda0b88705b9";
- #sha256 = "YiuLScDV9UfgI1MiYRtjgRkJ0VuA1TExATA2nJSJMhM=";
- repo = "picom";
- owner = "jonaburg";
- rev = "e3c19cd7d1108d114552267f302548c113278d45";
- sha256 = "4voCAYd0fzJHQjJo4x3RoWz5l3JJbRvgIXn1Kg6nz6Y=";
- };
- }); # Override picom to use pijulius' version
+ services.picom = {
+ enable = true;
+ package = pkgs.picom.overrideAttrs(o: {
+ src = pkgs.fetchFromGitHub {
+ #repo = "picom";
+ #owner = "pijulius";
+ #rev = "982bb43e5d4116f1a37a0bde01c9bda0b88705b9";
+ #sha256 = "YiuLScDV9UfgI1MiYRtjgRkJ0VuA1TExATA2nJSJMhM=";
+ repo = "picom";
+ owner = "jonaburg";
+ rev = "e3c19cd7d1108d114552267f302548c113278d45";
+ sha256 = "4voCAYd0fzJHQjJo4x3RoWz5l3JJbRvgIXn1Kg6nz6Y=";
+ };
+ }); # Override picom to use pijulius' version
- backend = "glx"; # Rendering either with glx or xrender. You'll know if you need to switch this.
- vSync = true; # Should fix screen tearing
+ backend = "glx"; # Rendering either with glx or xrender. You'll know if you need to switch this.
+ vSync = true; # Should fix screen tearing
- #activeOpacity = 0.93; # Node transparency
- #inactiveOpacity = 0.93;
- #menuOpacity = 0.93;
+ #activeOpacity = 0.93; # Node transparency
+ #inactiveOpacity = 0.93;
+ #menuOpacity = 0.93;
- shadow = false; # Shadows
- shadowOpacity = 0.75;
- fade = true; # Fade
- fadeDelta = 10;
- opacityRules = [ # Opacity rules if transparency is prefered
- # "100:name = 'Picture in picture'"
- # "100:name = 'Picture-in-Picture'"
- # "85:class_i ?= 'rofi'"
- "80:class_i *= 'discord'"
- "80:class_i *= 'emacs'"
- "80:class_i *= 'Alacritty'"
- # "100:fullscreen"
- ]; # Find with $ xprop | grep "WM_CLASS"
+ shadow = false; # Shadows
+ shadowOpacity = 0.75;
+ fade = true; # Fade
+ fadeDelta = 10;
+ opacityRules = [ # Opacity rules if transparency is prefered
+ # "100:name = 'Picture in picture'"
+ # "100:name = 'Picture-in-Picture'"
+ # "85:class_i ?= 'rofi'"
+ "80:class_i *= 'discord'"
+ "80:class_i *= 'emacs'"
+ "80:class_i *= 'Alacritty'"
+ # "100:fullscreen"
+ ]; # Find with $ xprop | grep "WM_CLASS"
- settings = {
- daemon = true;
- use-damage = false; # Fixes flickering and visual bugs with borders
- resize-damage = 1;
- refresh-rate = 0;
- corner-radius = 5; # Corners
- round-borders = 5;
+ settings = {
+ daemon = true;
+ use-damage = false; # Fixes flickering and visual bugs with borders
+ resize-damage = 1;
+ refresh-rate = 0;
+ corner-radius = 5; # Corners
+ round-borders = 5;
- # Animations Pijulius
- #animations = true; # All Animations
- #animation-window-mass = 0.5;
- #animation-for-open-window = "zoom";
- #animation-stiffness = 350;
- #animation-clamping = false;
- #fade-out-step = 1; # Will fix random border dots from not disappearing
+ # Animations Pijulius
+ #animations = true; # All Animations
+ #animation-window-mass = 0.5;
+ #animation-for-open-window = "zoom";
+ #animation-stiffness = 350;
+ #animation-clamping = false;
+ #fade-out-step = 1; # Will fix random border dots from not disappearing
- # Animations Jonaburg
- transition-length = 300;
- transition-pow-x = 0.5;
- transition-pow-y = 0.5;
- transition-pow-w = 0.5;
- transition-pow-h = 0.5;
- size-transition = true;
+ # Animations Jonaburg
+ transition-length = 300;
+ transition-pow-x = 0.5;
+ transition-pow-y = 0.5;
+ transition-pow-w = 0.5;
+ transition-pow-h = 0.5;
+ size-transition = true;
- # Extras
- detect-rounded-corners = true; # Below should fix multiple issues
- detect-client-opacity = false;
- detect-transient = true;
- detect-client-leader = false;
- mark-wmwim-focused = true;
- mark-ovredir-focues = true;
- unredir-if-possible = true;
- glx-no-stencil = true;
- glx-no-rebind-pixmap = true;
- }; # Extra options for picom.conf (mostly for pijulius fork)
- };
+ # Extras
+ detect-rounded-corners = true; # Below should fix multiple issues
+ detect-client-opacity = false;
+ detect-transient = true;
+ detect-client-leader = false;
+ mark-wmwim-focused = true;
+ mark-ovredir-focues = true;
+ unredir-if-possible = true;
+ glx-no-stencil = true;
+ glx-no-rebind-pixmap = true;
+ }; # Extra options for picom.conf (mostly for pijulius fork)
};
}
M modules/services/redshift.nix => modules/services/redshift.nix +7 -9
@@ 4,14 4,12 @@
{ config, lib, pkgs, ...}:
{
- config = lib.mkIf (config.xsession.enable) { # Only evaluate code if using X11
- services = {
- redshift = {
- enable = true;
- temperature.night = 3000;
- latitude = 50.2332933;
- longitude = 14.3225926;
- };
+ services = {
+ redshift = {
+ enable = true;
+ temperature.night = 3000;
+ latitude = 50.2332933;
+ longitude = 14.3225926;
};
- };
+ };
}
A => +5 -0
@@ 0,0 1,5 @@
#!/usr/bin/env bash
dunstctl close-all
notify-send.py a --hint boolean:deadd-notification-center:true \
string:type:clearPopups
A scripts/notifications/pause.sh => scripts/notifications/pause.sh +7 -0
@@ 0,0 1,7 @@
+#!/usr/bin/env bash
+
+notify-send "Pausing notifications"
+notify-send.py a --hint boolean:deadd-notification-center:true \
+ string:type:pausePopups
+sleep 3
+dunstctl set-paused true
A scripts/notifications/show-center.sh => scripts/notifications/show-center.sh +6 -0
@@ 0,0 1,6 @@
+#!/usr/bin/env bash
+
+dunstctl history-pop
+dunstctl history-pop
+dunstctl history-pop
+dunstctl history-pop
A scripts/notifications/unpause.sh => scripts/notifications/unpause.sh +6 -0
@@ 0,0 1,6 @@
+#!/usr/bin/env bash
+
+dunstctl set-paused false
+notify-send.py a --hint boolean:deadd-notification-center:true \
+ string:type:unpausePopups
+notify-send "Unpaused notifications"