#+TITLE: Qtile Config #+PROPERTY: header-args :tangle config.py * 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. #+BEGIN_SRC python ################################################################################################################################################## ### 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.command import lazy from libqtile import layout, bar, widget, hook from libqtile.lazy import lazy from typing import List # noqa: F401 #+END_SRC ** 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. #+BEGIN_SRC python mod = "mod4" # Sets mod key to SUPER/WINDOWS myTerm = "alacritty" # My terminal of choice #+END_SRC * 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. #+BEGIN_SRC python keys = [ #+END_SRC *** 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 #+BEGIN_SRC python 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' ), #+END_SRC *** Other Programs Here are the most programms that I used listed. They are all launched by using Mod + [Key] #+BEGIN_SRC python 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' ), #+END_SRC ** 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. #+BEGIN_SRC python # 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")) ]) ]) ] #+END_SRC #+RESULTS: * 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. #+BEGIN_SRC python 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] #+END_SRC ** 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. #+BEGIN_SRC python 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 #+END_SRC ** 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. #+BEGIN_SRC python layout_theme = {"border_width": 2, "margin": 6, "border_focus": "e1acff", "border_normal": "1D2330" } #+END_SRC ** 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. #+BEGIN_SRC python 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) ] #+END_SRC * 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: #+BEGIN_SRC python 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]) #+END_SRC ** 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. #+BEGIN_SRC python widget_defaults = dict( font="Mononoki Nerd Font", fontsize = 12, padding = 2, background=colors[2] ) extension_defaults = widget_defaults.copy() #+END_SRC ** 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. #+BEGIN_SRC python 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 = "Arch", display_format = "{updates} Updates", foreground = colors[2], execute = 'alacritty -e doas pacman -Syu', 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 #+END_SRC * 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. #+BEGIN_SRC python 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() #+END_SRC ** Making Windows movable If we want to move windows between groups, we need to have it in our config. #+BEGIN_SRC python 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) #+END_SRC ** 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- #+BEGIN_SRC python # 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 #+END_SRC ** 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. #+BEGIN_SRC python floating_layout = layout.Floating(float_rules=[ *layout.Floating.default_float_rules ]) #+END_SRC ** 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: #+Begin_SRC python auto_fullscreen = True focus_on_window_activation = "smart" auto_minimize = False #+END_SRC ** 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. #+BEGIN_SRC python @hook.subscribe.startup_once def start_once(): home = os.path.expanduser('~') subprocess.Popen([home + '/.config/qtile/autostart.sh']) #+END_SRC ** 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'. #+BEGIN_SRC python wmname = "qtile" #+END_SRC