├── .gitignore ├── screenshots ├── notification.gif └── 20200907-5D479D.png ├── setup.cfg ├── readme.rst ├── startup.sh ├── x11.py ├── power_menu.py ├── scratchpad.py ├── tags.py ├── groups.py ├── mindfulness.py ├── wayland.py └── config.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /screenshots/notification.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-col/qtile-config/HEAD/screenshots/notification.gif -------------------------------------------------------------------------------- /screenshots/20200907-5D479D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-col/qtile-config/HEAD/screenshots/20200907-5D479D.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 110 3 | ignore = 4 | E265, # Block comments should start with "# " 5 | E241 # Multiple spaces after "," 6 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | Qtile 2 | ===== 3 | 4 | This is my Qtile setup. 5 | 6 | Screenshot @ 8fe9b96 7 | 8 | .. image:: /screenshots/20200907-5D479D.png 9 | :alt: Screenshot @ 8fe9b96 10 | 11 | Graphical notifications @ 7647bc8 12 | 13 | .. image:: /screenshots/notification.gif 14 | :alt: Graphical notifications @ 7647bc8 15 | -------------------------------------------------------------------------------- /startup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Qtile Wayland startup script 4 | 5 | foot --server & 6 | 7 | # Wallpaper 8 | ( 9 | swww init 10 | sleep 1 11 | swww img --filter Nearest --transition-step=1 --transition-fps 60 \ 12 | --transition-duration 12 ~/pictures/Wallpapers/Leuh6wm-slow.gif 13 | ) & 14 | 15 | run_if_new() { ps aux | grep -v grep | grep -q $1 || $@; } 16 | 17 | [[ -z "$QTILE_XEPHYR" ]] && { 18 | # Session setup 19 | systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP & 20 | dbus-update-activation-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP & 21 | 22 | # Services 23 | kanshi & 24 | wlsunset & 25 | swayidle & 26 | swaync & 27 | mpDris2 & 28 | sway-mpris-idle-inhibit & 29 | playerctld daemon & 30 | nm-applet --indicator & 31 | #kdeconnect-indicator & 32 | darkman run & 33 | 34 | # Startup programs 35 | run_if_new keepassxc & 36 | sleep 2 37 | run_if_new firefox & 38 | run_if_new evolution & 39 | 40 | # Notify me if any systemd services failed 41 | check_systemd & 42 | 43 | # Send all output to Qtile's log 44 | } &>> ~/.local/share/qtile/qtile.log 45 | 46 | 47 | # Clean up 48 | clean() { 49 | pkill -P $$ 50 | } 51 | trap clean SIGINT SIGTERM 52 | sleep infinity & 53 | wait $! 54 | -------------------------------------------------------------------------------- /x11.py: -------------------------------------------------------------------------------- 1 | """ 2 | X11-specific configuration 3 | """ 4 | 5 | import os 6 | 7 | from libqtile import hook, qtile 8 | from libqtile.lazy import lazy 9 | 10 | IS_XEPHYR = int(os.environ.get("QTILE_XEPHYR", 0)) 11 | mod = "mod1" if IS_XEPHYR else "mod4" 12 | 13 | 14 | # term = "xterm" if IS_XEPHYR else "urxvt" # urxvt doesn't like xephyr 15 | term = "xterm" 16 | wmname = "LG3D" 17 | keys_backend = [] 18 | 19 | 20 | # Keys to run X11-only launchers 21 | keys_backend.extend( 22 | [ 23 | # ([mod], 'd', 24 | # lazy.spawn('rofi -show run -theme ~/.config/rofi/common-large.rasi'), "rofi: run"), 25 | #([], "XF86PowerOff", lazy.spawn("power-menu"), "Power menu"), 26 | ([mod, "shift"], "x", lazy.spawn("set_monitors"), "Configure monitors"), 27 | # ( 28 | # [mod, "shift"], 29 | # "i", 30 | # lazy.spawn("slock systemctl suspend -i"), 31 | # "Suspend system and lock", 32 | # ), 33 | ] 34 | ) 35 | 36 | 37 | # Auto-float some windows 38 | @hook.subscribe.client_new 39 | def _(window): 40 | if window.window.get_wm_type() == "desktop": 41 | window.static(qtile.current_screen.index) 42 | return 43 | 44 | hints = window.window.get_wm_normal_hints() 45 | if hints and 0 < hints["max_width"] < 1920: 46 | window.floating = True 47 | -------------------------------------------------------------------------------- /power_menu.py: -------------------------------------------------------------------------------- 1 | from libqtile.lazy import lazy 2 | from qtile_extras.popup.toolkit import PopupImage, PopupRelativeLayout, PopupText 3 | 4 | 5 | def show_power_menu(qtile): 6 | controls = [ 7 | PopupImage( 8 | filename="~/pictures/icons/lock.svg", 9 | pos_x=0.15, 10 | pos_y=0.1, 11 | width=0.1, 12 | height=0.5, 13 | mouse_callbacks={"Button1": lazy.spawn("swaylock")}, 14 | ), 15 | PopupImage( 16 | filename="~/pictures/icons/sleep.svg", 17 | pos_x=0.45, 18 | pos_y=0.1, 19 | width=0.1, 20 | height=0.5, 21 | mouse_callbacks={"Button1": lazy.spawn("systemctl suspend", shell=True)}, 22 | ), 23 | PopupImage( 24 | filename="~/pictures/icons/shutdown.svg", 25 | pos_x=0.75, 26 | pos_y=0.1, 27 | width=0.1, 28 | height=0.5, 29 | highlight="A00000", 30 | mouse_callbacks={"Button1": lazy.spawn("backup-home-and-poweroff")}, 31 | ), 32 | PopupText( 33 | text="Lock", pos_x=0.1, pos_y=0.7, width=0.2, height=0.2, h_align="center" 34 | ), 35 | PopupText( 36 | text="Sleep", pos_x=0.4, pos_y=0.7, width=0.2, height=0.2, h_align="center" 37 | ), 38 | PopupText( 39 | text="Shutdown", 40 | pos_x=0.7, 41 | pos_y=0.7, 42 | width=0.2, 43 | height=0.2, 44 | h_align="center", 45 | ), 46 | ] 47 | PopupRelativeLayout( 48 | qtile, 49 | width=1000, 50 | height=200, 51 | controls=controls, 52 | background="000000c0", 53 | initial_focus=2, 54 | ).show(centered=True) 55 | 56 | 57 | keys_power_menu = [ 58 | ([], "XF86PowerOff", lazy.function(show_power_menu), "Display the power menu.") 59 | ] 60 | -------------------------------------------------------------------------------- /scratchpad.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scratchpad and DropDowns 3 | ======================== 4 | 5 | The scratchpads are the same with either backend, but the terminal used differs. I 6 | prefer these terminals over something like alacritty, which would be an easier 7 | alternative to configure because it works under both Wayland and X. 8 | """ 9 | 10 | import os 11 | 12 | from libqtile import qtile 13 | from libqtile.config import DropDown, ScratchPad 14 | from libqtile.lazy import lazy 15 | 16 | HOME: str = os.path.expanduser("~") 17 | IS_WAYLAND: bool = qtile.core.name == "wayland" 18 | IS_XEPHYR: bool = int(os.environ.get("QTILE_XEPHYR", 0)) > 0 19 | mod = "mod1" if IS_XEPHYR else "mod4" 20 | 21 | 22 | if IS_WAYLAND: 23 | term = "foot " 24 | else: 25 | term = "xterm -e " 26 | 27 | 28 | conf = { 29 | "warp_pointer": False, 30 | "on_focus_lost_hide": False, 31 | "opacity": 1, 32 | } 33 | 34 | GHCI = "ghci" 35 | # GHCI = "ghci-9.2.2" 36 | 37 | dropdowns = [ 38 | DropDown("tmux", term + "tmux", height=0.4, **conf), 39 | DropDown( 40 | "ncmpcpp", term + "ncmpcpp", x=0.12, y=0.2, width=0.56, height=0.7, **conf 41 | ), 42 | DropDown("python", term + "python", x=0.05, y=0.1, width=0.2, height=0.3, **conf), 43 | DropDown(GHCI, term + GHCI, y=0.6, height=0.4, **conf), 44 | ] 45 | 46 | 47 | # Keybindings to open each DropDown 48 | keys_scratchpad = [ 49 | ( 50 | [mod, "shift"], 51 | "Return", 52 | lazy.group["scratchpad"].dropdown_toggle("tmux"), 53 | "Toggle tmux scratchpad", 54 | ), 55 | ( 56 | [mod, "control"], 57 | "m", 58 | lazy.group["scratchpad"].dropdown_toggle("ncmpcpp"), 59 | "Toggle ncmpcpp scratchpad", 60 | ), 61 | ( 62 | [mod], 63 | "c", 64 | lazy.group["scratchpad"].dropdown_toggle("python"), 65 | "Toggle python scratchpad", 66 | ), 67 | ( 68 | [mod], 69 | "g", 70 | lazy.group["scratchpad"].dropdown_toggle(GHCI), 71 | "Toggle GHCI scratchpad", 72 | ), 73 | ] 74 | 75 | scratchpad = ScratchPad("scratchpad", dropdowns) 76 | -------------------------------------------------------------------------------- /tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tags 3 | ==== 4 | 5 | This workflow replaces groups. There is only one group defined per (possible) screen. No 6 | keybindings are defined to switch between groups or to move windows between groups. If 7 | only one monitor is connected then only the first group is ever used, etc. 8 | 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import os 14 | from collections import defaultdict 15 | from typing import TYPE_CHECKING 16 | 17 | from libqtile import hook, qtile 18 | from libqtile.backend.base import Window 19 | from libqtile.config import Group, Match 20 | from libqtile.lazy import lazy 21 | from libqtile.log_utils import logger 22 | 23 | if TYPE_CHECKING: 24 | from typing import List, Tuple 25 | 26 | 27 | groups: List[Group] = [Group("")] 28 | 29 | tags: List[Tuple[str, Match, List[Window]]] = [ 30 | ("terms", Match(wm_class="foot"), []), 31 | ("firefox", Match(wm_class="firefox"), []), 32 | ("thunar", Match(wm_class="thunar"), []), 33 | ] 34 | 35 | 36 | @hook.subscribe.client_new 37 | def _(window): 38 | """ 39 | This adds windows to any tags that match it. 40 | """ 41 | if isinstance(window, Window): # Static windows ignored 42 | for name, match, windows in tags: 43 | if match.compare(window): 44 | windows.append(window) 45 | tag_hidden = windows[0].minimized 46 | window.minimized = tag_hidden 47 | qtile.current_screen.group.add( 48 | window, focus=window.can_steal_focus and tag_hidden 49 | ) 50 | 51 | 52 | def _toggle_tag(_qtile, to_toggle: str): 53 | """ 54 | This is bound to keys to show/hide all windows of a given tag. It toggles their 55 | minimized state. 56 | """ 57 | for name, match, windows in tags: 58 | if name == to_toggle: 59 | for window in windows: 60 | window.toggle_minimize() 61 | return 62 | 63 | 64 | mod = "mod1" if int(os.environ.get("QTILE_XEPHYR", 0)) else "mod4" 65 | 66 | keys_group: Tuple[List[str], str, Any, str] = [] 67 | 68 | for i, (name, _, _) in enumerate(tags): 69 | keys_group.extend( 70 | [ 71 | ([mod], str(i + 1), lazy.function(_toggle_tag, name), f"Toggle tag {name}"), 72 | ] 73 | ) 74 | -------------------------------------------------------------------------------- /groups.py: -------------------------------------------------------------------------------- 1 | """ 2 | Groups 3 | ====== 4 | 5 | - 8 groups 6 | - 4 bound to each screen when using two monitors 7 | 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import os 13 | from typing import TYPE_CHECKING 14 | 15 | from libqtile import hook, qtile 16 | from libqtile.config import Group, Match 17 | from libqtile.lazy import lazy 18 | 19 | if TYPE_CHECKING: 20 | from typing import Any, Callable 21 | 22 | from libqtile.core.manager import Qtile 23 | 24 | 25 | mod = "mod1" if int(os.environ.get("QTILE_XEPHYR", 0)) else "mod4" 26 | 27 | keys_group: list[tuple[list[str], str, Any, str]] = [] 28 | 29 | 30 | groups: list[Group] = [ 31 | Group("1", label="html"), 32 | Group("2", label="disc", matches=[Match(wm_class="discord")]), 33 | Group("3", label="mail", matches=[Match(wm_class="evolution")]), 34 | Group("4", label="tune", matches=[ 35 | Match(wm_class="dev.alextren.Spot"), Match(title="Spotify"), 36 | ]), 37 | Group("q", label="term"), 38 | Group("w", label="term"), 39 | Group("e", label="term"), 40 | Group("r", label="term"), 41 | ] 42 | 43 | 44 | def _go_to_group(qtile: Qtile, name: str) -> None: 45 | """ 46 | This creates lazy functions that jump to a given group. When there is more than one 47 | screen, the first 4 and second 4 groups are kept on the first and second screen. 48 | E.g. going to the fifth group when the first group (and first screen) is focussed 49 | will also change the screen to the second screen. 50 | """ 51 | if len(qtile.screens) == 1: 52 | qtile.groups_map[name].toscreen(toggle=True) 53 | return 54 | 55 | old = qtile.current_screen.group.name 56 | if name in "1234": 57 | qtile.focus_screen(0) 58 | if old in "1234" or qtile.current_screen.group.name != name: 59 | qtile.groups_map[name].toscreen(toggle=True) 60 | else: 61 | qtile.focus_screen(1) 62 | if old in "qwer" or qtile.current_screen.group.name != name: 63 | qtile.groups_map[name].toscreen(toggle=True) 64 | 65 | 66 | for i in groups: 67 | keys_group.extend( 68 | [ 69 | ( 70 | [mod], 71 | i.name, 72 | lazy.function(_go_to_group, i.name), 73 | f"Go to group {i.name}", 74 | ), 75 | ( 76 | [mod, "shift"], 77 | i.name, 78 | lazy.window.togroup(i.name), 79 | f"Send window to group {i.name}", 80 | ), 81 | ] 82 | ) 83 | 84 | 85 | def _scroll_screen(qtile: Qtile, direction: int) -> None: 86 | """ 87 | Scroll to the next/prev group of the subset allocated to a specific screen. This 88 | will rotate between e.g. 1->2->3->4->1 when the first screen is focussed. 89 | """ 90 | if len(qtile.screens) == 1: 91 | current = qtile.groups.index(qtile.current_group) 92 | destination = (current + direction) % 8 93 | qtile.groups[destination].toscreen() 94 | return 95 | 96 | current = qtile.groups.index(qtile.current_group) 97 | if current < 4: 98 | destination = (current + direction) % 4 99 | else: 100 | destination = ((current - 4 + direction) % 4) + 4 101 | qtile.groups[destination].toscreen() 102 | 103 | 104 | keys_group.extend( 105 | [ 106 | ([mod], "m", lazy.function(_scroll_screen, 1), "Screen groups forward"), 107 | ([mod], "n", lazy.function(_scroll_screen, -1), "Screen groups backward"), 108 | ] 109 | ) 110 | 111 | 112 | @hook.subscribe.startup 113 | def _(): 114 | # Set initial groups 115 | if len(qtile.screens) > 1: 116 | qtile.groups_map["1"].toscreen(0, toggle=False) 117 | qtile.groups_map["q"].toscreen(1, toggle=False) 118 | qtile.focus_screen(1) 119 | 120 | 121 | @hook.subscribe.screens_reconfigured 122 | def _(): 123 | # Set groups to screens 124 | if len(qtile.screens) > 1: 125 | if qtile.screens[0].group.name not in "1234": 126 | qtile.groups_map["1"].toscreen(0, toggle=False) 127 | if qtile.screens[1].group.name in "1234": 128 | qtile.groups_map["q"].toscreen(1, toggle=False) 129 | -------------------------------------------------------------------------------- /mindfulness.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Matt Colligan 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import os 22 | from random import randint 23 | from urllib import request 24 | 25 | import gi 26 | 27 | gi.require_version("Gst", "1.0") 28 | from gi.repository import Gst 29 | from libqtile.log_utils import logger 30 | from libqtile.utils import get_cache_dir 31 | from libqtile.widget import base 32 | 33 | Gst.init(None) 34 | 35 | 36 | class ReMindfulness(base.ThreadPoolText): 37 | """ 38 | Mindfulness reminder widget (Proof of concept / work in progress) 39 | 40 | Inspired by Kyle MacLeod's mindfulnotifier Android app 41 | (https://github.com/kmac/mindfulnotifier). The widget will periodically play a 42 | calming tone at semi-random intervals. 43 | 44 | Note, if the specified audio_file does not exist (i.e. the first time the widget is 45 | configured, leaving the default value for ``audio_file``), one will be downloaded. 46 | It is fetched via the mindfulnotifier app's github repository. 47 | """ 48 | 49 | default_path = os.path.join(get_cache_dir(), "tibetan_bell_ding_b.mp3") 50 | default_url = "https://github.com/kmac/mindfulnotifier/raw/master/media/tibetan_bell_ding_b.mp3" 51 | 52 | defaults = [ 53 | ("audio_file", default_path, "Lower bound for the interval between reminders"), 54 | ("interval_minimum", 60 * 60, "Lower bound for the interval between reminders"), 55 | ("interval_maximum", 90 * 60, "Upper bound for the interval between reminders"), 56 | ("reminder_background", "ff0000", "Background colour when reminding"), 57 | ("reminder_foreground", "ff0000", "Foreground colour when reminding"), 58 | ("reminder_duration", 5, "Duration of reminders (i.e. for appearance change)"), 59 | ("reminder_text", None, "Text"), 60 | ] 61 | 62 | def __init__(self, text="", **config): 63 | base.ThreadPoolText.__init__(self, text, **config) 64 | self.add_defaults(ReMindfulness.defaults) 65 | self._just_started = True 66 | self._original_foreground = self.foreground 67 | self._original_background = self.background 68 | 69 | self.add_callbacks( 70 | { 71 | "Button1": self._reset_colours, 72 | } 73 | ) 74 | 75 | def _configure(self, qtile, bar): 76 | self._just_started = not self.configured 77 | base.ThreadPoolText._configure(self, qtile, bar) 78 | 79 | def poll(self): 80 | self.update_interval = randint(self.interval_minimum, self.interval_maximum) 81 | 82 | if self._just_started: 83 | return self.text 84 | 85 | if not os.path.exists(self.audio_file): 86 | logger.info("Downloading default tone for ReMindfulness widget") 87 | request.urlretrieve(self.default_url, self.audio_file) 88 | 89 | playbin = Gst.ElementFactory.make("playbin", "playbin") 90 | playbin.props.uri = "file://" + self.audio_file 91 | playbin.set_state(Gst.State.PLAYING) 92 | 93 | self.foreground = self.reminder_foreground 94 | self.background = self.reminder_background 95 | self.timeout_add(self.reminder_duration, self._reset_colours) 96 | 97 | return self.text 98 | 99 | def _reset_colours(self): 100 | self.foreground = self._original_foreground 101 | self.background = self._original_background 102 | -------------------------------------------------------------------------------- /wayland.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wayland-specific configuration 3 | """ 4 | 5 | import asyncio 6 | import os 7 | import subprocess 8 | 9 | from libqtile import hook, qtile 10 | from libqtile.backend.wayland import InputConfig 11 | from libqtile.backend.wayland.xdgwindow import XdgWindow 12 | from libqtile.backend.wayland.xwindow import XWindow 13 | from libqtile.backend.wayland.layer import LayerStatic 14 | from libqtile.lazy import lazy 15 | 16 | IS_XEPHYR = int(os.environ.get("QTILE_XEPHYR", 0)) 17 | mod = "mod1" if IS_XEPHYR else "mod4" 18 | 19 | term = "footclient" 20 | keys_backend = [] 21 | 22 | 23 | # Keys to change VT 24 | keys_backend.extend( 25 | [ 26 | ([mod], "F1", lazy.core.change_vt(1), "Change to VT 1"), 27 | ([mod], "F2", lazy.core.change_vt(2), "Change to VT 2"), 28 | ([mod], "F3", lazy.core.change_vt(3), "Change to VT 3"), 29 | ([mod], "F4", lazy.core.change_vt(4), "Change to VT 4"), 30 | ] 31 | ) 32 | 33 | # Inputs configuration 34 | wl_input_rules = { 35 | "type:keyboard": InputConfig( 36 | kb_options="caps:swapescape,altwin:swap_alt_win", 37 | kb_layout="gb", 38 | kb_repeat_rate=50, 39 | kb_repeat_delay=250, 40 | ), 41 | # All touchpads 42 | "type:touchpad": InputConfig(drag=True, tap=True, dwt=False, pointer_accel=0.3), 43 | # Roccat Kiro mouse 44 | "7805:11320:ROCCAT ROCCAT Kiro Mouse": InputConfig( 45 | #left_handed=True, 46 | pointer_accel=-1.0, 47 | ), 48 | # Anker mouse 49 | "7119:5:USB Optical Mouse": InputConfig(pointer_accel=-1.0), 50 | } 51 | 52 | 53 | # Shadows configuration 54 | # wl_shadows = { 55 | # "radius": 12, 56 | # "color": "#05001088", 57 | # "offset": (-6, -6), 58 | # } 59 | wl_shadows = { 60 | "radius": 5, 61 | "color": "#ff000044", 62 | "offset": (0, 0), 63 | } 64 | 65 | 66 | @hook.subscribe.client_new 67 | def _(win): 68 | # Auto-float some windows 69 | if isinstance(win, XdgWindow): 70 | max_width = win.surface.toplevel._ptr.current.max_width 71 | if 0 < max_width < 1920: 72 | win.floating = True 73 | elif isinstance(win, XWindow): 74 | if hints := win.surface.size_hints: 75 | max_width = hints.max_width 76 | if 0 < max_width < 1920: 77 | win.floating = True 78 | 79 | 80 | @hook.subscribe.client_managed 81 | async def _(win): 82 | # Some other miscellaneous rules 83 | if win.name == "Firefox — Sharing Indicator": 84 | win.place(win.x + win.borderwidth, 0, win.width, win.height, 0, None) 85 | return 86 | 87 | wm_class = win.get_wm_class() or [] 88 | if win.name == "Navigator" and "libreoffice-startcenter" in wm_class: 89 | x = qtile.current_screen.x 90 | win.place(x, 240, 450, 600, win.borderwidth, win.bordercolor) 91 | return 92 | 93 | if "mpv" in wm_class: 94 | # Resizing mpv if it's sized itself too big to fit on screen 95 | sw = qtile.current_screen.width 96 | sh = qtile.current_screen.height 97 | x = y = w = h = None 98 | if win.height > sh: 99 | h = sh 100 | y = qtile.current_screen.y 101 | if win.width > sw: 102 | w = sw 103 | x = qtile.current_screen.x 104 | if w is not None or h is not None: 105 | if h is None: 106 | h = win.height 107 | y = win.y 108 | else: 109 | w = win.width 110 | x = win.x 111 | bw = win.borderwidth 112 | win.place(x - bw, y - bw, w + 2 * bw, h + 2 * bw, 0, None) 113 | return 114 | 115 | 116 | @hook.subscribe.startup_once 117 | async def _(): 118 | # Run a startup script 119 | HOME = os.path.expanduser("~") 120 | p = subprocess.Popen(f"{HOME}/.config/qtile/startup.sh") 121 | hook.subscribe.shutdown(p.terminate) 122 | 123 | 124 | ## I don't use this monitor, but keep this around for testing 125 | # 126 | #@hook.subscribe.screen_change 127 | #def _(*_): 128 | # # Temporary hacky fix for the dell monitor on my desk which for some mysterious 129 | # # reason doesn't advertise that it supports any HD mode. Qtile does support setting 130 | # # custom modes via the protocol, but neither kanshi nor wdisplays do. So instead, 131 | # # I'll detect if that monitor is present and set the desired custom mode here. 132 | # for output in qtile.core.outputs: 133 | # wlr_output = output.wlr_output 134 | # # Not only does this monitor not report modes correctly, it ALSO doesn't report 135 | # # a make or model. 136 | # if wlr_output.make == wlr_output.model == "": 137 | # if wlr_output.current_mode.width == 1024: 138 | # break 139 | # else: 140 | # return 141 | # 142 | # wlr_output.set_custom_mode(1920, 1080, 0) 143 | # # Lastly, while reconfigure_screens will be fired right after this hook, as it 144 | # # gets subscribed to this hook right after the config is loaded, the backend doesn't 145 | # # get a chance to actually apply the mode, so let's flush it. 146 | # qtile.core.flush() 147 | 148 | 149 | ## UNNECESSARY? The backend should update the output with reconfigre_screens=True 150 | # 151 | #if IS_XEPHYR: 152 | # # To adapt to whatever window size it was given 153 | # @hook.subscribe.startup_once 154 | # async def _(): 155 | # await asyncio.sleep(0.5) 156 | # qtile.reconfigure_screens() 157 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qtile main config file 3 | ====================== 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | import importlib 9 | import os 10 | import re 11 | import subprocess 12 | import sys 13 | from typing import TYPE_CHECKING 14 | 15 | from libqtile import bar, hook, layout, qtile, utils, widget 16 | from libqtile.backend import base 17 | from libqtile.config import Click, Drag, Key, Match, Screen 18 | from libqtile.lazy import lazy 19 | from libqtile.widget.backlight import ChangeDirection 20 | from libqtile.widget.battery import Battery, BatteryState 21 | 22 | import qtile_extras.widget as widget_extras 23 | 24 | assert qtile is not None 25 | 26 | if TYPE_CHECKING: 27 | from typing import Any 28 | 29 | from libqtile.core.manager import Qtile 30 | 31 | 32 | # Config imports 33 | def reload(module): 34 | if module in sys.modules: 35 | importlib.reload(sys.modules[module]) 36 | 37 | 38 | import traverse 39 | from toggle_debug import toggle_debug 40 | 41 | use_tags = False 42 | 43 | if use_tags: 44 | reload("tags") 45 | from tags import groups, keys_group 46 | else: 47 | reload("groups") 48 | from groups import groups, keys_group 49 | 50 | reload("scratchpad") 51 | from scratchpad import keys_scratchpad, scratchpad 52 | 53 | reload("power_menu") 54 | from power_menu import keys_power_menu 55 | 56 | IS_WAYLAND: bool = qtile.core.name == "wayland" 57 | IS_XEPHYR: bool = int(os.environ.get("QTILE_XEPHYR", 0)) > 0 58 | 59 | reload(qtile.core.name) 60 | if IS_WAYLAND: 61 | from wayland import keys_backend, term, wl_input_rules, wl_shadows # noqa: F401 62 | else: 63 | from x11 import keys_backend, term, wmname # noqa: F401 64 | 65 | # Colours 66 | theme = dict( 67 | foreground="#CFCCD6", 68 | background="#0C1F20", 69 | color0="#030405", 70 | color8="#1f1c32", 71 | color1="#8742a5", 72 | color9="#9a5eb3", 73 | color2="#406794", 74 | color10="#5fd75f", 75 | color3="#653c21", 76 | color11="#6e9fcd", 77 | color4="#8f4ff0", 78 | color12="#BBC2E2", 79 | color5="#5d479d", 80 | color13="#998dd1", 81 | color6="#3e3e73", 82 | color14="#9a9dcc", 83 | color7="#495068", 84 | color15="#e1e1e4", 85 | ) 86 | 87 | colours = [theme[f"color{n}"] for n in range(16)] 88 | background = theme["background"] 89 | foreground = theme["foreground"] 90 | colour_focussed = "#6fa3e0" 91 | colour_unfocussed = "#0e101c" 92 | inner_gaps = 8 93 | outer_gaps = 0 94 | 95 | # https://coolors.co/1b2021-cfccd6-bbc2e2-b7b5e4-847979 96 | 97 | 98 | # Keys 99 | 100 | if IS_XEPHYR: 101 | mod = "mod1" 102 | alt = "control" 103 | else: 104 | mod = "mod4" 105 | alt = "mod1" 106 | 107 | # My keybindings - they are converted at the bottom of this file 108 | my_keys: list[tuple[list[str], str, Any, str]] = [] 109 | my_keys.extend(keys_backend) 110 | my_keys.extend(keys_group) 111 | my_keys.extend(keys_scratchpad) 112 | my_keys.extend(keys_power_menu) 113 | 114 | 115 | def float_to_front(qtile: Qtile) -> None: 116 | """Bring all floating windows of the group to front""" 117 | for window in qtile.current_group.windows: 118 | if window.floating: 119 | window.bring_to_front() 120 | 121 | 122 | my_keys.extend( 123 | [ 124 | # Window management 125 | ([mod, "control"], "q", lazy.window.kill(), "Close window"), 126 | ([mod], "f", lazy.window.toggle_fullscreen(), "Toggle fullscreen"), 127 | ([mod, "shift"], "space", lazy.window.toggle_floating(), "Toggle floating"), 128 | ([mod], "s", lazy.spawn("ddg_rofi"), "Search web via rofi"), 129 | ([mod, "shift"], "s", lazy.window.static(), "Make window static"), 130 | ( 131 | [mod], 132 | "space", 133 | lazy.function(float_to_front), 134 | "Move floating windows to the front", 135 | ), 136 | ([mod], "j", lazy.function(traverse.down), "Traverse down"), 137 | ([mod], "k", lazy.function(traverse.up), "Traverse up"), 138 | ([mod], "h", lazy.function(traverse.left), "Traverse left"), 139 | ([mod], "l", lazy.function(traverse.right), "Traverse right"), 140 | ([mod, "shift"], "j", lazy.layout.shuffle_down(), "Shuffle down"), 141 | ([mod, "shift"], "k", lazy.layout.shuffle_up(), "Shuffle up"), 142 | ([mod, "shift"], "h", lazy.layout.shuffle_left(), "Shuffle left"), 143 | ([mod, "shift"], "l", lazy.layout.shuffle_right(), "Shuffle right"), 144 | ([mod, alt], "j", lazy.layout.grow_down(), "Grow down"), 145 | ([mod, alt], "k", lazy.layout.grow_up(), "Grow up"), 146 | ([mod, alt], "h", lazy.layout.grow_left(), "Grow left"), 147 | ([mod, alt], "l", lazy.layout.grow_right(), "Grow right"), 148 | ([mod, "control"], "r", lazy.reload_config(), "Reload the config"), 149 | ([mod, "control"], "Escape", lazy.shutdown(), "Shutdown Qtile"), 150 | ([mod], "Tab", lazy.next_layout(), "Next layout"), 151 | ([mod], "b", lazy.hide_show_bar("bottom"), "Hide bar"), 152 | ([mod, "control"], "d", lazy.function(toggle_debug), "Toggle debug logging"), 153 | # Volume control - MyVolume widget is defined further down 154 | ([], "XF86AudioMute", lazy.widget["myvolume"].mute(), "Mute audio"), 155 | ([mod], "F10", lazy.widget["myvolume"].mute(), "Mute audio"), 156 | ( 157 | [], 158 | "XF86AudioRaiseVolume", 159 | lazy.widget["myvolume"].increase_vol(), 160 | "Increase volume", 161 | ), 162 | ([mod], "F12", lazy.widget["myvolume"].increase_vol(), "Increase volume"), 163 | ( 164 | [], 165 | "XF86AudioLowerVolume", 166 | lazy.widget["myvolume"].decrease_vol(), 167 | "Decrease volume", 168 | ), 169 | ([mod], "F11", lazy.widget["myvolume"].decrease_vol(), "Decrease volume"), 170 | # Backlight control 171 | ( 172 | [], 173 | "XF86MonBrightnessUp", 174 | lazy.widget["backlight"].change_backlight(ChangeDirection.UP, 3), 175 | "Increase backlight", 176 | ), 177 | ( 178 | [], 179 | "XF86MonBrightnessDown", 180 | lazy.widget["backlight"].change_backlight(ChangeDirection.DOWN, 3), 181 | "Decrease backlight", 182 | ), 183 | ( 184 | [mod], 185 | "F6", 186 | lazy.widget["backlight"].change_backlight(ChangeDirection.UP, 3), 187 | "Increase backlight", 188 | ), 189 | ( 190 | [mod], 191 | "F5", 192 | lazy.widget["backlight"].change_backlight(ChangeDirection.DOWN, 3), 193 | "Decrease backlight", 194 | ), 195 | # Music control 196 | ( 197 | [mod], 198 | "bracketright", 199 | lazy.spawn("playerctl next"), 200 | "Next song", 201 | ), 202 | ([], "XF86AudioNext", lazy.spawn("playerctl next"), "Next song"), 203 | ( 204 | [mod], 205 | "bracketleft", 206 | lazy.spawn("playerctl previous"), 207 | "Previous song", 208 | ), 209 | ([], "XF86AudioPrev", lazy.spawn("playerctl previous"), "Previous song"), 210 | ([], "Pause", lazy.spawn("playerctl play-pause"), "Play/unpause music"), 211 | ([], "XF86AudioPlay", lazy.spawn("playerctl play-pause"), "Play/unpause music"), 212 | # Launchers 213 | ( 214 | [mod], 215 | "d", 216 | lazy.spawn("rofi -show run -theme ~/.config/rofi/common.rasi"), 217 | "Spawn with rofi", 218 | ), 219 | ([mod], "Return", lazy.spawn(term), "Spawn terminal"), 220 | ([mod, "shift"], "f", lazy.spawn("firefox"), "Spawn Firefox"), 221 | ( 222 | [mod, "control"], 223 | "f", 224 | lazy.spawn("cleanfox"), 225 | "Spawn Firefox in clean profile", 226 | ), 227 | ([], "Print", lazy.spawn("screenshot copy"), "Screenshot to clipboard"), 228 | (["shift"], "Print", lazy.spawn("screenshot"), "Screenshot to file"), 229 | ([mod], "p", lazy.spawn("get_password_rofi"), "Keepass passwords"), 230 | ([mod], "i", lazy.spawn("systemctl suspend -i"), "Suspend system"), 231 | ( 232 | [mod, "shift"], 233 | "i", 234 | lazy.spawn("gtklock & systemctl suspend -i", shell=True), 235 | "Suspend and lock", 236 | ), 237 | ( 238 | ["control"], 239 | "space", 240 | lazy.spawn("swaync-client -t -sw"), 241 | "Toggle notification panel", 242 | ), 243 | ( 244 | [mod, "control"], 245 | "y", 246 | lazy.spawn("yt-first"), 247 | "yt-first script", 248 | ), 249 | ] 250 | ) 251 | 252 | 253 | # Mouse control 254 | mouse = [ 255 | Click([mod], "Button2", lazy.window.kill()), 256 | 257 | # Move with drag or swipe 258 | Drag( 259 | [mod], 260 | "Button1", 261 | lazy.window.set_position_floating(), 262 | start=lazy.window.get_position(), 263 | ), 264 | #Swipe( 265 | # [mod], 266 | # 3, 267 | # lazy.window.set_position_floating(), 268 | # start=lazy.window.get_position(), 269 | #), 270 | 271 | # Resize with drag or swipe 272 | Drag( 273 | [mod, alt], 274 | "Button1", 275 | lazy.window.set_size_floating(), 276 | start=lazy.window.get_size(), 277 | ), 278 | #Swipe( 279 | # [mod, alt], 280 | # 3, 281 | # lazy.window.set_size_floating(), 282 | # start=lazy.window.get_size(), 283 | #), 284 | 285 | # Slide between groups 286 | Drag( 287 | [mod, "shift"], 288 | "Button1", 289 | lazy.screen.group_slide(), 290 | start=lazy.screen.start_group_slide(scale=2.5), 291 | ), 292 | ] 293 | 294 | if IS_WAYLAND: 295 | try: 296 | from libqtile.config import Swipe 297 | except ImportError: 298 | pass 299 | else: 300 | 301 | def _slide(qtile): 302 | groups = qtile.groups 303 | if len(qtile.screens) > 1: 304 | if qtile.current_screen.index == 0: 305 | groups = groups[:4] 306 | else: 307 | groups = groups[4:] 308 | return qtile.current_screen.start_group_slide( 309 | groups=groups, scale=1.6, inertia_threshold=15, 310 | ) 311 | 312 | mouse.append( 313 | Swipe( 314 | [], 315 | 3 if IS_XEPHYR else 4, 316 | lazy.screen.group_slide(), 317 | start=lazy.function(_slide), 318 | ), 319 | ) 320 | 321 | 322 | ## Firefox 3-finger vertical swipe to zoom 323 | #def swipe_zoom(qtile, _dx: int, dy: int) -> None: 324 | # if dy % 3: 325 | # # Only send key every 10 steps 326 | # return 327 | # if dy < 0: 328 | # qtile.spawn("wlrctl keyboard type +", shell=True) 329 | # else: 330 | # qtile.spawn("wlrctl keyboard type -", shell=True) 331 | # 332 | # 333 | #mouse.append( 334 | # Swipe( 335 | # [], 336 | # 3, 337 | # lazy.function(swipe_zoom).when(focused=Match(wm_class="firefox")), 338 | # ) 339 | #) 340 | 341 | # Layouts 342 | border_focus = [colours[5], "323974"] 343 | border_normal = "001122" 344 | 345 | 346 | class MyColumns(layout.Columns): 347 | """ 348 | I only override this method so I can make 'foot' have wide margins at each side if 349 | it's the only window open. 350 | """ 351 | 352 | def configure(self, client, screen_rect): 353 | pos = 0 354 | for col in self.columns: 355 | if client in col: 356 | break 357 | pos += col.width 358 | else: 359 | client.hide() 360 | return 361 | if client.has_focus: 362 | color = self.border_focus if col.split else self.border_focus_stack 363 | else: 364 | color = self.border_normal if col.split else self.border_normal_stack 365 | border = self.border_width 366 | margin_size = self.margin 367 | if len(self.columns) == 1 and (len(col) == 1 or not col.split): 368 | if not self.border_on_single: 369 | border = 0 370 | if "foot" in (client.get_wm_class() or []): 371 | margin_size = [0, 200, 0, 200] 372 | width = int(0.5 + col.width * screen_rect.width * 0.01 / len(self.columns)) 373 | x = screen_rect.x + int( 374 | 0.5 + pos * screen_rect.width * 0.01 / len(self.columns) 375 | ) 376 | if col.split: 377 | pos = 0 378 | for c in col: 379 | if client == c: 380 | break 381 | pos += col.heights[c] 382 | height = int( 383 | 0.5 + col.heights[client] * screen_rect.height * 0.01 / len(col) 384 | ) 385 | y = screen_rect.y + int(0.5 + pos * screen_rect.height * 0.01 / len(col)) 386 | client.place( 387 | x, 388 | y, 389 | width - 2 * border, 390 | height - 2 * border, 391 | border, 392 | color, 393 | margin=margin_size, 394 | ) 395 | client.unhide() 396 | elif client == col.cw: 397 | client.place( 398 | x, 399 | screen_rect.y, 400 | width - 2 * border, 401 | screen_rect.height - 2 * border, 402 | border, 403 | color, 404 | margin=margin_size, 405 | ) 406 | client.unhide() 407 | else: 408 | client.hide() 409 | 410 | 411 | # Weird custom behaviour here but isn't that what qtile is for? 412 | # I want a Columns layout but also sometimes i want 'foot' windows to be thinner and 413 | # centred in a 'central' column, so I define a whole new MyColumns layout that does that 414 | # and I just jump between the two when I want that. 415 | col_opts = dict( 416 | insert_position=1, 417 | border_width=5, 418 | border_focus=border_focus, 419 | border_normal="00000000", 420 | border_on_single=False, 421 | wrap_focus_columns=False, 422 | wrap_focus_rows=False, 423 | margin=0, 424 | # margin_on_single=30, # 4 425 | ) 426 | 427 | layouts = [ 428 | MyColumns(**col_opts), 429 | layout.Columns(**col_opts), 430 | ] 431 | 432 | floating_layout = layout.Floating( 433 | border_width=3, 434 | border_focus=border_focus, 435 | border_normal="00000000", 436 | fullscreen_border_width=0, 437 | float_rules=[ 438 | Match(func=base.Window.has_fixed_size), 439 | Match(func=base.Window.has_fixed_ratio), 440 | Match(func=lambda c: bool(c.is_transient_for())), 441 | Match(role="gimp-file-export"), 442 | Match(title="Bluetooth Devices"), 443 | Match(title="File Operation Progress", wm_class=re.compile("[Tt]hunar")), 444 | Match(title="Firefox — Sharing Indicator"), 445 | Match(title="KDE Connect Daemon"), 446 | Match(title="Open File"), 447 | Match(title="Unlock Database - KeePassXC"), 448 | Match(title="KeePassXC - Access Request"), 449 | Match(title=re.compile("Presenting: .*"), wm_class="libreoffice-impress"), 450 | Match(wm_class="Arandr"), 451 | Match(wm_class="Dragon"), 452 | Match(wm_class="Dragon-drag-and-drop"), 453 | Match(wm_class="Pinentry-gtk-2"), 454 | Match(wm_class="Xephyr"), 455 | Match(wm_class="confirm"), 456 | Match(wm_class="dialog"), 457 | Match(wm_class="download"), 458 | Match(wm_class="eog"), 459 | Match(wm_class="error"), 460 | Match(wm_class="file_progress"), 461 | Match(wm_class="imv"), 462 | Match(wm_class="io.github.celluloid_player.Celluloid"), 463 | Match(wm_class="lxappearance"), 464 | Match(wm_class="matplotlib"), 465 | #Match(wm_class="mpv"), 466 | Match(wm_class="nm-connection-editor"), 467 | Match(wm_class="notification"), 468 | Match(wm_class="org.gnome.clocks"), 469 | Match(wm_class="org.kde.ark"), 470 | Match(wm_class="pavucontrol"), 471 | Match(wm_class="qt5ct"), 472 | Match(wm_class="ssh-askpass"), 473 | Match(wm_class="thunar"), 474 | Match(wm_class="toolbar"), 475 | Match(wm_class="tridactyl"), 476 | Match(wm_class="wdisplays"), 477 | Match(wm_class="wlroots"), 478 | Match(wm_class="zoom"), 479 | Match(wm_type="dialog"), 480 | ], 481 | ) 482 | 483 | 484 | # Screens and Bars 485 | widget_defaults = { 486 | "padding": 10, 487 | "foreground": colours[6], 488 | "font": "Font Awesome 5 Free", 489 | "fontsize": 16, 490 | } 491 | 492 | icon_font_size = 22 493 | 494 | groupbox_config = { 495 | "active": foreground, 496 | "highlight_method": "block", 497 | "this_current_screen_border": colour_focussed, 498 | "other_current_screen_border": colours[5], 499 | "highlight_color": [background, colours[5]], 500 | "disable_drag": True, 501 | "padding": 4, 502 | "font": "TamzenForPowerline Bold", 503 | "fontsize": 12, 504 | } 505 | 506 | # mpd2 = widget.Mpd2( 507 | # no_connection="", 508 | # status_format="{artist} - {title}", 509 | # status_format_stopped="", 510 | # foreground=colours[12], 511 | # idle_format="", 512 | # font="TamzenForPowerline Bold", 513 | # update_interval=10, 514 | # scroll=True, 515 | # ) 516 | 517 | mpd2 = widget.Mpris2( 518 | name="mpris", 519 | width=1000, 520 | objname=None, 521 | format="{xesam:title} - {xesam:artist}", 522 | font="TamzenForPowerline Bold", 523 | ) 524 | 525 | 526 | class MyVolume(widget.Volume): 527 | def _configure(self, qtile, bar): 528 | widget.Volume._configure(self, qtile, bar) 529 | self.volume = self.get_volume() 530 | if self.volume <= 0: 531 | self.text = "" 532 | elif self.volume <= 15: 533 | self.text = "" 534 | elif self.volume < 50: 535 | self.text = "" 536 | else: 537 | self.text = "" 538 | 539 | def _update_drawer(self): 540 | if self.volume <= 0: 541 | self.text = "" 542 | elif self.volume <= 15: 543 | self.text = "" 544 | elif self.volume < 50: 545 | self.text = "" 546 | else: 547 | self.text = "" 548 | self.draw() 549 | 550 | def increase_vol(self): 551 | subprocess.run("amixer -c PCH set PCM 3%+".split(), capture_output=True) 552 | self.volume = self.get_volume() 553 | 554 | def decrease_vol(self): 555 | subprocess.run("amixer -c PCH set PCM 3%-".split(), capture_output=True) 556 | self.volume = self.get_volume() 557 | 558 | def mute(self): 559 | subprocess.run("amixer -c PCH set PCM toggle".split(), capture_output=True) 560 | self.volume = self.get_volume() 561 | 562 | 563 | bklight = widget.Backlight( 564 | backlight_name=os.listdir("/sys/class/backlight")[-1], 565 | step=1, 566 | update_interval=None, 567 | format="", 568 | fontsize=icon_font_size, 569 | change_command=None, 570 | ) 571 | 572 | volume = MyVolume( 573 | fontsize=icon_font_size, 574 | channel="PCM", 575 | font="Font Awesome 5 Free", 576 | update_interval=60, 577 | cardid="PCH", 578 | device=None, 579 | ) 580 | 581 | if IS_WAYLAND: 582 | systray = widget_extras.StatusNotifier(padding=20) 583 | else: 584 | systray = widget.Systray(padding=20, icon_size=24) 585 | 586 | 587 | class MyBattery(Battery): 588 | """ 589 | This is basically the Battery widget except it uses some icons, and if you click it 590 | it will show the percentage numerically for 1 second. 591 | """ 592 | 593 | def build_string(self, status): 594 | if self.layout is not None: 595 | self.layout.colour = self.foreground 596 | if ( 597 | status.state == BatteryState.DISCHARGING 598 | and status.percent < self.low_percentage 599 | ): 600 | self.background = self.low_background 601 | else: 602 | self.background = self.normal_background 603 | if status.state == BatteryState.DISCHARGING: 604 | if status.percent > 0.75: 605 | char = "" 606 | elif status.percent > 0.45: 607 | char = "" 608 | elif status.percent > 0.25: 609 | char = "" 610 | else: 611 | char = "" 612 | elif status.percent >= 1 or status.state == BatteryState.FULL: 613 | char = "" 614 | elif status.state == BatteryState.EMPTY or ( 615 | status.state == BatteryState.UNKNOWN and status.percent == 0 616 | ): 617 | char = "" 618 | else: 619 | char = "" 620 | return self.format.format(char=char, percent=status.percent) 621 | 622 | def restore(self): 623 | self.format = "{char}" 624 | self.font = "Font Awesome 5 Free" 625 | self.timer_setup() 626 | 627 | def button_press(self, x, y, button): 628 | self.format = "{percent:2.0%}" 629 | self.font = "TamzenForPowerline Bold" 630 | self.timer_setup() 631 | self.timeout_add(1, self.restore) 632 | 633 | 634 | battery = MyBattery( 635 | format="{char}", 636 | low_background=colours[1], 637 | show_short_text=False, 638 | low_percentage=0.12, 639 | notify_below=12, 640 | fontsize=icon_font_size + 10, 641 | ) 642 | 643 | date = widget.Clock( 644 | format="%e/%m/%g", 645 | fontsize=16, 646 | font="TamzenForPowerline Bold", 647 | update_interval=60, 648 | name="date", 649 | ) 650 | 651 | time = widget.Clock( 652 | fontsize=20, 653 | font="TamzenForPowerline Medium", 654 | update_interval=60, 655 | name="time", 656 | ) 657 | 658 | groupboxes = [ 659 | widget.GroupBox(**groupbox_config), 660 | widget.GroupBox(**groupbox_config, visible_groups=["q", "w", "e", "r"]), 661 | ] 662 | 663 | 664 | @hook.subscribe.startup 665 | def _(): 666 | # Set up initial GroupBox visible groups 667 | if len(qtile.screens) > 1: 668 | groupboxes[0].visible_groups = ["1", "2", "3", "4"] 669 | else: 670 | groupboxes[0].visible_groups = None 671 | 672 | 673 | @hook.subscribe.screens_reconfigured 674 | def _(): 675 | # Reconfigure GroupBox visible groups 676 | if len(qtile.screens) > 1: 677 | groupboxes[0].visible_groups = ["1", "2", "3", "4"] 678 | else: 679 | groupboxes[0].visible_groups = None 680 | if hasattr(groupboxes[0], "bar"): 681 | groupboxes[0].bar.draw() 682 | 683 | 684 | #bar_border_width = [0, 3, 0, 3] 685 | #bar_border_color = ["000000", colours[5], "000000", colours[5]] 686 | 687 | screens = [ 688 | Screen( 689 | top=bar.Bar( 690 | [ 691 | groupboxes[0], 692 | widget.Spacer(name="s1"), 693 | mpd2, 694 | widget.Spacer(name="s2"), 695 | systray, 696 | volume, 697 | bklight, 698 | battery, 699 | date, 700 | time, 701 | ], 702 | 28, 703 | background=background, 704 | ), 705 | bottom=bar.Gap(outer_gaps), 706 | left=bar.Gap(outer_gaps), 707 | right=bar.Gap(outer_gaps), 708 | #wallpaper="~/pictures/Wallpapers/fractal.bmp", 709 | #wallpaper_mode="fill", 710 | ), 711 | Screen( 712 | top=bar.Bar( 713 | [ 714 | groupboxes[1], 715 | widget.Spacer(name="s3"), 716 | mpd2, 717 | widget.Spacer(name="s4"), 718 | volume, 719 | bklight, 720 | battery, 721 | date, 722 | time, 723 | ], 724 | 28, 725 | background=background, 726 | ), 727 | #wallpaper="~/pictures/Wallpapers/fractal.bmp", 728 | #wallpaper_mode="fill", 729 | bottom=bar.Gap(outer_gaps), 730 | left=bar.Gap(outer_gaps), 731 | right=bar.Gap(outer_gaps), 732 | ), 733 | ] 734 | 735 | 736 | @hook.subscribe.client_focus 737 | def _(_): 738 | # Keep Static windows on top 739 | for window in qtile.windows_map.values(): 740 | if isinstance(window, base.Static): 741 | window.bring_to_front() 742 | 743 | 744 | reconfigure_screens = True 745 | follow_mouse_focus = True 746 | bring_front_click = True 747 | cursor_warp = False 748 | auto_fullscreen = True 749 | focus_on_window_activation = "focus" 750 | 751 | keys = [Key(mods, key, cmd, desc=desc) for mods, key, cmd, desc in my_keys] 752 | groups.append(scratchpad) 753 | --------------------------------------------------------------------------------