Dotfiles/.config/qtile/config.org
2024-09-22 13:03:17 +02:00

23 KiB
Executable file

Qtile Config

What is this?

This is my configuration for the Qtile Window Manager written and configured in Python.

Table of contents

Basic stuff

In the following points, stuff is defined, which is needed to be done for a properly working config, without directly influencing the UX in any way.

Imports

Imports are needed for the Window Manager to work properly. They grab the needed libraries so we can do cool stuff inside the window manager.

##################################################################################################################################################
### IF YOU ARE READING CONFIG.PY, SWITCH TO CONFIG.ORG IN EMACS!! THIS VERSION IS TANGELD FROM A LITERATE CONFIG AND HAS NEXT TO NO COMMENTS!! ###
##################################################################################################################################################

import os
import re
import subprocess
from libqtile.config import KeyChord, Key, Screen, Group, Drag, Click
from libqtile.lazy import lazy
from libqtile import layout, bar, widget, hook
from libqtile.lazy import lazy
from typing import List  # noqa: F401
None

Variables

Variables in a Programming language can contain a string or a number. While configuring a Window Manager, this is useful, if you want to repeat the same string a lot of times. If, for example, I used my terminal a lot of times in the config and I were to change my default terminal, I would have to replace every occurence of the Terminal name in the config. In this case, where I only have to change that string once, that is when defining the string.

mod = "mod4"                                     # Sets mod key to SUPER/WINDOWS
myTerm = "alacritty"                             # My terminal of choice

Keybindings

The Keybindings are the core point in using a tiling Window Manager. In Qtile, you can have both normal hotkey-like keybindings, as well as 'Keychords'.

Normal Keybindings

These are normal Keybindings, accessed by pressing the mod key (together with shift, in some cases) plus the corresponding key to each binding.

keys = [

Essentials and WM Controls

In this area of the config, basic programs (like Terminal and Run launcher) as well as Window Manger controls are located. I follow a simple rule there:

  • Mod + Shift + [Key] for Window Manager controls like
  • Mod + [Key] for a normal program

           Key([mod], "Return",
               lazy.spawn(myTerm),
               desc='Launches My Terminal'
               ),
           Key([mod], "r",
               lazy.spawn("rofi -show drun"),
               desc='Rofi Run Launcher'
               ),
           Key([mod], "Tab",
               lazy.next_layout(),
               desc='Toggle through layouts'
               ),
           Key([mod], "x",
               lazy.window.kill(),
               desc='Kill active window'
               ),
           Key([mod, "shift"], "r",
               lazy.restart(),
               desc='Restart Qtile'
               ),
           Key([mod, "shift"], "q",
               lazy.shutdown(),
               desc='Shutdown Qtile'
               ),
           ### Window controls
           Key([mod], "k",
               lazy.layout.down(),
               desc='Move focus down in current stack pane'
               ),
           Key([mod], "j",
               lazy.layout.up(),
               desc='Move focus up in current stack pane'
               ),
           Key([mod, "shift"], "k",
               lazy.layout.shuffle_down(),
               desc='Move windows down in current stack'
               ),
           Key([mod, "shift"], "j",
               lazy.layout.shuffle_up(),
               desc='Move windows up in current stack'
               ),
           Key([mod], "h",
               lazy.layout.grow(),
               lazy.layout.increase_nmaster(),
               desc='Expand window (MonadTall), increase number in master pane (Tile)'
               ),
           Key([mod], "l",
               lazy.layout.shrink(),
               lazy.layout.decrease_nmaster(),
               desc='Shrink window (MonadTall), decrease number in master pane (Tile)'
               ),
           Key([mod], "y",
               lazy.layout.normalize(),
               desc='normalize window size ratios'
               ),
           Key([mod, "shift"], "f",
               lazy.window.toggle_floating(),
               desc='toggle floating'
               ),
           Key([mod, "shift"], "w",
               lazy.to_screen(0),
               desc='Move focus to monitor one'
               ),
           Key([mod, "shift"], "e",
               lazy.to_screen(1),
               desc='Move focus to monitor two'
               ),

Other Programs

Here are the most programms that I used listed. They are all launched by using Mod + [Key]

         Key([mod], "f",
             lazy.spawn("firefox"),
             desc='Firefox'
             ),
         Key([mod], "n",
             lazy.spawn("/home/emma/AppImages/Vesktop.AppImage"),
             desc='Vesktop'
             ),
         Key([mod, "shift"], "n",
             lazy.spawn("discord"),
             desc='Standard Discord Client'
             ),
         Key([mod], "w",
             lazy.spawn("lowriter"),
             desc='Libre Office Writer'
             ),
         Key([mod], "p",
             lazy.spawn("pcmanfm"),
             desc='PCmanFM'
             ),
         Key([mod], "v",
             lazy.spawn("vlc"),
             desc='VLC Media Player'
             ),
         Key([mod], "d",
             lazy.spawn("deadbeef"),
             desc='The DeaDBeeF Music Player'
             ),
         Key([mod], "m",
             lazy.spawn("mailspring --password-store='gnome-libsecret'"),
             desc='My Email Client of Choice'
             ),

KeyChords

KeyChords are a special kind of keybindings, which require a keycombination (like Mod + E) pressed, which is then followed up by another key. This is common behaviour in Emacs and useful if you want a program to be launched with different arguments.

         # Gscreenshot
         KeyChord([mod], "g", [
             Key([], "s", lazy.spawn("flameshot gui")),
             Key([], "g", lazy.spawn("flameshot full -c -p /home/emma/Bilder/flameshots"))
         ]),

         # Emacs
         KeyChord([mod], "e", [
             Key([], "e", lazy.spawn("emacsclient -c -a 'emacs'")),
             Key([], "p", lazy.spawn("emacsclient -c -a 'emacs' ~/Projektordner/Hauptprojekt.org")),
             Key([], "q", lazy.spawn("emacsclient -c -a 'emacs' ~/.config/qtile/config.org"))
         ]),

         # Switching Keyboard Layouts
         KeyChord([mod], "space", [
             Key([], "e", lazy.spawn("setxkbmap -layout us -variant altgr-intl")),
             Key([], "p", lazy.spawn("setxkbmap -layout pl")),
             Key([], "g", lazy.spawn("setxkbmap -layout gr")),
         ]),

         # Steam
         KeyChord([mod], "z", [
             ### Steam Runtime
             Key([], "z", lazy.spawn("steam")),

             ### Hollow Knight
             Key([], "h", lazy.spawn("steam steam://rungameid/367520")),

             #### Code Vein
             Key([], "c", lazy.spawn("steam steam://rungameid/678960")),

             ### Red Dead Redemption
             Key([], "r", lazy.spawn("steam steam://rungameid/1174180")),

             ### Souls Series
             KeyChord([], "d", [
                 Key([], "1", lazy.spawn("steam steam://rungameid/570940")),
                 Key([], "3", lazy.spawn("steam steam://rungameid/374320"))
             ]),

             ### SAO Series
             KeyChord([], "s", [
                 Key([], "1", lazy.spawn("steam steam://rungameid/607890")),
                 Key([], "2", lazy.spawn("steam steam://rungameid/626690"))
             ]),

             ### Outlast Series
             KeyChord([], "o", [
                 Key([], "1", lazy.spawn("steam steam://rungameid/238320")),
                 Key([], "2", lazy.spawn("steam steam://rungameid/414700"))
             ]),


             ### Tomb Raider Series
             KeyChord([], "t", [
                 Key([], "1", lazy.spawn("steam steam://rungameid/203160")),
                 Key([], "2", lazy.spawn("steam steam://rungameid/391220")),
                 Key([], "3", lazy.spawn("steam steam://rungameid/750920"))
             ])
         ])
]

Layouts and Workspaces

Layouts, or 'groups' in Qtile are basically a set of rules how a Windows behave when on a workspace with a certain layout. They are on of the most important features when using a tiling WM, so this is a pretty big Deal

Workspaces and their default layouts

When the WM is started, each Workspace gets a default layout, which is the one they get started with. In this Code Block, the Names of the Workspaces are defined and their default layout gets assigned. There is a rule in my head, based on which the default layout gets assigned:

  • Normal Windows: Pretty much every window (except the ones discussed later) fall under this category. Workspaces on which I intend to only open normal windows get assigned with the 'Monadtall' layout. It's the default layout in most tiling WMs and tends to be the most useful one, as well.
  • Floating Windows: Some Windows should behave in a more "traditional" way, where they aren't automatically sized, but float over each other. This includes the Steam runtime. which tends to open up quite a few other windows, as well as programs like GIMP which behave similar.
group_names = [("Term", {'layout': 'monadtall'}),
               ("Web", {'layout': 'monadtall'}),
               ("Chat", {'layout': 'monadtall'}),
               ("Dev", {'layout': 'monadtall'}),
               ("Game", {'layout': 'floating'}),
               ("Text", {'layout': 'monadtall'}),
               ("Music", {'layout': 'monadtall'}),
               ("Misc", {'layout': 'floating'}),
               ("Qbit", {'layout': 'monadtall'})]

groups = [Group(name, **kwargs) for name, kwargs in group_names]

Numbering Workspaces

In Qtile's dafault behaviour, each workspace has the name of a letter, and by pressing mod + [workspace letter] the Workspace can be accessed. This does not work anymore, if we want to give our workspace more meaningful names (as defined above), because the string-comparisson does not work anymore. Also, if we want our letters to open programs, this is not very practical, since many of the letters were already used by basic Window Manager controls. If you want your workspaces to have meaningful names, as well as being accessed by using a number (Which is defalt behaviour in many other tiling Window Managers), a little work is needed.

for i, (name, kwargs) in enumerate(group_names, 1):
    keys.append(Key([mod], str(i), lazy.group[name].toscreen()))        # Switch to another group
    keys.append(Key([mod, "shift"], str(i), lazy.window.togroup(name))) # Send current window to another group

Layout theming

The Layout theme is an additional set of rules of a layout, like defining Window Margin or the border color. Since we want to have a unison look in all our layout, it makes sense to define the theme in a list (like a variable with multiple entries), which is then just called, when the workspaces are being set up on startup.

layout_theme = {"border_width": 2,
                "margin": 6,
                "border_focus": "e1acff",
                "border_normal": "1D2330"
                }

Layouts

In the following list, the layouts to be used in the session are defined. The layouts which are commented out will not be able to be used, while the others will.

layouts = [
    # layout.MonadWide(**layout_theme),
    # layout.Bsp(**layout_theme),
    # layout.Stack(stacks=2, **layout_theme),
    # layout.Columns(**layout_theme),
    # layout.Tile(shift_windows=True, **layout_theme),
    # layout.RatioTile(**layout_theme),
    # layout.VerticalTile(**layout_theme),
    # layout.Zoomy(**layout_theme),
    # layout.Matrix(**layout_theme),
    # layout.Max(**layout_theme),
    # layout.Stack(**layout_theme, num_stacks=2),
    layout.MonadTall(**layout_theme),
    layout.Floating(**layout_theme)
]

The Bar

The Bar is displayed on the top and displays useful information or just looking nice. In the following area, everything concerning the bar is configured. Note: This is NOT a traditional Taskbar, but a rather minimalist one, which displays basic information, in a way which is very riceable.

Colors

If you want your colors to be consistant and your config to be readable, just defining a list with every color meant to be used is the best way:

colors = [["#282c34", "#282c34"], # panel background
          ["#434758", "#434758"], # background for current screen tab
          ["#ffffff", "#ffffff"], # font color for group names
          ["#ff5555", "#ff5555"], # border line color for current tab
          ["#74438f", "#74438f"], # border line color for other tab and odd widgets
          ["#4f76c7", "#4f76c7"], # color for the even widgets
          ["#e1acff", "#e1acff"]] # window name

# Sets the format for Qtile's run prompt. The config doesn't work if this isn't set but I didn't want to make a new Source Block and a literrate explanation for this, since it's only a thing that makes stuff work
prompt = "{0}@{1}: ".format(os.environ["USER"], os.uname()[1])

Widgets

Widgets are what actually gets displayed in the bar. Loke with every design thing, having a list with the default settings is a good practice.

widget_defaults = dict(
    font="Mononoki Nerd Font",
    fontsize = 12,
    padding = 2,
    background=colors[2]
)

extension_defaults = widget_defaults.copy()

Initializing the bar

A function is created here, which will later be called at the startup. This basically just is an amalgamation of different Widgets spammed after one another.

def init_widgets_list():
    widgets_list = [
              widget.Sep(
                       linewidth = 0,
                       padding = 6,
                       foreground = colors[2],
                       background = colors[0]
                       ),
              widget.GroupBox(
                       font = "Mononoki Nerd Font",
                       fontsize = 9,
                       margin_y = 3,
                       margin_x = 0,
                       padding_y = 5,
                       padding_x = 3,
                       borderwidth = 3,
                       active = colors[2],
                       inactive = colors[2],
                       rounded = False,
                       highlight_color = colors[1],
                       highlight_method = "line",
                       this_current_screen_border = colors[3],
                       this_screen_border = colors [4],
                       other_current_screen_border = colors[0],
                       other_screen_border = colors[0],
                       foreground = colors[2],
                       background = colors[0]
                       ),
              widget.Prompt(
                       prompt = prompt,
                       font = "Mononoki Nerd Font",
                       padding = 10,
                       foreground = colors[3],
                       background = colors[1]
                       ),
              widget.Sep(
                       linewidth = 0,
                       padding = 40,
                       foreground = colors[2],
                       background = colors[0]
                       ),
              widget.WindowName(
                       foreground = colors[6],
                       background = colors[0],
                       padding = 0
                       ),
              widget.TextBox(
                       text='',
                       background = colors[0],
                       foreground = colors[4],
                       padding = -5,
                       fontsize = 37
                       ),
              widget.CPU(
                       foreground = colors[2],
                       background = colors[4],
                       padding = 3
                       ),
              widget.TextBox(
                       text = '',
                       background = colors[4],
                       foreground = colors[5],
                       padding = -5,
                       fontsize = 37
                       ),
              widget.Memory(
                       foreground = colors[2],
                       background = colors[5],
                       padding = 5
                       ),
              widget.TextBox(
                       text = '',
                       background = colors[5],
                       foreground = colors[4],
                       padding = -5,
                       fontsize = 37
                       ),
              widget.Net(
                       foreground = colors[2],
                       background = colors[4],
                       padding = 5
                       ),
              widget.TextBox(
                       text='',
                       background = colors[4],
                       foreground = colors[5],
                       padding = -5,
                       fontsize = 37
                       ),
              widget.TextBox(
                       text = " ⟳",
                       padding = 2,
                       foreground = colors[2],
                       background = colors[5],
                       fontsize = 14
                       ),
              widget.CheckUpdates(
                       update_interval = 1800,
                       distro = "Gentoo_eix",
                       display_format = "{updates} Updates",
                       foreground = colors[2],
                       execute = 'alacritty -e doas emerge --sync && doas emerge -auDN @world && doas emerge -acv',
                       background = colors[5]
                       ),
              widget.TextBox(
                       text = '',
                       background = colors[5],
                       foreground = colors[4],
                       padding = -5,
                       fontsize = 37
                       ),
              widget.CurrentLayout(
                       foreground = colors[2],
                       background = colors[4],
                       padding = 5
                       ),
              widget.TextBox(
                       text = '',
                       background = colors[4],
                       foreground = colors[5],
                       padding = -5,
                       fontsize = 37
                       ),
              widget.Clock(
                       foreground = colors[2],
                       background = colors[5],
                       execute = 'gnome-calendar',
                       format = "%A, %B %d  [ %H:%M:%S ]"
                       ),
              widget.Sep(
                       linewidth = 0,
                       padding = 10,
                       foreground = colors[0],
                       background = colors[5]
                       ),
              widget.TextBox(
                       text = '',
                       background = colors[5],
                       foreground = colors[4],
                       padding = -5,
                       fontsize = 37
                       ),
              widget.Systray(
                       background = colors[4],
                       padding = 2
                       ),
              widget.Sep(
                       linewidth = 0,
                       padding = 5,
                       foreground = colors[2],
                       background = colors[4]
                       ),
              ]
    return widgets_list

Setting everything up

Here are a couple of functions defined, which will be called once the Window manager starts

Initializing the screen

Everything we got configured earkier will now be out on the screens, when the Window Manager starts.

def init_widgets_screen1():
    widgets_screen1 = init_widgets_list()
    return widgets_screen1

def init_widgets_screen2():
    widgets_screen2 = init_widgets_list()
    widgets_screen2 = widgets_screen2[:-3] + widgets_screen2[-1:]
    return widgets_screen2

def init_screens():
    return [Screen(top=bar.Bar(widgets=init_widgets_screen1(), opacity=1.0, size=20)),
            Screen(top=bar.Bar(widgets=init_widgets_screen2(), opacity=1.0, size=20))]

if __name__ in ["config", "__main__"]:
    screens = init_screens()

Making Windows movable

If we want to move windows between groups, we need to have it in our config.

def window_to_prev_group(qtile):
    if qtile.currentWindow is not None:
        i = qtile.groups.index(qtile.currentGroup)
        qtile.currentWindow.togroup(qtile.groups[i - 1].name)

def window_to_next_group(qtile):
    if qtile.currentWindow is not None:
        i = qtile.groups.index(qtile.currentGroup)
        qtile.currentWindow.togroup(qtile.groups[i + 1].name)

The Mouse

While a Tiling Window Manager is mostly navigated by using the Keyboard, having mouse functionality for Window controls can be very useful, especially in floating mode. To get our mouse to do Window Controlling Stuff, we have need to define a few rules-

# A list, which tells the mouse what to do when which button is pressed together with the mod key.
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
main = None
follow_mouse_focus = False
bring_front_click = True
cursor_warp = False

Automatic floating

Some windows (like Splashscreens) are not meant to be displayed in a fullscreen way. By default, qtile does it anyway. By giving it these rules, it will let these windows float, no matter which layout the workspace has, the window is currently on.

floating_layout = layout.Floating(float_rules=[
    *layout.Floating.default_float_rules
])

Additional Window rules

If you want the window manager to do stuff (which doesn't directly has anything to do with one of the earlier things in this config), it's time to define these rules:

auto_fullscreen = True
focus_on_window_activation = "smart"
auto_minimize = False

Autostart

Programs like Daemons or the ones that set wallpapers should automatically be started when the window manager starts. The easiest way is to write a bash script (with execute permissions) that just does all the things for us. This will be called at startup and we're gut to go.

@hook.subscribe.startup_once
def start_once():
    home = os.path.expanduser('~')
    subprocess.Popen([home + '/.config/qtile/autostart.sh'])

The Window Manager

The config gets finished by defining thesystem internal name of the Window Manager. In my case, just using 'qtile' is fine for me, but if you are running into trubble with some Java-GUIs, you might want to consider changing it to 'LG3D'.

wmname = "qtile"