My Qtile window manager configuration
git clone https://github.com/m-col/qtile-config
Files | Refs | Readme

Commit a87a0bbff8f34034ec7ef02c7fd06e5efc36d98b
Parent: 1549b3a7b8b7e9384dd05efa2563358ff4982386
Author: mcol <mcol@posteo.net>
Date: 2021-06-14 22:52:17 +0100
Committer: mcol <mcol@posteo.net>
Committed: 2021-06-14 22:52:17 +0100

Add my current config at time of git init

This is the config I've used for the past year or so.

traverse.py Added

@@ -0,0 +1,91 @@
+"""
+This plugin exports four functions - up, down, left and right - that when called will
+move window focus to the first window in that general direction. Focussing is based
+entirely on position and geometry, so is independent of screens, layouts and whether
+windows are floating or tiled. It can also move focus to and from empty screens.
+
+Example usage:
+
+    import traverse
+    keys.extend([EzKey(k, v) for k, v in {
+        'M-k': lazy.function(traverse.up)
+        'M-j': lazy.function(traverse.down)
+        'M-h': lazy.function(traverse.left)
+        'M-l': lazy.function(traverse.right)
+    }.items()])
+
+"""
+
+from libqtile.config import Screen
+
+
+def up(qtile):
+    _focus_window(qtile, -1, 'y')
+
+
+def down(qtile):
+    _focus_window(qtile, 1, 'y')
+
+
+def left(qtile):
+    _focus_window(qtile, -1, 'x')
+
+
+def right(qtile):
+    _focus_window(qtile, 1, 'x')
+
+
+def _focus_window(qtile, dir, axis):
+    win = None
+    win_wide = None
+    dist = 10000
+    dist_wide = 10000
+    cur = qtile.current_window
+    if not cur:
+        cur = qtile.current_screen
+
+    if axis == 'x':
+        dim = 'width'
+        band_axis = 'y'
+        band_dim = 'height'
+        cur_pos = cur.x
+        band_min = cur.y
+        band_max = cur.y + cur.height
+    else:
+        dim = 'height'
+        band_axis = 'x'
+        band_dim = 'width'
+        band_min = cur.x
+        cur_pos = cur.y
+        band_max = cur.x + cur.width
+
+    cur_pos += getattr(cur, dim) / 2
+
+    windows = [w for g in qtile.groups if g.screen for w in g.windows]
+    windows.extend([s for s in qtile.screens if not s.group.windows])
+
+    if cur in windows:
+        windows.remove(cur)
+
+    for w in windows:
+        if isinstance(w, Screen) or not w.minimized:
+            pos = getattr(w, axis) + getattr(w, dim) / 2
+            gap = dir * (pos - cur_pos)
+            if gap > 5:
+                band_pos = getattr(w, band_axis) + getattr(w, band_dim) / 2
+                if band_min < band_pos < band_max:
+                    if gap < dist:
+                        dist = gap
+                        win = w
+                else:
+                    if gap < dist_wide:
+                        dist_wide = gap
+                        win_wide = w
+
+    if not win:
+        win = win_wide
+    if win:
+        qtile.focus_screen(win.group.screen.index)
+        win.group.focus(win, True)
+        if not isinstance(win, Screen):
+            win.focus(False)

startup.sh Added

@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+#
+# Qtile Wayland startup script
+
+foot --server &
+
+FIFO=/tmp/wob-$WAYLAND_DISPLAY
+test -e $FIFO || mkfifo $FIFO
+tail -f $FIFO | wob -a top &
+
+
+# Things after this are only run on a TTY
+[[ -n "$QTILE_XEPHYR" ]] && exit  
+
+
+run_if_new() { pgrep $1 &> /dev/null || $@ &; }
+
+run_if_new mako
+run_if_new keepassxc
+
+pgrep wlsunset && pkill -SIGKILL wlsunset
+wlsunset -t 3100 -T 5700 -l 55.7 -L -3.1 &> /dev/null
+
+sleep 5
+check_systemd
+kanshi &> /dev/null &

config.py Added

@@ -0,0 +1,640 @@
+"""
+Qtile main config file
+======================
+"""
+
+import asyncio
+import os
+import sys
+import subprocess
+from functools import partial
+
+from libqtile import layout, hook, bar, widget, qtile
+from libqtile.config import Key, EzKey, Screen, Group, Drag, Click, Match, ScratchPad, DropDown
+from libqtile.lazy import lazy
+from libqtile.log_utils import logger
+from libqtile.widget.backlight import ChangeDirection
+
+import traverse
+
+
+## Basic vars
+IS_WAYLAND = qtile.core.name == "wayland"
+
+if IS_WAYLAND:
+    term = 'footclient'
+else:
+    term = 'xterm'
+
+xephyr = int(os.environ.get("QTILE_XEPHYR", 0))
+if xephyr:
+    mod = "mod1"
+    alt = "control"
+    if not IS_WAYLAND:
+        # urxvt doesn't like xephyr
+        term = 'xterm'
+else:
+    mod = "mod4"
+    alt = "mod1"
+
+HOME = os.path.expanduser('~')
+
+
+## Colours
+theme = dict(
+    foreground='#bcbcbe',
+    background='#030405',
+    color0='#030405',
+    color8='#1f1c32',
+    color1='#8742a5',
+    color9='#9a5eb3',
+    color2='#406794',
+    color10='#5fd75f',
+    color3='#653c21',
+    color11='#6e9fcd',
+    color4='#8f4ff0',
+    color12='#bd9ef9',
+    color5='#5d479d',
+    color13='#998dd1',
+    color6='#3e3e73',
+    color14='#9a9dcc',
+    color7='#495068',
+    color15='#e1e1e4',
+)
+
+colours = [theme[f'color{n}'] for n in range(16)]
+background = theme['background']
+foreground = theme['foreground']
+wm_bw = 5
+wm_cr = 5
+wm_colorfocused = '#6fa3e0'
+wm_colorunfocused = '#0e101c'
+wm_gapsinner = 2
+wm_gapsouter = 2
+
+
+## Keys
+
+@lazy.function
+def float_to_front(qtile):
+    """ Bring all floating windows of the group to front """
+    for window in qtile.current_group.windows:
+        if window.floating:
+            window.cmd_bring_to_front()
+
+
+keys = [
+    # Window management
+    ([mod, 'control'],  'q',        lazy.window.kill(),                 "Close window"),
+    ([mod],             'f',        lazy.window.toggle_fullscreen(),    "Toggle fullscreen"),
+    ([mod, 'shift'],    'space',    lazy.window.toggle_floating(),      "Toggle floating"),
+    ([mod],             'space',    float_to_front,                     "Move floating windows to the front"),
+    ([mod],             'j',        lazy.function(traverse.down),       "Traverse down"),
+    ([mod],             'k',        lazy.function(traverse.up),         "Traverse up"),
+    ([mod],             'h',        lazy.function(traverse.left),       "Traverse left"),
+    ([mod],             'l',        lazy.function(traverse.right),      "Traverse right"),
+    ([mod, 'shift'],    'j',        lazy.layout.shuffle_down(),         "Shuffle down"),
+    ([mod, 'shift'],    'k',        lazy.layout.shuffle_up(),           "Shuffle up"),
+    ([mod, 'shift'],    'h',        lazy.layout.shuffle_left(),         "Shuffle left"),
+    ([mod, 'shift'],    'l',        lazy.layout.shuffle_right(),        "Shuffle right"),
+    ([mod, alt],        'j',        lazy.layout.grow_down(),            "Grow down"),
+    ([mod, alt],        'k',        lazy.layout.grow_up(),              "Grow up"),
+    ([mod, alt],        'h',        lazy.layout.grow_left(),            "Grow left"),
+    ([mod, alt],        'l',        lazy.layout.grow_right(),           "Grow right"),
+    ([mod, 'shift'],    'r',        lazy.restart(),                     "Restart Qtile"),
+    ([mod],             'Tab',      lazy.next_layout(),                 "Next layout"),
+    ([mod, 'control'],  'h',        lazy.window.toggle_minimize(),      "Minimise window"),
+    ([mod],             's',        lazy.window.static(),               "Make window static"),
+    ([mod, 'control'],  'Escape',   lazy.shutdown(),                    "Shutdown Qtile"),
+
+    # Volume control
+    ([], 'XF86AudioMute',         lazy.widget['myvolume'].mute(),         "Mute audio"),
+    ([], 'F10',                   lazy.widget['myvolume'].mute(),         "Mute audio"),
+    ([], 'XF86AudioRaiseVolume',  lazy.widget['myvolume'].increase_vol(), "Increase volume"),
+    ([], 'F12',                   lazy.widget['myvolume'].increase_vol(), "Increase volume"),
+    ([], 'XF86AudioLowerVolume',  lazy.widget['myvolume'].decrease_vol(), "Decrease volume"),
+    ([], 'F11',                   lazy.widget['myvolume'].decrease_vol(), "Decrease volume"),
+
+    # Backlight control
+    ([], 'XF86MonBrightnessUp',   lazy.widget['backlight'].change_backlight(ChangeDirection.UP, 3),   "Increase backlight"),
+    ([], 'XF86MonBrightnessDown', lazy.widget['backlight'].change_backlight(ChangeDirection.DOWN, 3), "Decrease backlight"),
+    ([], 'F6',                    lazy.widget['backlight'].change_backlight(ChangeDirection.UP, 3),   "Increase backlight"),
+    ([], 'F5',                    lazy.widget['backlight'].change_backlight(ChangeDirection.DOWN, 3), "Decrease backlight"),
+
+    # Launchers
+    ([mod],             'Return',   lazy.spawn(term),                           "Spawn terminal"),
+    ([mod, 'shift'],    'f',        lazy.spawn("firefox"),                      "Spawn Firefox"),
+    ([mod, 'control'],  'f',        lazy.spawn("tor-browser --allow-remote") ,  "Spawn Tor Browser"),
+    ([],                'Print',    lazy.spawn("screenshot copy"),              "Screenshot to clipboard"),
+    (['shift'],         'Print',    lazy.spawn('screenshot'),                   "Screenshot to file"),
+    ([mod],             'p',        lazy.spawn('get_password_rofi'),            "Keepass passwords"),
+    ([mod],             'i',        lazy.spawn('systemctl suspend -i'),         "Suspend system"),
+]
+
+
+# Backend-specific launchers
+if IS_WAYLAND:
+    keys.extend([
+        ([mod], 'd', lazy.spawn('wofi --gtk-dark --show run'), "wofi: run"),
+    ])
+
+else:
+    keys.extend([
+        ([mod],             'd',    lazy.spawn('rofi -show run -theme ~/.config/rofi/common-large.rasi'), "rofi: run"),
+        ([],    'XF86PowerOff',     lazy.spawn('power-menu'),                   "Power menu"),
+        ([mod, 'shift'],    'x',    lazy.spawn('set_monitors'),                 "Configure monitors"),
+        ([mod, 'shift'],    'i',    lazy.spawn('slock systemctl suspend -i'),   "Suspend system and lock"),
+    ])
+
+
+# Changing VT
+if IS_WAYLAND:
+    keys.extend([
+        ([mod], 'F1',     lazy.change_vt(1),    "Change to VT 1"),
+        ([mod], 'F2',     lazy.change_vt(2),    "Change to VT 2"),
+        ([mod], 'F3',     lazy.change_vt(3),    "Change to VT 3"),
+        ([mod], 'F4',     lazy.change_vt(4),    "Change to VT 4"),
+        ([mod], 'F5',     lazy.change_vt(5),    "Change to VT 5"),
+        ([mod], 'F6',     lazy.change_vt(6),    "Change to VT 6"),
+    ])
+
+
+## Mouse control
+mouse = [
+    Drag([mod], "Button1",      lazy.window.set_position_floating(),    start=lazy.window.get_position()),
+    Drag([mod, alt], "Button1", lazy.window.set_size_floating(), start=lazy.window.get_size()),
+    Click([mod], "Button4",     lazy.layout.grow_up()),
+    Click([mod], "Button5",     lazy.layout.grow_down()),
+]
+
+
+## Layouts
+#border_focus = [colours[13], colours[5]]
+border_focus = colours[13]
+border_normal = colours[8]
+border_width = 4
+
+#import qtools.borders
+#qtools.borders.enable('cde')
+#border_focus = [border_focus, colours[5]]
+#border_normal = [colours[0], colours[8]]
+
+
+layouts = [
+    layout.Columns(
+        insert_position = 1,
+        border_width = border_width,
+        border_focus = border_focus,
+        border_normal = border_normal,
+        border_on_single = True,
+        wrap_focus_columns = False,
+        wrap_focus_rows = False,
+        margin = wm_gapsinner,
+        corner_radius = wm_cr,
+    ),
+    layout.Max(),
+]
+
+floating_layout = layout.Floating(
+    border_width = border_width,
+    border_focus = border_focus,
+    border_normal = border_normal,
+    corner_radius = wm_cr,
+    float_rules=[
+        Match(title='wlroots - X11-1'),
+        Match(title='wlroots - X11-2'),
+        Match(title='Open File'),
+        Match(wm_class='Arandr'),
+        Match(wm_class='confirm'),
+        Match(wm_class='dialog'),
+        Match(wm_class='download'),
+        Match(wm_class='Dragon'),
+        Match(wm_class='error'),
+        Match(wm_class='fiji-Main'),
+        Match(wm_class='file_progress'),
+        Match(wm_class='fontforge'),
+        Match(wm_class='imv'),
+        Match(wm_class='love'),
+        Match(wm_class='lxappearance'),
+        Match(wm_class='mpv'),
+        Match(wm_class='Nm-connection-editor'),
+        Match(wm_class='notification'),
+        Match(wm_class='Oomox'),
+        Match(wm_class='Pavucontrol'),
+        Match(wm_class='Pinentry-gtk-2'),
+        Match(wm_class='qt5ct'),
+        Match(wm_class='ssh-askpass'),
+        Match(wm_class='tinyterm'),
+        Match(wm_class='Dragon-drag-and-drop'),
+        Match(wm_class='toolbar'),
+        Match(wm_class='Xephyr'),
+        Match(wm_type='dialog'),
+        Match(role='gimp-file-export'),
+        Match(func=lambda c: c.has_fixed_size()),
+        Match(func=lambda c: bool(c.is_transient_for())),
+    ]
+)
+
+
+## Screens and Bars
+widget_defaults = {
+    'padding': 10,
+    'foreground': foreground,
+    'font': 'Font Awesome 5 Free',
+    'fontsize': 16,
+}
+
+trim = widget.TextBox(' ', background=colours[8])
+
+groupbox_config = {
+    'active': foreground,
+    'highlight_method': 'line',
+    'this_current_screen_border': wm_colorfocused,
+    'other_current_screen_border': colours[5],
+    'highlight_color': [background, colours[5]],
+    'disable_drag': True,
+    'padding': 8,
+}
+
+mpd2 = widget.Mpd2(
+    no_connection='',
+    status_format='{artist} - {title}',
+    status_format_stopped='',
+    foreground=colours[12],
+    idle_format='',
+    fontsize=16,
+    font='TamzenForPowerline Bold',
+    update_interval=10,
+)
+ 
+cpugraph = widget.CPUGraph(
+    graph_color=colours[12],
+    fill_color=wm_colorunfocused,
+    border_width=0,
+    margin_x=10,
+    margin_y=4,
+    samples=50,
+    line_width=4,
+    width=200,
+    type='box',
+    frequency=1,
+)
+
+bklight = widget.Backlight(
+    backlight_name=os.listdir('/sys/class/backlight')[0],
+    step=1,
+    update_interval=None,
+    format="",
+    change_command=None,
+    foreground=colours[3],
+)
+
+class MyVolume(widget.Volume):
+    def _configure(self, qtile, bar):
+        widget.Volume._configure(self, qtile, bar)
+        self.volume = self.get_volume()
+        if self.volume <= 0:
+            self.text = ''
+        elif self.volume <= 15:
+            self.text = ''
+        elif self.volume < 50:
+            self.text = ''
+        else:
+            self.text = ''
+        # drawing here crashes Wayland
+
+        self.wob = "/tmp/wob-" + qtile.core.display_name
+
+    def _update_drawer(self):
+        if self.volume <= 0:
+            self.text = ''
+        elif self.volume <= 15:
+            self.text = ''
+        elif self.volume < 50:
+            self.text = ''
+        else:
+            self.text = ''
+        self.draw()
+        with open(self.wob, 'a') as f:
+            f.write(str(self.volume) + "\n")
+
+    def cmd_increase_vol(self):
+        subprocess.call('amixer set PCM 4%+'.split())
+        self.volume = self.get_volume()
+        self._update_drawer()
+    def cmd_decrease_vol(self):
+        subprocess.call('amixer set PCM 4%-'.split())
+        self.volume = self.get_volume()
+        self._update_drawer()
+    def cmd_mute(self):
+        subprocess.call('amixer set Master toggle'.split())
+        self.channel = 'Master'
+        self.volume = self.get_volume()
+        self.channel = 'PCM'
+        if self.volume == 0:
+            self.volume = self.get_volume()
+        self._update_drawer()
+
+volume = MyVolume(
+    fontsize=18,
+    channel='PCM',
+    font='Font Awesome 5 Free',
+    foreground=colours[4],
+    update_interval=60,
+)
+
+if IS_WAYLAND:
+    systray = trim
+else:
+    systray = widget.Systray(padding=20)
+
+wlan = widget.Wlan(
+    format='',
+    disconnected_message='',
+    mouse_callbacks={
+        'Button1': partial(qtile.cmd_spawn, 'nm-connection-editor'),
+    },
+    foreground=colours[5],
+    update_interval=5,
+)
+
+from libqtile.widget.battery import Battery, BatteryState
+class MyBattery(Battery):
+    def build_string(self, status):
+        if self.layout is not None:
+            if status.state == BatteryState.DISCHARGING and status.percent < self.low_percentage:
+                self.layout.colour = self.low_foreground
+            else:
+                self.layout.colour = self.foreground
+        if status.state == BatteryState.DISCHARGING:
+            if status.percent > 0.75:
+                char = ''
+            elif status.percent > 0.45:
+                char = ''
+            else:
+                char = ''
+        elif status.percent >= 1 or status.state == BatteryState.FULL:
+            char = ''
+        elif status.state == BatteryState.EMPTY or \
+                (status.state == BatteryState.UNKNOWN and status.percent == 0):
+            char = ''
+        else:
+            char = ''
+        return self.format.format(char=char, percent=status.percent)
+
+    def restore(self):
+        self.format = '{char}'
+        self.font = 'Font Awesome 5 Free'
+        self.timer_setup()
+
+    def button_press(self, x, y, button):
+        old = self.format
+        self.format = '{percent:2.0%}'
+        self.font='TamzenForPowerline Bold'
+        self.timer_setup()
+        self.timeout_add(1, self.restore)
+
+battery = MyBattery(
+    format='{char}',
+    low_foreground=colours[1],
+    show_short_text=False,
+    low_percentage=0.12,
+    foreground=colours[6],
+    notify_below=12,
+)
+
+date = widget.Clock(
+    format='%e/%m/%g',
+    fontsize=16,
+    font='TamzenForPowerline Bold',
+    foreground=colours[7],
+    update_interval=60,
+)
+time = widget.Clock(
+    fontsize=20,
+    font='TamzenForPowerline Medium',
+    update_interval=60,
+)
+
+groupboxes = [
+    widget.GroupBox(**groupbox_config, visible_groups=['1', '2', '3', 'q', 'w', 'e']),
+    widget.GroupBox(**groupbox_config, visible_groups=['q', 'w', 'e']),
+]
+
+gaps = wm_gapsinner + wm_gapsouter
+screens = [
+    Screen(
+        top=bar.Bar(
+            [
+                trim, groupboxes[0], trim, cpugraph, widget.Spacer(), mpd2,
+                widget.Spacer(), systray, bklight, volume, wlan,
+                battery, trim, date, time, trim
+            ],
+            40,
+            background=background,
+            margin=[gaps, gaps, wm_gapsouter, gaps],
+        ),
+        wallpaper_mode='fill',
+        bottom=bar.Gap(wm_gapsouter),
+        left=bar.Gap(wm_gapsouter),
+        right=bar.Gap(wm_gapsouter),
+        wallpaper=f'{HOME}/pictures/pixart/diver.png'
+    ),
+    Screen(
+        top=bar.Bar(
+            [
+                trim, groupboxes[1], trim, cpugraph, widget.Spacer(), mpd2,
+                widget.Spacer(), bklight, volume, wlan,
+                battery, trim, date, time, trim
+            ],
+            40,
+            background=background,
+            margin=[gaps, gaps, wm_gapsouter, gaps],
+        ),
+        wallpaper_mode='fill',
+        bottom=bar.Gap(wm_gapsouter),
+        left=bar.Gap(wm_gapsouter),
+        right=bar.Gap(wm_gapsouter),
+        wallpaper=f'{HOME}/pictures/pixart/diver.png'
+    ),
+]
+
+## Groups
+groups = [
+    Group('1', label=''),
+    Group('2', label='', matches=[Match(wm_class='Tor Browser'), Match(wm_class='hyperspace desktop')]),
+    Group('3', label='', matches=[Match(wm_class='firefox')]),
+    Group('q', label=''),
+    Group('w', label='', matches=[Match(wm_class='Slack')]),
+    Group('e', label='', matches=[Match(wm_class='halebird')]),
+]
+
+def _go_to_group(group):
+    def _inner(qtile):
+        if len(qtile.screens) > 1:
+            old = qtile.current_screen.group.name
+            if group in '123':
+                qtile.focus_screen(0)
+                if old in '123' or qtile.current_screen.group.name != group:
+                    qtile.groups_map[group].cmd_toscreen()
+            else:
+                qtile.focus_screen(1)
+                if old in 'qwe' or qtile.current_screen.group.name != group:
+                    qtile.groups_map[group].cmd_toscreen()
+        else:
+            qtile.groups_map[group].cmd_toscreen()
+    return _inner
+
+for i in groups:
+    keys.extend([
+        ([mod],             i.name, lazy.function(_go_to_group(i.name)), f"Go to group {i.name}"),
+        ([mod, 'shift'],    i.name, lazy.window.togroup(i.name), f"Send window to group {i.name}"),
+    ])
+
+
+def _scroll_screen(direction):
+    """ Scroll next group of subset allocated to specific screen """
+    def _inner(qtile):
+        if len(qtile.screens) > 1:
+            current = qtile.groups.index(qtile.current_group)
+            if current < 3:
+                destination = (current + direction) % 3
+            else:
+                destination = ((current - 3 + direction) % 3) + 3
+            qtile.groups[destination].cmd_toscreen()
+        else:
+            current = qtile.groups.index(qtile.current_group)
+            destination = (current + direction) % 6
+            qtile.groups[destination].cmd_toscreen()
+    return _inner
+
+keys.extend([
+    ([mod], 'm', lazy.function(_scroll_screen(1)),  "Screen groups forward"),
+    ([mod], 'n', lazy.function(_scroll_screen(-1)), "Screen groups backward"),
+])
+
+
+# Dropdowns and Scratchpad
+
+dd_defaults = {
+    "warp_pointer": False,
+    "on_focus_lost_hide": True,
+    "opacity": 1,
+}
+
+if IS_WAYLAND:
+    dropdowns = [
+        DropDown("tmux", "foot tmux", height=0.4, **dd_defaults),
+        DropDown("email", f"foot -D {HOME}/Downloads mutt", x=0.1, y=0.05, width=0.8, height=0.9, **dd_defaults),
+        DropDown("irc", "foot irc", x=0.1, y=0.05, width=0.8, height=0.9, **dd_defaults),
+        DropDown("ncmpcpp", "foot ncmpcpp", x=0.12, y=0.2, width=0.56, height=0.7, **dd_defaults),
+        DropDown("python", "foot python", x=0.05, y=0.1, width=0.2, height=0.3, **dd_defaults),
+        #DropDown("newsboat", "foot newsboat", x=0.2, y=0.05, width=0.6, height=0.9, **dd_defaults),
+    ]
+else:
+    dropdowns = [
+        DropDown("tmux", "urxvt -e tmux", height=0.4, **dd_defaults),
+        DropDown("email", f"urxvt -cd {HOME}/Downloads -e mutt", x=0.1, y=0.05, width=0.8, height=0.9, **dd_defaults),
+        DropDown("irc", "urxvt -b 100 -e irc", x=0.1, y=0.05, width=0.8, height=0.9, **dd_defaults),
+        DropDown("ncmpcpp", "urxvt -e ncmpcpp", x=0.12, y=0.2, width=0.56, height=0.7, **dd_defaults),
+        DropDown("python", "urxvt -e python", x=0.05, y=0.1, width=0.2, height=0.3, **dd_defaults),
+        #DropDown("newsboat", "urxvt -e newsboat", x=0.2, y=0.05, width=0.6, height=0.9, **dd_defaults),
+    ]
+
+groups.append(ScratchPad("scratchpad", dropdowns))
+
+keys.extend([
+    ([mod, 'shift'],     'Return',   lazy.group['scratchpad'].dropdown_toggle('tmux'), "Toggle tmux scratchpad"),
+    ([mod, 'control'],   'e',        lazy.group['scratchpad'].dropdown_toggle('email'), "Toggle email scratchpad"),
+    ([mod, 'control'],   'w',        lazy.group['scratchpad'].dropdown_toggle('irc'), "Toggle irc scratchpad"),
+    ([mod, 'control'],   'm', lazy.group['scratchpad'].dropdown_toggle('ncmpcpp'), "Toggle ncmpcpp scratchpad"),
+    ([mod],              'c',        lazy.group['scratchpad'].dropdown_toggle('python'), "Toggle python scratchpad"),
+    #([mod, 'control'],   'n', lazy.group['scratchpad'].dropdown_toggle('newsboat'), "Toggle newsboat scratchpad"),
+])
+
+
+# Hooks
+
+@hook.subscribe.startup
+def _():
+    # Set initial groups
+    if len(qtile.screens) > 1:
+        qtile.groups_map['1'].cmd_toscreen(0, toggle=False)
+        qtile.groups_map['q'].cmd_toscreen(1, toggle=False)
+        groupboxes[0].visible_groups = ['1', '2', '3']
+    else:
+        groupboxes[0].visible_groups = ['1', '2', '3', 'q', 'w', 'e']
+
+
+if IS_WAYLAND:
+    from libqtile.backend.wayland.window import Window
+    @hook.subscribe.client_new
+    def _(window):
+        # Auto-float windows
+        if type(window) is Window:
+            state = window.surface.toplevel._ptr.current
+            if 0 < state.max_width < 1920:
+                window.floating = True
+            else:
+                # Just incase I find something new I want to auto-float
+                logger.warning(
+                    (window.name, window.get_wm_class(), (state.min_width, state.max_width))
+                )
+
+else:
+    @hook.subscribe.client_new
+    def _(window):
+        # Auto-float windows
+        hints = window.window.get_wm_normal_hints()
+        if hints and 0 < hints['max_width'] < 1920:
+            window.floating = True
+
+
+@hook.subscribe.screen_change
+async def _(_):
+    # Set up GroupBox visible groups
+    await asyncio.sleep(1)
+    if len(qtile.screens) > 1:
+        groupboxes[0].visible_groups = ['1', '2', '3']
+        if qtile.screens[0].group.name not in "123":
+            qtile.groups_map['1'].cmd_toscreen(0, toggle=False)
+        qtile.groups_map['q'].cmd_toscreen(1, toggle=False)
+    else:
+        groupboxes[0].visible_groups = ['1', '2', '3', 'q', 'w', 'e']
+    if hasattr(groupboxes[0], 'bar'):
+        groupboxes[0].bar.draw()
+
+
+## Startup script for Wayland
+if IS_WAYLAND:
+    @hook.subscribe.startup_once
+    async def auto_start():
+        qtile.cmd_spawn(f"{HOME}/.config/qtile/startup.sh", shell=True)
+
+    if xephyr:
+        @hook.subscribe.startup_once
+        async def _():
+            qtile.cmd_reconfigure_screens()
+
+
+## General settings
+reconfigure_screens = True
+follow_mouse_focus = True
+bring_front_click = True
+cursor_warp = False
+auto_fullscreen = True
+focus_on_window_activation = 'smart'
+wmname = 'LG3D'
+
+
+# Reformat keys
+keys = [Key(mods, key, cmd, desc=desc) for mods, key, cmd, desc in keys]
+
+
+# Configure libinput devices
+if IS_WAYLAND:
+    from libqtile.backend.wayland import InputConfig
+
+    wayland_libinput_config = {
+        "type:pointer": InputConfig(drag=True, tap=True)
+    }