How to configure Qtile for efficient coding workflow in Linux

How to configure Qtile for efficient coding workflow in Linux

·

8 min read

There are so many tiling window manager out there, I have tried some of them and from my experience, Qtile is best fit for me and my workflow.

Little introduction about Qtile

Qtile is written and configured fully in python so as a python developer, I feel like home when using Qtile. There is no need to learn other language like lua to config awesome, haskell for xmonad, i3 config syntax for i3wm, xml for openbox, C for edit dwm source code, ...

Quick compare Qtile and the other, I must state some point that:

  • Qtile functionality is similar to xmonad, but I find it is easy to learn python than haskell
  • Both Qtile and i3wm are well documented but i3wm lack of some useful tiling layout
  • Qtile and openbox are two different concept of window manager, one is tiling, one is floating
  • Qtile and dwm have a huge difference in the way of config. Qtile configuring is straight forward meanwhile dwm require user skill to edit and patch source code, manual compile and install from that source

Install and setup

To install Qtile, type one of these command to your terminal (chose the right for your distro)

Ubuntu and Debian base

As official document of Qtile, they say that Ubuntu has drop Qtile package from main repo so we need to install qtile use pip

pip install xcffib
pip install qtile

Fedora

Since stable version of Qtile is not in fedora repo so from the document, they say that user should install from source instruction

Arch and Arch base

sudo pacman -S qtile

Void linux

sudo xbps-install qtile

Next go to default config and copy all the default configuration to $HOME/.config/qtile/config.py

Starting Qtile

There are two common ways

  • From any default login manager like gdm, lightdm, sddm, ... all have a button for you to choose the session you want to use
  • Use startx command from linux tty. For this, copy /etc/X11/xinit/xinitrc to $HOME/.xinitrc, remove these line if it has
    twm &
    xclock -geometry 50x50-1+1 &
    xterm -geometry 80x50+494+51 &
    xterm -geometry 80x20+494-0 &
    exec xterm -geometry 80x66+0+0 -name login
    
    Then add
    qtile start
    
    to the end of .xinitrc

Configurations

By default, Qtile has all config in a single config.py. I recommend to split it into three file

  • bar_type.py store all your design of the status bar
  • colors.py store the definition of your color scheme
  • config.py mainly define your shortcut

Note that these file must be in ~/.config/qtile/

Now I will share the content of my config file

config

from libqtile import bar, layout
from libqtile.config import Click, Drag, Group, Key, Match, Screen
from libqtile.lazy import lazy
from colors import color
import bar_type


mod = "mod4"

keys = [
    Key([mod], "h", lazy.layout.left(), desc="Move focus to left"),
    Key([mod], "l", lazy.layout.right(), desc="Move focus to right"),
    Key([mod], "j", lazy.layout.down(), desc="Move focus down"),
    Key([mod], "k", lazy.layout.up(), desc="Move focus up"),

    Key([mod, "shift"], "h", lazy.layout.swap_left(), desc="Move window to the left"),
    Key([mod, "shift"], "l", lazy.layout.swap_right(), desc="Move window to the right"),
    Key([mod, "shift"], "j", lazy.layout.shuffle_down(), desc="Move window down"),
    Key([mod, "shift"], "k", lazy.layout.shuffle_up(), desc="Move window up"),
    Key([mod], "b", lazy.layout.grow(), desc="Increase size of window"), Key([mod], "s", lazy.layout.shrink(),
desc="Decrease size of window"),
    Key([mod], "f", lazy.window.toggle_floating(), desc="Toggle floating",),
    Key([mod], "m", lazy.window.toggle_fullscreen(), desc="Toggle fullscreen"),
    Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),

    Key([mod], "q", lazy.window.kill(), desc="Kill focused window"),
    Key([mod], "Escape", lazy.spawn("xsecurelock"), desc='Lock screen'),

    Key([mod], "Return", lazy.spawn("alacritty"), desc="Launch terminal"),
    Key(["control"], "space", lazy.spawn("rofi -show run"), desc="Spawn a command"),
    Key([mod], "a", lazy.spawn("xfce4-appfinder"), desc="Find applications"),

    Key([], "XF86AudioRaiseVolume", lazy.spawn("amixer set Master 5%+"), desc='Volume up'),
    Key([], "XF86AudioLowerVolume", lazy.spawn("amixer set Master 5%-"), desc='Volume down'),
    Key([], "XF86AudioMute", lazy.spawn("amixer set Master toggle"), desc='Toggle volume'),
    Key([], "XF86AudioMicMute", lazy.spawn("amixer sset Capture toggle"), desc='Toggle volume'),
    Key([], "Print", lazy.spawn("xfce4-screenshooter"), desc='Screenshot'),
    Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
    Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"),
]
groups = [Group(i) for i in "12345678"]
for i in groups:
    keys.extend(
        [
            Key(
                [mod],
                i.name,
                lazy.group[i.name].toscreen(),
                desc="Switch to group {}".format(i.name),
            ),
            Key(
                [mod, "shift"],
                i.name,
                lazy.window.togroup(i.name, switch_group=True),
                desc="Switch to & move focused window to group {}".format(i.name),
            ),
        ]
    )

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()),
]

layouts = [
    layout.MonadTall(
        border_focus=color['ac'],
        border_normal=color['dark_ac'],
        border_width=1,
        margin=12,
    ),
    layout.MonadWide(
        border_focus=color['ac'],
        border_normal=color['dark_ac'],
        border_width=1,
        margin=12,
    ),
]

widget_defaults = dict(
    font="Fira Code",
    fontsize=22,
    padding=10,
)
extension_defaults = widget_defaults.copy()

screens = [
    Screen(
        top=bar.Bar(bar_type.bar_theme, 44, background=color['black']),
    ),
]

dgroups_key_binder = None
dgroups_app_rules = []
follow_mouse_focus = True bring_front_click = False cursor_warp = False floating_layout = layout.Floating( border_focus=color['ac'], float_rules=[ # Run the utility of "xprop" to see the wm class and name of an X client. *layout.Floating.default_float_rules, Match(wm_class="confirmreset"), # gitk Match(wm_class="makebranch"), # gitk Match(wm_class="maketag"), # gitk Match(wm_class="ssh-askpass"), # ssh-askpass Match(wm_class="tk"), #application in python tk Match(wm_class="python3.10"), #application in python Match(title="Application Finder"), # app finder Match(title="branchdialog"), # gitk Match(title="pinentry"), # GPG key password entry Match(title="steam"), # steam ] ) auto_fullscreen = True focus_on_window_activation = "smart" reconfigure_screens = True auto_minimize = False wl_input_rules = None wmname = "Qtile"
bar_type

from libqtile import widget
from colors import color

bar_theme = [
    widget.TextBox(
        text=' ',
        padding=0.1,
        background=color['black'],
    ),
    widget.GroupBox(
        background=color['black'],
        disable_drag=True,
        highlight_method='line',
        urgent_alert_method='text',
        highlight_color=color['black'],
        this_current_screen_border=color['fg'],
        this_screen_border=color['fg'],
        other_current_screen_border=color['fg'],
        other_screen_border=color['fg'],
        active=color['fg'],
        inactive=color['dark_fg'],
        urgent_text=color['fg'],
        use_mouse_wheel=False,
    ),
    widget.Spacer(),
    widget.Battery(
        format='Power {percent:2.0%}',
        foreground=color['fg'],
        background=color['black'],
        show_short_text=False,
        update_interval=60,
    ),
    widget.TextBox(
        text=' ',
        background=color['black'],
    ),
    widget.Clock(
        format="%a %I:%M %P",
        background=color['black'],
        foreground=color['fg'],
        update_interval=60,
    ),
    widget.TextBox(
        text=' ',
        padding=0.1,
        background=color['black'],
    ),
]

colors

color = {
    'ac':           '#689d6a',
    'dark_ac':      '#282828',
    'fg':           '#ffffff',
    'dark_fg':      '#808080',
    'black':        '#000000CC',
}

Let me explain details about these files

Config file

First section

from libqtile import bar, layout
from libqtile.config import Click, Drag, Group, Key, Match, Screen
from libqtile.lazy import lazy
from colors import color
import bar_type


mod = "mod4"

Just import module need for qtile configuration. Notice that I also include the bar and the color from bar_type module and colors module

Next, I set mod variable to mod4 which indicate the super key (usually the key with window logo in laptop). this variable is then use for key binding setting.

If you prefer Alt, set mod = "mod1"

Second section

keys = [
    Key([mod], "h", lazy.layout.left(), desc="Move focus to left"),
    Key([mod], "l", lazy.layout.right(), desc="Move focus to right"),
    Key([mod], "j", lazy.layout.down(), desc="Move focus down"),
    Key([mod], "k", lazy.layout.up(), desc="Move focus up"),

    Key([mod, "shift"], "h", lazy.layout.swap_left(), desc="Move window to the left"),
    Key([mod, "shift"], "l", lazy.layout.swap_right(), desc="Move window to the right"),
    Key([mod, "shift"], "j", lazy.layout.shuffle_down(), desc="Move window down"),
    Key([mod, "shift"], "k", lazy.layout.shuffle_up(), desc="Move window up"),
    Key([mod], "b", lazy.layout.grow(), desc="Increase size of window"), Key([mod], "s", lazy.layout.shrink(), desc="Decrease size of window"),
    Key([mod], "f", lazy.window.toggle_floating(), desc="Toggle floating",),
    Key([mod], "m", lazy.window.toggle_fullscreen(), desc="Toggle fullscreen"),
    Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),

    Key([mod], "q", lazy.window.kill(), desc="Kill focused window"),
    Key([mod], "Escape", lazy.spawn("xsecurelock"), desc='Lock screen'),

    Key([mod], "Return", lazy.spawn("alacritty"), desc="Launch terminal"),
    Key(["control"], "space", lazy.spawn("rofi -show run"), desc="Spawn a command"),
    Key([mod], "a", lazy.spawn("xfce4-appfinder"), desc="Find applications"),

    Key([], "XF86AudioRaiseVolume", lazy.spawn("amixer set Master 5%+"), desc='Volume up'),
    Key([], "XF86AudioLowerVolume", lazy.spawn("amixer set Master 5%-"), desc='Volume down'),
    Key([], "XF86AudioMute", lazy.spawn("amixer set Master toggle"), desc='Toggle volume'),
    Key([], "XF86AudioMicMute", lazy.spawn("amixer sset Capture toggle"), desc='Toggle volume'),
    Key([], "Print", lazy.spawn("xfce4-screenshooter"), desc='Screenshot'),
    Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
    Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"),
]

The keys array contains my key binding in the form

Key(prefix, key on keyboard, function to execute, description)

There are some Qtile builtin function like lazy.layout.left() to move focus to the left window, lazy.shutdown() to exit Qtile.

If you need execute bash command, Qtile also offer lazy.spawn("your bash command") to fit your need

Third section

groups = [Group(i) for i in "12345678"]
for i in groups:
    keys.extend(
        [
            Key(
                [mod],
                i.name,
                lazy.group[i.name].toscreen(),
                desc="Switch to group {}".format(i.name),
            ),
            Key(
                [mod, "shift"],
                i.name,
                lazy.window.togroup(i.name, switch_group=True),
                desc="Switch to & move focused window to group {}".format(i.name),
            ),
        ]
    )

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()),
]

The key and mouse binding are just the default, I only modify groups array, which will set how many workspace I want to use. You can see the workspace indicator in the left side of the bar.

Fourth section

layouts = [
    layout.MonadTall(
        border_focus=color['ac'],
        border_normal=color['dark_ac'],
        border_width=1,
        margin=12,
    ),
    layout.MonadWide(
        border_focus=color['ac'],
        border_normal=color['dark_ac'],
        border_width=1,
        margin=12,
    ),
]

This part define which layout I want to use, see built-in layout.

Layout is basically an algorithm to arrange windows on screen, I have try all the default but to me, only MonadTall and MonadWide is usefull for daily use.

Fifth section


widget_defaults = dict(
    font="Fira Code",
    fontsize=22,
    padding=10,
)
extension_defaults = widget_defaults.copy()

screens = [
    Screen(
        top=bar.Bar(bar_type.bar_theme, 44, background=color['black']),
    ),
]

The code told everything, this section define how the bar look.

The bar contains widget such as clock, battery info,... I will explain more in next few sections.

Last section of config file

dgroups_key_binder = None
dgroups_app_rules = []  
follow_mouse_focus = True
bring_front_click = False
cursor_warp = False
floating_layout = layout.Floating(
    border_focus=color['ac'],
    float_rules=[
        # Run the utility of "xprop" to see the wm class and name of an X client.
        *layout.Floating.default_float_rules,
        Match(wm_class="confirmreset"),  # gitk
        Match(wm_class="makebranch"),  # gitk
        Match(wm_class="maketag"),  # gitk
        Match(wm_class="ssh-askpass"),  # ssh-askpass
        Match(wm_class="tk"),  #application in python tk
        Match(wm_class="python3.10"),  #application in python 
        Match(title="Application Finder"),  # app finder
        Match(title="branchdialog"),  # gitk
        Match(title="pinentry"),  # GPG key password entry
        Match(title="steam"), # steam
    ]
)
auto_fullscreen = True
focus_on_window_activation = "smart"
reconfigure_screens = True
auto_minimize = False
wl_input_rules = None
wmname = "Qtile"

In this part, only focus on float_rules array. It manage which window will be float.

To indicate which type of window will be floated, install xorg-xprop, then type xprop after open the deserve window, the mouse will turn to + symol. Click on that window and all information include WM_CLASS(STRING) = "......." appear in terminal that run xprop.

Copy the string and add too Match(wm_class="class tring here")

Colors define file

This is a dictionary that store the color I want to use in Qtile

Bar_type file

The configuration of bar is an array that contain widgets, the index of widgets is counted from 0 which corresponding to the left most widget on the bar.

In my case, the left most is TextBox because I want some padding to left. The next one is GroupBox which show information about the workspace I'm using.

The widget is in the form widget.NameOfWidget( configuration for widget ), for example:

    widget.Battery(
        format='Power {percent:2.0%}',
        foreground=color['fg'],
        background=color['black'],
        show_short_text=False,
        update_interval=60,
    ),

The battery widget is used, the format to show on bar is Power "current percentage"%, update_interval is the amount of time count in second between two different updates.

You can find and add more widget here widget