QTILE(1)

📦
version 0.36.0 · 3 versions
, in ubuntu stonking

💖 QTILE(1) — the qtile manual, but make it a group chat 💖

okay bestie, welcome to the qtile man page, manual section 1, and I am so excited to walk you through this. grab a snack. 🍓


✨ NAME ✨

qtile — Qtile [image]

Qtile is a full-featured, hackable tiling window manager written and configured in Python. She's available both as an X11 window manager and also as a Wayland compositor <#wayland>. Versatile queen 👑

This documentation is designed to help you install and configure Qtile. Once it's up and running you'll probably want to start adding your own customisations to have it running exactly the way you want. and yes, "exactly the way you want" is the whole vibe here, that's the point 💅

You'll find a lot of what you need within these docs but, if you still have some questions, you can find support in the following places:


🌈 INSTALLATION 🌈

🧚 Distro Guides

Below are the preferred installation methods for specific distros. If you are running something else, please see Installing From Source. (she's down there, don't worry, we get to her 🫶)

🎀 Installing on Arch Linux

Stable versions of Qtile are currently packaged for Arch Linux. To install this package, run:

pacman -S qtile

Please see the ArchWiki for more information on Qtile: <https://wiki.archlinux.org/index.php/Qtile>.

🌸 Installing on Fedora 40 or later

Both Qtile X11 and Wayland stable versions can be installed via the DNF Package Manager.

If you want to pick a specific version (Such as the wayland version) then double-click Tab after "qtile".

dnf install qtile

🦋 Installing on Funtoo

Latest versions of Qtile are available on Funtoo. To install it, run:

emerge -av x11-wm/qtile

You can also install the development version from GitHub:

echo "x11-wm/qtile-9999 **" >> /etc/portage/package.accept_keywords
emerge -av qtile

💜 Customize

You can customize your installation with the following useflags:

  • dbus
  • widget-khal-calendar
  • widget-imap
  • widget-keyboardkbdd
  • widget-launchbar
  • widget-mpd
  • widget-mpris
  • widget-wlan

The dbus useflag is enabled by default. Disable it only if you know what it is and know you don't use/need it. no cap, don't go turning things off you don't understand 🙈

All widget-* useflags are disabled by default because these widgets require additional dependencies while not everyone will use them. Enable only widgets you need to avoid extra dependencies thanks to these useflags. minimal girlie behaviour, we respect it ✨

Visit Funtoo Qtile documentation <https://www.funtoo.org/Package:Qtile> for more details on Qtile installation on Funtoo.

💖 Installing on Ubuntu or Debian

Newer versions of Debian-based distros (Debian >= 13, Ubuntu >= 25.04) have Qtile packages. Exact versions for each distro release can be found either on packages.debian.org <https://packages.debian.org/search?keywords=qtile> or packages.ubuntu.com <https://packages.ubuntu.com/search?keywords=qtile&searchon=names&section=all>

🕰️ Older Releases

Ubuntu and Debian >=11 comes with the necessary packages for installing Qtile. Starting from a minimal Debian installation, the following packages are required:

sudo apt install xserver-xorg xinit
sudo apt install libpangocairo-1.0-0
sudo apt install uv  # if your distro doesn't have uv, try:
# curl -LsSf https://astral.sh/uv/install.sh | sh

Either Qtile can then be downloaded from the package index or the Github repository can be used, see Installing From Source <#installing-from-source>:

uv tool install qtile

🌟 Installing on Slackware

Qtile is available on the SlackBuilds.org <https://slackbuilds.org/repository/14.2/desktop/qtile/> as:

  • qtile — Package Name: qtile, Description: stable branch (release)

(okay so that was a table in the original, and we don't do tables here, but the tea is: the package is called qtile and it's the stable branch, i.e. the release. 🫶)

🍓 Using slpkg (third party package manager)

The easy way to install Qtile is with slpkg <https://github.com/dslackw/slpkg>. For example:

slpkg -s sbo qtile

🧵 Manual installation

Download dependencies first and install them. The order in which you need to install is:

  • pycparser
  • cffi
  • futures
  • python-xcffib
  • trollius
  • cairocffi
  • qtile

Please see the HOWTO for more information on SlackBuild Usage HOWTO <https://slackbuilds.org/howto/>.

🐚 Installing on FreeBSD

Qtile is available via FreeBSD Ports <https://www.freshports.org/x11-wm/qtile/>. It can be installed with

pkg install qtile

❄️ Installing on NixOS

Qtile is available in the NixOS repos. To set qtile as your window manager, include this in your configuration.nix file:

services.xserver.windowManager.qtile.enable = true;

Other options for qtile can be declared within the services.xserver.windowManager.qtile attribute set.

You may add extra packages in the qtile python environment by putting them in the extraPackages list.

services.xserver.windowManager.qtile = {
  enable = true;
  extraPackages = python3Packages: with python3Packages; [
    qtile-extras
  ];
};

The Qtile package creates desktop files for both X11 and Wayland, to use one of the backends choose the right session in your display manager.

The configuration file can be changed from its default location ($XDG_CONFIG/qtile/config.py) by setting the configFile attribute:

qtile = {
  enable = true;
  configFile = ./my_qtile_config.py;
};
Note: Some options may change over time, please refer to see all the options for the latest stable: search.nixos.org <https://search.nixos.org/options?channel=24.05&from=0&size=50&sort=relevance&type=packages&query=qtile> if you have any doubt. 🔍

🏠 Home manager

If you are using home-manager, you can copy your qtile configuration by using the following:

xdg.configFile."qtile/config.py".source = ./my_qtile_config.py;

or, if you have a directory containing multiple python files:

xdg.configFile."qtile" = {
  source = ./src;
  recursive = true;
};

🌬️ Flake

Qtile provides a Nix flake in its repository. This can be useful for:

  • Running a bleeding-edge version of Qtile by specifying the flake input as the package.
  • Hacking on Qtile using a Nix develop shell.
Note: Nix flakes are still an experimental NixOS feature, but they are already widely used. This section is intended for users who are already familiar with flakes. ⚠️ (if that's not you, that's okay bestie, come back later 💜)

To run a bleeding-edge version of Qtile with the flake, add the Qtile repository to your flake inputs and define the package. For example:

{
  description = "A very basic flake";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";

    qtile-flake = {
      url = "github:qtile/qtile";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      self,
      nixpkgs,
      qtile-flake,
    }:
    {
      nixosConfigurations.demo = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";

        modules = [
          (
            {
              config,
              pkgs,
              lib,
              ...
            }:
            {
              services.xserver = {
                enable = true;
                windowManager.qtile = {
                  enable = true;
                  package = qtile-flake.packages.${pkgs.stdenv.hostPlatform.system}.default;
                };
              };

              # make qtile X11 the default session
              services.displayManager.defaultSession = lib.mkForce "qtile";

              # rest of your NixOS config
            }
          )
        ];
      };
    };
}

This flake can also be tested with a vm:

sudo nixos-rebuild build-vm --flake .#demo

Gives you a script to run that runs Qemu to test your config. For this to work you have to set a user with a password. 🔑

See also:

For a detailed walkthrough on setting up Qtile with flakes—from basic installation to package overrides, see Gurjaka's Qtile Flake Guide <https://gurjaka.codeberg.page/blog.html?post=qtile-flake>.

To hack on Qtile with Nix, simply run nix develop in a checkout of the repo. In the development shell, there are a few useful things:

  • qtile-run-tests-wayland: Run all Wayland tests
  • qtile-run-tests-x11: Run all X11 tests

⚙️ Running from systemd

This case will cover automatic login to Qtile after booting the system without using display manager. It logins in virtual console and init X by running through session.

🔓 Automatic login to virtual console

To get login into virtual console as an example edit getty service by running systemctl edit getty@tty1 and add instructions to /etc/systemd/system/getty@tty1.service.d/override.conf:

[Service]
ExecStart=
ExecStart=-/usr/bin/agetty --autologin username --noclear %I $TERM

username should be changed to current user name. don't leave it as literally "username", i'm begging 🫠

Check more for other examples <https://wiki.archlinux.org/index.php/Getty#Automatic_login_to_virtual_console>.

🚀 Autostart X session

After login X session should be started. That can be done by .bash_profile if bash is used or .zprofile in case of zsh. Other shells can be adjusted by given examples.

if systemctl -q is-active graphical.target && [[ ! $DISPLAY && $XDG_VTNR -eq 1 ]]; then
  exec startx
fi

And to start Qtile itself .xinitrc should be fixed:

# some apps that should be started before Qtile, ex.
#
#   [[ -f ~/.Xresources ]] && xrdb -merge ~/.Xresources
#   ~/.fehbg &
#   nm-applet &
#   blueman-applet &
#   dunst &
#
# or
#
#   source ~/.xsession

exec qtile start

🛠️ Installing From Source

🐍 Python interpreters

We aim to always support the last three versions of CPython, the reference Python interpreter. We usually support the latest stable version of PyPy <https://www.pypy.org/> as well. You can check the versions and interpreters we currently run our test suite against in our CI configuration file <https://github.com/qtile/qtile/blob/master/.github/workflows/ci.yml>. ✨


okay bestie we are NOT done yet, part 1 is just the setup arc, there's so much more coming 💅

There are genuinely not that many differences between versions, bestie — aside from which Python features you can or can't use in your config. PyPy is lowkey faster at runtime than any corresponding CPython version under most circumstances, especially for bits of Python code that get run over and over and over again. CPython, on the other hand, starts up faster than PyPy and has better compatibility for external libraries. So like… pick your fighter 💅


✨ Core Dependencies ✨

Okay here's the tea on Qtile's core runtime dependencies, plus the package names that hand them to you on Ubuntu. Big important note: Qtile can run with one of two backends — X11 and Wayland — so you only actually need the dependencies of one of these. Not both. Chill. 🧘

The Core Core 🍓

X11 Things 🦋

Wayland Things 🌸


🫶 Qtile 🫶

Once all those deps are in place, you can finally install the stable version of qtile straight from PyPI:

uv tool install qtile

Or, if you want sets of dependencies bundled in:

uv tool install qtile[widgets]  # for all widget dependencies
uv tool install qtile[all]      # for all dependencies

Or install qtile-git instead, if you're feeling spicy 🔥:

git clone https://github.com/qtile/qtile.git
cd qtile
uv tool install .                               # for minimal dependencies
uv tool install .[dev,widgets,optional-core]    # for all dependencies

Installing other dependencies 🌈

Heads up bestie, this one's important: if you use uv to install qtile, then any python modules you want to use with it must be installed in the same environment. You've got a few options for making that happen:

At installation time:

# Install package from pypi
uv tool install --with package-name qtile  # can use qtile[widgets] etc. as above

# Install from github repo
uv tool install --with git+https://github.com/elParaguayo/qtile-extras/ .

# Install from custom requirements file
uv tool install --with-requirements /path/to/requirements.txt .

Installing packages after installation is a bit more complicated and, ngl, does not seem to be officially supported when you're using uv tool. However, this following code should work:

cd $(uv tool dir)/qtile
uv pip install package-name

✨ Starting Qtile ✨

There are several ways to start Qtile. The most common one is just… through an entry in your X session manager's menu. The default Qtile behavior can be invoked by creating a qtile.desktop file (https://github.com/qtile/qtile/blob/master/resources/qtile.desktop) in /usr/share/xsessions.

A second way to start Qtile is a custom X session. This one slays because it lets you invoke Qtile with custom arguments, and it also lets you do whatever setup you want before Qtile starts — like special keyboard bindings (hello, mapping caps lock to control), setting your desktop background, all of that. If you're using an X session manager, you still may need to create a custom.desktop file similar to the qtile.desktop file above, but with Exec=/etc/X11/xsession. Then create your own ~/.xsession. There are several examples of user defined xsession s in the qtile-examples (https://github.com/qtile/qtile-examples) repository, if you need inspo 🎀

If there's no display manager such as SDDM, LightDM or other, and you need to start Qtile directly from ~/.xinitrc, do that by adding exec qtile start at the end.

In very special cases — ex. Qtile crashing during a session — the suggestion would be to start through a loop to save your running applications:

while true; do
    qtile
done

🌊 Wayland 🌊

Qtile can be run as a Wayland compositor rather than an X11 window manager. For this, Qtile uses wlroots (https://gitlab.freedesktop.org/wlroots/wlroots), a compositor library which is undergoing fast development. Be aware that some distributions package outdated versions of wlroots. We are trying our best to keep up with the latest wlroots release, fr fr.

With the Wayland dependencies in place, Qtile can be run either from a TTY, or within an existing X11 or Wayland session, where it will run inside a nested window:

qtile start -b wayland

See the Wayland (https://docs.qtile.org/#wayland) page for more information on running Qtile as a Wayland compositor.

Similar to the xsession example above, a wayland session file can be used to start qtile from a login manager. To use this, you should create a qtile-wayland.desktop file (https://github.com/qtile/qtile/blob/master/resources/qtile-wayland.desktop) in /usr/share/wayland-sessions.


💅 udev rules 💅

Qtile has widgets that support managing various kinds of hardware — LCD backlight, keyboard backlight, battery charge thresholds — via the kernel's exposed sysfs endpoints. Cute, right? But, to make any of that actually work, Qtile needs permission to write to these files. There's a udev rules file at /resources/99-qtile.rules in the tree, which users installing from source will want to install at /etc/udev/rules.d/ on their system. You can install it manually with:

# copy the in-tree udev rules file to the right place to make udev see it
cat ./resources/99-qtile.rules | sudo tee /etc/udev/rules.d/99-qtile.rules

🌊 RUNNING QTILE AS A WAYLAND COMPOSITOR

okay bestie, real talk: some stuff might just not be implemented yet in the Wayland compositor. It's giving "we're working on it" energy, but not in a bad way!! For the current state of development, go peek at the Wayland GitHub Project Board <https://github.com/orgs/qtile/projects/2>. Also go check out the unresolved Wayland-specific issues <https://github.com/qtile/qtile/issues?q=is%3Aissue+is%3Aopen+label%3A%22core%3A+wayland%22> and the troubleshooting section <#debugging-wayland> for the tips on how to actually debug Wayland problems, because you will want them, no shade 💜

🎀 Backend-Specific Configuration

So you want one config file that plays nice with different backends, but you want some options to hit different per backend? Iconic. You can literally just check the name of the current backend in your config, like this:

from libqtile import qtile

if qtile.core.name == "x11":
    term = "urxvt"
elif qtile.core.name == "wayland":
    term = "foot"

🌈 Running X11-Only Programs

Qtile supports XWayland, but — and this is the fine print, bestie — it requires that wlroots was built with XWayland support, and that XWayland is installed on the system from startup. Also: XWayland gets started the first time it's needed. So like, don't panic if you don't see it yet. Chill ✨

💀 XWayland windows sometimes don't receive mouse events

There is currently a known bug (<https://github.com/qtile/qtile/issues/3675>) which causes pointer events (hover/click/scroll) to propagate to the wrong window when switching focus. Not us guessing, that's just what it does rn 😭. So if your clicks are going feral, that's why.

🦋 Input Device Configuration

Wanna change keyboard configuration while everything's already running? You can use the core's set_keymap command — see below, babe 💅

👑 Core Commands

Go see the Wayland backend <#wayland-backend-commands> section in the API Commands documentation. That's where all the juice is.


🚧 CURRENT WAYLAND SITUATION IN QTILE

Okay so, everybody keeps asking what the status is of the Wayland backend, and honestly? Fair. Understandably so, because we were previously stuck on an old Wlroots (that's the Wayland library we use) version for quite some time. Not cute.

So to ease the development of the Wayland backend and implement new Wlroots versions faster, the Wayland backend has previously been rewritten from Python to C. Bonus: that should also improve performance in certain areas. The downside of this approach is that the backend is now written in two programming languages and we have to have some glue in between. However — this tradeoff is worth it, because on the long-term it's quicker than updating the Python Wlroots bindings every time a new Wlroots version is released. Periodt.

The goal of the backend is the following:

  • Move as much Wayland specifics to C
  • Keep the Qtile specific stuff in Python: layouts, config, window states (floating, tiled etc)
  • Explore new features by now being written in C

The status of this backend can be viewed on GitHub:

To help contributing to this backend there are some useful links:

Even if you're new to C or Wayland, this is a great chance to grow and work on something meaningful in open source. Bestie, that's your main character era, I don't make the rules 💖


🔍 TROUBLESHOOTING

🫶 So something has gone wrong... what do you do?

When Qtile is running, it logs error messages (and other messages) to its log file. This is found at ~/.local/share/qtile/qtile.log. This is the first place to check to see what is going on. If you are getting unexpected errors from normal usage or your configuration (and you're not doing something wacky) and believe you have found a bug, then please report a bug <#reporting>.

If you are hacking on Qtile <#hacking> and you want to debug your changes, this log is your best friend, fr fr. You can send messages to the log from within libqtile by using the logger:

from libqtile.log_utils import logger

logger.warning("Your message here")
logger.warning(variable_you_want_to_print)

try:
    # some changes here that might error
except Exception:
    logger.exception("Uh oh!")

logger.warning is convenient because its messages will always be visible in the log. logger.exception is helpful because it will print the full traceback of an error to the log. By sticking these amongst your changes you can look more closely at the effects of any changes you made to Qtile's internals. Chef's kiss 🔥

🧚 X11: Capturing an xtrace

Occasionally, a bug will be low level enough to require an xtrace of Qtile's conversations with the X server. To capture one of these, create an xinitrc or similar file with:

exec xtrace qtile >> ~/qtile.log

This will put the xtrace output in Qtile's logfile as well. You can then demonstrate the bug, and paste the contents of this file into the bug report. Easy peasy ✨

⚠️ Note that xtrace may be named x11trace on some platforms, for example, on Fedora and Arch. So don't be out here thinking it's missing — it just changed its government name 💅

🌸 Debugging in Wayland

To get incredibly verbose output of communications between clients and the server, you can set WAYLAND_DEBUG=1 in the environment before starting the process. Heads up: this applies to the server itself, so be aware that running qtile with this set will generate lots of output for Qtile and all clients that it launches. If you're including this output with a bug report please try to cut out just the relevant portions. Plsplspls, your maintainers are begging 🙈

If you're hacking on Qtile and would like this debug log output for it rather than any clients, it can be helpful to run the helper script at scripts/wephyr in the source from an existing session. You can then run clients from another terminal using the WAYLAND_DISPLAY value printed by Qtile, so that the debug logs printed by Qtile are only the server's.

If you suspect a client may be responsible for a bug, it can be helpful to look at the issue trackers for other compositors, such as sway <https://github.com/swaywm/sway/issues>. Similarly if you're hacking on Qtile's internals and think you've found an unexpected quirk it may be helpful to search the issue tracker for wlroots <https://gitlab.freedesktop.org/wlroots/wlroots/-/issues>. Detective era 🕵️💜


🚪 ENTRY POINTS

Qtile uses a subcommand structure; various subcommands are listed below. Additionally, two other commands available in the scripts/ section of the repository are also documented below.

⭐ qtile start

This is the entry point for the window manager, and what you should run from your .xsession or similar. This will make an attempt to detect if qtile is already running and fail if it is. See qtile start --help for more details.

🐚 qtile shell

The Qtile command shell is a command-line shell interface that provides access to the full complement of Qtile command functions. The shell features command name completion, and full command documentation can be accessed from the shell itself. The shell uses GNU Readline when it's available, so the interface can be configured to, for example, obey VI keybindings with an appropriate .inputrc file. See the GNU Readline documentation for more information.

The shell presents a filesystem-like interface to the command graph — the builtin cd and ls commands act like their familiar shell counterparts:

> ls
layout/  widget/  screen/  bar/     window/  group/

> cd screen
layout/  window/  bar/  widget/

> cd ..
/

> ls
layout/  widget/  screen/  bar/     window/  group/

If you try to access an object that has no "default" value then you will see an error message:

> ls
layout/  widget/  screen/  bar/     window/  group/

> cd bar
Item required for bar

> ls bar
bar[bottom]/

> cd bar/bottom
bar['bottom']> ls
screen/  widget/

Please refer to Navigating the command graph <#object-graph-selectors> for a summary of which objects need a specified selector and the type of selector required. Using ls will show which selectors are available for an object. Please see below for an explanation about how Qtile displays shell paths.

Alternatively, the items() command can be run on the parent object to show which selectors are available. The first value shows whether a selector is optional (False means that a selector is required) and the second value is a list of selectors:

> ls
layout/  widget/  screen/  bar/     window/  group/

> items(bar)
(False, ['bottom'])

🌸 Displaying the shell path

Note that the shell provides a "short-hand" for specifying node keys (as opposed to children). The following is a valid shell path:

> cd group/4/window/31457314

The command prompt will, however, always display the Python node path that should be used in scripts and key bindings:

group['4'].window[31457314]>

📖 Live Documentation

The shell help command provides the canonical documentation for the Qtile API:

> cd layout/1

layout[1]> help
help command   -- Help for a specific command.

Builtins
========
cd    exit  help  ls    q     quit

Commands for this object
========================
add           commands      current       delete        doc
down          get_info      items         next          previous
rotate        shuffle_down  shuffle_up    toggle_split  up

layout[1]> help previous
previous()
Focus previous stack.

🔄 qtile migrate

qtile migrate is a tool to help users update their configs to reflect any breaking changes/deprecations introduced in later versions.

The tool can automatically apply updates but it can also be used to highlight impacted lines, allowing users to update their configs manually.

The tool can take a whole lil' menu of options when it runs, bestie:

  • -c, --config — sets the path to the config file. Default: ~/.config/qtile/config.py.
  • --list-migrations — lists every migration that's available for the tool to run. Default: n/a.
  • --info ID — shows you way more detail about the migration that's implemented by ID. Default: n/a.
  • --after-version VERSION — only runs the migrations relating to changes implemented after release VERSION. Default: not set (aka it runs all the migrations).
  • -r ID, --run-migrations ID — runs the selected migrations identified by ID. Comma separated list if you're using multiple values. Default: not set (aka runs all migrations).
  • --yes — automatically applies changes without asking you for confirmation. Default: not set (aka users will need to confirm application of changes).
  • --show-diff — when used with --yes, this will cause diffs to still be shown, for information purposes only. Default: not set.
  • --no-colour — disables colour output for the diff. Default: not set.
  • --lint — outputs linting lines showing the location of changes. No changes are made to the config. Default: not set.

🌈 Available migrations 🌈

The following migrations are currently available.

🏃♀️ Running migrations 🏃♀️

So assuming your config file is chilling in the default location, running qtile migrate is enough to kick off the whole migration process. Slay.

Let's say you had a config file with these contents, okay:

import libqtile.command_client

keys = [
    KeyChord(
        [mod],
        "x",
        [Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink())],
        mode="Resize layout",
    )
]

qtile.cmd_spawn("alacritty")

Running qtile migrate will run each available migration, and wherever the migration would result in changes, a diff gets shown and you'll be asked whether you wanna apply those changes. Cute, right?

UpdateKeychordArgs: Updates ``KeyChord`` argument signature.

--- original
+++ modified
@@ -5,7 +5,8 @@

        [mod],
        "x",
        [Key([], "Up", lazy.layout.grow()), Key([], "Down", lazy.layout.shrink())],
-        mode="Resize layout",
+        name="Resize layout",
+    mode=True,
    )
]

Apply changes? (y)es, (n)o, (s)kip file, (q)uit.

You can see from that output up there that you get shown the name of the migration being applied and what its purpose is, along with all the changes that will be implemented. Very transparent of it 💅

If you pick quit, the migration gets stopped and any changes that were already applied get reversed.

Once all the migrations have been run on a file, you'll then be asked whether you wanna save the changes to that file:

Save all changes to config.py? (y)es, (n)o.

At the end of the migration, backups of your original config will still be sitting in your config folder. NB these will be overwritten if you re-run qtile migrate. So like, don't come crying to me 😭

🔍 Linting 🔍

If you don't want the script modifying your config directly, you can use the --lint option to show you exactly where changes are required. No touchy, just looky.

Running qtile migrate --lint on the same config shown above will give you this output:

config.py:
[Ln 1, Col 7]: The 'libqtile.command_*' modules have been moved to 'libqtile.command.*'. (ModuleRenames)
[Ln 8, Col 8]: The use of mode='mode name' for KeyChord is deprecated. Use mode=True and value='mode name'. (UpdateKeychordArgs)
[Ln 12, Col 6]: Use of 'cmd_' prefix is deprecated. 'cmd_spawn' should be replaced with 'spawn' (RemoveCmdPrefix)

📖 Explanations of migrations 📖

The table below provides more detail of the available migrations.


🛠️ qtile cmd-obj 🛠️

This is a simple lil' tool that exposes qtile.command functionality to the shell. You can use it standalone, or inside other shell scripts. Super versatile of her 🫶

✨ How it works ✨

qtile cmd-obj works by selecting a command object and calling a specified function of that object.

As per Architecture, Qtile's command graph has seven nodes: layout, window, group, bar, widget, screen, and a special root node. These are the objects that can be accessed via qtile cmd-obj.

Running the command against a selected object without a function (-f) will run the help command and list the commands available to that object. Commands shown with an asterisk ("*") require arguments to be passed via the -a flag. Iykyk 💀

🎯 Selecting an object 🎯

With the exception of cmd, all objects need an identifier so the correct object can be selected. Refer to Navigating the command graph for more information. Go read it bestie, it's giving required reading.

Note: You'll see from the graph on Architecture that certain objects can be accessed from other objects. For example, qtile cmd-obj -o group term layout will list the commands for the current layout on the term group.

ℹ️ Information on functions ℹ️

Running a function with the -i flag will give you additional detail about that function — like what it does and what arguments it expects. Receipts, basically 🧾

📨 Passing arguments to functions 📨

Arguments can be passed to a function by using the -a flag. For example, to change the label for the group named "1" to "A", you would run qtile cmd-obj -o group 1 -f set_label -a A.

Warning: It is not currently possible to pass non-string arguments to functions via qtile cmd-obj. Doing so will result in an error. Don't try it, you WILL get humbled 😭

💫 Examples: 💫

Output of qtile cmd-obj -h

usage: qtile cmd-obj [-h] [--object OBJ_SPEC [OBJ_SPEC ...]]
                 [--function FUNCTION] [--args ARGS [ARGS ...]] [--info]

Simple tool to expose qtile.command functionality to shell.

optional arguments:
  -h, --help            show this help message and exit
  --object OBJ_SPEC [OBJ_SPEC ...], -o OBJ_SPEC [OBJ_SPEC ...]
                        Specify path to object (space separated). If no
                        --function flag display available commands.
  --function FUNCTION, -f FUNCTION
                        Select function to execute.
  --args ARGS [ARGS ...], -a ARGS [ARGS ...]
                        Set arguments supplied to function.
  --info, -i            With both --object and --function args prints
                        documentation for function.

Examples:
 qtile cmd-obj
 qtile cmd-obj -o root  # same as above, root node is default
 qtile cmd-obj -o root -f prev_layout -i
 qtile cmd-obj -o root -f prev_layout -a 3 # prev_layout on group 3
 qtile cmd-obj -o group 3 -f focus_back
 qtile cmd-obj -o widget textbox -f update -a "New text"
 qtile cmd-obj -f restart # restart qtile

Output of qtile cmd-obj -o group 3

okay bestie, so when you aim cmd-obj at a group (here, group 3 — your third workspace, she's booked, she's busy, she's thriving 💅), this is the full menu of functions you can pull on her. quick heads up: those little * marks are straight from the original output, so we're keepin' em exactly where they were 🙈

  • -o group 3 -f commands — hands you back a list of every command this object even accepts ✨
  • -o group 3 -f doc * — gives you the documentation for a command name you specify 📖
  • -o group 3 -f eval * — evaluates code in the exact same context as this function 🔥
  • -o group 3 -f focus_back — Focus the window that had focus before the current one got it.
  • -o group 3 -f focus_by_name * — Focus the first window with the given name. Do nothing if the name is
  • -o group 3 -f function * — calls a function, passing the current object in as the argument 🫶
  • -o group 3 -f info — Returns a dictionary of info for this group
  • -o group 3 -f info_by_name * — Get the info for the first window with the given name without giving it
  • -o group 3 -f items * — Returns a list of contained items for the specified name
  • -o group 3 -f next_window — Focus the next window in group.
  • -o group 3 -f prev_window — Focus the previous window in group.
  • -o group 3 -f set_label * — Set the display name of current group to be used in GroupBox widget.
  • -o group 3 -f setlayout — no description attached, she's mysterious like that 🦋
  • -o group 3 -f switch_groups * — Switch position of current group with name
  • -o group 3 -f toscreen * — Pull a group to a specified screen.
  • -o group 3 -f unminimize_all — Unminimise all windows in this group
two of those descriptions just straight-up cut off mid-sentence in the original (focus_by_name and info_by_name). that's not me being lazy, that's the page. mystery, mama 🙈

Output of qtile cmd-obj -o root

and now the root object, aka the main character 👑. this is the big one — everything from log levels to restarting the whole dang thing lives here. same deal with the * marks, kept verbatim:

  • -o root -f add_rule * — Add a dgroup rule, returns rule_id needed to remove it
  • -o root -f addgroup * — Add a group with the given name
  • -o root -f commands — Returns a list of possible commands for this object
  • -o root -f critical — Set log level to CRITICAL
  • -o root -f debug — Set log level to DEBUG
  • -o root -f delgroup * — Delete a group with the given name
  • -o root -f display_kb * — Display table of key bindings
  • -o root -f doc * — Returns the documentation for a specified command name
  • -o root -f error — Set log level to ERROR
  • -o root -f eval * — Evaluates code in the same context as this function
  • -o root -f findwindow * — Launch prompt widget to find a window of the given name
  • -o root -f focus_by_click * — Bring a window to the front
  • -o root -f function * — Call a function with current object as argument
  • -o root -f get_info — Prints info for all groups
  • -o root -f get_state — Get pickled state for restarting qtile
  • -o root -f get_test_data — Returns any content arbitrarily set in the self.test_data attribute.
  • -o root -f groups — Return a dictionary containing information for all groups
  • -o root -f hide_show_bar * — Toggle visibility of a given bar
  • -o root -f info — Set log level to INFO
  • -o root -f internal_windows — Return info for each internal window (bars, for example)
  • -o root -f items * — Returns a list of contained items for the specified name
  • -o root -f list_widgets — List of all addressible widget names
  • -o root -f next_layout * — Switch to the next layout.
  • -o root -f next_screen — Move to next screen
  • -o root -f next_urgent — Focus next window with urgent hint
  • -o root -f pause — Drops into pdb
  • -o root -f prev_layout * — Switch to the previous layout.
  • -o root -f prev_screen — Move to the previous screen
  • -o root -f qtile_info — Returns a dictionary of info on the Qtile instance
  • -o root -f qtilecmd * — Execute a Qtile command using the client syntax
  • -o root -f remove_rule * — Remove a dgroup rule by rule_id
  • -o root -f restart — Restart qtile
  • -o root -f run_extension * — Run extensions
  • -o root -f run_external * — Run external Python script
  • -o root -f screens — Return a list of dictionaries providing information on all screens
  • -o root -f shutdown — Quit Qtile
  • -o root -f simulate_keypress * — Simulates a keypress on the focused window.
  • -o root -f spawn * — Run cmd in a shell.
  • -o root -f spawncmd * — Spawn a command using a prompt widget, with tab-completion.
  • -o root -f status — Return "OK" if Qtile is running
  • -o root -f switch_groups * — Switch position of groupa to groupb
  • -o root -f switchgroup * — Launch prompt widget to switch to a given group to the current screen
  • -o root -f sync — Sync the X display. Should only be used for development
  • -o root -f to_layout_index * — Switch to the layout with the given index in self.layouts.
  • -o root -f to_screen * — Warp focus to screen n, where n is a 0-based screen number
  • -o root -f togroup * — Launch prompt widget to move current window to a given group
  • -o root -f tracemalloc_dump — Dump tracemalloc snapshot
  • -o root -f tracemalloc_toggle — Toggle tracemalloc status
  • -o root -f warning — Set log level to WARNING
  • -o root -f windows — Return info for each client window

qtile repl

this one creates a REPL client — that's a Read-Evaluate-Print-Loop, bestie, and yes she loops 🔁 — with access to all of qtile's internals. it's giving backstage pass 🎀

running qtile repl starts the server with access to the qtile instance. but you can also sneak your own extra stuff into the REPL's environment by starting the REPL server from your config before you launch the client, like this:

keys = [
    ...,
    Key([mod], "z", lazy.start_repl_server(locals_dict=globals())),
    ...
]

which means every variable from your config gets dumped into the REPL environment, so you can go trigger your own functions and everything. iconic 🌈

the REPL server runs on port 41414. that's hardcoded for now, but the page says we expect to make changes to qtile's wire format in the future 👀


qtile run-cmd

run a command and let the rules apply to the new windows — like, you can start a window straight into a specific group, make it floating, make it intrusive, whatever fits the vibe 💜

the windows must have NET_WM_PID, no exceptions, that's the whole requirement fr fr
# run xterm floating on group "test-group"
qtile run-cmd -g test-group -f xterm

qtile top

qtile top is a top-like tool that measures the memory usage of Qtile's internals 📊

Note: to use qtile top you need tracemalloc enabled. you can do that by setting the environmental variable PYTHONTRACEMALLOC=1 before starting qtile. OR you can force-start tracemalloc — but heads up, you'll lose the early traces 💀
>>> from libqtile.command.client import InteractiveCommandClient
>>> i=InteractiveCommandClient()
>>> i.eval("import tracemalloc;tracemalloc.start()")

dqtile-cmd

a Rofi/dmenu interface to qtile-cmd — and it accepts all arguments of qtile-cmd 😌

Examples:

Output of dqtile-cmd -o cmd:

[image]

Output of dqtile-cmd -h:

dqtile-cmd

    A Rofi/dmenu interface to qtile-cmd. Excepts all arguments of qtile-cmd
    (see below).

usage: dqtile-cmd [-h] [--object OBJ_SPEC [OBJ_SPEC ...]]
                  [--function FUNCTION] [--args ARGS [ARGS ...]] [--info]

Simple tool to expose qtile.command functionality to shell.

optional arguments:
  -h, --help            show this help message and exit
  --object OBJ_SPEC [OBJ_SPEC ...], -o OBJ_SPEC [OBJ_SPEC ...]
                        Specify path to object (space separated). If no
                        --function flag display available commands.
  --function FUNCTION, -f FUNCTION
                        Select function to execute.
  --args ARGS [ARGS ...], -a ARGS [ARGS ...]
                        Set arguments supplied to function.
  --info, -i            With both --object and --function args prints
                        documentation for function.

Examples:
 dqtile-cmd
 dqtile-cmd -o cmd
 dqtile-cmd -o cmd -f prev_layout -i
 dqtile-cmd -o cmd -f prev_layout -a 3 # prev_layout on group 3
 dqtile-cmd -o group 3 -f focus_back

If both rofi and dmenu are present rofi will be selected as default, to change this us --force-dmenu as the first argument.

iqshell

on top of the standard qtile shell shell interface, there's also a kernel you can run through Jupyter that hooks into the qshell client 🧚 the command structure and syntax are the same as qshell, so the page says you should go read that one for the full story.

Dependencies

to run iqshell you must have ipykernel and jupyter_console:

you can install them right when you install qtile, like:

$ pip install qtile[ipython]

or honestly just install the two packages separately, either through PyPI or through your distribution's package manager 🍓

Installing and Running the Kernel

once you've got the dependencies, you can run the kernel immediately:

$ python3 -m libqtile.interactive.iqshell_kernel

but that only spawns a kernel instance — you'll still have to run a separate frontend that connects to it. kinda extra, ngl.

the more convenient way is registering the kernel with Jupyter:

$ python3 -m libqtile.interactive.iqshell_install

if you run that as a non-root user, or pass the --user flag, it installs to the user Jupyter kernel directory ✨ then you can invoke the kernel directly when starting a Jupyter frontend, for example:

$ jupyter console --kernel qshell

the iqshell script launches a Jupyter terminal console with the qshell kernel 🌸

iqshell vs qtile shell

one of the main drawbacks of running through a Jupyter kernel: the frontend has no way to query the current node of the kernel, and because of that there's no way to set a custom prompt. if you wanna know your current node, you can call pwd 🙈

but — and this is the slay part — it unlocks a ton of Jupyter frontend benefits, including being able to save, run and re-run code cells in frontends like the Jupyter notebook 💅

the Jupyter kernel also enables more advanced help, text completion and introspection capabilities — though the page is honest that these are currently not implemented at a level much beyond what the standard qtile shell already gives you 🌈

🎀 DEFAULT CONFIG FILE 🎀

Okay bestie, listen — Qtile ships with a whole default config file, and it's not a sad little stub. It's a full working setup. The way it works: if you don't have a config file yet when you fire up Qtile for the very first time, this thing gets copied straight into your home config folder at ~/.config/qtile/config.py. Free starter kit, no cap. 🫶

Here it is, in all its glory — every line, every comment, untouched:

import os
from collections.abc import Callable

import libqtile.resources
from libqtile import bar, layout, qtile, widget
from libqtile.config import Click, Drag, Group, Key, Match, Output, Screen
from libqtile.lazy import lazy
from libqtile.utils import guess_terminal

mod = "mod4"
terminal = guess_terminal()

keys = [
    # A list of available commands that can be bound to keys can be found
    # at https://docs.qtile.org/en/latest/manual/config/lazy.html
    # Switch between windows
    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], "space", lazy.layout.next(), desc="Move window focus to other window"),
    # Move windows between left/right columns or move up/down in current stack.
    # Moving out of range in Columns layout will create new column.
    Key([mod, "shift"], "h", lazy.layout.shuffle_left(), desc="Move window to the left"),
    Key([mod, "shift"], "l", lazy.layout.shuffle_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"),
    # Grow windows. If current window is on the edge of screen and direction
    # will be to screen edge - window would shrink.
    Key([mod, "control"], "h", lazy.layout.grow_left(), desc="Grow window to the left"),
    Key([mod, "control"], "l", lazy.layout.grow_right(), desc="Grow window to the right"),
    Key([mod, "control"], "j", lazy.layout.grow_down(), desc="Grow window down"),
    Key([mod, "control"], "k", lazy.layout.grow_up(), desc="Grow window up"),
    Key([mod], "n", lazy.layout.normalize(), desc="Reset all window sizes"),
    # Toggle between split and unsplit sides of stack.
    # Split = all windows displayed
    # Unsplit = 1 window displayed, like Max layout, but still with
    # multiple stack panes
    Key(
        [mod, "shift"],
        "Return",
        lazy.layout.toggle_split(),
        desc="Toggle between split and unsplit sides of stack",
    ),
    Key([mod], "Return", lazy.spawn(terminal), desc="Launch terminal"),
    # Toggle between different layouts as defined below
    Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),
    Key([mod], "w", lazy.window.kill(), desc="Kill focused window"),
    Key(
        [mod],
        "f",
        lazy.window.toggle_fullscreen(),
        desc="Toggle fullscreen on the focused window",
    ),
    Key([mod], "t", lazy.window.toggle_floating(), desc="Toggle floating on the focused window"),
    Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
    Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"),
    Key([mod], "r", lazy.spawncmd(), desc="Spawn a command using a prompt widget"),
]

# Add key bindings to switch VTs in Wayland.
# We can't check qtile.core.name in default config as it is loaded before qtile is started
# We therefore defer the check until the key binding is run by using .when(func=...)
for vt in range(1, 8):
    keys.append(
        Key(
            ["control", "mod1"],
            f"f{vt}",
            lazy.core.change_vt(vt).when(func=lambda: qtile.core.name == "wayland"),
            desc=f"Switch to VT{vt}",
        )
    )

groups = [Group(i) for i in "123456789"]

for i in groups:
    keys.extend(
        [
            # mod + group number = switch to group
            Key(
                [mod],
                i.name,
                lazy.group[i.name].toscreen(),
                desc=f"Switch to group {i.name}",
            ),
            # mod + shift + group number = switch to & move focused window to group
            Key(
                [mod, "shift"],
                i.name,
                lazy.window.togroup(i.name, switch_group=True),
                desc=f"Switch to & move focused window to group {i.name}",
            ),
            # Or, use below if you prefer not to switch to that group.
            # # mod + shift + group number = move focused window to group
            # Key([mod, "shift"], i.name, lazy.window.togroup(i.name),
            #     desc="move focused window to group {}".format(i.name)),
        ]
    )

layouts = [
    layout.Columns(border_focus_stack=["#d75f5f", "#8f3d3d"], border_width=4),
    layout.Max(),
    # Try more layouts by unleashing below layouts.
    # layout.Stack(num_stacks=2),
    # layout.Bsp(),
    # layout.Matrix(),
    # layout.MonadTall(),
    # layout.MonadWide(),
    # layout.RatioTile(),
    # layout.Tile(),
    # layout.TreeTab(),
    # layout.VerticalTile(),
    # layout.Zoomy(),
]

widget_defaults = dict(
    font="sans",
    fontsize=12,
    padding=3,
)
extension_defaults = widget_defaults.copy()

logo = os.path.join(os.path.dirname(libqtile.resources.__file__), "logo.png")
screens = [
    Screen(
        bottom=bar.Bar(
            [
                widget.CurrentLayout(),
                widget.GroupBox(),
                widget.Prompt(),
                widget.WindowName(),
                widget.Chord(
                    chords_colors={
                        "launch": ("#ff0000", "#ffffff"),
                    },
                    name_transform=lambda name: name.upper(),
                ),
                widget.TextBox("default config", name="default"),
                widget.TextBox("Press &lt;M-r&gt; to spawn", foreground="#d75f5f"),
                # NB Systray is incompatible with Wayland, consider using StatusNotifier instead
                # widget.StatusNotifier(),
                widget.Systray(),
                widget.Clock(format="%Y-%m-%d %a %I:%M %p"),
                widget.QuickExit(),
            ],
            24,
            # border_width=[2, 0, 2, 0],  # Draw top and bottom borders
            # border_color=["ff00ff", "000000", "ff00ff", "000000"]  # Borders are magenta
        ),
        background="#000000",
        wallpaper=logo,
        wallpaper_mode="center",
        # You can uncomment this variable if you see that on X11 floating resize/moving is laggy
        # By default we handle these events delayed to already improve performance, however your system might still be struggling
        # This variable is set to None (no cap) by default, but you can set it to 60 to indicate that you limit it to 60 events per second
        # x11_drag_polling_rate = 60,
    ),
]

# Instead of screens, you can define a function here to specify which Screen
# should correspond to which Output.
fake_screens: list[Screen] | None = None

# Instead of screens or fake screens, you can define a function here that
# returns a list of Screen objects based on the list of Outputs; that way you
# can decide based on e.g. the number of screens, or which ports are plugged
# in exactly what do render in each bar for each screen.
generate_screens: Callable[[list[Output]], list[Screen]] | None = None

# Drag floating layouts.
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
follow_mouse_focus = True
bring_front_click = False
floats_kept_above = True
cursor_warp = False
floating_layout = layout.Floating(
    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(title="branchdialog"),  # gitk
        Match(title="pinentry"),  # GPG key password entry
    ]
)
auto_fullscreen = True
focus_on_window_activation = "smart"
focus_previous_on_window_remove = False
reconfigure_screens = True

# If things like steam games want to auto-minimize themselves when losing
# focus, should we respect this or not?
auto_minimize = True

# When using the Wayland backend, this can be used to configure input devices.
wl_input_rules = None

# xcursor theme (string or None) and size (integer) for Wayland backend
wl_xcursor_theme = None
wl_xcursor_size = 24

idle_timers = []  # type: list
idle_inhibitors = []  # type: list

# XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this
# string besides java UI toolkits; you can see several discussions on the
# mailing lists, GitHub issues, and other WM documentation that suggest setting
# this string if your java app doesn't work correctly. We may as well just lie
# and say that we're a working one by default.
#
# We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in
# java that happens to be on java's whitelist.
wmname = "LG3D"

🌸 THE CONFIG FILE 🌸

Okay so here's the whole vibe: Qtile is configured in Python. Not YAML, not TOML, not some cursed bespoke syntax — actual Python, bestie. 🐍💖

A script (which is ~/.config/qtile/config.py by default, hiiii) gets evaluated, and then a small set of configuration variables gets pulled out of its global namespace. That's it, that's the whole system. You write Python, Qtile reads the globals. Iconic behaviour honestly.

✨ Configuration lookup order ✨

So when Qtile goes hunting for your config file, it checks these places, in order:

  • The location specified by the -c argument.
  • $XDG_CONFIG_HOME/qtile/config.py, if it is set
  • ~/.config/qtile/config.py
  • first qtile/config.py found in $XDG_CONFIG_DIRS (defaults to /etc/xdg)
  • Ok so it reads the module libqtile.resources.default_config, which ships with every single Qtile installation btw, no downloads, it just comes with it.

Qtile will try to create the configuration file as a copy of the default config — if it doesn't exist yet, it gets dropped into $XDG_CONFIG_HOME/qtile/config.py (if that's set) or ~/.config/qtile/config.py. So it does the whole "make yourself at home" thing for you 🫶


✨ Default Configuration ✨

So the default configuration gets invoked when qtile literally cannot find a configuration file. And also — plot twist — if qtile gets restarted, or the config gets reloaded, qtile will load the default configuration if the config file it does find has some kind of error in it. Bestie will not let you sit there with a broken setup.

The documentation below walks through the configuration lookup process, plus what the actual key bindings are in the default config.

Real talk though: the default config is not intended to be suitable for all users. It's mostly just there so qtile does something when it fires up, and so that it doesn't crash and cause you to lose all your work if you reload a bad config. It's giving "safety net", not "aesthetic". 🕸️

✨ Configuration variables ✨

A Qtile configuration is literally a file with a bunch of variables in it. Qtile imports that file and runs it as a Python file to derive its final configuration. Yes queen, your config is a Python program and it knows it. 🐍

The docs below describe the most common configuration variables. More advanced configuration lives in the qtile-examples repository — https://github.com/qtile/qtile-examples — which includes a number of real-world configurations showing how you can tune Qtile to your liking.

(And feel free to issue a pull request to add your own configuration to the mix! Share the slay. 💅)


✨ Lazy objects ✨

Lazy objects are how you execute any of the commands available in Qtile's commands API.

The name "lazy" refers to the fact that the commands are not executed at the time of the call. Instead, the lazy object creates a reference to the relevant command, and that reference is only executed when the relevant event is triggered (e.g. on a keypress). So it's like ✨manifesting✨ a command for later.

Typically, for config files, the commands are used to manipulate windows, layouts and groups, as well as application commands like exiting, restarting, reloading the config file, etc.


🌸 Example 🌸

from libqtile.config import Key
from libqtile.lazy import lazy

keys = [
    Key(
        ["mod1"], "k",
        lazy.layout.down()
    ),
    Key(
        ["mod1"], "j",
        lazy.layout.up()
    )
]
Note: As noted above, lazy calls do not call the relevant command but only create a reference to it. While this makes it ideal for binding commands to key presses and mouse_callbacks for widgets, it also means that lazy calls cannot be included in user-defined functions. So don't try it, bestie, it will not go the way you want it to. 😭

✨ Lazy functions ✨

Here's the overview of the commonly used functions for key bindings. These functions can be called from commands on the REPLACE object or on another object in the command tree.

Some examples are given below. For a complete list of available commands, please refer to the Commands API.


💖 General functions 💖

  • lazy.spawn("application") — run the application
  • lazy.spawncmd() — open command prompt on the bar. See prompt widget.
  • lazy.reload_config() — reload the config.
  • lazy.restart() — restart Qtile. In X11, it won't close your windows.
  • lazy.shutdown() — close the whole Qtile

💜 Group functions 💜

  • lazy.next_layout() — use next layout on the actual group
  • lazy.prev_layout() — use previous layout on the actual group
  • lazy.screen.next_group() — move to the group on the right
  • lazy.screen.prev_group() — move to the group on the left
  • lazy.screen.toggle_group() — move to the last visited group
  • lazy.group.next_window() — switch window focus to next window in group
  • lazy.group.prev_window() — switch window focus to previous window in group
  • lazy.group["group_name"].toscreen() — move to the group called group_name. Takes an optional toggle parameter (defaults to False). If this group is already on the screen, it does nothing by default; to toggle with the last used group instead, use toggle=True. 🎀
  • lazy.layout.increase_ratio() — increase the space for master window at the expense of slave windows
  • lazy.layout.decrease_ratio() — decrease the space for master window in the advantage of slave windows

✨ Window functions ✨

okay these are the window-y ones, the ones you bind to keys to boss your windows around 💅

  • lazy.window.kill() — closes the window that's currently focused. bye bestie 👋
  • lazy.layout.next() — moves window focus over to the other pane(s) of the stack.
  • lazy.window.togroup("group_name") — moves the focused window into the group called group_name.
  • lazy.window.toggle_floating() — flips the focused window in or out of floating mode.
  • lazy.window.toggle_fullscreen() — flips the focused window in or out of fullscreen mode.
  • lazy.window.move_up() — moves the window up above the next window in the stack.
  • lazy.window.move_down() — moves the window down below the previous window in the stack.
  • lazy.window.move_to_top() — moves the window above every other window that has similar priority (so like, a "normal" window will not be moved above a kept_above window).
  • lazy.window.move_to_bottom() — moves the window below every other window that has similar priority (again, a "normal" window will not be moved below a kept_below window).
  • lazy.window.keep_above() — keeps the window above other windows.
  • lazy.window.keep_below() — keeps the window below other windows.
  • lazy.window.bring_to_front() — brings the window above all other windows. It ignores kept_above priority, no cap 🧢

🌈 Screen functions 🌈

  • lazy.screen.set_wallpaper(path, mode=None) — sets the wallpaper to the image you picked 🎀. Modes available: None = no resizing, 'fill' = centre and resize to fill the screen, 'stretch' = stretch it to fill the screen. pick your vibe ✨

🦋 ScratchPad DropDown functions 🦋

  • lazy.group["group_name"].dropdown_toggle("name") — toggles whether the DropDown window you named is visible or not. heads up tho: on the first use, the configured process gets spawned.
  • lazy.group["group_name"].hide_all() — hides all the DropDown windows at once.
  • lazy.group["group_name"].dropdown_reconfigure("name", **configuration) — updates the configuration of the DropDown you named.

👑 User-defined functions 👑

  • lazy.function(func, *args, **kwargs) — calls func(qtile, *args, **kwargs). NB. the qtile object gets automatically passed in as the first argument, so don't you dare add it yourself 🙈

🍓 Examples 🍓

lazy.function can also be used as a decorator for functions. it's giving main character energy honestly ✨

from libqtile.config import Key
from libqtile.lazy import lazy

@lazy.function
def my_function(qtile):
    ...

keys = [
    Key(
        ["mod1"], "k",
        my_function
    )
]

plus, you can pass arguments to your user-defined function in one of two ways — pick your fighter:

  1. In-line definition

Arguments get added right there in the lazy.function call.

from libqtile.config import Key
from libqtile.lazy import lazy
from libqtile.log_utils import logger

def multiply(qtile, value, multiplier=10):
    logger.warning(f"Multiplication results: {value * multiplier}")

keys = [
    Key(
        ["mod1"], "k",
        lazy.function(multiply, 10, multiplier=2)
    )
]
  1. Decorator

Arguments can also be passed to the decorated function.

from libqtile.config import Key
from libqtile.lazy import lazy
from libqtile.log_utils import logger

@lazy.function
def multiply(qtile, value, multiplier=10):
    logger.warning(f"Multiplication results: {value * multiplier}")

keys = [
    Key(
        ["mod1"], "k",
        multiply(10, multiplier=2)
    )
]

💖 Groups 💖

okay so, a group is basically a lil container for a bunch of windows — think of it like workspaces in literally every other window manager. Every single client window that qtile manages belongs to exactly one group, no sharing, no polyamory, one window one group periodt. The groups config file variable should be initialized to a list of Group objects. ✨

Group objects come with a bunch of options for group config — you can make groups show/hide themselves when they're empty, spawn apps for them when they start up, auto-acquire certain groups, and a whole menu of other stuff. very main character energy honestly.

🌸 Example 🌸

from libqtile.config import Group, Match

groups = [
    Group("a"),
    Group("b"),
    Group("c", matches=[Match(wm_class="Firefox")]),
]

# allow mod3+1 through mod3+0 to bind to groups; if you bind your groups
# by hand in your config, you don't need to do this.
from libqtile.dgroups import simple_key_binder

dgroups_key_binder = simple_key_binder("mod3")

🎀 Reference 🎀

✨ Group Matching ✨

🌈 ScratchPad and DropDown 🌈

ScratchPad is a special group — by default invisible, iykyk — that acts as a container for DropDown configurations. A DropDown can be configured to spawn a defined process and bind that process' window to it. Then that associated window can be shown and hidden by the lazy command dropdown_toggle() (peep the Lazy objects <#lazy> ) from the ScratchPad group. So like — for example — your favorite terminal emulator turns into a quake-like terminal, all under qtile's control. iconic. 🔥

If the DropDown window turns visible, it gets placed as a floating window on top of the current group. If the DropDown is hidden, it's simply switched back to the ScratchPad group. very demure, very mindful.

🦋 Example 🦋

from libqtile.config import Group, ScratchPad, DropDown, Key
from libqtile.lazy import lazy

groups = [
    ScratchPad("scratchpad", [
        # define a drop down terminal.
        # it is placed in the upper third of screen by default.
        DropDown("term", "urxvt", opacity=0.8),

        # define another terminal exclusively for ``qtile shell` at different position
        DropDown("qtile shell", "urxvt -hold -e 'qtile shell'",
                 x=0.05, y=0.4, width=0.9, height=0.6, opacity=0.9,
                 on_focus_lost_hide=True) ]),
    Group("a"),
]

keys = [
  # toggle visibiliy of above defined DropDown named "term"
  Key([], 'F11', lazy.group['scratchpad'].dropdown_toggle('term')),
  Key([], 'F12', lazy.group['scratchpad'].dropdown_toggle('qtile shell')),
]

ngl, important heads up: if the window is set to not floating, it gets detached from DropDown and ScratchPad, and a fresh new process is spawned the next time the DropDown is set visible. so like, don't be surprised bestie. 💅

Some programs run in a server-like mode where the spawned process does not directly own the window that gets created — instead it's created by a background process. In that case, the window may not be correctly caught in the scratchpad group 😭. To work around it, you can pass a Match object to the corresponding DropDown. See below.

⭐ Reference ⭐


👑 Keys 👑

The keys variable defines Qtile's key bindings. the whole vibe check of your setup fr.

💜 Default Key Bindings 💜

The mod key for the default config is mod4, which is typically bound to the "Super" keys — the things like the windows key and the mac command key. The basic operation is:

  • mod + k or mod + j: switch windows on the current stack
  • mod + <space>: put focus on the other pane of the stack (when in stack layout)
  • mod + <tab>: switch layouts
  • mod + w: close window
  • mod + <ctrl> + r: reload the config
  • mod + <group name>: switch to that group
  • mod + <shift> + <group name>: send a window to that group
  • mod + <enter>: start terminal guessed by libqtile.utils.guess_terminal
  • mod + r: start a little prompt in the bar so users can run arbitrary commands

The default config defines one screen and 8 groups, one for each letter in asdfuiop. It has a basic bottom bar that includes a group box, the current window name, a little text reminder that you're using the default config, a system tray, and a clock. so cute, so functional. 🎀

The default configuration has several more advanced key combinations, but the above should be enough for basic usage of qtile.

Peep Keybindings in images <#keybinding-img> for visual keybindings in keyboard layout. 👀

🫶 Defining key bindings 🫶

Individual key bindings are defined with Key as demonstrated in the following example. Note that you may specify more than one callback functions.

from libqtile.config import Key

keys = [
   # Pressing "Meta + Shift + a".
   Key(["mod4", "shift"], "a", callback, ...),

   # Pressing "Control + p".
   Key(["control"], "p", callback, ...),

   # Pressing "Meta + Tab".
   Key(["mod4", "mod1"], "Tab", callback, ...),
]

The above may also be written more concisely with the help of the EzKey helper class. The following example is functionally equivalent to the above:

from libqtile.config import EzKey as Key

keys = [
   Key("M-S-a", callback, ...),
   Key("C-p",   callback, ...),
   Key("M-A-<Tab>", callback, ...),
]

The EzKey modifier keys (i.e. MASC) can be overwritten through the EzKey.modifier_keys dictionary. The defaults are:

modifier_keys = {
   'M': 'mod4',
   'A': 'mod1',
   'S': 'shift',
   'C': 'control',
}

Callbacks can also be configured to work only under certain conditions by using the when() method. Currently, the following conditions are supported:

from libqtile.config import Key

keys = [
    # Only trigger callback for a specific layout
    Key(
        [mod, 'shift'],
        "j",
        lazy.layout.grow().when(layout='verticaltile'),
        lazy.layout.grow_down().when(layout='columns')
    ),

    # Limit action to when the current window is not floating
    Key([mod], "f", lazy.window.toggle_fullscreen().when(when_floating=False))

    # Limit action to when the current window is floating
    Key([mod], "f", lazy.window.toggle_fullscreen().when(when_floating=True))

    # Also matches are supported on the current window
    # For example to match on the wm_class for fullscreen do the following
    Key([mod], "f", lazy.window.toggle_fullscreen().when(focused=Match(wm_class="yourclasshere"))
]

🧚 KeyChords 🧚

Qtile also allows sequences of keys to trigger callbacks. These sequences are known as chords and are defined with KeyChord. Chords get added to the keys section of the config file. obsessed. 💖

from libqtile.config import Key, KeyChord

keys = [
    KeyChord([mod], "z", [
        Key([], "x", lazy.spawn("xterm"))
    ])
]

The above code will launch xterm when the user presses Mod + z, followed by x.

Warning: bestie pls read this one 💀 — Users should note that key chords are aborted by pressing <escape>. In the above example, if the user presses Mod + z, any following key presses will still be sent to the currently focussed window. If <escape> has not been pressed, the next press of x will launch xterm.

🌸 Modes 🌸

Chords can optionally persist until a user presses <escape>. You do this by setting mode=True. This can be useful for configuring a subset of commands for a particular situation (i.e. similar to vim modes). very giving vim, very giving gamer. 🎮

from libqtile.config import Key, KeyChord

keys = [
    KeyChord([mod], "z", [
        Key([], "g", lazy.layout.grow()),
        Key([], "s", lazy.layout.shrink()),
        Key([], "n", lazy.layout.normalize()),
        Key([], "m", lazy.layout.maximize())],
        mode=True,
        name="Windows"
    )
]

In the above example, pressing Mod + z triggers the "Windows" mode. Users can then resize windows by just pressing g (to grow the window), s to shrink it, etc. — as many times as needed. To exit the mode, press <escape>.

Note: The Chord widget (Chord) will display the name of the active chord (as set by the name parameter). This is particularly useful where the chord is a persistent mode, because this will indicate when the chord's mode is still active. so handy honestly ✨

🍓 Chains 🍓

Chords can also be chained to make even longer sequences. the way that escalates 💅

from libqtile.config import Key, KeyChord

keys = [
    KeyChord([mod], "z", [
        KeyChord([], "x", [
            Key([], "c", lazy.spawn("xterm"))
        ])
    ])
]

Modes can also be added to chains if required. The following example demonstrates the behaviour when using the mode argument in chains:

from libqtile.config import Key, KeyChord

keys = [
    KeyChord([mod], "z", [
        KeyChord([], "y", [
            KeyChord([], "x", [
                Key([], "c", lazy.spawn("xterm"))
            ], mode=True, name="inner")
        ])
    ], mode=True, name="outer")
]

After pressing Mod+z y x c, the "inner" mode will remain active. When pressing <escape>, the "inner" mode is exited. Since the mode in between does not have mode set, it is also left. Arriving at the "outer" mode (which has this argument set) stops the "leave" action, and "outer" now becomes the active mode. so it's like a little stack of modes, no cap. 🔥

Note: If you want to bind a custom key to leave the current mode (e.g. Control + G in addition to <escape>), you can specify lazy.ungrab_chord() as the key action. To leave all modes and return to the root bindings, use lazy.ungrab_all_chords().

🌈 Modifiers 🌈

On most systems mod1 is the Alt key — you can see which modifiers, which are enclosed in a list, map to which keys on your system by running the xmodmap command. This example binds Alt-k to the "down" command on the current layout. This command is standard on all the included layouts, and switches to the next window (where "next" is defined differently in different layouts). The matching "up" command switches to the previous window.

Modifiers include: "shift", "lock", "control", "mod1", "mod2", "mod3", "mod4", and "mod5". They can be used in combination by appending more than one modifier to the list:

Key(
    ["mod1", "control"], "k",
    lazy.layout.shuffle_down()
)

🎹 Special keys

okay so these are the special keys you'll reach for like 90% of the time. for the complete list, go peep the code at https://github.com/qtile/qtile/blob/master/libqtile/backend/x11/xkeysyms.py. you can make bindings on them exactly like you do for regular keys, no special treatment required. for example, Key(["mod1"], "F4", lazy.window.kill()). 💅

  • Return
  • BackSpace
  • Tab
  • space
  • Home, End
  • Left, Up, Right, Down
  • F1, F2, F3, ...
  • (this row is literally blank in the original table — no cap, it's just vibing there, and we're keeping it because details are details 💀)
  • XF86AudioRaiseVolume
  • XF86AudioLowerVolume
  • XF86AudioMute
  • XF86AudioNext
  • XF86AudioPrev
  • XF86MonBrightnessUp
  • XF86MonBrightnessDown

📚 Reference

🪟 Layouts

a layout is the algorithm that decides how windows in a group get arranged on your screen. since qtile is a tiling window manager, that basically means: we're trying to use your space as efficiently as humanly possible, and we hand you a whole pile of commands you can bind to keys so you can actually boss your layouts around. ✨

the layouts variable is where you define the list of layouts qtile will use. the first layout in the list is the default one. and lowkey, if you define more than one layout, you're probably gonna want key bindings to jump to the next and previous layouts too. just saying. 👑

see Built-in Layouts <#ref-layouts> for the full rundown of what's available.

✨ Example

from libqtile import layout

layouts = [
    layout.Max(),
    layout.Stack(stacks=2)
]

🐭 Mouse

the mouse config file variable defines a set of global mouse actions, and it's a list of Click and Drag objects — those are the things that say what should happen when a window gets clicked or dragged. 🫶

🖱️ Default Mouse Bindings

by default, holding your mod key and left-clicking (and holding) a window lets you drag it around like a floating window. hold mod and right-click (and hold) a window and you'll resize it — and heads up, that also makes it float if it wasn't floating already. two-for-one, ate. 😭

✨ Example

from libqtile.config import Click, Drag
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())
]

and bestie, the above can also be written way more concisely with the EzClick and EzDrag helpers:

from libqtile.config import EzClick as Click, EzDrag as Drag

mouse = [
    Drag("M-1", lazy.window.set_position_floating(),
        start=lazy.window.get_position()),
    Drag("M-3", lazy.window.set_size_floating(),
        start=lazy.window.get_size()),
    Click("M-2", lazy.window.bring_to_front())
]

📚 Reference

🖥️ Screens

the screens configuration variable is where the physical screens live — plus their associated bars, plus all the widgets chilling inside those bars (see Built-in Widgets <#ref-widgets> for the listing of available widgets). it's giving whole setup, honestly. 🌈

✨ Example

tying screens, bars and widgets all together, we get something like this:

from libqtile.config import Screen
from libqtile import bar, widget

window_name = widget.WindowName()

screens = [
    Screen(
        bottom=bar.Bar([
            widget.GroupBox(),
            window_name,
            ], 30),
        ),
    Screen(
        bottom=bar.Bar([
            widget.GroupBox(),
            window_name,
            ], 30),
        )
    ]

quick note: a widget can be passed to multiple bars (and likewise multiple times to the same bar). its contents get mirrored across every copy, which is super handy when you want identical content everywhere — like the name of the focussed window, exactly like in this example. 🪞

bars support both solid background colors and gradients, just by supplying a list of colors that make up a linear gradient. for example, bar.Bar(..., background="#000000") gives you a black background (that's the default, btw), while bar.Bar(..., background=["#000000", "#FFFFFF"]) gives you a background that fades from black into white. gradient slay. 🎀

bars (and widgets!) also support transparency — you just add an alpha value onto the color you want. for example, bar.Bar(..., background="#00000000") results in a fully transparent bar. and important: widget contents are not impacted by this, i.e. this is different from the opacity parameter, which sets the transparency of the entire window. don't mix them up bestie, they are not the same girl. 💀

Note: on X11 backends, transparency gets disabled in a bar if the background color is fully opaque.

you can add borders to the bar with the border_width and border_color parameters. one single value sets the value for all four sides, but you can customise each side individually by setting four values in a list — in (top, right, bottom, left) order. e.g. border_width=[2, 0, 2, 0] would draw a border 2 pixels thick on the top and bottom of the bar. ✨

🖥️🖥️ Multiple Screens

you'll see from the example above that screens is a list of individual Screen objects. and the order of the screens in that list should match the order of screens as seen by your display server. periodt.

X11

you can view the current order of your screens by running xrandr --listmonitors.

examples of how to set the order of your screens can be found on the Arch wiki <https://wiki.archlinux.org/title/Multihead>.

Wayland

the Wayland backend supports the wlr-output-management protocol for configuration of outputs by tools such as Kanshi <https://github.com/emersion/kanshi>. 🦋

🔄 Dynamic Screen Configuration

instead of defining screens as a static list, you can define a generate_screens function — it gets called with the list of currently connected outputs, and it returns the appropriate list of Screen objects. this is the move when you want your bar layout to adapt to different hardware setups, like a laptop flying solo, docked with a second monitor, or posted up at a desk with multiple displays. main character adaptivity. 👑

the function signature must be:

from libqtile.config import Output, Screen

def generate_screens(outputs: list[Output]) -> list[Screen]:
    ...

every Output has these attributes:

  • port — the connector name as seen by the display server (e.g. "HDMI-1", "DP-1")
  • make — manufacturer string reported by the monitor
  • model — model string reported by the monitor
  • serial — serial number reported by the monitor (stable across reboots, which makes it useful for identifying specific displays)
  • rect — a ScreenRect describing the geometry of the output

✨ Example

the following example defines three helper functions — one per number of connected screens — and dispatches based on how many outputs are active. the three_screens helper additionally matches each output to a fixed screen configuration using the monitor serial number, so that each display always gets the same bar layout regardless of which port it happens to be plugged into. iconic, honestly. 💖

import subprocess
from libqtile import bar, widget
from libqtile.config import Output, Screen

def one_screen():
    # As an example: we want to render the appropriate world clock based on
    where we are, but also the time at home. Dynamically compute the clock
    widgets necessary to do this.
    current = subprocess.check_output(
        ["timedatectl", "show", "--value", "--property=Timezone"]
    ).decode("utf-8")

    # only display american clocks in 12 hour format
    fmt = '%Y-%m-%d %a %I:%M %p'
    if not current.startswith("America"):
        fmt = '%Y-%m-%d %a %H:%M'

    clocks = [
        widget.Clock(format=fmt, **widget_defaults),
    ]

    # display the time at home if not at home
    if current != "America/Denver\n":
        clocks.insert(0, widget.Clock(
            format='%I:%M %p Mountain',
            timezone='America/Denver',
            **widget_defaults,
        ))

    return [
        Screen(top=bar.Bar([
            widget.GroupBox(**widget_defaults),
            widget.Prompt(**widget_defaults),
            widget.Clipboard(timeout=None, width=bar.STRETCH, max_width=None),
            widget.Battery(**widget_defaults),
            widget.Systray(**widget_defaults),
            ] + clocks,
            height,
        )),
    ]
def two_screens():
    # A docked laptop, for example. Or maybe a plugged in projector while
    giving a conference talk.
    return [
        Screen(top=bar.Bar([
            widget.GroupBox(**widget_defaults),
            widget.Prompt(**widget_defaults),
            widget.Spacer(),
        ], height)),
        Screen(top=bar.Bar([
            widget.GroupBox(**widget_defaults),
            widget.Clipboard(timeout=None, width=bar.STRETCH, max_width=None),
            widget.Systray(**widget_defaults),
            widget.Clock(format='%Y-%m-%d %a %I:%M %p', **widget_defaults),
        ], height)),
    ]

def three_screens(outputs: list[Output]):
    """
    Bind screen configuration to a set of outputs regardless of their port
    names (e.g. HDMI-1, DP-1-1-2, etc.) by using the serial number of the
    monitors, which is stable across reboots.
    """
    screens = []
    for output in outputs:
        if output.serial == "M2GCR1AM28PL":
            # left screen
            scr = Screen(top=bar.Bar([
                widget.GroupBox(**widget_defaults),
                widget.Prompt(**widget_defaults),
                widget.Spacer(),
            ], height))
        elif output.serial == "1B8W0P3":
            # middle screen
            scr = Screen(top=bar.Bar([
                widget.GroupBox(**widget_defaults),
                widget.Systray(**widget_defaults),
                widget.Clock(format='%Y-%m-%d %a %I:%M %p', **widget_defaults),
            ], height))
        elif output.serial == "M2GCR1AS21NL":
            # right screen
            scr = Screen(top=bar.Bar([
                widget.GroupBox(**widget_defaults),
                widget.Spacer(),
                widget.Clock(format='%Y-%m-%d %a %I:%M %p', **widget_defaults),
            ], height))
        else:
            raise Exception(f"unknown output {output}")
        screens.append(scr)
    return screens

def generate_screens(outputs: list[Output]) -> list[Screen]:
    if len(outputs) == 1:
        return one_screen()
    elif len(outputs) == 2:
        return two_screens()
    else:
        return three_screens(outputs)
💀 Note: generate_screens and screens are mutually exclusive, bestie. If generate_screens is defined it takes precedence and the screens variable is ignored. Pick one, you cannot have both. Periodt.

🎭 FAKE SCREENS 🎭

ok so instead of using the variable screens, you can use the variable fake_screens to literally split a physical monitor into multiple screens 👀 and they get used like this:

from libqtile.config import Screen
from libqtile import bar, widget

# screens look like this
#     600         300
#  |-------------|-----|
#  |          480|     |580
#  |   A         |  B  |
#  |----------|--|     |
#  |       400|--|-----|
#  |   C      |        |400
#  |----------|   D    |
#     500     |--------|
#                 400
#
# Notice there is a hole in the middle
# also D goes down below the others

fake_screens = [
  Screen(
      bottom=bar.Bar(
          [
              widget.Prompt(),
              widget.Sep(),
              widget.WindowName(),
              widget.Sep(),
              widget.Systray(),
              widget.Sep(),
              widget.Clock(format='%H:%M:%S %d.%m.%Y')
          ],
          24,
          background="#555555"
      ),
      x=0,
      y=0,
      width=600,
      height=480
  ),
  Screen(
      top=bar.Bar(
          [
              widget.GroupBox(),
              widget.WindowName(),
              widget.Clock()
          ],
          30,
      ),
      x=600,
      y=0,
      width=300,
      height=580
  ),
  Screen(
      top=bar.Bar(
          [
              widget.GroupBox(),
              widget.WindowName(),
              widget.Clock()
          ],
          30,
      ),
      x=0,
      y=480,
      width=500,
      height=400
  ),
  Screen(
      top=bar.Bar(
          [
              widget.GroupBox(),
              widget.WindowName(),
              widget.Clock()
          ],
          30,
      ),
      x=500,
      y=580,
      width=400,
      height=400
  ),
]

(the lil ASCII map is doing the absolute most and honestly? it ate 💅 that hole in the middle is real and screen D really does dip below the others, no cap)


☕ THIRD-PARTY BARS ☕

sometimes you've got reasons to use third-party bars, bestie 🫶 like maybe you rolled in from another window manager and you already have dzen2, xmobar, or something else all set up and gorgeous. slay! they can 100% be used with Qtile too. in fact? any additional configurations aren't needed at all. just run the bar and qtile will adapt. that's it. iconic behaviour honestly ✨


📚 REFERENCE 📚

🪝 HOOKS 🪝

Qtile gives you a whole mechanism for subscribing to certain events in libqtile.hook — think of it like following your fave accounts so you get notified 👑 to subscribe to a hook in your configuration, you simply decorate a function with the hook you wish to subscribe to.

see the Built-in Hooks reference (#ref-hooks) for a listing of available hooks, iykyk.


🍓 EXAMPLES 🍓

🪟 Automatic floating dialogs 🪟

ok so say we wanted to automatically float all dialog windows — real talk, this code isn't actually necessary, Qtile floats all dialogs by default anyway 🙈 — we would subscribe to the client_new hook so it tells us when a new window has opened, and if the type is "dialog", we can set the window to float. in our config file it'd look a lil something like this:

from libqtile import hook

@hook.subscribe.client_new
def floating_dialogs(window):
    dialog = window.window.get_wm_type() == 'dialog'
    transient = window.window.get_wm_transient_for()
    if dialog or transient:
        window.floating = True

a list of available hooks can be found in the Built-in Hooks reference (#ref-hooks) 🌈

🚀 Autostart 🚀

if you want to run commands or spawn some applications when Qtile starts, you're gonna wanna look at the startup and startup_once hooks 💖 startup is emitted every single time Qtile starts (restarts included, bestie), whereas startup_once is only emitted on the very first startup. main character debut energy.

let's create an executable file ~/.config/qtile/autostart.sh that'll start a few programs when Qtile first runs. don't forget to chmod +x ~/.config/qtile/autostart.sh so it can actually be executed 🙏

#!/bin/sh
pidgin &
dropbox start &

then we can subscribe to startup_once to run this script:

import os
import subprocess

from libqtile import hook

@hook.subscribe.startup_once
def autostart():
    home = os.path.expanduser('~/.config/qtile/autostart.sh')
    subprocess.call(home)

🫶 Accessing the qtile object 🫶

if you want to do something with the Qtile manager instance inside a hook, it can be imported straight into your config like this:

from libqtile import qtile

⚡ Async hooks ⚡

hooks can also be defined as coroutine functions using async def, which runs them asynchronously in the event loop 🌸

@hook.subscribe.focus_change
async def _():
    ...

🦋 Transient hooks 🦋

transient hooks can be created by having the hooked function return True. this will automtically unsubscribe the hook after is has been run ✨ (yes, one and done, it understood the assignment)


💜 MATCHING WINDOWS 💜

Qtile's config provides a number of situations where the behaviour depends on whether the relevant window matches some specified criteria 👀

these situations include:

  • Defining which windows should be floated by default
  • Assigning windows to specific groups
  • Assigning window to a master section of a layout

in each instance, the criteria are defined via a Match object. the properties of the object get compared to a Window to determine if its properties match. it can match by title, wm_class, role, wm_type, wm_instance_class, net_wm_pid, or wid 💅 plus, a function may be passed, which takes in the Window to be compared against and returns a boolean.

so a basic rule would look something like:

Match(wm_class="mpv")

this would match against any window whose class was mpv 🎯

where a string is provided as an argument, the value must match exactly. more flexibility can be achieved by using regular expressions. for example:

import re

Match(wm_class=re.compile(r"mpv"))

this would still match a window whose class was mpv, but it would also match any class starting with mpv, e.g. mpvideo 🔥

💀 Note: when providing a regular expression, qtile applies the .match method. this matches from the start of the string so, if you want to match any substring, you will need to adapt the regular expression accordingly, e.g. python import re Match(wm_class=re.compile(r".*mpv")) this would match any string containing mpv 🌟

🧠 CREATING ADVANCED RULES 🧠

while the func parameter lets users create more complex matches, that requires knowing qtile's internal objects 😭 an alternative is to combine Match objects using logical operators & (and), | (or), ~ (not) and ^ (xor) 💅

for example, to create a rule that matches all windows with a fixed aspect ratio except for mpv windows, you would provide the following:

Match(func=lambda c: c.has_fixed_ratio()) & ~Match(wm_class="mpv")

it's also possible to use wrappers for Match objects if you don't vibe with the operators. the following wrappers are available 👑

  • MatchAll(Match(...), ...) — equivalent to an "and" test. All matches must match.
  • MatchAny(Match(...), ...) — equivalent to an "or" test. At least one match must match.
  • MatchOnlyOne(Match(...), Match(...)) — equivalent to "xor". Only one match must match.
  • InvertMatch(Match(...)) — equivalent to "not". Inverts the result of the match.

so to recreate the above rule using the wrappers, you'd write the following:

from libqtile.config import InvertMatch, Match, MatchAll

MatchAll(Match(func=lambda c: c.has_fixed_ratio()), InvertMatch(Match(wm_class="mpv")))

in addition to the above variables, there are several other boolean configuration variables that control specific aspects of Qtile's behavior:

  • auto_fullscreen — default: True ✨ If a window requests to be fullscreen, it is automatically fullscreened. Set this to false if you only want windows to be fullscreen if you ask them to be. No sneaky fullscreen jumpscares, bestie.
  • auto_minimize — default: True 🫶 If things like steam games want to auto-minimize themselves when losing focus, should we respect this or not? Steam game said “not rn” — do we let it?
  • bring_front_click — default: False 💅 When clicked, should the window be brought to the front or not. If this is set to "floating_only", only floating windows will get affected (This sets the X Stack Mode to Above.). This will ignore the layering rules and will therefore bring windows above other windows, even if they have been set as "kept_above". This may cause issues with docks and other similar apps as these may end up hidden behind other windows. Setting this to False or "floating_only" may therefore be required when using these apps. > ⚠️ Docks can get hidden, so keep that in mind.
  • cursor_warp — default: False 🦋 If true, the cursor follows the focus as directed by the keyboard, warping to the center of the focused window. When switching focus between screens, If there are no windows in the screen, the cursor will warp to the center of the screen. Cursor said “I’m coming with you.”
  • dgroups_app_rules — default: [] 🌈 A list of Rule objects which can send windows to various groups based on matching criteria. Rule objects doing matchmaking, fr fr.
  • dgroups_key_binder — default: None 👑 A function which generates group binding hotkeys. It takes a single argument, the DGroups object, and can use that to set up dynamic key bindings.

A sample implementation is available in libqtile/dgroups.py (https://github.com/qtile/qtile/blob/master/libqtile/dgroups.py) called simple_key_binder(), which will bind groups to mod+shift+0-10 by default. Dynamic group binds, slay.

  • extension_defaults — defaults to same as widget_defaults 🪞✨ literally the default settings for extensions. That's it, that's the vibe.
  • floating_layout — defaults to layout.Floating(float_rules=[...]). This is the default floating layout you're running with, and it's the moment where you get to set custom floating rules — among other things, if you feel like it, no pressure bestie 💅. Peep the configuration file for the default float_rules, that's where they live.
  • floats_kept_above — defaults to True. Floating windows are kept above tiled windows 👑 (Currently x11 only. Wayland support coming soon. 🫶 we're waiting patiently).
  • focus_on_window_activation — defaults to 'smart'. This one's the behavior of the _NET_ACTIVE_WINDOW message that applications send out. Here's the menu of choices, bestie:
    • urgent: the urgent flag is set for the window 🚨
    • focus: automatically focus the window
    • smart: automatically focus if the window is in the current group 🧠
    • never: never automatically focus any window that requests it, periodt 🙅
    • it can also be a function which takes the window as an argument:
      • returns True: focus window ✨
      • returns False: doesn't do anything 💀
  • focus_previous_on_window_remove — defaults to False. So when a window gets closed, the next focused window is not always the previous window, depending on the current window state. Not me finding that out the hard way 😭. Set this to true if you want the previous window focused in all circumstances. All of them. No cap.
  • follow_mouse_focus — defaults to True. This controls whether or not focus follows the mouse around as it moves across windows in a layout 🖱️✨. Otherwise, set this to "click_or_drag_only" to change focus only when you're doing a Click or Drag action. Iconic either way.
  • reconfigure_screens — default True — this one controls whether or not we automagically reconfigure screens whenever there's a change in randr output configuration ✨
  • widget_defaults — default dict(font='sans', fontsize=12, padding=3) — the default settings for your bar widgets 🎀
  • wmname — default 'LG3D' — okay bestie, gasp! We're lying here 💀 for real, nobody actually uses or cares about this string except java UI toolkits. There are several discussions on the mailing lists, GitHub issues, and other WM documentation that suggest setting this string if your java app doesn't work correctly. So we may as well just lie and say that we're a working one by default. We choose LG3D to maximize the irony: it is a 3D non-reparenting WM written in java that happens to be on java's whitelist 👑

Testing your configuration

The best way to test changes to your configuration is with the provided scripts at ./scripts/xephyr (X11) or ./scripts/wephyr (Wayland) 🧚 This runs Qtile with your config.py inside a nested window, and it prevents your already-running instance of Qtile from crashing if something goes wrong. Protective queen behavior fr fr 💖

See Hacking Qtile for more information on using Xephyr.


🌈 BUILT-IN LAYOUTS

🎀 BUILT-IN WIDGETS

✨ BUILT-IN HOOKS

🦋 BUILT-IN EXTENSIONS

👑 KEYBINDINGS IN IMAGES

Default configuration

[image] [image] [image] [image]

Generate your own images

Qtile provides a tiny helper script to generate keybindings images from a config file ✨ In the repository, the script lives under scripts/gen-keybinding-img.

This script accepts a configuration file and an output directory. If no argument is given, the default configuration will be used and files will be placed in same directory where the command has been run 🫶

usage: gen-keybinding-img [-h] [-c CONFIGFILE] [-o OUTPUT_DIR]

Qtile keybindings image generator

optional arguments:
    -h, --help          show this help message and exit
    -c CONFIGFILE, --config CONFIGFILE
                        use specified configuration file. If no presented
                        default will be used
    -o OUTPUT_DIR, --output-dir OUTPUT_DIR
                        set directory to export all images to

🌸 WINDOW STACKING

A number of window commands (move_up/down(), bring_to_front() etc.) relate to the stacking order of windows.

The aim of this page is to provide more details as to how stacking is implemented in Qtile 👑

Important: Currently, stacking is only implemented in the X11 background. Support will be added to the Wayland backend in future and this page will be updated accordingly. So like… X11 only for now bestie, don't come for us 💅

Layer priority groups

We have tried to adhere to the EWMH specification. Windows are therefore stacked, from the bottom, according to the following priority rules:

  • windows of type _NET_WM_TYPE_DESKTOP
  • windows having state _NET_WM_STATE_BELOW
  • windows not belonging in any other layer
  • windows of type _NET_WM_TYPE_DOCK (unless they have state _NET_WM_TYPE_BELOW) and windows having state _NET_WM_STATE_ABOVE
  • focused windows having state _NET_WM_STATE_FULLSCREEN

Qtile had then added an additional layer so that Scratchpad windows are placed above everything else ⭐

Tiled windows will open in the default, "windows not belonging in any other layer", layer. If floats_kept_above is set to True in the config then new floating windows will have the _NET_WM_STATE_ABOVE property set which will ensure they remain above tiled windows 🦋

Moving windows

Imagine you have four tiled windows stacked (from the top) as follows:

"One"
"Two"
"Three"
"Four"

If you call move_up() on window "Four", the result will be:

"One"
"Two"
"Four"
"Three"

If you now call move_to_top() on window "Three", the result will be:

"Three"
"One"
"Two"
"Four"
Note: bring_to_front() has a special behaviour in Qtile. This will bring any window to the very top of the stack, disregarding the priority rules set out above. When that window loses focus, it will be restacked in the appropriate location.
This can cause undesirable results if the config contains bring_front_click=True and the user has an app like a dock which is activated by mousing over the window. In this situation, tiled windows will be displayed above the dock making it difficult to activate 😭 To fix this, set bring_front_click to False to disable the behaviour completely, or "floating_only" to only have this behaviour apply to floating windows. Periodt.

💤 IDLE EVENTS

Qtile supports idle events natively on both the X11 and Wayland backends ✨ Custom actions can be triggered after a specified period of inactivity. In addition, users can define rules to prevent idle timers from activating (e.g. when a video player is fullscreened.).

The Wayland backend includes support for the ext_idle_notifier_v1 and zwp_idle_inhibit_manager_v1 protocols 🧚 This means clients like swayidle are supported by qtile.

Note: If an inhibitor prevents a timer from firing, the timer will not be fired if the inhibitor is removed and the system is still in an idle state. Similarly, a resume action will not be fired when exiting the idle state if there was an active inhibitor when the timeout period was completed. Important, bestie 💅

Idle timers

Timers are defined in the idle_timers section of the config file. This should be a list of IdleTimer objects 🎀

from libqtile.config import IdleTimer
from libqtile.lazy import lazy

idle_timers = [
   IdleTimer(300, action=lazy.spawn("/path/to/screen_dimmer.sh"), resume=lazy.spawn("/path/to/restore_screen.sh")),
   IdleTimer(900, action=lazy.spawn("/path/to/screen_off.sh"))
]

Idle inhibitors

An active idle inhibitor will prevent a timer action being triggered 🔥 Users can define rules for inhibitors in the idle_inhibitors section of the config file. This is a list of IdleInhibitor objects. Each inhibitor defines a Match to check whether the rule should apply to a window. There is also a setting to determine what state the window must be in to activate the inhibitor 👑

from libqtile.config import IdleInhibitor, Match
from libqtile.lazy import lazy

idle_inhibitors = [
   IdleInhibitor(match=Match(wm_class="vlc"), when="fullscreen"),
]

✨ ARCHITECTURE ✨

Okay bestie, buckle up, because this page is literally about how Qtile's API does its thing and how u can get in on that action 💅. If u just wanna speedrun to the actual command list, no shame, just jump over to the API commands page <>. We support u either way 🫶.

So Qtile's command API? It's built on a whole graph of objects 🌈, where every object has its own lil set of commands attached, combined with a bunch of interfaces that let u navigate around the graph and actually run those commands. It's giving very organized. Very demure.

This page is ur overview of the command graph + all the interfaces u can reach as a user. And the docs also have every single command exposed by objects on the graph, so nothing is hiding from u 👀.

Note: yeah so, users can technically poke at the internal python objects (like, via a qtile instance and stuff), but bestie — that is NOT part of the "official" API. These objects and methods aren't in the docs rn, but u can go look at the source code on github if ur nosy (valid). And heads up: changes to commonly-used internal objects will be kept to a minimum, so it's relatively safe, but still. Official API only for the girlies who want stability 💖.

Now, where do the graph and its object commands actually get used? Everywhere, bestie:

  • Commands can be bound to keys <#config-keys> in the Qtile config file using the lazy interface. Yes, lazy (the name is a whole mood, we don't judge).
  • Commands can be called from a script using any of the various available interfaces <#scripting-interfaces> so u can talk to Qtile from Python or shell scripts 🔥.

And if ur feeling extra and want something more interactive, there's more:

  • Commands can be called through qtile shell <#qtile-shell>, which is literally the Qtile shell.
  • The shell can also be hooked into a Jupyter kernel called iqshell <#iqshell> (NB this interface is currently broken 🙈).

If all this graph talk is giving u a headache, please, bestie, take a sec and just go explore the API using the qtile shell command shell ok? The shell lets u walk the graph, so u can literally see how the nodes are connected. U can see available nodes with the ls command, and command lists + detailed docs live in the built-in help command. Oh and — u can execute commands from this shell too. Iconic behavior, honestly 💅.

🌸 The Command Graph 🌸

The objects in Qtile's command graph come in eight flavours, matching the eight basic components of the window manager: layouts, windows, groups, bars, widgets, screens, core, and a special root node (the main character ofc 👑).

Objects get addressed by a path specification that starts at the root and follows the available paths through the graph. Here's what the graph looks like:

Every arrow can be read as "holds a reference to". So, for example, we can see that a widget object holds a reference to objects of type bar and screen. Slay.

Let's do some simple examples of how the addressing works, shall we? Which particular objects we hold a reference to depends on the context — like, widgets hold a reference to the screen they appear on, and the bar they're attached to. Context matters, bestie 💜.

So let's look at an example, starting at the root node. This little script runs the status command on the root node, which in this case is represented by the InteractiveCommandClient object:

from libqtile.command.client import InteractiveCommandClient
c = InteractiveCommandClient()
print(c.status())

InteractiveCommandClient is a class that lets us traverse the command graph using attributes to select child nodes or commands. In this example, we've resolved the status() command on the root object. The interactive command client will automatically find and connect to a running Qtile instance, and it'll use that to dispatch the call and print out the return. Auto-magic, no cap ✨.

An alternative is the CommandClient, which lets u resolve command graph objects more precisely, but it's not as easy to vibe with from a REPL:

from libqtile.command.client import CommandClient
c = CommandClient()
print(c.call("status"))

Just like the interactive client, the command client will automatically connect to a running Qtile instance. Here, we first resolve the status() command with the .call("status"), which simply located the function, and then we can invoke the call with no arguments. Easy peasy 🍓.

For the rest of this example, we're gonna use the interactive command client. From the graph, we can see that the root node holds a reference to group nodes. We can access the "info" command on the current group like so:

c.group.info()

To access a specific group, regardless of whether or not it's current, we use the Python mapping lookup syntax. This command sends group "b" to screen 1 (via the libqtile.config.Group.toscreen() method):

c.group["b"].toscreen(1)

In different contexts, it's possible to access a default object, where in other contexts a key is required. From the root of the graph, the current group, layout, screen and window can be accessed by simply leaving the key specifier out. But the key specifier is mandatory for widget and bar nodes. No skipping for u, widget and bar 💀.

With this context, we can now drill down deeper into the graph, following the edges in the graphic above. To access the screen currently displaying group "b", we can do this:

c.group["b"].screen.info()

Be aware though, bestie 😭 — group "b" might not currently be displayed. In that case, it has no associated screen, the path resolves to a non-existent node, and we get an exception:

libqtile.command.CommandError: No object screen in path 'group['b'].screen'

So like, tread carefully 🫣.

Also important: the graph is not a tree, because it can contain cycles. This path (redundantly) specifies the group belonging to the screen that belongs to group "b":

c.group["b"].screen.group

And this amount of connectivity makes it easy to reach out from a given object when callbacks and events fire on that object, to all its related objects. Very connected. Very social butterfly 🦋.

As noted before, some objects require a selector to make sure the correct object gets selected, while other nodes provide a default object without a selector. The table below shows what selectors are required for the different nodes, and whether the selector is optional (i.e. if it can be omitted to select the default object):

ObjectKeyOptional?Example
bar <>"top", "bottom" (Note: if accessing this node from the root, users on multi-monitor setups may wish to navigate via a screen node to ensure that they select the correct object.)Noc.screen.bar["bottom"]
group <>Name stringYesc.group["one"]<br>c.group
layout <>Integer indexYesc.layout[2]<br>c.layout
screen <>Integer indexYesc.screen[1]<br>c.screen
widget <>Widget name (This is usually the name of the widget class in lower case but can be set by passing the name parameter to the widget.)Noc.widget["textbox"]
window <>Integer window IDYesc.window[123456]<br>c.window
core <>Non/ac.core
└───────────┴─────────────────────────────────────────────────────────────────────┴───────────┴────────────────────────┘

💖 Command graph development

Okay so this bit goes deep on how Qtile's command graph actually works. If you're just here to script your Qtile window manager, the earlier info <> plus the docs on all the available commands <> is literally all you need to get going, fr.

But if you wanna develop Qtile itself — like, get in the engine room — we gotta dig into how Qtile represents these objects, and from there, how the commands get dispatched. 🧚


🌸 Client-Server Scripting Model

Qtile runs a client-server control model, bestie. The main Qtile instance sits there listening on a named pipe, and over that pipe, marshalled command calls and response data get passed back and forth. Which means — yes — Qtile can be controlled completely from external scripts. 👑

The remote interaction happens through an instance of the libqtile.command.interface.IPCCommandInterface class. That class is the one that establishes a connection to the currently running instance of Qtile. Then a libqtile.command.client.InteractiveCommandClient can use that connection to dispatch commands to the running instance. 🦋

And the ✨magic✨ is that the commands just show up as methods with the appropriate signature on the InteractiveCommandClient object. The object hierarchy is described in the Architecture <#commands-api> section of this manual, and full command documentation lives in the Qtile Shell <#qtile-shell>. iykyk.


💅 Digging Deeper: Command Objects

Every single configured object that Qtile sets up is a CommandObject subclass. They're named that way because — you guessed it — we can issue commands against them using the command scripting API. 📝

Look through the code and you'll notice the exposed commands are exactly the ones decorated with the @expose_command() decorator. So when you're writing custom layouts, widgets, or literally any other object, you can add your own custom functions and — once you slap that decorator on — they become callable through the standard command infrastructure. Slay. An available command can be pulled out by calling .command() with the name of the command.

But wait, there's more. Each command object doesn't just have a set of associated commands, it also has a collection of items associated with it — and that is what forms the graph shown above. ⭐

For a given object type, the items() method returns all the names of the associated objects of that type, plus whether or not there's a defaultable value. So for example, from the root, .items("group") returns the name of all of the groups and confirms that yes, there is a default value — the currently focused group. 💖

To navigate from one command object to the next, you use the .select() method. It resolves a requested object from the command graph by iteratively selecting objects. So a selector like [("group", "b"), ("screen", None)] means: first resolve group "b", then the screen associated to the group. Easy. periodt.


🎀 The Command Graph

To help with specifying command objects, there's the abstract command graph structure. This thing lets us address any valid command object and issue any command against it — without needing a Qtile instance running, and without needing anything to resolve the objects to. 🫶

Which is super useful when you're constructing lazy calls, where the Qtile instance doesn't exist yet to specify the path that'll be resolved when the command actually executes. The only limitation of traversing the command graph? It must follow the allowed edges specified in the first section above. No shortcuts, no vibes-based traversal. 🙈

Every object in the command graph is represented by a CommandGraphNode. Any call can be resolved from a given node. On top of that, every node knows about all the children objects reachable from it, and can .navigate() to the other nodes in the command graph. ✨

Each object type is represented as a CommandGraphObject type, and the root node of the graph — the CommandGraphRoot — represents the Qtile instance itself. When a call is performed on an object, it returns a CommandGraphCall. Every call knows its own name and can resolve the path through the command graph to find itself. Iconic.

Heads up: the command graph can totally stand alone — there's no other functionality within Qtile that it depends on. We could have started right here and built up, but it genuinely helps to understand the objects the graph is meant to represent, because the graph is just a representation of a traversal of the real objects in a running Qtile window manager. To tie the running Qtile instance to the abstract command graph, we move on to the command interface. 👇

🔥 Executing graph commands: Command Interface

The CommandInterface is what lets us take an abstract call on the command graph and resolve it against a running command object. Or, put another way: it's the thing that takes the graph traversal .group["b"].screen.info() and actually executes the info() command against the addressed screen object. 💀 (in the best way)

There's extra functionality you can use to check that a given traversal resolves to actual objcets, and that the requested command actually exists. But — and this is important — by construction of the command graph, the traversals here must be feasible, even if they can't be resolved for a given configuration state.

For example: it is possible to check the screen assoctiated to a group, even though the group may not be on a screen. But it is not possible to check the widget associated to a group. So don't try it. 💅

The simplest form of the command interface is QtileCommandInterface, which can take an in-process Qtile instance as the root CommandObject and execute requested commands. This is typically how the unit tests for Qtile get run. 🧪

The other main example is IPCCommandInterface, which is able to route all calls through an IPC client connected to a running Qtile instance. In that case, the command graph call can be constructed on the client side without having to dispatch to Qtile at all — and once the call is constructed and deemed valid, it can be executed. 🌈

In both of these cases, executing a command on a command interface returns the result of executing the command on a running Qtile instance. But to support lazy execution, the LazyCommandInterface instead returns a LazyCall, which can be resolved later by the running Qtile instance when it's configured to fire. 🌸


💜 Tying it together: Command Client

So, recap. We've got our running Command Objects, the Command Interface to dispatch commands against those objects, and the Command Graph structure itself which encodes how to traverse the connections between the objects. 🦋

The final component that ties everything together is the Command Client, which lets us navigate through the graph to resolve objects, find their associated commands, and execute those commands against the held command interface.

The whole idea of the command client: it's created with a reference into the command graph and a command interface. All navigation happens against the command graph, and traversal works by creating a new command client starting from the new node. When a command gets executed against a node, that command is dispatched to the held command interface. 👑

The key decision is how to perform the traversal, and the command client comes in two different flavors:

  • The standard CommandClient — good for handling more programmatic traversal of the graph, calling methods to traverse the graph.
  • The InteractiveCommandClient — behaves more like a standard Python object, traversing by accessing properties and performing key lookups. 💖

Returning to our examples above, now we have the full context to see what's going on when we call:

from libqtile.command.client import CommandClient
c = CommandClient()
print(c.call("status")())
from libqtile.command.client import InteractiveCommandClient
c = InteractiveCommandClient()
print(c.status())

In both cases, the command clients are constructed with the default command interface — which sets up an IPC connection to the running Qtile instance — and starts the client at the graph root. So when we call c.call("status") or c.status, we navigate the command client to the status command on the root graph object. And when those get invoked, the commands graph calls are dispatched via the IPC command interface, and the results come back and get printed on the local command line. ✨

The power you unlock by separating out the traversal and resolution of objects in the command graph from actually invoking or looking up any objects within the graph can be seen in the lazy module. By creating a lazy evaluated command client, we can expose the graph traversal and object resolution functionality through the same InteractiveCommandClient that's used to perform live command execution in the Qtile prompt. 🌟

okay bestie, we're deep in the manpage now and this is the part where i tell you how to actually talk to qtile's command graph, no cap 💅

✨ INTERFACES ✨

🎀 Introduction 🎀

soooo this page is your little tour of every interface you can use to vibe with Qtile's command graph. here's the lineup:

  • lazy calls
  • when you're running qtile shell
  • when you're running qtile cmd-obj
  • when you're using CommandClient or InteractiveCommandClient in python

the way you call these commands is different depending on which one you pick, BUT — and this is the tea — they all follow the same basic recipe: navigate to the object you want, then run a command on it. periodt. check out these examples showing the same command across every interface:

Lazy call:
lazy.widget["volume"].increase_volume()

qtile shell:
> cd widget/volume
widget[volume] > increase_volume()

qtile cmd-obj:
qtile cmd-obj -o widget volume -f increase_volume

CommandClient:
>>> from libqtile.command.client import CommandClient
>>> c = CommandClient()
>>> c.navigate("widget", "volume").call("increase_volume")

InteractiveCommandClient:
>>> from libqtile.command.client import InteractiveCommandClient
>>> c = InteractiveCommandClient()
>>> c.widget["volume"].increase_volume()

🌸 The Interfaces 🌸

so from those examples you can see there are five main interfaces for interacting with Qtile's command graph. which one you pick is fully up to how you're trying to use it, because each one hits different:

  • the lazy interface lives in config scripts, where you bind commands to keys and mouse callbacks ✨
  • the qtile shell is a little tool for exploring the graph — it shows it off like a filesystem. it is not designed for scripting.
  • if you're making shell scripts, qtile cmd-obj is the one bestie, recommended choice fr fr
  • if you wanna control Qtile from a python script, there are two options: libqtile.command.client.CommandClient and libqtile.command.client.InteractiveCommandClient. users are advised to use InteractiveCommandClient because it simplifies the syntax for navigating the graph and calling commands 🫶

🦋 The Lazy interface 🦋

lazy.lazy is this special helper object that specifies a command to run later. Lazy objects are usually people's first ever run-in with Qtile's command graph — but honestly they might not even realise it 😭. understanding this will help you when you start poking at the other interfaces on this page.

the basic syntax for a lazy command is:

lazy.node[selector].command(arguments)

no node is required when accessing commands on the root node. and bonus: you can sequence multiple nodes if you need to navigate to a specific object. say you wanna bind a key that focuses the next window on the active group on screen 2 — you'd make a lazy object like this:

lazy.screen[1].group.next_window()
Note: like we said up there, lazy calls do NOT actually call the relevant command — they only create a reference to it. that's what makes them perfect for binding commands to key presses and mouse_callbacks for widgets. but it ALSO means lazy calls cannot be included in user-defined functions. 💀

💜 qtile shell 💜

qtile shell maps the command graph onto a virtual filesystem you can navigate in a similar way. while it's not really meant for scripting, the qtile shell interface is genuinely an excellent way for you to navigate and familiarise yourself with the command graph ✨

for more info, please refer to qtile shell <>


🍓 qtile cmd-obj 🍓

qtile cmd-obj is a command line interface for executing commands on the command graph. you can use it standalone (like, straight from the terminal) or incorporated into shell scripts 👑

for more info, please refer to qtile cmd-obj <>


💀 CommandClient 💀

the CommandClient interface is a low-level python interface for accessing and navigating the command graph. and because it's low-key low-level, navigation steps have to be called explicitly instead of being inferred from the body of the calling command. like:

from libqtile.command.client import CommandClient

c = CommandClient()

# Call info command on clock widget
info = c.navigate("widget", "clock").call("info")

# Call info command on the screen displaying the clock widget
info = c.navigate("widget", "clock").navigate("screen", None).call("info")

notice from that last example that every navigation step has to be called separately. the arguments passed to navigate() are node and selector. selector is None when you want to access the default object on that node (like, the current screen).

more technical explanation about the python command clients can be found at Executing graph commands: Command Interface <#command-interface>. 🔥


🧚 InteractiveCommandClient 🧚

the InteractiveCommandClient is probably gonna be the more popular interface for users who wanna access the command graph via external python scripts. one of the key differences between InteractiveCommandClient and the CommandClient above is that InteractiveCommandClient removes the need to call navigate and call explicitly. instead, the syntax mimics the lazy interface ✨

so for example, to call the same commands from the example above:

from libqtile.command.client import InteractiveCommandClient

c = InteractiveCommandClient()

# Call info command on clock widget
info = c.widget["clock"].info()

# Call info command on the screen displaying the clock widget
info = c.widget["clock"].screen.info()

👑 COMMANDS API 👑

the following pages list all the commands that Qtile's command graph exposes. so, like, every single one of these commands is accessible through ANY of the various interfaces Qtile provides (e.g. the lazy interface for keybindings and mouse callbacks). slay.

🌈 Qtile root object 🌈

the root node represents the main Qtile manager instance. that's why so many commands on this node are related to the running of the application itself.

the root can access every other node in the command graph. certain objects can be accessed without a selector, which means the current object gets selected (e.g. current group, screen, layout, window).

🎀 Layout objects 🎀

layouts position windows according to their own specific rules. layout commands typically include moving windows around the layout and changing the size of windows.

layouts can access the windows being displayed, the group holding the layout, and the screen displaying the layout.

⭐ Window objects ⭐

the size and position of windows is determined by the current layout. but bestie, windows can still change their appearance in multiple ways (toggling floating state, fullscreen, opacity).

windows can access objects relevant to the display of the window (i.e. the screen, group and layout).

💅 Group objects 💅

groups are Qtile's workspaces. groups are not responsible for positioning windows (that's handled by the layouts <>) so the available commands are a bit more limited in scope.

groups have access to the layouts in that group, the windows in the group, and the screen displaying the group.

💖 Bar objects 💖

the bar is mainly used to display widgets on the screen, so it doesn't need many of its own commands. iconic minimalism.

to select a bar on the command graph, you MUST use a selector (there's no default bar). the selector is the position of the bar on the screen, i.e. "top", "bottom", "left" or "right".

the bar can access the screen it's on and the widgets it contains via the command graph.

🌸 Widget objects 🌸

widgets are little scripts used to provide content or add functionality to the bar. some widgets will expose commands so functionality can be triggered indirectly (e.g. via a keypress).

widgets can access the parent bar and screen via the command graph.

🫶 Screen objects 🫶

screens are the display area that holds bars and an active group. screen commands include changing the current group and changing the wallpaper.

screens can access objects displayed on that screen, e.g. bar, widgets, groups, layouts and windows.

😭 Backend core objects 😭

the backend core is the link between the Qtile objects (windows, layouts, groups etc.) and the specific backend (X11 or Wayland). this core should be largely invisible to users and, because of that, these objects don't expose many commands.

nevertheless, both backends DO contain important commands — notably set_keymap on X11 and change_vt used to change to a different TTY on Wayland 👀

the backend core has no access to other nodes on the command graph.

💜 X11 backend 💜

🦋 Wayland backend 🦋

🌸 Requirements 🌸

ok bestie, listen — before you start poking at Qtile's guts, here are Qtile's extra dependencies that might be required for tests. It's giving group-project energy, so here's who brings what:

  • pytest (https://docs.pytest.org) — Ubuntu package: python3-pytest — needed for running tests 🧪
  • pre-commit (https://pre-commit.com/) — Ubuntu package: pre-commit — needed for running linters ✨
  • PyGObject — Ubuntu package: python3-gi — needed for running tests (test windows)
  • Xephyr (https://freedesktop.org/wiki/Software/Xephyr) — Ubuntu package: xserver-xephyr — needed for testing with X11 backend (optional, see below 👇)
  • mypy — Ubuntu package: python3-mypy — needed for testing qtile check (optional)
  • imagemagick>=6.8 — Ubuntu package: imagemagick — needed for test/test_images* (optional)
  • gtk-layer-shell — Ubuntu package: libgtk-layer-shell0 — needed for testing notification windows in Wayland (optional)
  • dbus-launch — Ubuntu package: dbus-x11 — needed for testing dbus-using widgets (optional)
  • notify-send — Ubuntu package: libnotify-bin — needed for testing Notify widget (optional)
  • xvfb — Ubuntu package: xvfb — needed for testing with X11 headless (optional)

no cap, half of these say "optional" and they mean it 🫶


🦋 Backends 🦋

The test suite can be run using the X11 or Wayland backend, or literally both at once. By default, only the X11 backend is used for tests.

Wanna test just one backend or both? Pass them as arguments to pytest:

pytest --backend wayland  # Test just Wayland backend
pytest --backend x11 --backend wayland  # Test both

Testing with the X11 backend requires Xephyr (https://freedesktop.org/wiki/Software/Xephyr) — and xvfb for headless mode — on top of the core dependencies. So like, don't show up to the X11 party empty-handed 💅


🎀 Setting up the environment 🎀

In the root of the project, run uv venv. It will create a virtualenv called .venv. iconic behaviour, we love her.

Activate this virtualenv with:

  • bash, zsh: source .venv/bin/activate
  • fish: source .venv/bin/activate.fish

Deactivate it with the deactivate command (yes bestie, you can just leave, it's not a toxic relationship).

To install every python dependency that Qtile needs, use make deps.

To build the wayland backend, use make build-wayland.

Install locally into the virtualenv with uv pip install ..


⭐ Building the documentation ⭐

To build the documentation, you'll also need to install graphviz (https://www.graphviz.org/download/).

Build the documentation with make docs from the root directory.

Check the result by opening docs/_build/html/index.html in your browser. go look at her, she's beautiful 😭

Note: The screenshots can take a while to build, so you can export QTILE_NO_BUILD_SCREENSHOTS to disable building them. thank me later 🌈

💖 Development and testing 💖

In practice, the development cycle looks something like this — it's a whole loop, iykyk:

  1. make minor code change
  2. run appropriate test: pytest tests/test_module.py or pytest -k PATTERN
  3. GOTO 1, until hackage is complete
  4. run entire test suite to make sure you didn't break anything else: pytest
  5. try to commit, get changes and feedback from the pre-commit hooks
  6. GOTO 5, until your changes actually get committed

Tests and pre-commit hooks will be run by our CI on every pull request as well, so you can see whether or not your contribution passes. the CI is watching, bestie 👀


💜 Coding style 💜

While not all of our code follows PEP8 (https://www.python.org/dev/peps/pep-0008/), we do try to adhere to it where possible. All new code should be PEP8 compliant. periodt.

The make lint command (or pre-commit run -a) will run our linters and formatters with our configuration over the whole libqtile to ensure your patch complies with reasonable formatting constraints. We also request that git commit messages follow the standard format (https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).


🍓 Logging 🍓

Logs are important to us because they are our best way to see what Qtile is doing when something abnormal happens. However, our goal is not to have as many logs as possible, because that hinders readability. What we want are relevant logs. quality over quantity, always ✨

To decide which log level to use, refer to the following scenarios:

  • ERROR: a problem affects the behavior of Qtile in a way that is noticeable to the end user, and we can't work around it. 😭
  • WARNING: a problem causes Qtile to operate in a suboptimal manner.
  • INFO: the state of Qtile has changed.
  • DEBUG: information is worth giving to help the developer better understand which branch the process is in.

Be careful not to overuse DEBUG and clutter the logs. No information should be duplicated between two messages.

Also, keep in mind that any other level than DEBUG is aimed at users who don't necessarily have advanced programming knowledge; adapt your message accordingly. If it can't make sense to your grandma, it's probably meant to be a DEBUG message. 💀 fr fr


🧚 Using Xephyr 🧚

Qtile has a very extensive test suite, using the Xephyr nested X server. When tests are run, a nested X server with a nested instance of Qtile is fired up, and then tests interact with the Qtile instance through the client API. The fact that we can do this is a great demonstration of just how completely scriptable Qtile is. In fact, Qtile is designed expressly to be scriptable enough to allow unit testing in a nested environment. main character design decision tbh 👑

The Qtile repo includes a tiny helper script to let you quickly pull up a nested instance of Qtile in Xephyr, using your current configuration. Run it from the top-level of the repository, like this:

./scripts/xephyr

Change the screen size by setting the SCREEN_SIZE environment variable. Default: 800x600. Example:

SCREEN_SIZE=1920x1080 ./scripts/xephyr

Change the log level by setting the LOG_LEVEL environment variable. Default: INFO. Example:

LOG_LEVEL=DEBUG ./scripts/xephyr

The script will also pass any additional options to Qtile. For example, you can use a specific configuration file like this:

./scripts/xephyr -c ~/.config/qtile/other_config.py

Once the Xephyr window is running and focused, you can enable capturing the keyboard shortcuts by hitting Control+Shift. Hitting them again will disable the capture and let you use your personal keyboard shortcuts again. 🎀

You can close the Xephyr window by enabling the capture of keyboard shortcuts and hit Mod4+Control+Q. Mod4 (or Mod) is usually the Super key (or Windows key). You can also close the Xephyr window by running qtile cmd-obj -o cmd -f shutdown in a terminal (from inside the Xephyr window of course).

You don't need to run the Xephyr script in order to run the tests, as the test runner will launch its own Xephyr instances. it's independent like that 💅


🌈 Second X Session 🌈

Some users prefer to test Qtile in a second, completely separate X session: Just switch to a new tty and run startx normally to use the ~/.xinitrc X startup script.

It's likely though that you want to use a different, customized startup script for testing purposes, for example ~/.config/qtile/xinitrc. You can do so by launching X with:

startx ~/.config/qtile/xinitrc

startx deals with multiple X sessions automatically. If you want to use xinit instead, you need to first copy /etc/X11/xinit/xserverrc to ~/.xserverrc; when launching it, you have to specify a new session number:

xinit ~/.config/qtile/xinitrc -- :1

Examples of custom X startup scripts are available in qtile-examples (https://github.com/qtile/qtile-examples). go browse, it's giving inspiration 🦋


💅 Debugging in PyCharm 💅

Make sure to have all the requirements installed and your development environment setup.

PyCharm should automatically detect the venv virtualenv when opening the project. If you are using another virtualenv, just instruct PyCharm to use it in Settings -> Project: qtile -> Project interpreter.

In the project tree, on the left, right-click on the libqtile folder, and click on Mark Directory as -> Sources Root.

and that's it bestie, you're officially set up to contribute 🫶

okay so next up you're gonna add a Configuration using a Python template, and bestie it needs exactly these fields:

  • Script path: libqtile/scripts/main.py, or the absolute path to it 🫶
  • Parameters: -c libqtile/resources/default_config.py, or literally nothing if you wanna use your own config file in ~/.config/qtile/config.py
  • Environment variables: PYTHONUNBUFFERED=1;DISPLAY=:1
  • Working directory: the root of the project
  • Add contents root to PYTHONPATH: yes
  • Add source root to PYTHONPATH: yes

Then, in a terminal, run:

Xephyr +extension RANDR -screen 1920x1040 :1 -ac &

Not me pointing out that we used the same display, :1, in both the terminal command and the PyCharm configuration environment variables 👀 Feel free to change the screen size to fit your own screen tho, no pressure.

Finally, place your breakpoints in the code and click on Debug! periodt ✨

Once you finished debugging, you can close the Xephyr window with kill PID (use the jobs builtin to get its PID). iykyk 💅

Debugging in VSCode

Make sure you have all the requirements installed and your development environment setup. that's the vibe check, don't skip it 🙈

Open the root of the repo in VSCode. If you've created it, VSCode should detect the venv virtualenv — if not, select it. slay 🎀

Create a launch.json file with the following lines.

{
  "version": "0.2.0",
  "configurations": [
      {
          "name": "Python: Qtile",
          "type": "python",
          "request": "launch",
          "program": "${workspaceFolder}/libqtile/scripts/main.py",
          "cwd": "${workspaceFolder}",
          "args": ["-c", "libqtile/resources/default_config.py"],
          "console": "integratedTerminal",
          "env": {"PYTHONUNBUFFERED":"1", "DISPLAY":":1"}
      }
  ]
}

Then, in a terminal, run:

Xephyr +extension RANDR -screen 1920x1040 :1 -ac &

Note that we used the same display, :1, in both the terminal command and the VSCode configuration environment variables. Then debug usually in VSCode. Feel free to change the screen size to fit your own screen 💖

Profiling

py-spy <https://github.com/benfred/py-spy> is a sampling profiler that attaches to any running Python process with no code changes and no restart required, iconic behaviour honestly:

pip install py-spy

# Record a 30-second flamegraph (interactive SVG)
py-spy record -o flamegraph.svg -d 30 --pid $(pidof qtile)

Open flamegraph.svg in a browser to explore the call stack interactively 🔥 py-spy top gives a live top-style view of where time is being spent. it's giving detective energy fr.

For memory, use the built-in qtile top <> subcommand, which wraps tracemalloc:

qtile top          # live curses view of top allocators
qtile top --raw    # one-shot snapshot

Resources

Here are a number of resources that may come in handy 🦋:


CONTRIBUTING

Reporting bugs

Perhaps the easiest way to contribute to Qtile is to report any bugs you run into on the GitHub issue tracker <https://github.com/qtile/qtile/issues>. low-key the most powerful move in the whole repo bestie 💅

Useful bug reports are ones that get bugs fixed. A useful bug report normally has two qualities:

  1. Reproducible. If your bug is not reproducible it will never get fixed. You should clearly mention the steps to reproduce the bug. Do not assume or skip any reproducing step. Describe the issue, step-by-step, so that it is easy to reproduce and fix. no cap, every single step matters 🙏
  2. Specific. Do not write an essay about the problem. Be specific and to the point. Try to summarize the problem in a succinct manner. Do not combine multiple problems even if they seem to be similar. Write different reports for each problem. one bug per report, one report per bug, periodt.

Ensure to include any appropriate log entries from ~/.local/share/qtile/qtile.log and/or ~/.xsession-errors! Sometimes, an xtrace is requested. If that is the case, refer to capturing an xtrace <#capturing-an-xtrace>. those logs are literally the receipts 📝

Writing code

To get started writing code for Qtile, check out our guide to Hacking on Qtile <#hacking>. A more detailed page on creating widgets is available here <#widget-creation> 🧚

Important: Use a separate git branch to make rebasing easy. Ideally, you would git checkout -b <my_feature_branch_name> before starting your work. See also: using git <#using-git>.

Submit a pull request

You've done your hacking and are ready to submit your patch to Qtile. Great! 🎉 Now it's time to submit a pull request <https://help.github.com/articles/using-pull-requests> to our issue tracker <https://github.com/qtile/qtile/issues> on GitHub.

Important: Pull requests are not considered complete until they include all of the following:
  • Code that conforms to our linters and formatters. Run pre-commit install to install pre-commit hooks that will make sure your code is compliant before any commit. ✨
  • Unit tests that pass locally and in our CI environment (More below). Please add unit tests to ensure that your code works and stays working! 🫶
  • Documentation updates on an as needed basis.
  • A qtile migrate migration is required for config-breaking changes. See here <> for current migrations and see below for further information.
  • Code that does not include unrelated changes. Examples for this are formatting changes, replacing quotes or whitespace in other parts of the code or "fixing" linter warnings popping up in your editor on existing code. Do not include anything like the above! 🙈
  • Widgets don't need to catch their own exceptions, or introduce their own polling infrastructure. The code in libqtile.widget.base.* does all of this. Your widget should generally only include whatever parsing/rendering code is necessary, any other changes should go at the framework level. Make sure to double-check that you are not re-implementing parts of libqtile.widget.base. no duplicating the framework bestie, it already ate 💅
  • Commit messages are more important that Github PR notes, since this is what people see when they are spelunking via git blame. Please include all relevant detail in the actual git commit message (things like exact stack traces, copy/pastes of discussion in IRC/mailing lists, links to specifications or other API docs are all good). If your PR fixes a Github issue, it might also be wise to link to it with #1234 in the commit message. spill the whole story, the commit message is the main character ✍️
  • PRs with multiple commits should not introduce code in one patch to then change it in a later patch. Please do a patch-by-patch review of your PR, and make sure each commit passes CI and makes logical sense on its own. In other words: do introduce your feature in one commit and maybe add the tests and documentation in a separate commit. Don't push commits that partially implement a feature and are basically broken. each commit gotta stand on its own, fr fr 💀
Note: Others might ban force-pushes, we allow them and prefer them over incomplete commits or commits that have a bad and meaningless commit description.

Feel free to add your contribution (no matter how small) to the appropriate place in the CHANGELOG as well! 🌸

Unit testing

We must test each unit of code to ensure that new changes to the code do not break existing functionality. The framework we use to test Qtile is pytest <https://docs.pytest.org>. How pytest works is outside of the scope of this documentation, but there are tutorials online that explain how it is used.

Our tests are written inside the test folder at the top level of the repository. Reading through these, you can get a feel for the approach we take to test a given unit. Most of the tests involve an object called manager. This is the test manager (defined in test/helpers.py), which exposes a command client at manager.c that we use to test a Qtile instance running in a separate thread as if we were using a command client from within a running Qtile session. it's giving parallel-universe-Qtile vibe 🌀

For any Qtile-specific question on testing, feel free to ask on our issue tracker <https://github.com/qtile/qtile/issues> or on IRC (#qtile on irc.oftc.net). 💜

Running tests locally

You can run our various CI bits locally with:

make lint  # to run the pre-commit checks
make check  # to run both backends for the minimum python version
make QTILE_CI_PYTHON=3.13 QTILE_CI_BACKEND=x11  # to run a specific backend/python version combination
uv run --python=3.13 pytest --backend=wayland ./test/widgets/test_widgetbox.py::test_widgetbox_widget # to run a specific test

See .github/workflows/ci.yml for the full matrix of supported python+backend versions. All of these will matter, but for most changes it is only necessary to run one set locally to confirm the change is correct. you don't gotta run the whole army bestie, one is enough for most stuff 🙌

Important: The CI is configured to run all the environments. Hence it can be time-consuming to make all the tests pass. As stated above, pull requests that don't pass the tests are considered incomplete. Don't forget that this does not only include the functionality, but the style, typing annotations (if necessary) and documentation as well! 😭

Writing migrations

Migrations are needed when a commit introduces a change which makes a breaking change to a user's config. Examples include renaming classes, methods, arguments and moving modules or class definitions.

Where these changes are made, it is strongly encouraged to support the old syntax where possible and warn the user about the deprecations. be nice to the users, they're just vibing 💖

Whether or not a deprecation warning is provided, a migration script should be provided that will modify the user's config when they run qtile migrate.

Click here for detailed instructions on How to write a migration script <>.

How to write a migration script

Qtile's migration scripts should provide two functions:

  • Update config files to fix any breaking changes introduced by a commit
  • Provide linting summary of errors in existing configs

To do this, we use LibCST <https://libcst.readthedocs.io/en/latest/> to parse the config file and make changes as appropriate. Basic tips for using LibCST are included below but it is recommended that you read their documentation to familiarise yourself with the available functionalities. 📚

Structure of a migration file

Migrations should be saved as a new file in libqtile/scripts/migrations.

A basic migration will look like this:

from libqtile.scripts.migrations._base import MigrationTransformer, _QtileMigrator, add_migration

class MyMigration(MigrationTransformer):
    """The class that actually modifies the code."""
    ...

class Migrator(_QtileMigrator):
    ID = "MyMigrationName"

    SUMMARY = "Summary of migration."
    HELP = """
    Longer text explaining purpose of the migration and, ideally,
    giving code examples.

    """

    AFTER_VERSION = "0.22.1"

    TESTS = []

    visitor = MyMigration

add_migration(Migrator)

✨ Providing details about the migration ✨

Okay so bestie, the whole point of the Migrator class in that code up there? It's literally just to hand over the info about the migration. 💅

And like — it's so important that this info be as helpful as it can possibly be, because it gets used in like, a bunch of different places. 👑

  • ID — a short, unique name that IDs the migration. It's what lets people cherry-pick exactly which migrations to run, via qtile migrate --run-migrations ID. 🎯
  • SUMMARY — a lil' brief summary of the migration. It pops up when someone runs qtile migrate --list-migrations, and it also lands in the documentation. 📝
  • HELP — same energy, but it's what the script uses (qtie migrate --info ID) and also the docs. This text should be longer, and it can totes include example code. And because it feeds the docs, it's gotta use RST syntax (like .. code:: python for code blocks and all that). ✨
  • AFTER_VERSION — set this to the name of the current release. That's what lets users filter migrations down to only the ones added after the last release. 🗓️
  • visitor — a link to the class definition (not an instance of the class, okay??) for the transformer you wanna use. 🔗
  • add_migration — that call at the very end is required, no skipping, because it makes sure the migration gets loaded into the list of available migrations. 🚨
  • For the deets on TESTS, scroll down. 👇

🏃 How migrations are run

You're pretty much free to transform the code however you see fit, babe. 💫 By default, the script runs the visit method on the parsed code and passes in the visitor attribute of the _QtileMigrator class object. So if every single one of your transformations can live inside a single visitor? Then you don't have to do anything else in the Migrator class at all. 😌

BUT — what if you wanna run multiple visitors, transformers, codemods? Totally doable, you just override the run method of the _QtileMigrator class. For example, the RemoveCmdPrefix migrator has this code:

def run(self, original):
    # Run the base migrations
    transformer = CmdPrefixTransformer()
    updated = original.visit(transformer)
    self.update_lint(transformer)

    # Check if we need to add an import line
    if transformer.needs_import:
        # We use the built-in visitor to add the import
        context = codemod.CodemodContext()
        AddImportsVisitor.add_needed_import(
            context, "libqtile.command.base", "expose_command"
        )
        visitor = AddImportsVisitor(context)

        # Run the visitor over the updated code
        updated = updated.visit(visitor)

    return original, updated

So in this migration, it might be required to add an import statement. LibCST has a built-in transformation for doing that, so we can run it after our own transformation has already done its thing. 🔧

Important: the run method must return a tuple of the original code and the updated code. No exceptions, fr. 🫠

🪄 Transforming the code

It is recommended that you use a transformed visitor/transformer (https://libcst.readthedocs.io/en/latest/tutorial.html#Build-Visitor-or-Transformer) to update the code. For convenience, a MigrationTransformer class is defined in libqtile.scripts.migrations._base. This class definition includes some metadata information and a lint method for outputting details of errors. 🧚

Let's look at an example transformer so you can actually get how the migration works. The code below shows how to change a positional argument into a keyword argument in the WidgetBox widget. 🎀

class WidgetboxArgsTransformer(MigrationTransformer):
    @m.call_if_inside(
        m.Call(func=m.Name("WidgetBox")) | m.Call(func=m.Attribute(attr=m.Name("WidgetBox")))
    )
    @m.leave(m.Arg(keyword=None))
    def update_widgetbox_args(self, original_node, updated_node) -> cst.Arg:
        """Changes positional  argumentto 'widgets' kwargs."""
        self.lint(
            original_node,
            "The positional argument should be replaced with a keyword argument named 'widgets'.",
        )
        return updated_node.with_changes(keyword=cst.Name("widgets"), equal=EQUALS_NO_SPACE)

Our class (which inherits from MigrationTransformer) defines a single method to perform the transformation. We're taking advantage of LibCST and its Matchers (https://libcst.readthedocs.io/en/latest/matchers_tutorial.html) to narrow the scope of when the transformation runs. 🌈

We're trying to modify an argument, so we use the @m.leave(m.Arg()) decorator to call the function at the end of parsing an argument. We can restrict when that's called by specifying m.Arg(keyword=None), so it only fires for positional arguments. And then — because we only want it called on WidgetBox instantiation lines — we add an additional decorator @m.call_if_inside(m.Call()). That ensures the method only gets called when we're inside a call. On its own that's not helpful, since args would almost always be part of some call. But we can say we only wanna match calls to WidgetBox. The reason for the long syntax above is that LibCST parses WidgetBox() and widget.WidgetBox() differently. In the first one, WidgetBox is in the func property of the call. But in the second, the func is an Attribute because it's a dotted name, so we need to check the attr property. 🕵️

The decorated method takes two arguments, original_mode and updated_node (note: the original_node should not be modified). The method should also confirm the return type. 💯

The above method provides a linting message by calling self.lint and passing the original node plus a helpful message. 📣

Finally, the method updates the code by calling updated_node.with_changes(). In this instance, we add a keyword ("widgets") to the argument. We also remove the spaces around the equals sign, since those are added by default by LibCST. The updated node gets returned. ✨

🧰 Helper classes

Helper classes are provided for common transformations, bestie. 🫶

  • RenamerTransformer — updates all instances of a name, replacing it with another one. This class also handles the necessary linting for you. 🎁
class RenameHookTransformer(RenamerTransformer):
    from_to = ("window_name_change", "client_name_updated")

🧪 Testing the migration

All migrations must be tested, ideally with a bunch of scenarios to confirm the migration works as expected. 👑

Unlike other tests, the tests for the migrations live inside the TESTS attribute. 📚

It's a list that should take a Check, Change or NoChange object (all of them are imported from libqtile.scripts.migrations._base).

  • A Change object needs two parameters: the input code and the expected output.
  • A NoChange object just defines the input (because the output should be the same).
  • A Check object is identical to Change, however, when the test suite runs, the migrated code gets verified with qtile check. So that code will need to include all the relevant imports etc. 🧾

Based on all that, here's what's recommended as best practice:

  • Define one Check test that addresses every situation the migration anticipates. 🎯
  • Use as many Change tests as required to test individual scenarios, in a minimal way.
  • Use NoChange tests wherever there are specific cases that should not be modified. 🚫
  • Depending on how simple the migration is, a single Check might be all you need.

For example, the RemoveCmdPrefix migration has the following TESTS:

TESTS = [
    Change("""qtile.cmd_spawn("alacritty")""", """qtile.spawn("alacritty")"""),
    Change("""qtile.cmd_groups()""", """qtile.get_groups()"""),
    Change("""qtile.cmd_screens()""", """qtile.get_screens()"""),
    Change("""qtile.current_window.cmd_hints()""", """qtile.current_window.get_hints()"""),
    Change(
        """qtile.current_window.cmd_opacity(0.5)""",
        """qtile.current_window.set_opacity(0.5)""",
    ),
    Change(
        """
        class MyWidget(widget.Clock):
            def cmd_my_command(self):
                pass
        """,
        """
        from libqtile.command.base import expose_command

        class MyWidget(widget.Clock):
            @expose_command
            def my_command(self):
                pass
        """
    ),
    NoChange(
        """
        def cmd_some_other_func():
            pass
        """
    ),
    Check(
        """
        from libqtile import qtile, widget

        class MyClock(widget.Clock):
            def cmd_my_exposed_command(self):
                pass

        def my_func(qtile):
            qtile.cmd_spawn("rickroll")
            hints = qtile.current_window.cmd_hints()
            groups = qtile.cmd_groups()
            screens = qtile.cmd_screens()
            qtile.current_window.cmd_opacity(0.5)

        def cmd_some_other_func():
            pass
        """,
        """
        from libqtile import qtile, widget
        from libqtile.command.base import expose_command

        class MyClock(widget.Clock):
            @expose_command
            def my_exposed_command(self):
                pass

        def my_func(qtile):
            qtile.spawn("rickroll")
            hints = qtile.current_window.get_hints()
            groups = qtile.get_groups()
            screens = qtile.get_screens()
            qtile.current_window.set_opacity(0.5)

        def cmd_some_other_func():
            pass
        """
    )
]

The tests check:

  • The cmd_ prefix gets removed on method calls, updating the specific changes as required. ✔️
  • Exposed methods inside a class should use the expose_command decorator (adding the import if it's not already included). ✔️
  • No change is made to a function definition (because it's not part of a class definition). ✔️
Note: Tests will fail in the following scenarios: - If no tests are defined - If a Change test does not result in linting output - If no Check test is defined

You can check your tests by running pytest -k <YourMigrationID>. Heads up though: mpypy must be installed for the Check tests to be run. 🫡

🕰️ Deprecation Policy 💅

okay so here's the tea on when stuff actually gets yeeted: interfaces that have been deprecated for at least two years after the first release that contained the deprecation notice can be deleted. and because all new breaking changes should have a migration (periodt, that's the rule), users can use qtile migrate to bootstrap across versions when migrations are deleted, if necessary. cute, right? 🎀

and the old stuff? deprecated interfaces that do not have a migration — like, the ones whose deprecation got noted before the migration feature even existed — are all fair game to be deleted, since the migration feature is more than two years old at this point. so yeah, no legacy exceptions bestie, they're gone gone 💀


🧚 Source Code Repositories

these are some repositories with genuinely useful Wayland source code, go look at them i'm begging:

📚 Articles & Documentation

some outdated/old resources but that still are useful, ngl they ate:

🎥 Video & Talks

for the girlies who learn by watching, no shame, same:


💖 FREQUENTLY ASKED QUESTIONS 💖

🦋 Why the name Qtile?

bestie, everyone asks this. does the Q have anything to do with Qt? nope, absolutely not, it does not. here's an actual IRC excerpt where cortesi spills the lore of the great trial that birthed Qtile, all thanks to the benevolence of the Open Source Gods. praise be to the OSG! 🙏

ramnes:  what does Qtile mean?
ramnes:  what's the Q?
@tych0:  ramnes: it doesn't :)
@tych0:  cortesi was just looking for the first letter that wasn't registered
         in a domain name with "tile" as a suffix
@tych0:  qtile it was :)
cortesi: tych0, dx: we really should have something more compelling to
         explain the name. one day i was swimming at manly beach in sydney,
         where i lived at the time. suddenly, i saw an enormous great white
         right beside me. it went for my leg with massive, gaping jaws, but
         quick as a flash, i thumb-punched it in both eyes. when it reared
         back in agony, i saw that it had a jagged, gnarly scar on its
         stomach... a scar shaped like the letter "Q".
cortesi: while it was distracted, i surfed a wave to shore. i knew that i
         had to dedicate my next open source project to the ocean gods, in
         thanks for my lucky escape. and thus, qtile got its name...

😭 When I first start xterm/urxvt/rxvt containing an instance of Vim, I see text and layout corruption. What gives?

okay so the tea here is that Vim isn't handling terminal resizes correctly. the fix? start your xterm with the -wf option, like this:

xterm -wf -e vim

alternatively, you can just cycle through your layouts a few times, which usually seems to fix it. lowkey a vibe. 🌀


👑 How do I know which modifier specification maps to which key?

to see a list of modifier names and their matching keys, use the xmodmap command. on the manpage author's system, the output looks like this:

$ xmodmap
xmodmap:  up to 3 keys per modifier, (keycodes in parentheses):

shift       Shift_L (0x32),  Shift_R (0x3e)
lock        Caps_Lock (0x9)
control     Control_L (0x25),  Control_R (0x69)
mod1        Alt_L (0x40),  Alt_R (0x6c),  Meta_L (0xcd)
mod2        Num_Lock (0x4d)
mod3
mod4        Super_L (0xce),  Hyper_L (0xcf)
mod5        ISO_Level3_Shift (0x5c),  Mode_switch (0xcb)

no cap, that's the whole map right there. ✨


🎀 My "pointer mouse cursor" isn't the one I expect it to be!

Qtile should set the default cursor to left_ptr — but heads up, you must install xcb-util-cursor if you want support for themed cursors. it's giving "install the thing bestie". 💅


🙈 LibreOffice menus don't appear or don't stay visible

a workaround for this mouse problem with libreoffice is setting the environment variable »SAL_USE_VCLPLUGIN=gen«. it's dependent on your system configuration as to where you do this — e.g. ArchLinux with libreoffice-fresh in /etc/profile.d/libreoffice-fresh.sh. 🌸


💜 How can I get my groups to stick to screens?

this behaviour can be replicated by configuring your keybindings to not move groups between screens. for example, if you want groups "1", "2" and "3" on one screen and "q", "w", and "e" on the other, instead of binding keys to lazy.group[name].toscreen(), use this:

groups = [
    # Screen affinity here is used to make
    # sure the groups startup on the right screens
    Group(name="1", screen_affinity=0),
    Group(name="2", screen_affinity=0),
    Group(name="3", screen_affinity=0),
    Group(name="q", screen_affinity=1),
    Group(name="w", screen_affinity=1),
    Group(name="e", screen_affinity=1),
]

def go_to_group(name: str):
    def _inner(qtile):
        if len(qtile.screens) == 1:
            qtile.groups_map[name].toscreen()
            return

        if name in '123':
            qtile.focus_screen(0)
            qtile.groups_map[name].toscreen()
        else:
            qtile.focus_screen(1)
            qtile.groups_map[name].toscreen()

    return _inner

for i in groups:
    keys.append(Key([mod], i.name, lazy.function(go_to_group(i.name))))

and to be able to move windows across these groups while switching groups, a similar function can be used:

def go_to_group_and_move_window(name: str):
    def _inner(qtile):
        if len(qtile.screens) == 1:
            qtile.current_window.togroup(name, switch_group=True)
            return

        if name in "123":
            qtile.current_window.togroup(name, switch_group=False)
            qtile.focus_screen(0)
            qtile.groups_map[name].toscreen()
        else:
            qtile.current_window.togroup(name, switch_group=False)
            qtile.focus_screen(1)
            qtile.groups_map[name].toscreen()

    return _inner

for i in groups:
    keys.append(Key([mod, "shift"], i.name, lazy.function(go_to_group_and_move_window(i.name))))

if you use the GroupBox widget you can make it reflect this behaviour:

groupbox1 = widget.GroupBox(visible_groups=['1', '2', '3'])
groupbox2 = widget.GroupBox(visible_groups=['q', 'w', 'e'])

and if you jump between having single and double screens, then modifying the visible groups on the fly may be useful:

@hook.subscribe.screens_reconfigured
async def _():
    if len(qtile.screens) > 1:
        groupbox1.visible_groups = ['1', '2', '3']
    else:
        groupbox1.visible_groups = ['1', '2', '3', 'q', 'w', 'e']
    if hasattr(groupbox1, 'bar'):
        groupbox1.bar.draw()

main character energy, truly. 🫶


🌈 Where can I find example configurations and other scripts?

please visit our qtile-examples repo at <https://github.com/qtile/qtile-examples> which contains examples of users' configurations, scripts and other useful links. it's a goldmine fr fr. ⭐


🍓 Where are the log files for Qtile?

the log files for qtile are at ~/.local/share/qtile/qtile.log. bookmark it bestie, you'll want it. 💖


🧚 How can I match the bar with picom?

you can use "QTILE_INTERNAL:32c = 1" in your picom.conf to match the bar. this will match all internal Qtile windows, so if you want to avoid that — or to target bars individually — you can set a custom property and match that instead:

mybar = Bar(...)

@hook.subscribe.startup
def _():
    mybar.window.window.set_property("QTILE_BAR", 1, "CARDINAL", 32)

this would enable matching on mybar's window using "QTILE_BAR:32c = 1". see 2526 at <https://github.com/qtile/qtile/issues/2526> and 1515 at <https://github.com/qtile/qtile/issues/1515> for more discussion. 👑


💀 Why do get a warning that fonts cannot be loaded?

when installing Qtile on a new system, when running the test suite or the Xephyr script (./scripts/xephyr), you might see errors in the output like the following or similar:

  • Xephyr script:
xterm: cannot load font "-Misc-Fixed-medium-R-*-*-13-120-75-75-C-120-ISO10646-1"
xterm: cannot load font "-misc-fixed-medium-r-semicondensed--13-120-75-75-c-60-iso10646-1"
  • pytest:
---------- Captured stderr call ----------
Warning: Cannot convert string "8x13" to type FontStruct
Warning: Unable to load any usable ISO8859 font
Warning: Unable to load any usable ISO8859 font
Error: Aborting: no font found

-------- Captured stderr teardown --------
Qtile exited with exitcode: -9

if it happens, it might be because you're missing fonts on your system.

on ArchLinux, you can fix this by installing xorg-fonts-misc:

sudo pacman -S xorg-fonts-misc

try to search for "xorg fonts misc" with your distribution name on the internet to find how to install them. you got this. 💪


🔥 I've upgraded and Qtile's broken. What do I do?

if you've recently upgraded, the first thing to do is check the changelog at <https://github.com/qtile/qtile/blob/master/CHANGELOG> and see if any breaking changes were made.

next, check your log file (see above) to see if any error messages explain what the problem is.

if you're still stuck, come and ask for help on Discord, IRC or GitHub. we don't bite (unlike that shark). 🦈


🎨 HOW TO CREATE A WIDGET 🎨

the aim of this page is to explain the main components of qtile widgets, how they work, and how you can use them to create your own widgets. let's get into it bestie.

Note: this page is not meant to be an exhaustive summary of everything needed to make a widget. it is highly recommended that users wishing to create their own widget refer to the source documentation of existing widgets to familiarise themselves with the code. however, the detail below may prove helpful when read in conjunction with the source code.

🌸 What is a widget?

in Qtile, a widget is a small drawing that is displayed on the user's bar. the widget can display text, images and drawings. in addition, the widget can be configured to update based on timers, hooks, dbus_events etc. and can also respond to mouse events (clicks, scrolls and hover). multitasking icon behaviour, honestly. ✨


💅 Widget base classes

Qtile provides a number of base classes for widgets than can be used to implement commonly required features (e.g. display text).

your widget should inherit one of these classes. whichever base class you inherit for your widget, if you override either the __init__ and/or _configure methods, you should make sure that your widget calls the equivalent method from the superclass. don't skip the super() call, i'm begging. 😭

class MyCustomWidget(base._TextBox):

    def __init__(self, **config):
        super().__init__("", **config)
        # My widget's initialisation code here

the functions of the various base classes are explained further below.


🦋 _Widget

this is the base widget class that defines the core components required for a widget. all other base classes are based off this class.

this is like a blank canvas so you're free to do what you want, but you don't have any of the extra functionality provided by the other base classes.

the base._Widget class is therefore typically used for widgets that want to draw graphics on the widget as opposed to displaying text. artistic freedom periodt. 🎨


💖 _TextBox

the base._TextBox class builds on the bare widget and adds a drawer.TextLayout which is accessible via the self.layout property. the widget will adjust its size to fit the amount of text to be displayed.

text can be updated via the self.text property — but note that this does not trigger a redrawing of the widget. heads up!

parameters including font, fontsize, fontshadow, padding and foreground (font colour) can be configured. it is recommended not to hard-code these parameters as users may wish to have consistency across units. respect the users' aesthetic choices bestie. 🎀


🌈 InLoopPollText

the base.InLoopPollText class builds on the base._TextBox by adding a timer to periodically refresh the displayed text.

widgets using this class should override the poll method to include a function that returns the required text.


Note, bestie: this loop runs inside the event loop, so it's so so important that your poll method does not call some blocking function. If you absolutely need that, your widget should inherit the base.BackgroundPoll class instead (peep below 👇).

✨ BackgroundPoll ✨

So base.BackgroundPoll is like, basically the same girl as base.InLoopPollText 💅. The one big glow-up difference? The poll method runs asynchronously and fires off a callback the second the function finishes. That means ur widget can pull text from long-running functions without blocking Qtile at all. No freezing, no lag, just vibes 🦋.


🎀 Mixins 🎀

And ok, as if inheriting from one of the base classes above wasn't enough, widgets can also inherit one or more mixins to sprinkle on some extra functionality. Little flavor boosters, if you will ✨.


💜 PaddingMixin 💜

This one hands you the padding(_x|_y|) attributes so you can change how the widget looks, plus the padding(_side|_top|) properties that give you the right value based on which way the bar is oriented 👑


🌸 MarginMixin 🌸

Essentially exactly the same as the one before it, no cap — except instead it gives you margin(_x|_y|) attributes. And the bar-oriented margin(_side|_top|) properties. Matching set, slay 🫶.


🦋 Configuration 🦋

So now you know which class to base ur widget on, next up: how the widget actually gets configured. Sit with me.


⭐ Defining Parameters ⭐

Every widget is gonna have a bunch of parameters that users can tweak to customise the look, the feel, and/or the behaviour to fit their own needs.

So your widget should provide the default values of those parameters as a class attribute called defaults. The format of this attribute is a list of tuples:

defaults = [
    ("parameter_name",
     default_parameter_value,
     "Short text explaining what parameter does")
]

Then users can override the default value when they're making their config.py file:

MyCustomWidget(parameter_name=updated_value)

And once the widget is initialised, those parameters are just there for you at self.parameter_name. Easy 💅.


🔥 The __init__ method 🔥

Parameters that users should not be able to change can be defined in the __init__ method instead.

This method runs when the widgets are initially created — and heads up, that happens before the qtile object is available. So don't go reaching for things that aren't there yet 🙈.


👑 The _configure method 👑

The _configure method gets called by the bar object, and it sets the self.bar and self.qtile attributes on the widget. It also creates the self.drawer attribute, which you need for displaying any content. No drawer, no art 🎨.

Once this method has run, your widget should be ready to display content, because the bar draws once it's finished its configuration.

So: any calls to methods needed to prep your widget's content should be made from this method, not from __init__. Important bestie, don't mix these up.


💖 Displaying output 💖

A Qtile widget is literally just a drawing that's displayed at a certain location on the user's bar. So the widget's whole job is to create a small drawing surface that can get placed in the right spot on the bar. That's it. That's the assignment 🌈.


💅 The "draw" method 💅

draw gets called whenever the widget needs to update its appearance. That can be triggered by the widget itself (say, if the content changed) or by the bar (say, if the bar has to redraw its entire contents).

So this method needs to hold all the relevant code to draw the various components that make up the widget. Examples of displaying text, icons and drawings are laid out below 👇.

It's important to note that the bar controls the placing of the widget by assigning the offsetx value (horizontal positioning) and the offsety value (vertical positioning). Meanwhile the widget controls its own width and height. Those four values should be used at the end of the draw method. And honestly, it's recommended to just call this helper function and let it do the work automatically:

self.draw_at_default_position()
Note: if you need to trigger a redrawing of your widget, call self.draw() if the width of your widget is unchanged. Otherwise you need to call self.bar.draw(), because that method makes the bar recalculate the position of all the widgets. Know the difference bestie, it matters 😭.

✨ Displaying text ✨

Text is displayed using a drawer.TextLayout object. If all you're doing is displaying text, then it is highly recommended that you use the base._TextBox superclass, because it simplifies adding and updating text. Let it do the heavy lifting 💅.

If you wanna implement it manually though, you can create your own drawer.TextLayout by calling the widget's self.drawer.textlayout method (only available after the _configure method has run, remember).

Some extra formatting of Text can be shown using pango markup, and you get that by making sure the markup parameter is set to True.

self.layout = self.drawer.textlayout(
    "Text",
    "ffffff",      # Font colour
    "sans",        # Font family
    12,            # Font size
    None,          # Font shadow
    markup=False,  # Pango markup (False by default)
    wrap=True,     # Wrap long lines (True by default)
)
Note: in base._TextBox inherited widgets, after _configure has been run, modifications to the widget's font, fontsize, fontshadow, foreground and markup parameters will not be automatically applied to the text layout. You have to modify those values directly on the self.layout.* attributes. And since those attributes have different names to the widget parameters, you can use the set_font method — available in these widgets — to update the text layout attributes, with the huge perk of using the same parameter names as the widget parameters: # All parameters are optional self.set_font( font="mono", fontsize=14, fontshadow="000000", foreground="ff0000", markup=True )

🎀 Displaying icons and images 🎀

Qtile gives you a helper library to convert images into a surface the widget can draw. If the images are static, then you should only load them once when the widget is configured. Given how tiny the bar is, this is most commonly used to draw icons, but the same method applies to other images too 🖼️.

from libqtile import images

def setup_images(self):

    self.surfaces = {}

    # File names to load (will become keys to the `surfaces` dictionary)
    names = (
        "audio-volume-muted",
        "audio-volume-low",
        "audio-volume-medium",
        "audio-volume-high"
    )

    d_images = images.Loader(self.imagefolder)(*names)  # images.Loader can take more than one folder as an argument

    new_height = self.bar.size - 2
    for name, img in d_images.items():
        img.resize(height=new_height)   # Resize images to fit widget
        self.surfaces[name] = img.pattern  # Images added to the `surfaces` dictionary

Drawing the image afterwards is literally just a matter of painting it to the relevant surface:

def draw(self):
    self.drawer.ctx.set_source(self.surfaces[img_name])  # Use correct key here for your image
    self.drawer.ctx.paint()
    self.draw_at_default_position()

🌈 Drawing shapes 🌈

You can draw shapes directly to the widget. The Drawer class (available in your widget after configuration as self.drawer) provides some basic functions: rounded_rectangle, rounded_fillrect, rectangle and fillrect.

On top of that, you can access the Cairo <https://pycairo.readthedocs.io/en/latest/reference/context.html> context drawing functions via self.drawer.ctx 👩‍🎨.

For example, this next bit of code can draw a wifi icon showing signal strength:

import math

...

def to_rads(self, degrees):
    return degrees * math.pi / 180.0

def draw_wifi(self, percentage):

    WIFI_HEIGHT = 12
    WIFI_ARC_DEGREES = 90

    y_margin = (self.bar.size - WIFI_HEIGHT) / 2
    half_arc = WIFI_ARC_DEGREES / 2

    # Draw grey background
    self.drawer.ctx.new_sub_path()
    self.drawer.ctx.move_to(WIFI_HEIGHT, y_margin + WIFI_HEIGHT)
    self.drawer.ctx.arc(WIFI_HEIGHT,
                        y_margin + WIFI_HEIGHT,
                        WIFI_HEIGHT,
                        self.to_rads(270 - half_arc),
                        self.to_rads(270 + half_arc))
    self.drawer.set_source_rgb("666666")
    self.drawer.ctx.fill()

    # Draw white section to represent signal strength
    self.drawer.ctx.new_sub_path()
    self.drawer.ctx.move_to(WIFI_HEIGHT, y_margin + WIFI_HEIGHT)
    self.drawer.ctx.arc(WIFI_HEIGHT
                        y_margin + WIFI_HEIGHT,
                        WIFI_HEIGHT * percentage,
                        self.to_rads(270 - half_arc),
                        self.to_rads(270 + half_arc))
    self.drawer.set_source_rgb("ffffff")
    self.drawer.ctx.fill()

And this creates something that looks like this: [image: wifi_image] [image] 📶✨.


💜 Background 💜

At the start of the draw method, the widget should clear the drawer by drawing the background. Usually you do this by dropping the following line in at the very start of the method:

self.drawer.clear(self.background or self.bar.background)

The background can be a single colour, or a list of colours — and a list gives you a linear gradient from top to bottom. Cute right? 🌈


🦋 Vertical Orientation 🦋

If you're planning to support vertical orientation in your widget, then after calling self.drawer.clear and self.drawer.ctx.save, pop this function into the draw method:

self.rotate_drawer()

🍓 Updating the widget 🍓

Widgets will usually need to update their content periodically. There are so many ways to do this. Here are some of the most common ones, summarised below 👇.


⭐ Timers ⭐

A non-blocking timer can be called by using the self.timeout_add method.

self.timeout_add(delay_in_seconds, method_to_call, (method_args))
Note: consider using the BackgroundPoll superclass whenever you're calling a function repeatedly and displaying its output as text. iykyk 💅

🧚 Hooks 🧚

Qtile has a bunch of hooks built in, and they get triggered on certain events.

The WindowCount widget is a good example of using hooks to trigger updates. It includes this following method, which runs when the widget is configured:

from libqtile import hook

...

def _setup_hooks(self):
    hook.subscribe.client_killed(self._win_killed)
    hook.subscribe.client_managed(self._wincount)
    hook.subscribe.current_screen_change(self._wincount)
    hook.subscribe.setgroup(self._wincount)

Read the Built-in Hooks <#ref-hooks> page for the details of which hooks are available and which arguments get passed to the callback function 👑.


💀 Using dbus 💀

Qtile uses dbus-fast for interacting with dbus.

If you just want to listen for signals then Qtile provides a helper method called add_signal_receiver which can subscribe to a signal and trigger a callback whenever that signal is broadcast. 💫

Note: Qtile uses the asyncio based functions of dbus-fast so your widget must make sure, where necessary, calls to dbus are made via coroutines.

There is a _config_async coroutine in the base widget class which can be overridden to provide an entry point for asyncio calls in your widget.

For example, the Mpris2 widget uses the following code:

from libqtile.utils import add_signal_receiver

...

async def _config_async(self):
    subscribe = await add_signal_receiver(
                    self.message,  # Callback function
                    session_bus=True,
                    signal_name="PropertiesChanged",
                    bus_name=self.objname,
                    path="/org/mpris/MediaPlayer2",
                    dbus_interface="org.freedesktop.DBus.Properties")

dbus-fast can also be used to query properties, call methods etc. on dbus interfaces. Refer to the dbus-fast documentation <https://python-dbus-fast.readthedocs.io/en/latest/> for more information on how to use the module. 📚

Mouse events 🖱️

By default, widgets handle button presses and will call any function that is bound to the button in the mouse_callbacks dictionary. The dictionary keys are as follows:

  • Button1: Left click
  • Button2: Middle click
  • Button3: Right click
  • Button4: Scroll up
  • Button5: Scroll down
  • Button6: Scroll left
  • Button7: Scroll right

You can then define your button bindings in your widget (e.g. in __init__):

class MyWidget(widget.TextBox)

    def __init__(self, *args, **config):
        widget.TextBox.__init__(self, *args, **kwargs)
        self.add_callbacks(
            {
                "Button1": self.left_click_method,
                "Button3": self.right_click_method
            }
        )
Note: As well as functions, you can also bind LazyCall objects to button presses. For example:
self.add_callbacks(
    {
        "Button1": lazy.spawn("xterm"),
    }
)

In addition to button presses, you can also respond to mouse enter and leave events. For example, to make a clock show a longer date when you put your mouse over it, you can do the following:

class MouseOverClock(widget.Clock):
    defaults = [
        (
            "long_format",
            "%A %d %B %Y | %H:%M",
            "Format to show when mouse is over widget."
        )
    ]

    def __init__(self, **config):
        widget.Clock.__init__(self, **config)
        self.add_defaults(MouseOverClock.defaults)
        self.short_format = self.format

    def mouse_enter(self, *args, **kwargs):
        self.format = self.long_format
        self.bar.draw()

    def mouse_leave(self, *args, **kwargs):
        self.format = self.short_format
        self.bar.draw()

Exposing commands to the IPC interface 📡

If you want to control your widget via lazy or scripting commands (such as qtile cmd-obj), you will need to expose the relevant methods in your widget. Exposing commands is done by adding the @expose_command() decorator to your method. For example:

from libqtile.command.base import expose_command
from libqtile.widget import TextBox

class ExposedWidget(TextBox):

    @expose_command()
    def uppercase(self):
        self.update(self.text.upper())

Text in the ExposedWidget can now be made into upper case by calling lazy.widget["exposedwidget"].uppercase() or qtile cmd-onj -o widget exposedwidget -f uppercase.

If you want to expose a method under multiple names, you can pass these additional names to the decorator. For example, decorating a method with:

@expose_command(["extra", "additional"])
def mymethod(self):
    ...

will make make the method visible under mymethod, extra and additional. 👑

Debugging 🐛

You can use the logger object to record messages in the Qtile log file to help debug your development.

from libqtile.log_utils import logger

...

logger.debug("Callback function triggered")
Note: The default log level for the Qtile log is INFO so you may either want to change this when debugging or use logger.info instead.

Debugging messages should be removed from your code before submitting pull requests.

Submitting the widget to the official repo 🚀

The following sections are only relevant for users who wish for their widgets to be submitted as a PR for inclusion in the main Qtile repo.

Including the widget in libqtile.widget

You should include your widget in the widgets dict in libqtile.widget.__init__.py. The relevant format is {"ClassName": "modulename"}.

This has a number of benefits:

  • Lazy imports
  • Graceful handling of import errors (useful where widget relies on third party modules)
  • Inclusion in basic unit testing (see below)

Testing

Any new widgets should include an accompanying unit test.

Basic initialisation and configurations (using defaults) will automatically be tested by test/widgets/test_widget_init_configure.py if the widget has been included in libqtile.widget.__init__.py (see above).

However, where possible, it is strongly encouraged that widgets include additional unit tests that test specific functionality of the widget (e.g. reaction to hooks).

See Unit testing <#unit-testing> for more.

Documentation

It is really important that we maintain good documentation for Qtile. Any new widgets must therefore include sufficient documentation in order for users to understand how to use/configure the widget.

The majority of the documentation is generated automatically from your module. The widget's docstring will be used as the description of the widget. Any parameters defined in the widget's defaults attribute will also be displayed. It is essential that there is a clear explanation of each new parameter defined by the widget.

Screenshots 📸

While not essential, it is strongly recommended that the documentation includes one or more screenshots.

Screenshots can be generated automatically with a minimal amount of coding by using the fixtures created by Qtile's test suite.

A screenshot file must satisfy the following criteria:

  • Be named ss_[widgetname].py
  • Any function that takes a screenshot must be prefixed with ss_
  • Define a pytest fixture named widget

An example screenshot file is below:

import pytest

from libqtile.widget import wttr
from test.widgets.docs_screenshots.conftest import vertical_bar, widget_config

RESPONSE = "London: +17°C"

@pytest.fixture
def widget(monkeypatch):
    def result(self):
        return RESPONSE

    monkeypatch.setattr("libqtile.widget.wttr.Wttr.fetch", result)
    yield wttr.Wttr

@widget_config([{"location": {"London": "Home"}}])
def ss_wttr(screenshot_manager):
    screenshot_manager.take_screenshot()

@vertical_bar
def ss_wttr_vertical(screenshot_manager):
    screenshot_manager.take_screenshot()

The widget fixture returns the widget class (not an instance of the widget). Any monkeypatching of the widget should be included in this fixture.

The screenshot function (here, called ss_wttr) must take an argument called screenshot_manager. The function can also be parameterized, in which case, each dict object will be used to configure the widget for the screenshot (and the configuration will be displayed in the docs). If you want to include parameterizations but also want to show the default configuration, you should include an empty dict ({}) as the first object in the list.

Taking a screenshot is then as simple as calling screenshot_manager.take_screenshot(). The method can be called multiple times in the same function.

Screenshots can also be taken in a vertical bar orientation by using the @vertical_bar decorator as shown in the above example.

screenshot_manager.take_screenshot() only takes a picture of the widget. If you need to take a screenshot of the bar then you need a few extra steps:

def ss_bar_screenshot(screenshot_manager):
    # Generate a filename for the screenshot
    target = screenshot_manager.target()

    # Get the bar object
    bar = screenshot_manager.c.bar["top"]

    # Take a screenshot. Will take screenshot of whole bar unless
    # a `width` parameter is set.
    bar.take_screenshot(target, width=width)

HOW TO CREATE A LAYOUT ✨

The aim of this page is to explain the main components of qtile layouts, how they work, and how you can use them to create your own layouts or hack existing layouts to make them work the way you want them. 🛠️

Note: It is highly recommended that users wishing to create their own layout refer to the source documentation of existing layouts to familiarise themselves with the code.

What is a layout? 🤔

In Qtile, a layout is essentially a set of rules that determine how windows should be displayed on the screen. The layout is responsible for positioning all windows other than floating windows, "static" windows, internal windows (e.g. the bar) and windows that have requested not to be managed by the window manager.

Base classes 🧱

To simplify the creation of layouts, a couple of base classes are available to users.

The Layout class

As a bare minimum, all layouts should inherit the base Layout class object as this class defines a number of methods required for basic usage and will also raise errors if the required methods are not implemented. Further information on these required methods is set out below.

The _SimpleLayoutBase class

This class implements everything needed for a basic layout with the exception of the configure method. Therefore, unless your layout requires special logic for updating and navigating the list of clients, it is strongly recommended that your layout inherits this base class

The _ClientList class

This class defines a list of clients and the current client.

The collection is meant as a base or utility class for special layouts, which need to maintain one or several collections of windows, for example Columns or Stack, which use this class as base for their internal helper.

The property current_index get and set the index to the current client, whereas current_client property can be used with clients directly. 💅

💖 Required methods 💖

okay bestie, listen up — to make a minimal, actually-functioning layout, your layout has to have all of these. no skipping, no "i'll add it later", the whole set, periodt:

  • __init__
  • configure
  • add_client
  • remove
  • focus_first
  • focus_last
  • focus_next
  • focus_previous
  • next
  • previous

and like, quick reminder from up above (iykyk) — if you build your layout on top of the _SimpleLayoutBase class, you only actually need to define configure (plus _init__ if you've got custom parameters). but for this whole doc we're showing you every required method anyway, so you get the full picture. slay. ✨


🌸 __init__ 🌸

this is where you set up your layout's variables, bestie. mostly you're just loading whatever default parameters your layout defines — and those live in a class attribute called defaults. the format? a list of tuples. that's it.

from libqtile.layout import base

class TwoByTwo(base.Layout):
    """
    A simple layout with a fixed two by two grid.

    By default, unfocused windows are smaller than the focused window.
    """
    defaults = [
        ("border_width", 5, "Window border width"),
        ("border_colour", "00ff00", "Window border colour"),
        ("margin_focused", 5, "Margin width for focused windows"),
        ("margin_unfocused", 50, "Margin width for unfocused windows")
    ]

    def __init__(self, **config):
        base.Layout.__init__(self, **config)
        self.add_defaults(TwoByTwo.defaults)
        self.clients = []
        self.current_client = None

once your layout is initialised, all those parameters are just there, chillin' — self.border_width and friends. so convenient it's giving luxury. 💅


🦋 configure 🦋

THIS IS WHERE THE MAGIC HAPPENS. this method is literally the one responsible for figuring out how to position a window on the screen. main character energy, honestly.

so what it does: it configures the dimensions and borders of a window using the window's .place() method. and hey, your layout can also call either hide() or .unhide() on the window if it wants to. your call, your rules.

def configure(self, client: Window, screen_rect: ScreenRect) -> None:
    """Simple example breaking screen into four quarters."""
    try:
        index = self.clients.index(client)
    except ValueError:
        # Layout not expecting this window so ignore it
        return

    # We're only showing first 4 windows
    if index > 3:
        client.hide()
        return

    # Unhide the window in case it was hiddent before
    client.unhide()

    # List to help us calculate x and y values of
    quarters = [
        (0, 0),
        (0.5, 0),
        (0, 0.5),
        (0.5, 0.5)
    ]

    # Calculate size and position for each window
    xpos, ypos = quarters[index]

    x = int(screen_rect.width * xpos) + screen_rect.x
    y = int(screen_rect.height * ypos) + screen_rect.y
    w = screen_rect.width // 2
    h = screen_rect.height // 2

    if client is self.current_client:
        margin = self.margin_focused
    else:
        margin = self.margin_unfocused

    client.place(
        x,
        y,
        w - self.border_width * 2,
        h - self.border_width * 2,
        self.border_width,
        self.border_colour,
        margin=[margin] * 4,
    )

🎀 add_client 🎀

this one gets called every single time a window gets added to the group — doesn't matter if the layout is the current one or not, it's happening. all your layout should do here is add the window to its internal datastructures. no mapping, no configuring, no displaying. just tuck it away for later, like saving a post to your collection. 🫶

def add_client(self, client: Window) -> None:
    # Assumes self.clients is simple list
    self.clients.insert(0, client)
    self.current_client = client

🍓 remove 🍓

mirror image of the one above, bestie — this fires whenever a window is removed from the group, again regardless of whether the layout is current or not. your layout just de-registers the window from its data structures. and do NOT unmap the window. we don't do that here.

oh and also? the method must return the "next" window that should gain focus — or None if there aren't any other windows left. important!

def remove(self, client: Window) -> Window | None:
    # Assumes self.clients is a simple list
    # Client already removed so ignore this
    if client not in self.clients:
        return None
    # Client is only window in the list
    elif len(self.clients) == 1:
        self.clients.remove(client)
        self.current_client = None
        # There are no other windows so return None
        return None
    else:
        # Find position of client in our list
        index = self.clients.index(client)
        # Remove client
        self.clients.remove(client)
        # Ensure the index value is not greater than list size
        # i.e. if we closed the last window in the list, we need to return
        # the first one (index 0).
        index %= len(self.clients)
        next_client = self.clients[index]
        self.current_client = next_client
        return next_client

💜 focus_first 💜

called when the first client in the layout should get the focus. this method should just return the first client in the layout, if there is one.

lowkey crucial detail: the method should not focus the client itself — the caller handles that. you're just the messenger, not the one pressing the button. 😭
def focus_first(self) -> Window | None:
    if not self.clients:
        return None

    return self.client[0]

🌈 focus_last 🌈

called when the last client in the layout should be focused. just return the last client in the layout, if any. and same rule as above — do not focus the client yourself, the caller does that. stay in your lane bestie, it's cuter that way. ✨

def focus_last(self) -> Window | None:
    if not self.clients:
        return None

    return self.client[-1]

⭐ focus_next ⭐

called when the next client in the layout should be focused. return the next client, if there is one.

BIG note: the layout should not cycle clients when it hits the end of the list. why? because there are other methods in the group for cycling windows that focus floating windows once you reach the end of the tiled client list. so no wrapping around here, okay?

also, and i cannot stress this enough — the method should not focus the client.

def focus_next(self, win: Window) -> Window | None:
    try:
        return self.clients[self.clients.index(win) + 1]
    except IndexError:
        return None

🧚 focus_previous 🧚

called when the previous client in the layout should be focused. return the previous client, if any.

again: no cycling when you hit the end of the list. the group has other methods for cycling windows that focus floating windows once the end of the tiled client list is reached. let them do their job.

and yes, once more with feeling: the method should not focus the client.

def focus_previous(self, win: Window) -> Window | None:
    if not self.clients or self.clients.index(win) == 0
        return None

    try:
        return self.clients[self.clients.index(win) - 1]
    except IndexError:
        return None

👑 next 👑

this method focuses the next tiled window — and this one can cycle back to the beginning of the list. yep, this is the one that gets to loop around. iconic behavior, honestly.

def next(self) -> None:
    if self.current_client is None:
        return
    # Get the next client or, if at the end of the list, get the first
    client = self.focus_next(self.current_client) or self.focus_first()
    self.group.focus(client, True)

💀 previous 💀

the twin to next: this method focuses the previous tiled window, and it can cycle back to the end of the list.

def previous(self) -> None:
    if self.current_client is None:
        return
    # Get the previous client or, if at the end of the list, get the last
    client = self.focus_previous(self.current_client) or self.focus_last()
    self.group.focus(client, True)

🌸 Additional methods 🌸

these aren't strictly essential to implement, but you can define them if you want. optional but cute, yk?

  • clone
  • show
  • hide
  • swap
  • focus
  • blur

💅 clone 💅

every group gets its own copy of the layout, and clone is the method that makes that copy. the default implementation in Layout looks like this:

def clone(self, group: _Group) -> Self:
    c = copy.copy(self)
    c._group = group
    return c

🌸 show 🌸

you can use this one to run code when the layout is being displayed. it takes one argument — the ScreenRect for the screen that's showing the layout.

the default implementation is a total no-op, just vibes and nothing else:

def show(self, screen_rect: ScreenRect) -> None:
    pass

🎀 hide 🎀

this one is for running code when the layout is being hidden.

default implementation? also a no-op, bestie:

def hide(self) -> None:
    pass

🦋 swap 🦋

this method is how you change the position of two windows in the layout. little switcheroo, love that for them.

def swap(self, c1: Window, c2: Window) -> None:
    if c1 not in self.clients and c2 not in self.clients:
        return

    index1 = self.clients.index(c1)
    index2 = self.clients.index(c2)

    self.clients[index1], self.clients[index2] = self.clients[index2], self.clients[index1]

⭐ focus ⭐

this one fires when a given window is being focused.

def focus(self, client: Window) -> None:
    if client not in self.clients:
        self.current_client = None
        return

    index = self.clients.index(client)

    # Check if window is not visible
    if index > 3:
        c = self.clients.pop(index)
        self.clients.insert(0, c)

    self.current_client = client

💜 blur 💜

called when the layout loses focus. bye bye focus, we hardly knew ye.

def blur(self) -> None:
    self.current_client = None

🔥 Adding commands 🔥

adding commands is how you let your users mess with the behaviour of the layout — main character feature, honestly. to make commands available through the command interface (like via lazy.layout calls, iykyk), your layout needs this import:

from libqtile.command.base import expose_command

then you just decorate your commands with @expose_command. like this:

@expose_command
def rotate(self, clockwise: bool = True) -> None:
    if not self.clients:
        return

    if clockwise:
        client = self.clients.pop(-1)
        self.clients.insert(0, client)
    else:
        client = self.clients.pop(0)
        self.clients.append(client)

and that's how you spin your windows around like a little carousel. 🎠 periodt.

              # Check if current client has been rotated off the screen
              if self.current_client and self.clients.index(self.current_client) > 3:
                  if clockwise:
                      self.current_client = self.clients[3]
                  else:
                      self.current_client = self.clients[0]

              # Redraw the layout
              self.group.layout_all()

okay so that lil block is just the tail end of the rotate vibes from before — it's making sure that if the client we're focused on just got rotated right off the screen (index > 3), we don't leave the focus floating in the void 💀 we just reassign self.current_client to the client sitting at index 3 when we went <pink>clockwise</pink> or index 0 when we went the other way, and then we hit self.group.layout_all() to redraw everything and make it look cute again ✨


💖 The info command 💖

Layouts should also implement an info method to give us the tea ☕ about the layout itself — like, what even is going on in here.

As a minimum, the test suite (yeah, the one down below 👇) will expect a layout to hand back the following:

  • Its name 👑
  • Its group 🫶
  • The clients managed by the layout 🪟
NB bestie, that last one is not included in Layout's own implementation of the method, so you have to add it yourself when you define a class that inherits that base. No cap. 🙈
@expose_command
def info(self) -> dict[str, Any]:
    inf = base.Layout.info(self)
    inf["clients"] = self.clients
    return inf

🌈 Adding layout to main repo 🌈

If you genuinely think your layout is amazing — and honestly, if you built it, it probably is 💅 — and you wanna share it with the other qtile girlies by getting it into the main repo, there's a couple of extra steps you gotta take. Not scary. Just steps. 🎀

⭐ Add to list of layouts ⭐

You must save the layout in libqtile/layout and then add a line importing the layout definition to libqtile/layout/__init__.py — like so:

from libqtile.layout.twobytwo import TwoByTwo

🦋 Add tests 🦋

Basic functionality for all layouts is handled automatically by the core test suite, so you don't have to do anything for the vanilla stuff. However, you should create tests for any custom functionality of your layout (e.g. testing the rotate command we defined above). Don't skip it — future you will be obsessed with past you for it. 🫶

🍓 Full example 🍓

The full code for the example layout is as follows — buckle up, it's long but it ATE:

from __future__ import annotations

from typing import TYPE_CHECKING

from libqtile.command.base import expose_command
from libqtile.layout import base

if TYPE_CHECKING:
    from libqtile.backend.base import Window
    from libqtile.config import ScreenRect
    from libqtile.group import _Group

class TwoByTwo(base.Layout):
    """
    A simple layout with a fixed two by two grid.

    By default, unfocused windows are smaller than the focused window.
    """
    defaults = [
        ("border_width", 5, "Window border width"),
        ("border_colour", "00ff00", "Window border colour"),
        ("margin_focused", 5, "Margin width for focused windows"),
        ("margin_unfocused", 50, "Margin width for unfocused windows")
    ]

    def __init__(self, **config):
        base.Layout.__init__(self, **config)
        self.add_defaults(TwoByTwo.defaults)
        self.clients = []
        self.current_client = None

    def configure(self, client: Window, screen_rect: ScreenRect) -> None:
        """Simple example breaking screen into four quarters."""
        try:
            index = self.clients.index(client)
        except ValueError:
            # Layout not expecting this window so ignore it
            return

        # We're only showing first 4 windows
        if index > 3:
            client.hide()
            return

        # Unhide the window in case it was hiddent before
        client.unhide()

        # List to help us calculate x and y values of
        quarters = [
            (0, 0),
            (0.5, 0),
            (0, 0.5),
            (0.5, 0.5)
        ]

        # Calculate size and position for each window
        xpos, ypos = quarters[index]

        x = int(screen_rect.width * xpos) + screen_rect.x
        y = int(screen_rect.height * ypos) + screen_rect.y
        w = screen_rect.width // 2
        h = screen_rect.height // 2

        if client is self.current_client:
            margin = self.margin_focused
        else:
            margin = self.margin_unfocused

        client.place(
            x,
            y,
            w - self.border_width * 2,
            h - self.border_width * 2,
            self.border_width,
            self.border_colour,
            margin=[margin] * 4,
        )

    def add_client(self, client: Window) -> None:
        # Assumes self.clients is simple list
        self.clients.insert(0, client)
        self.current_client = client

    def remove(self, client: Window) -> Window | None:
        # Assumes self.clients is a simple list
        # Client already removed so ignore this
        if client not in self.clients:
            return None
        # Client is only window in the list
        elif len(self.clients) == 1:
            self.clients.remove(client)
            self.current_client = None
            # There are no other windows so return None
            return None
        else:
            # Find position of client in our list
            index = self.clients.index(client)
            # Remove client
            self.clients.remove(client)
            # Ensure the index value is not greater than list size
            # i.e. if we closed the last window in the list, we need to return
            # the first one (index 0).
            index %= len(self.clients)
            next_client = self.clients[index]
            self.current_client = next_client
            return next_client

    def focus_first(self) -> Window | None:
        if not self.clients:
            return None

        return self.client[0]

    def focus_last(self) -> Window | None:
        if not self.clients:
            return None

        return self.client[-1]

    def focus_next(self, win: Window) -> Window | None:
        try:
            return self.clients[self.clients.index(win) + 1]
        except IndexError:
            return None

    def focus_previous(self, win: Window) -> Window | None:
        if not self.clients or self.clients.index(win) == 0:
            return None

        try:
            return self.clients[self.clients.index(win) - 1]
        except IndexError:
            return None

    def next(self) -> None:
        if self.current_client is None:
            return
        # Get the next client or, if at the end of the list, get the first
        client = self.focus_next(self.current_client) or self.focus_first()
        self.group.focus(client, True)

    def previous(self) -> None:
        if self.current_client is None:
            return
        # Get the previous client or, if at the end of the list, get the last
        client = self.focus_previous(self.current_client) or self.focus_last()
        self.group.focus(client, True)

    def swap(self, c1: Window, c2: Window) -> None:
        if c1 not in self.clients and c2 not in self.clients:
            return

        index1 = self.clients.index(c1)
        index2 = self.clients.index(c2)

        self.clients[index1], self.clients[index2] = self.clients[index2], self.clients[index1]

    def focus(self, client: Window) -> None:
        if client not in self.clients:
            self.current_client = None
            return

        index = self.clients.index(client)

        # Check if window is not visible
        if index > 3:
            c = self.clients.pop(index)
            self.clients.insert(0, c)

        self.current_client = client

    def blur(self) -> None:
        self.current_client = None

    @expose_command
    def rotate(self, clockwise: bool = True) -> None:
        if not self.clients:
            return

        if clockwise:
            client = self.clients.pop(-1)
            self.clients.insert(0, client)
        else:
            client = self.clients.pop(0)
            self.clients.append(client)

        # Check if current client has been rotated off the screen
        if self.current_client and self.clients.index(self.current_client) > 3:
            if clockwise:
                self.current_client = self.clients[3]
            else:
                self.current_client = self.clients[0]

        # Redraw the layout
        self.group.layout_all()

    @expose_command
    def info(self) -> dict[str, Any]:
        inf = base.Layout.info(self)
        inf["clients"] = self.clients
        return inf

This should result in a layout looking like this: [image: layout_image] [image] . 📸✨

🦋 USING GIT 🦋

okay bestie, so git is the version control system holding down all of the source code. It's very powerful, but ngl it might be lowkey terrifying at first. This page is just a lil quick overview — for the full ✨complete✨ guide you're gonna have to search the web on your own, fr fr. Another iconic resource if you wanna get started practically without having to try out the freshly-learned commands on an existing repo is learn git branching at https://learngitbranching.js.org. You should probs learn the basic git vocabulary first and then come back to find out how you can use all of that practically. This guide is oriented on how to create a pull request, so the order of things might be diff from the intro guides. iykyk 💅

⚠️ Warning: This guide is not complete and never will be. If something isn't clear, consult other sources until you are confident you know what you are doing. no cap.

🎀 I want to try out a feature somebody is working on 🎀

If you see a pull request on GitHub at https://www.github.com/qtile/qtile/pulls that you wanna try out, go look at the line where it says:

user wants to merge n commits into qtile:master from user:branch

Right now you probably have one remote you can fetch changes from — the origin. If you cloned qtile/qtile, then git remote show origin will spit out the upstream url. If you cloned your fork, origin points to it and you probably wanna run git remote add upstream https://www.github.com/qtile/qtile. To try out somebody's work, you can add their fork as a new remote:

git remote add <user> https://www.github.com/user/qtile

where you fill in the username from the line we asked you to search for before. 👑 Then you can load data from that remote with git fetch and then ultimately check out the branch with git checkout <user>/<branch>.

Alternatively — and this is giving peak convenience — it is also possible to fetch and checkout pull requests without needing to add other remotes. The upstream remote is sufficient:

git fetch upstream pull/<id>/head:pr<id>
git checkout pr<id>

The numeric pull request id can be found in the url or next to the title (preceded by a # symbol).

📝 Note: Having the feature branch checked out doesn't mean that it is installed and will be loaded when you restart qtile. You might still need to install it with uv. just so u know 🫶

💅 I committed changes and the tests failed 💅

You can easily change your last commit, bestie. After you have done your work, git add everything you need and use git commit --amend to change your last commit. This causes the git history of your local clone to be diverged from your fork on GitHub, so you need to force-push your changes with:

git push -f <origin> <feature-branch>

where origin might be your user name or origin if you cloned your fork, and feature-branch is to be replaced by the name of the branch you are working on.

Assuming the feature branch is currently checked out, you can usually omit it and just specify the origin. slay 🎀

🌈 I was told to rebase my work 🌈

If upstream/master is changed and you happened to change the same files as the commits that were added upstream, you should rebase your work onto the most recent upstream/master. Checkout your master, pull from upstream, checkout your branch again and then rebase it:

git checkout master
git pull upstream/master
git checkout <feature-branch>
git rebase upstream/master

You will be asked to solve conflicts where your diff cannot be applied with confidence to the work that was pushed upstream. If that is the case, open the files in your text editor and resolve the conflicts manually. You possibly need to git rebase --continue after you have resolved conflicts for one commit if you are rebasing multiple commits. 🫠

🚨 Note that the above doesn't work if you didn't create a branch. In that case you will find guides elsewhere to fix this problem — ideally by creating a branch and resetting your master branch to where it should be. no judgement, we've all been there 😭

✨ I was told to squash some commits ✨

If you introduce changes in one commit and replace them in another, you are told to squash these changes into one single commit without the intermediate step:

git rebase -i master

opens a text editor with your commits and a comment block reminding you what you can do with your commits. You can reword them to change the commit message, reorder them or choose fixup to squash the changes of a commit into the commit on the line above. it's giving ✨editing history✨

This also changes your git history and you will need to force-push your changes afterwards.

Note that interactive rebasing also allows you to split, reorder and edit commits. obsessed.

🍓 I was told to edit a commit message 🍓

If you need to edit the commit message of the last commit you did, use:

git commit --amend

to open an editor giving you the possibility to reword the message. If you want to reword the message of an older commit or multiple commits, use git rebase -i as above with the reword command in the editor.

👑 Releasing Qtile 👑

Here is a short document about how to tag Qtile releases. 💖

  1. create a "Release vX.XX.XX" commit. I like to thank all the contributors; I find that list with something like:
git log --oneline --no-merges --format=%an $(git describe --tags --abbrev=0)..HEAD | sort -u

note that this can sometimes generate duplicates if people commit with slightly different names; I typically clean it up manually.

Be sure that you GPG-sign (i.e. the -S argument to git commit) this commit.

  1. Create a GPG-signed annotated tag (git tag -a -s vX.XX.XX); I usually just use exactly the same commit message as the actual release commit above.
  2. Push your tag to qtile/qtile directly:
git push origin vX.XX.XX
  1. Check the Release action at https://github.com/qtile/qtile/actions/workflows/release.yml; the "Test PyPI upload" action should build and upload the wheels to the test environment. If this step breaks, you can delete your tag (git push ... :vX.XX.XX), fix the bug, re-tag, and hopefully that will work. 🙏
  2. Create a new Github release at https://github.com/qtile/qtile/releases/new; this is what will trigger the actions in .github/workflows/release.yml to actually do the real pypi upload.
  3. Make sure all of these actions complete as green. The release should show up in a few minutes after completion here: https://pypi.org/project/qtile/
  4. Push your tag commit to master.
  5. Update the release string at https://github.com/qtile/qtile.org/blob/master/config.toml#L49 on qtile.org.
  6. Relax and enjoy a $beverage. Thanks for releasing! 🥤🎉

📜 LICENSE 📜

This project is distributed under the MIT license. periodt.

Copyright (c) 2008-2025, the Qtile contributors. All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


🌸 CHANGELOG 🌸

  • Qtile x.xx.x, released xxxx-xx-xx:
    • features
    • bugfixes
  • Qtile 0.36.0, released 2026-05-22:
    • features
      • The Clock widget now uses the stdlib zoneinfo module for string timezone lookup. pytz is no longer a (optional) dependency.
      • prompt widget: add options for block, invisible, and bar cursor
      • df widget: add used space formatting parameter
      • wayland: add ability to set opacity on windows
      • wayland: add grab click support
      • Add client_focus_by_click hook
      • Allow WindowName to have calculated width
    • bugfixes
      • Fix timers on BackgrounPoll-based widgets not clearing on config reload
      • prompt widget: fix sanitization issue
      • df widget: use floating point division for better precision
      • Thermal sensor widget: make threaded again
      • wayland: dispatch pointer enter/leave/motion to bar widgets
      • x11: ungrab click on focus by click regardless of origin
      • wayland: modify keep-above behaviour
      • wayland: update active keyboard when destroyed
      • wayland: ignore qtile key bindings if a layer has exclusive focus
      • wayland: disable client focus when exclusive_layer held
      • wayland: fix build when xwayland support is absent
      • Fix qtile cmd-obj -f spawn crash
      • Fix qtile shell on python 3.14
      • Improve battery name getter for Linux in the battery widget
      • Remove scratchpad hooks when client killed
      • Restore solid background for systray widgets
      • Fix Volume widget crash on reload
      • Fix window losing focus when dragged between screens
      • Initialise fontconfig before starting qtile
      • nix: add fontconfig to runtime library configuration
      • Only pass button presses to bar when press is inside bar
      • Stop Prompt widget cancelling text entry when window closes
  • Qtile 0.35.0, released 2026-03-20:
    • !!! Breaking changes !!!
      • Removed the cmus and moc widgets. These were GPL licensed, and as such incompatible with our MIT license. They were added to the tree by mistake.
      • Removed cmd_ prefix fallback. This has been deprecated since 0.23.0, and the core CommandObject code (lazy.*, CommandClient) doesn't use it, so it seems like there are probably no users left. This has the added bonus that we undefine __getattr__, and as a result do a lot more type checking in the core codebase.
    • features
      • a new generate_screens() config API for dynamically generating Screen objects
    • bugfixes
      • many (many) wayc backends
  • Qtile 0.34.1, released 2025-12-14:
    • bugfixes
      • many wayc bugfixes
      • a few x11 bugfixes for floating

🫶 Qtile 0.34.0, released 2025-11-22

!!! Breaking changes !!! okay bestie sit down for this bit, these ones will actually rearrange your setup 💀
  • The Wayland backend has been completely rewritten in C. The old backend based on pywlroots has been removed. Wayland-related dependencies have changed:
    • Removed python dependencies: pywlroots, pywayland, python-xkbcommon
    • Upgraded: wlroots 0.17.0 -> 0.19.0
    • Added system dependencies: wayland-scanner, wayland-protocols
  • Wanna see the current Wayland backend status? It lives here: https://docs.qtile.org/en/stable/manual/wayland_status.html
  • dropped support for python 3.11 🪦

✨ features ✨

  • consolidate development infrastructure on uv
  • rename ThreadPoolText -> BackgroundPoll with a new async def apoll() method for widgets to optionally run in the main event loop. There is a migration for this people can run via qtile migrate, and old widgets with a def poll() will continue to use that as the preferred method for polling, running in a separate thread as it always has.
  • the GenPollUrl widget now uses the asyncio infrastructure of BackgroundPoll above, and depends on aiohttp as a result. in-tree widgets that use GenPollUrl also depend on aiohttp now.
  • Add swap method to Bsp layout 👑
  • Add swap method to Columns layout 👑

🐛 bugfixes 🐛

  • fix which xdg dependency we (optionally) require: xdg -> pyxdg
  • fix programs launched by spawn() having qtile's stdin as their own, causing hangs sometimes
  • x11: clear _NET_ACTIVE_WINDOW when no window is focused
  • port lots of in-loop widgets to asyncio, so that they can not block qtile's main loop if their polling takes a long time ✨
  • port lots of threaded widgets to asyncio (and aiohttp), so that they do not use lots of resources or cause races in drawing when executing in parallel
  • x11: don't focus twice in focus_by_click()
  • x11: use less memory when resolving window attributes
  • x11: use less memory when resolving which event handler to use
  • fix stretch widget allocation when there are multiple stretch widgets
  • fix GroupBox in center_aligned mode
  • drop use of asyncio.DefaultEventLoopPolicy since it has been removed in python 3.14
  • drop qtile udev and fix udev rules to chmod directly

🦋 Qtile 0.33.0, released 2025-07-24

!!! breaking changes !!! heads up bestie 🌸
  • Remove CurrentLayoutIcon, functionality merged with CurrentLayout

✨ features ✨

  • Add support for orientation vertical to the new CurrentLayout widget
  • Draw icon and text together with the same CurrentLayout widget 🎀

🐛 bugfixes 🐛

  • rearrange default screen ordering to try and match xrandr's

⭐ Qtile 0.32.0, released 2025-06-09

!!! breaking changes !!! read this one, i'm serious 🙈
  • Output of qtile cmd-obj is now in JSON.

✨ features ✨

  • Add support for single-pixel-buffer protocol for wayland
  • Add charging foreground/background colors to Battery widget

🐛 bugfixes 🐛

  • window.can_steal_focus = False should be set in a group_window_add hook instead of the previously recommended client_new hook.
  • Fix Xwayland popup windows that fail to reappear #5155
  • Wayland: retain keybindings when switching keyboard layouts

💜 Qtile 0.31.0, released 2025-03-07

!!! breaking changes !!! 🌈
  • Spiral layout: shuffle_up and shuffle_down commands now shuffle the focused window instead of rotating all windows.

✨ features ✨

  • Add TunedManager Widget to show the currently active power profile and cycle through a list of configurable power profiles

🐛 bugfixes 🐛

  • Fix StatusNotifier displaying wrong icon when icon changes too quickly (e.g. nm-applet)
  • PulseVolume widget: fix breakage with xrandr/wlr-randr + reload config #5111

🧚 Qtile 0.30.0, released 2025-01-06

!!! breaking changes !!! 👑
  • dbus-fast is now required for dbus support. dbus-next was removed as the package is unmaintained.

✨ features ✨

  • Add SwayNC widget to interact with Sway Notification Centre (wayland only)
  • Add swap method to Plasma layout
  • New click_or_drag_only option for follow_mouse_focus to change the focus to the window under the mouse when click or drag
  • Customize battery widget "Full" and "Empty" short text with full_short_text and empty_short_text

🐛 bugfixes 🐛

  • Make MonadWide layout up/down focus navigation behave like MonadTall's left/right

💖 Qtile 0.29.0, released 2024-10-19

✨ features ✨

  • A Nix flake has been added which can be used by Nix(OS) users to update to the latest version easily
  • Add group_window_remove hook when window is removed from group
  • Switched to ruff for formatting

🐛 bugfixes 🐛

  • Add returns to lazy.function and qtile.core.function
  • Fix a bug with newer versions of cairo breaking the bar
  • Fix a few TreeTab rendering bugs
  • Fix a TaskList crash
  • Fix a scratchpad window size bug

🍓 Qtile 0.28.1, released 2024-08-12

🐛 bugfixes 🐛

  • fix a crash in the StatusNotifier widget #4959 #4960

🌸 Qtile 0.28.0, released 2024-08-11

🐛 bugfixes 🐛

  • various bug fixes to widgets from previous releases
  • fix xrandr commands racing with qtile startup

✨ Qtile 0.27.0, released 2024-07-12

✨ features ✨

  • Make default Plasma add mode dynamic
  • Add background parameter to Screen to paint a solid colour background
  • Add ability to use key codes to bind keys. Will benefit users who change keyboard layouts but wish to retain same bindings, irrespective of layout.
  • Wayland: Add support for idle-notify-v1 protocol needed by swayidle.
  • Wayland: Make keybinds repeat according to the keyboard's repeat rate and delay. Previously the keybinds did not repeat.

🐛 bugfixes 🐛

  • Fix Plasma layout with ScreenSplit by implementing get_windows
  • Fix border bug in fullscreening/maximizing wayland windows
  • Fix automatic fullscreening for many XWayland applications (e.g. games) by checking if they want to fullscreen on map

💅 Qtile 0.26.0, released 2024-05-21

!!! breaking changes !!! okay this list is long but every single line matters bestie 😭
  • this release drops support for python 3.9
  • deleted the (very old) libqtile/command_* deprecation wrappers
  • SIGUSR2 no longer restarts qtile, instead it dumps stack traces
  • lazy.<object>.when(when_floating=X) now behaves differently: the lazy call will be executed independently of the current window's float state by default, and can be limited to when it is floating or tiled by passing when_floating as True or False respectively.
  • Dropped support for KDE idle protocol on Wayland
!!! Notice for packagers - Wayland backend !!! 📦
  • Qtile's Wayland backend now requires wlroots 0.17.x, pywlroots 0.17.x and pywayland >= 0.4.17.
!!! Notice for pip package - Pypy !!! 🐍

✨ features ✨

  • For Wayland you can now set the cursor theme and size to forcefully use in Qtile, set wl_xcursor_theme and wl_xcursor_size in the configuration
  • automatically lift types to their annotated type when specified via the qtile cmd-obj command line
  • Add Plasma layout. The original layout (https://github.com/numirias/qtile-plasma) appears to be unmaintained so we have added this to the main codebase.
  • Add ability to specify muted and unmuted formats for Volume and PulseVolume widgets.
  • Add back server-side opacity support for Wayland backend

🐛 bugfixes 🐛


🎀 Qtile 0.25.0, released 2024-04-06

✨ features ✨

  • The Battery widget now supports dynamic charge control, allowing for protecting battery life.
  • To support the above (plus the other widgets that modify sysfs), qtile now ships with its own udev rules, located at /resources/99-qtile.rules; distro packagers will probably want to install this rule set.

🐛 bugfixes 🐛

  • Fix groups marked with persist=False not being deleted when their last window is moved to another group.
  • Fallback icon in StatusNotifier widget

💜 Qtile 0.24.0, released 2024-01-20

🚨 !!! config breakage/changes !!!

  • Matches do NOT do the "include/substring" style matching anymore bestie — they match the string exactly. Back in the day on X11, if the WM_TYPE of a spawned window was, say, dialog, then a match with wm_type dialognoonereadschangelogs would return true 😭. And a window with an empty WM_CLASS (which, yeah, it happens) would match, like, anything. So if you were relying on that substring-matching lifestyle, either pass a regex to your match or use a function with func=.
  • Also, using a list of strings inside Match with role, title, wm_class, wm_instance_class, wm_type is deprecated now — use a regex instead, ok? Right now we replace the property with a regex if it's a list and warn you with a deprecation message. You can just run qtile migrate to migrate your config to this. 🫶

✨ features

  • Changed how tox runs tests. See https://docs.qtile.org/en/latest/manual/contributing.html#running-tests-locally for more info on how to run tests locally.
  • Added the ScreenSplit layout, which lets you have multiple layouts per screen 👑. Also adds a ScreenSplit widget to display the name of the active split.
  • Updated the Bluetooth widget so users can manage multiple devices in a single widget.
  • Added an align option to the Columns layout so new windows can be added to the left or right column.
  • .when() got two new parameters:
    • func: Callable: enables the call when the result of the callable evaluates to True
    • condition: bool: a boolean value that determines whether the lazy object should be run. Unlike func, the condition is evaluated once, when the config file is first loaded.
  • Added the ability to have the bar drawn over windows by adding reserve=False to the bar's config, which stops the bar from reserving screen space.
  • Added the ability for third-party code (widgets, layouts) to create hooks.
  • Added the ability to create user-defined hooks which can be fired from external scripts 🔥

🐛 bugfixes

  • Fixed two bugs in stacking transient windows in X11.
  • Checking configs containing qtile.core.name with python config.py don't fail anymore (but qtile.core.name will be None).
  • Fixed an error if a wayland xwindow has an unknown wm_type.

💖 Qtile 0.23.0, released 2023-09-24

🚨 !!! Dependency Changes !!!

  • xcffib must be upgraded to >= 1.4.0
  • cairocffi must be upgraded to >= 1.6.0
  • New optional dependency pulsectl-asyncio, required for the PulseVolume widget.

📦 !!! Notice for packagers — wlroots (optional dependency) bump !!!

  • Qtile's wayland backend now requires on wlroots 0.16 (and pywlroots 0.16).

🚨 !!! config breakage/changes !!!

  • The cmd_ prefix has been dropped from all commands (which means command names are common when accessed via the command interface or internal python objects).
  • Custom widgets should now expose command methods with the @expose_command decorator (available via from libqtile.command.base import expose_command).
  • Some commands have been renamed (on top of dropping the cmd_ prefix):
    • hints -> get_hints
    • groups -> get_groups
    • screens -> get_screens
  • Layouts need to rename some methods:
    • add to add_client
    • cmd_next to next
    • cmd_previous to previous
  • Layouts or widgets that redefine the commands property need to update the signature:
@expose_command()
def commands(self) -> list[str]:
  • Window.getsize has been renamed Window.get_size (i.e. merged with the get_size command).
  • Window.getposition has been renamed Window.get_position (i.e. merged with the get_position command).
  • The StockTicker widget's function option is being deprecated: rename it to func.
  • The formatting of NetWidget has changed — if you use the format parameter in your config, include up_suffix, total_suffix and down_suffix to display the respective units.
  • The Notify widget now has separate default_timeout properties for different urgency levels. Previously, default_timeout was None, which meant there was no timeout for all notifications (unless this had been set by the client sending the notification). Now, default_timeout is for normal urgency notifications and it's been set to a default of 10 seconds. default_timeout_urgent, for critical notifications, has a timeout of None.
  • The PulseVolume widget now depends on a third party library, pulsectl-asyncio, to interact with the pulse audio server. Users will now see an ImportError until they install that library 😭.

✨ features

  • Added the ability to set icon size in the LaunchBar widget.
  • Added a warp_pointer option to Drag that, when set, will warp the pointer to the bottom right of the window when dragging begins.
  • Added a currentsong status to the Mpd2 widget.
  • Added the ability to disable group toggling in the GroupBox widget.
  • Added the ability to have a different border color when windows are stacked in the Stack layout. Requires setting border_focus_stack and border_normal_stack variables.
  • Added the ability to have a different single border width for the Columns layout by setting the single_border_width key.
  • Added the ability to have different border and margin widths when the VerticalTile layout only contains 1 window, by setting the single_border_width and single_margin keys.
  • New widget: GenPollCommand 🎀
  • Added format and play_icon parameters for styling the cmus widget.
  • Added the ability to add a group at a specified index.
  • Added the ability to spawn the WidgetBox widget opened.
  • Added the ability to swap the focused window based on index, and change the order of windows inside the current group.
  • Added the ability to update the widget only once if update_interval is None.
  • Added a move_to_slice command to move the current window to a single layout in the Slice layout.
  • Made the NetWidget text formattable.
  • Qtile no longer floods the log following X server disconnection — instead it handles those errors.
  • Key and KeyChord bindings now have another argument, swallow. It indicates whether or not the pressed keys should be passed on to the focused client. By default the keys are not passed (swallowed), so this argument is set to True. When set to False, the keys are passed to the focused client. A key is never swallowed if the function is not executed, e.g. due to failing the .when() check.
  • Added the ability to set a custom "Undefined" status key value to Mpd2Widget.
  • Mpd2Widget now searches for the artist name in all similar keys (i.e albumartist, performer, etc.).
  • Added svg support to CustomLayoutIcon.
  • Added layering controls for X11 (Wayland support coming soon!):
    • lazy.window.keep_above()/keep_below() marks windows to be kept above/below other windows permanently. Calling the functions with no arguments toggles the state, otherwise pass enable=True or enable=False.
    • lazy.window.move_up()/move_down() moves windows up and down the z axis.
    • Added the only_focused setting to the Max layout, allowing you to draw multiple clients on top of each other when set to False.
  • Added a suspend hook to run functions before the system goes to sleep. 😴

🐛 bugfixes

  • Fixed a bug where Window.center() centers the window on the wrong screen when using multiple monitors.
  • Fixed a Notify bug when apps close notifications.
  • Fixed a CPU precision bug with a specific version of psutil.
  • Fixed config being reevaluated twice during reload (e.g. all hooks from the config were doubled).
  • Fixed PulseVolume high CPU usage when update_interval is set to 0.
  • Fixed the Battery widget on FreeBSD without an explicit battery index given.
  • Fixed the XMonad layout's faulty call to the nonexistent _shrink_up.
  • Fixed setting tiled position by mouse for layouts using _SimpleLayoutBase. To support this in other layouts, add a swap method taking two windows.
  • Fixed an unfullscreening bug in conjunction with Chromium based clients when auto_fullscreen is set to False.
  • Ensured CurrentLayoutIcon expands paths for custom folders.
  • Fixed vertical alignment of icons in the TaskList widget.
  • Fixed laggy resize/positioning of floating windows in X11 by handling motion notify events later. We also introduced a cap setting if you want to limit these events further, e.g. for limiting resource usage. This is configurable with the x11_drag_polling_rate variable for each Screen, which is set to None by default, indicating no cap.

🐍 python version support

  • We have added support for python 3.11 and pypy 3.9.
  • python 3.7, 3.8 and pypy 3.7 are no longer supported.
  • Fixed a bug where StatusNotifier does not update icons.

Qtile 0.22.0, released 2022-09-22

okay so this is the big one, shipped 2022-09-22, and ngl she came with receipts. slay, but also read the fine print first 👑

💔 !!! Config breakage !!!

no cap, this is the part where you gotta fix your config before you upgrade, or you're gonna have a bad time:

  • lazy.qtile.display_kb() no longer receives any arguments. If you passed it any arguments (which were ignored previously), remove them. like they were doing nothing anyway, but now they're officially not it.
  • If you have a custom startup Python script that you use instead of qtile start and run init_log manually, the signature has changed. Please check the source for the updated arguments — literally go look, bestie, I can't guess for you 🙈
  • KeyChord's signature has changed. mode is now a boolean to indicate whether the mode should persist. The name parameter should be used to name the chord (e.g. for the Chord widget). so cute, she got a whole personality now ✨

✨ features

  • Add ability to draw borders and add margins to the Max layout. yes the "everything is maximized" layout got a glow-up 💅
  • The default XWayland cursor is now set at startup to left_ptr, so an xsetroot call is not needed to avoid the ugly X cursor. no more cursed cursors, we love to see it 🦋
  • Wayland: primary clipboard should now behave same way as with X — after selecting something it should be copied into clipboard. the muscle memory finally transfers over 🎀
  • Add resume hook when computer resumes from sleep/suspend/hibernate. she wakes up and she knows she woke up 😌
  • Add text_only option for LaunchBar widget.
  • Add force_update command to ThreadPoolText widgets to simplify updating from key bindings. bind it, force it, done ✨
  • Add scrolling ability to _TextBox-based widgets. scroll scroll scroll 🌀
  • Add player controls (via mouse callbacks) to Mpris2 widget. click to skip, click to slay 🎵
  • Wayland: input inhibitor protocol support added (pywayland>=0.4.14 & pywlroots>=0.15.19)
  • Add commands to control Pomodoro widget. productivity era, we're obsessed 🍓
  • Add icon theme support to TaskList widget (available on X11 and Wayland backends).
  • Wayland: Use qtile cmd-obj -o core -f get_inputs to get input device identifiers for configuring inputs. Also input configs will be updated by config reloads (pywlroots>=0.15.21)

🩹 bugfixes

  • Widgets that are incompatible with a backend (e.g. Systray on Wayland) will no longer show as a ConfigError in the bar. Instead the widget is silently removed from the bar and a message included in the logs. ghosted, but politely 👻
  • Reduce error messages in StatusNotifier widget from certain apps. some apps were being SO loud 😭
  • Reset colours in Chord widget
  • Prevent crash in LaunchBar when using SVG icons
  • Improve scrolling in Mpris2 widget (options to repeat scrolling etc.)

Qtile 0.21.0, released 2022-03-23

✨ features

  • Add lazy.window.center() command to center a floating window on the screen. stage-center it, periodt 🎯
  • Wayland: added power-output-management-v1 protocol support, added idle protocol, added idle inhibit protocol
  • Add MonadThreeCol layout based on XMonad's ThreeColumns.
  • Add lazy.screen.set_wallpaper command.
  • Added ability to scale the battery icon's size. icon size, but make it fashion 🔋
  • Add Spiral layout 🌀
  • Add toggle argument to Window.togroup with the same functionality as in Group.toscreen.
  • Added margin_on_single and border_on_single to Bsp layout

🩹 bugfixes

  • Fix Systray crash on reconfigure_screens.
  • Fix bug where widgets can't be mirrored in same bar.
  • Fix various issues with setting fullscreen windows floating and vice versa.
  • Fix a bug where a .when() check for lazy functions errors out when matching on focused windows when none is focused. By default we do not match on focused windows, to change this set if_no_focused to True.
  • Widget with duplicate names will be automatically renamed by appending numeric suffixes. name, name1, name2... it's giving ✨
  • Fix resizing of wallpaper when screen scale changes (X11)
  • Two small bugfixes for StatusNotifier — better handling of Ayatana indicators
  • Fix bug where StatusNotifierItem crashes due to invalid object paths (e.g. Zoom). zoom really said "let me break things" 😭

Qtile 0.20.0, released 2022-01-24

✨ features

  • Add place_right option in the TreeTab layout to place the tab panel on the right side.
  • X11: Add support for _NET_DESKTOP_VIEWPORT. E.g. can be used by rofi to map on current output.
  • Wayland: Bump wlroots version. 0.15.x wlroots and 0.15.2+ pywlroots are required. yes those numbers matter, don't skip them 🔢
  • Add XWayland support to the Wayland backend. XWayland will start up as needed, if it is installed.

🩹 bugfixes

  • Remove non-commandable windows from IPC. Fixes bug where IPC would fail when trying to get info on all windows but Systray has icons (which are non-commandable _Windows.)
  • Fix bug where bars were not reconfigured correctly when screen layout changes.
  • Fix a Wayland bug where layer-shell surface like dunst would freeze up and stop updating.
  • Change timing of screens_reconfigured hook. Will now be called ONLY if cmd_reconfigure_screens has been called and completed. emphasis on ONLY, bestie, read it twice 💀
  • Fix order of icons in Systray widget when restarting/reloading config.
  • Fix rounding error in PulseVolume widget's reported volume.
  • Fix bug where Volume widget did not load images where theme_path had been set in widget_defaults.
  • Remove ability to have multiple Systray widgets. Additional Systray widgets will result in a ConfigError. one systray, that's the rule 🚫
  • Release notification name from dbus when finalising Notify widget. This allows other notification managers to request the name.
  • Fix bug where Battery widget did not retrieve background from widget_defaults.
  • Fix bug where widgets in a WidgetBox are rendered on top of bar borders.
  • Add ability to swap focused window based on index, and change the order of windows inside current group 🌈

Qtile 0.19.0, released 2021-12-22

✨ features

  • Add ability to draw borders to the Bar. Can customise size and colour per edge. per edge!! the customization 😭💖
  • Add StatusNotifier widget implementing the StatusNotifierItem specification. NB Widget does not provide context menus. that NB is important, don't come for me 🫶
  • Add total bandwidth format value to the Net widget.
  • Scratchpad groups could be defined as single so that only one of the scratchpad in the group is visible at a given time.
  • All scratchpads in a Scratchpad group can be hidden with hide_all() function.
  • For saving states of scratchpads during restart, we use wids instead of pids.
  • Scratchpads can now be defined with an optional matcher to match with window properties.
  • Qtile.cmd_reload_config is added for reloading the config without completely restarting. reload era 🔁
  • Window.cmd_togroup's argument groupName should be changed to group_name. For the time being a log warning is in place and a migration is added. In the future groupName will fail. so like, change it now, future you will thank you 🙏
  • Add min/max_ratio to Tile layout and fix bug where windows can extend offscreen.
  • Add ability for widget mouse_callbacks to take lazy calls (similar to keybindings)
  • Add aliases to lazy.spawncmd() which takes a dictionary mapping convenient aliases to full command lines.
  • Add a new 'prefix' option to the net widget to display speeds with a static unit (e.g. MB).
  • lazy.group.toscreen() now does not toggle groups by default. To get this behaviour back, use lazy.group.toscreen(toggle=True)
  • Tile layout has new margin_on_single and border_on_single option to specify whether to draw margin and border when there is only one window.
  • Thermal zone widget. 🌡️
  • Allow TextBox-based widgets to display in vertical bars.
  • Added a focused attribute to lazy.function.when which can be used to Match on focused windows.
  • Allow one to update Image widget with update() function by giving a new path.

🩹 bugfixes

  • Windows are now properly re-ordered in the layouts when toggled on and off fullscreen

Qtile 0.18.1, released 2021-09-16

✨ features

  • All layouts will accept a list of colors for border_* options with which they will draw multiple borders on the appropriate windows. multiple borders!! layered, textured, iconic 🌈💅

🌈 Qtile 0.18.0, released 2021-07-04

!!! Config breakage !!! bestie this one is REAL, read it before you upgrade 💀
  • The qtile entry point doesn't run qtile start by default anymore
  • New optional dependency for dbus related features: dbus-next. Replaces previous reliance on dbus/Glib and allows qtile to use async dbus calls within asyncio's eventloop.
  • widget.BatteryIcon no longer has a fallback text mode; use widget.Battery instead
  • MonadX layout key new_at_current is deprecated, use new_client_position.
  • libqtile.window has been moved to libqtile.backend.x11.window; a migration has been added for this.
!!! deprecation warning !!! 🫶
  • 'main' config functions, deprecated in 0.16.1, will no longer be executed.
!!! Notice for packagers - new dependencies !!! 👑
  • Tests now require the 'dbus-next' python module plus 'dbus-launch' and 'notify-send' applications

✨ features ✨

  • added transparency in x11 and wayland backends
  • added measure_mem and measure_swap attributes to memory widget to allow user to choose measurement units.
  • memory widget can now be displayed with decimal values
  • new "qtile migrate" command, which will attempt to upgrade previous configs to the current version in the case of qtile API breaks.
  • A new reconfigure_screens config setting. When True (default) it hooks Qtile.reconfigure_screens to the screen_change hook, reconfiguring qtile's screens in response to randr events. This removes the need to restart qtile when adding/removing external monitors.
  • improved key chord / sequence functionality. Leaving a chord with mode set brings you to a named mode you activated before, see #2264. A new command, lazy.ungrab_all_chords, was introduced to return to the root bindings. The enter_chord hook is now always called with a string argument. The third argument to KeyChord was renamed from submaping to submapping (typo fix).
  • added new argument for CheckUpdates widget: custom_command_modify which allows user to modify the the line count of the output of custom_command with a lambda function (i.e. lambda x: x-3). Argument defaults to lambda x: x and is overridden by distro argument's internal lambda.
  • added new argument for the WindowName, WindowTabs and Tasklist widgets: parse_text which allows users to define a function that takes a window name as an input, modify it in some way (e.g. str.replace(), str.upper() or regex) and show that modification on screen.
  • A Wayland backend has been added which can be used by calling qtile start -b wayland directly in your TTY. It requires the latest releases of wlroots, python-xkbcommon, pywayland and pywlroots. It is expected to be unstable so please let us know if you find any bugs!
  • The 'focus argument to Click and Drag objects in your config are no longer necessary (and are ignored).

🌸 Qtile 0.17.0, released 2021-02-13

!!! Python version breakage !!! 😭
  • Python 3.5 and 3.6 are no longer supported
!!! Config breakage !!! read this whole list fr fr
  • Pacman widget has been removed. Use CheckUpdates instead.
  • Mpris widget has been removed. Use Mpris2 instead.
  • property "masterWindows" of Tile layout renamed to master_length
  • Match objects now only allow one string argument for their wm name/class/etc. properties. to update your config, do e.g.
     Group('www', spawn='firefox', layout='xmonad',
-          matches=[Match(wm_class=['Firefox', 'google-chrome', 'Google-chrome'])]),
+          matches=[Match(wm_class='Firefox'), Match(wm_class='google-chrome'), Match(wm_class='Google-chrome')]),
  • properties wname, wmclass and role of Slice-layout replaced by Match-type property "match"
  • rules specified in layout.Floating's float_rules are now evaluated with AND-semantics instead of OR-semantics, i.e. if you specify 2 different property rules, both have to match
  • check the new float_rules for floating_layout in the default config and extend your own rules appropriately: some non-configurable auto-floating rules were made explicit and added to the default config
  • using dicts for layout.Floating's float_rules is now deprecated, please use config.Match objects instead
  • no_reposition_match in layout.Floating has been removed; use the list of config.Match-objects no_reposition_rules instead
  • Command line has been modernized to a single entry point, the qtile binary. Translations are below:
      qtile     -> qtile start
      qtile-cmd -> qtile cmd-obj
      qtile-run -> qtile run-cmd
      qtile-top -> qtile top
      qshell    -> qtile shell

iqshell and dqtile-cmd are no longer distributed with the package, as they were either user or developer scripts. Both are still available in the qtile repo in /scripts.

Running qtile without arguments will continue to work for the foreseeable future, but will be eventually deprecated. qtile prints a warning when run in this configuration.

  • Qtile.cmd_focus_by_click is no longer an available command.
  • Qtile.cmd_get_info is no longer an available command.
  • libqtile.command_* has been deprecated, it has been moved to libqtile.command.*
  • libqtile.widget.base.ThreadedPollText has been removed; out of tree widgets can use ThreadPoolText in the same package instead.
  • the YahooWeather widget was removed since Yahoo retired their free tier of the weather API
  • Deprecated hook window_name_change got removed, use client_name_updated instead.
  • show_state attribute from WindowName widget has been removed. Use format attribute instead.
      show_state = True  -> format = '{state}{name}'
      show_state = False -> format = '{name}'
  • mouse_callbacks no longer receives the qtile object as an argument (they receive no arguments); import it via from libqtile import qtile instead.

✨ features ✨

  • new WidgetBox widget
  • new restart and shutdown hooks
  • rules specified in layout.Floating's float_rules are now evaluated with AND-semantics, allowing for more complex and specific rules
  • Python 3.9 support
  • switch to Github Actions for CI
  • Columns layout has new margin_on_single option to specify margin size when there is only one window (default -1: use margin option).
  • new OpenWeather widget to replace YahooWeather
  • new format attribute for WindowName widget
  • new max_chars attribute for WindowName widget
  • libqtile now exports type information
  • add a new qtile check subcommand, which will check qtile configs for various things:
    • validates configs against the newly exported type information if mypy is present in the environment
    • validates that qtile can import the config file (e.g. that syntax is correct, ends in a .py extension, etc.)
    • validates Key and Mouse mod/keysym arguments are ok.
  • Columns layout now enables column swapping by using swap_column_left and swap_column_right
!!! warning !!! ⚠️
  • When (re)starting, Qtile passes its state to the new process in a file now, where previously it passed state directly as a string. This fixes a bug where some character encodings (i.e. in group names) were getting messed up in the conversion to/from said string. This change will cause issues if you update Qtile then restart it, causing the running old version to pass state in the previous format to the new process which recognises the new.

🎀 Qtile 0.16.1, released 2020-08-11

!!! Config breakage !!!
  • Hooks 'addgroup', 'delgroup' and 'screen_change' will no longer receive the qtile object as an argument. It can be accessed directly at libqtile.qtile.
!!! deprecation warning !!!
  • defining a main function in your config is deprecated. You should use @hook.subscribe.startup_complete instead. If you need access to the qtile object, import it from libqtile directly.

🦋 bugfixes 🦋

  • include tests in the release for distros to consume
  • don't resize 0th screen incorrectly on root ConfigureNotify
  • expose qtile object as libqtile.qtile (note that we still consider anything not prefixed with cmd_ to be a private API)
  • fix transparent borders
  • MonadTall, MonadWide, and TreeTab now work with Slice

🌈 Qtile 0.16.0, released 2020-07-20 💖

🚨 !!! Config breakage !!!

  • Imports from libqtile.widget now go through a function proxy, bestie, specifically to dodge the side effects of importing literally every widget at once. So if you subclass a widget in your config, you gotta import it from its own module. Like: e.g. from libqtile.widget.pomodoro import Pomodoro ✨

🔥 features

  • added guess_terminal in utils
  • added keybinding cheet sheet image generator 👑
  • custom keyboardlayout display
  • added native support for key chords 🎹 (yes, actual chords, slay)
  • validate config before restart, and straight up refuse to restart if the config is bad
  • added a bunch of type annotations to config objects (more to come)

💅 bugfixes

  • major focus rework; Java-based IDEs such as PyCharm, NetBrains, etc. now focus correctly (finally 😭)
  • fix a bug where spotify (or any window with focus-to=parent) was closed, nothing would be focused and no hotkeys would work 💀
  • support windows unsetting the input hint
  • respects window's/user's location setting if present (WM_SIZE_HINTS)
  • fixed YahooWeather widget for new API
  • fix a bug where _NET_WM_DESKTOPS wasn't correctly updated when switching screens in some cases
  • fix a crash in the BSP layout
  • fix a stacktrace when unknown keysyms are encounted
  • make qtile --version output more sane
  • fix a rendering issue with special characters in window names
  • keyboard widget no longer re-sets the keyboard settings every second
  • fix qtile-top with the new IPC model
  • Image widget respects its background setting now
  • correctly re-draw non-focused screens on qtile restart
  • fix a crash when decoding images
  • fix the .when() constraint for lazy objects

🦋 Qtile 0.15.1, released 2020-04-14

🍓 bugfixes

  • fix qtile reload (it was crashing) — that's it, that's the whole patch, and honestly? iconic 💀

💜 Qtile 0.15.0, released 2020-04-12

🚨 !!! Config breakage !!!

  • removed the mpd widget, which depended on python-mpd.
  • the Clock widget now requires pytz to handle timezones that are passed as string
  • libqtile.command.Client does not exist anymore and has been replaced by libqtile.command_client.CommandClient

⚠️ !!! deprecation warning !!!

  • libqtile.command.lazy is deprecated in favor of libqtile.lazy.lazy

🔥 features

  • Python 3.8 support 🐍
  • wallpaper and wallpaper_mode for screens
  • bars can now have margins
  • lazy.toscreen called twice will now toggle the groups (optional with the toggle parameter)
  • lazy.window.togroup now has switch_group parameter to follow the window to the group it is sent to
  • qtile now copies the default config if the config file does not exist
  • all widgets now use Pango markup by default
  • add an fmt option for all textbox widgets
  • new PulseVolume widget for controlling PulseAudio 🔊
  • new QuickExit widget, mainly for the default config
  • new non-graph CPU widget
  • KeyboardLayout widget: new options parameter
  • CheckUpdates widget: support ArchLinux yay
  • GroupBox widget: new block_highlight_text_color parameter
  • Mpd2 widget: new color_progress parameter
  • Maildir widget can now display the inbox grand total
  • the Net widget can now use bits as unit
  • Spacer widget: new background_color parameter
  • More consistent resize behavior in Columns layout
  • various improvements of the default config
  • large documentation update and improvements (e.g. widget dependencies) 📚

💅 bugfixes

  • qtile binary: don't fail if we can't set the locale
  • don't print help if qtile-cmd function returns nothing
  • Monad layout: fix margins when flipped

🧚 Qtile 0.14.2, released 2019-06-19

💅 bugfixes

  • previous release still exhibited same issues with package data, really fix it this time 🙈

🌸 Qtile 0.14.1, released 2019-06-19

💅 bugfixes

  • properly include png files in the package data to install included icons

⭐ Qtile 0.14.0, released 2019-06-19

🚨 !!! Python version breakage !!!

  • Python 2 is no longer supported
  • Python 3.4 and older is no longer supported

🚨 !!! Config breakage !!!

  • Many internal things were renamed from camel case to snake case. If your config uses main(), or any lazy.function() invocations that interact directly with the qtile object, you may need to forward port them. Also note that we do not consider the qtile object to be a stable api, so you will need to continue forward porting these things for future refactorings (for wayland, etc.). A better approach may be to add an upstream API for what you want to do ;)
  • Maildir's subFolder and maildirPath changed to maildir_path and sub_folder.
  • the graph widget requires the psutil library to be installed

🔥 features

  • add custom change_command to backlight widget
  • add CommandSet extension to list available commands
  • simplify battery monitoring widget interface and add freebsd compatible battery widget implementation
  • track last known mouse coordinates on the qtile manager
  • allow configuration of warping behavior in columns layout

💅 bugfixes

  • with cursor warp enabled, the cursor is warped on screen change
  • fix stepping groups to skip the scratch pad group
  • fix stack layout to properly shuffle
  • silence errors when unmapping windows

🎀 Qtile 0.13.0, released 2018-12-23

⚠️ !!! deprecation warning !!!

  • wmii layout is deprecated in terms of columns layout, which has the same behavior with different defaults, see the wmii definition for more details

🔥 features

  • add svg handling for images
  • allow addgroup command to set the layout
  • add command to get current log level
  • allow groupbox to hide unused groups
  • add caps lock indicator widget
  • add custom_command to check_update widget

💅 bugfixes

  • better shutdown handling
  • fix clientlist current client tracking
  • fix typo in up command on ratiotile layout
  • various fixes to check_update widget
  • fix 0 case for resize screen

🌈 Qtile 0.12.0, released 2018-07-20

🚨 !!! Config breakage !!!

  • Tile layout commands up/down/shuffle_up/shuffle_down changed to be more consistent with other layouts
  • move qcmd to qtile-cmd because of conflict with renameutils, move dqcmd to dqtile-cmd for symmetry

🔥 features

  • add add_after_last option to Tile layout to add windows to the end of the list.
  • add new formatting options to TaskList
  • allow Volume to open app on right click

💅 bugfixes

  • fix floating of file transfer windows and java drop-downs
  • fix exception when calling cmd_next and cmd_previous on layout without windows
  • fix caps lock affected behaviour of key bindings
  • re-create cache dir if it is deleted while qtile is running
  • fix CheckUpdates widget color when no updates
  • handle cases where BAT_DIR does not exist
  • fix the wallpaper widget when using wallpaper_command
  • fix Tile layout order to not reverse on reset
  • fix calling focus_previous/next with no windows
  • fix floating bug is BSP layout

🦋 Qtile 0.11.1, released 2018-03-01

💅 bug fix

  • fixed pip install of qtile

👑 Qtile 0.11.0, released 2018-02-28

!!! Completely changed extension configuration, see the documentation !!! !!! extention subpackage renamed to extension !!! !!! extentions configuration variable changed to extension_defaults !!!

🔥 features

  • qshell improvements
  • new MonadWide layout
  • new Bsp layout
  • new pomodoro widget
  • new stock ticker widget
  • new client_name_updated hook
  • new RunCommand and J4DmenuDesktop extension
  • task list expands to fill space, configurable via spacing parameter
  • add group.focus_by_name() and group.info_by_name()
  • add disk usage ratio to df widget
  • allow displayed group name to differ from group name
  • enable custom TaskList icon size
  • add qcmd and dqcmd to extend functionality around qtile.command functionality
  • add ScratchPad group that has configurable drop downs

💅 bugfixes

  • fix race condition in Window.fullscreen
  • fix for string formatting in qtile_top
  • fix unicode literal in tasklist
  • move mpris2 initialization out of constructor
  • fix wlan widget variable naming and division
  • normalize behavior of layouts on various commands
  • add better fallback to default config
  • update btc widget to use coinbase
  • fix cursor warp when using default layout implementation
  • don't crash when using widget with unmet dependencies
  • fix floating window default location

qtile 0.10.7, released 2017-02-14 💖

Okay so this one's a lil' Valentine's gift bestie 🌸

  • features ✨
    • new MPD widget, widget.MPD2, based on the mpd2 library 🎵
    • add option to ignore duplicates in prompt widget
    • add additional margin options to GroupBox widget
    • add option to ignore mouse wheel to GroupBox widget
    • add a watts formatting string option to Battery widgets 🔋
    • add volume commands to Volume widget 🔊
    • add Window.focus command
  • bugfixes 🩹
    • place transient windows in the middle of their parents
    • fix TreeTab layout
    • fix CurrentLayoutIcon in Python 3
    • fix xcb handling for xcffib 0.5.0
    • fix bug in Screen.resize
    • fix Qtile.display_kb command

qtile 0.10.6, released 2016-05-24 🎀

!!! qsh renamed to qshell !!! bestie they had to rename it so it wouldn't collide with other packages' names, no drama just a rename periodt
  • features ✨
    • Test framework changed to pytest
    • add startup_complete hook
  • bugfixes 🩹
    • restore dynamic groups on restart
    • correct placement of transient_for windows
    • major bug fixes with floating window handling
  • file path changes (XDG Base Directory specification) 📁
    • the default log file path changed from ~/.qtile.log to ~/.local/share/qtile/qtile.log
    • the cache directory changed from ~/.cache to ~/.cache/qtile
    • the prompt widget's history file changed from ~/.qtile_history to ~/.cache/qtile/prompt_history

qtile 0.10.5, released 2016-03-06 🌷

!!! Python 3.2 support dropped !!! !!! GoogleCalendar widget dropped for KhalCalendar widget !!! !!! qtile-session script removed in favor of qtile script !!!
  • features ✨
    • new Columns layout, composed of dynamic and configurable columns of windows
    • new iPython kernel for qsh, called iqsh, see docs for installing 👀
    • new qsh command display_kb to show current key binding
    • add json interface to IPC server
    • add commands for resizing MonadTall main panel
    • wlan widget shows when you are disconnected and uses a configurable format
  • bugfixes 🩹
    • fix path handling in PromptWidget
    • fix KeyboardLayout widget cycling keyboard
    • properly guard against setting screen to too large screen index

qtile 0.10.4, released 2016-01-19 🦋

!!! Config breakage !!! positional arguments to Slice layout removed, now side and width must be passed in as keyword arguments 💀
  • features ✨
    • add alt coin support to BitcoinTracker widget
  • bugfixes 🩹
    • don't use six.moves assignment (fix for >=setuptools-19.3)
    • improved floating and fullscreen handling
    • support empty or non-charging secondary battery in BatteryWidget
    • fix GoogleCalendar widget crash

qtile 0.10.3, released 2015-12-25 🎄

Christmas release bestie, iconic 🎁

  • features ✨
    • add wmii layout
    • add BSD support to graph widgets
  • bugfixes 🩹
    • fix (some) fullscreen problems
    • update google calendar widget to latest google api
    • improve multiple keyboard layout support
    • fix displaying Systray widget on secondary monitor
    • fix spawn file descriptor handling in Python 3
    • remove duplicate assert code in test_verticaltile.py
    • allow padding_{x,y} and margin_{x,y} widget attrs to be set to 0

qtile 0.10.2, released 2015-10-19 🍂

  • features ✨
    • add qtile-top memory monitoring
    • GroupBox can set visible groups
    • new GroupBox highlighting, line
    • allow window state to be hidden on WindowName widget
    • cmd_togroup can move to current group when None sent
    • added MOC playback widget 🎶
    • added memory usage widget
    • log truncation, max log size, and number of log backups configurable
    • add a command to change to specific layout index (lazy.to_layout_index(index))
  • bugfixes 🩹
    • fixed memory leak in dgroups
    • margin fixes for MonalTall layout
    • improved cursor warp
    • remove deprecated imp for Python >= 3.3
    • properly close file for NetGraph
    • fix MondadTall layout grow/shrink secondary panes for Python 2
    • Clock widget uses datetime.now() rather than .fromtimestamp()
    • fix Python 3 compatibility of ThermalSensor widget
    • various Systray fixes, including implementing XEMBED protocol
    • print exception to log during loading config
    • fixed xmonad layout margins between main and secondary panes
    • clear last window name from group widgets when closed
    • add toggleable window border to single xmonad layout
  • config breakage 💀
    • layouts.VerticalTile windows is now clients
    • layouts.VerticalTile focus_next/focus_previous now take a single argument, similar to other layouts

qtile 0.10.1, released 2015-07-08 🌈

This release fixes a problem that made the PyPI package uninstallable, qtile will work with a pip install now 🫶


qtile 0.10.0, released 2015-07-07 👑

!!! Config breakage !!! 😭 various deprecated commands have been removed: - Screen.cmd_nextgroup: use cmd_next_group - Screen.cmd_prevgroup: use cmd_prev_group - Qtile.cmd_nextlayout: use cmd_next_layout - Qtile.cmd_prevlayout: use cmd_prev_layout - Qtile.cmd_to_next_screen: use cmd_next_screen - Qtile.cmd_to_prev_screen: use cmd_prev_screen - Clock widget: remove fmt kwarg, use format kwarg - GmailChecker widget: remove settings parameter - Maildir widget: remove maildirPath, subFolders, and separator kwargs
  • Dependency updates 📦
    • cffi>=1.1 is now required, along with xcffib>=0.3 and cairocffi>=0.7 (the cffi 1.0 compatible versions of each)
    • Care must be taken that xcffib is installed before cairocffi ⚠️
  • features ✨
    • add support for themed cursors using xcb-cursor if available
    • add CheckUpdate widget, for checking package updates, this deprecates the Pacman widget
    • add KeyboardKbdd widget, for changing keyboard layouts
    • add Cmus widget, for showing song playing in cmus 🎧
    • add Wallpaper widget, for showing and cycling wallpaper
    • add EzConfig classes allowing shortcuts to define key bindings
    • allow GroupBox urgent highlighting through text
    • Bar can be placed vertically on sides of screens (widgets must be adapted for vertical viewing)
    • add recognizing brightness keys ☀️
  • bugfixes 🩹
    • deprecation warnings were not printing to logs, this has been fixed
    • fix calculation of y property of Gap
    • fix focus after closing floating windows and floating windows
    • fix various Python 3 related int/float problems
    • remember screen focus across restarts
    • handle length 1 list passed to Drawer.set_source_rgb without raising divide by zero error
    • properly close files opened in Graph widget
    • handle _NET_WM_STATE_DEMANDS_ATTENTION as setting urgency
    • fix get_wm_transient_for, request WINDOW, not ATOM

qtile 0.9.1, released 2015-02-13 💜

This is primarily a unicode bugfix release for 0.9.0; there were several nits related to the python2/3 unicode conversion that were simply wrong. This release also adds license headers to each file, which is necessary for distro maintainers to package Qtile 🫡

  • bugfixes 🩹
    • fix python2's importing of gobject
    • fix unicode handling in several places

qtile 0.9.0, released 2015-01-20 🧚

!!! Dependency Changes !!! — New dependencies will need to be installed for Qtile to work - drop xpyb for xcffib (XCB bindings) - drop py2cairo for cairocffi (Cairo bindings) - drop PyGTK for asyncio (event loop, pangocairo bindings managed internally) - Qtile still depends on gobject if you want to use anything that uses dbus (e.g. the mpris widgets or the libnotify widget)
  • features ✨
    • add Python 3 and pypy support (made possible by dependency changes)
    • new layout for vertical monitors
    • add startup_once hook, which is called exactly once per session (i.e. it is not called when qtile is restarted via lazy.restart()). This eliminates the need for the execute_once() function found in lots of user configs.
    • add a command for showing/hiding the bar (lazy.hide_show_bar())
    • warn when a widget's dependencies cannot be imported
    • make qtile.log more useful via better warnings in general, including deprecation and various other warnings that were previously nonexistent
    • new text-polling widget super classes, which enable easy implementation of various widgets that need to poll things outside the event loop.
    • add man pages 📖
    • large documentation update, widget/layout documentation is now autogenerated from the docstrings
    • new ImapWidget for checking imap mailboxes 📬
  • bugfixes 🩹
    • change default wmname to "LG3D" (this prevents some java apps from not working out of the box)
    • all code passes flake8
    • default log level is now WARNING
    • all widgets now use our config framework
    • windows with the "About" role float by default
    • got rid of a bunch of unnecessary bare except: clauses

🎀 Qtile 0.8.0, released 2014-08-18

  • ✨ features
    • massive widget/layout documentation update
    • new widget debuginfo for use in Qtile development
    • stack has new autosplit, fair options
    • matrix, ratiotile, stack, xmonad, zoomy get 'margin' option
    • new launchbar widget
    • support for matching WM_CLASS and pid in Match
    • add support for adding dgroups rules dynamically and via ipc
    • Clock supports non-system timezones
    • new mpris2 widget
    • volume widget can use emoji instead of numbers
    • add an 'eval' function to qsh at every object level
    • bar gradients support more colors
    • new Clipboard widget (very handy!)
  • 🐛 bugfixes
    • bitcoin ticker widget switched from MtGox (dead) to btc-e
    • all widgets now use Qtile's defaults system, so their defaults are settable globally, etc.
    • fix behavior when screens are cloned
    • all widgets use a unified polling framework
    • "dialog" WM_TYPEs float by default
    • respect xrandr --primary
    • use a consistent font size in the default config
    • default config supports mouse movements and floating
    • fix a bug where the bar was not redrawn correctly in some multiscreen environments
    • add travis-ci support and make tests vastly more robust
  • 💔 config breakage
    • libqtile.layout.Stack's stacks parameter is now num_stacks

🎀 Qtile 0.7.0, released 2014-03-30

  • ✨ features
    • new disk free percentage widget
    • new widget to display static image
    • per core CPU graphs
    • add "screen affinity" in dynamic groups
    • volume widget changes volume linear-ly instead of log-ly
    • only draw bar when idle, vastly reducing the number of bar draws and speeding things up
    • new Gmail widget
    • Tile now supports automatically managing master windows via the master_match parameter.
    • include support for minimum height, width, size increment hints
  • 🐛 bugfixes
    • don't crash on any exception in main loop
    • don't crash on exceptions in hooks
    • fix a ZeroDivisionError in CPU graph
    • remove a lot of duplicate and unused code
    • Steam windows are placed more correctly
    • Fixed several crashes in qsh
    • performance improvements for some layouts
    • keyboard layout widget behaves better with multiple keyboard configurations
  • 💔 config breakage
    • Tile's shuffleMatch is renamed to resetMaster

😭 Qtile 0.6, released 2013-05-11

!!! Config breakage !!! This release breaks your config file in several ways:
  • The Textbox widget no longer takes a name'' positional parameter, since it was redundant; you can use the name'' kwarg to define it.
  • manager.Group (now _Group) is not used to configure groups any more; config.Group replaces it. For simple configurations (i.e. Group("a") type configs), this should be a drop in replacement. config.Group also provides many more options for showing and hiding groups, assigning windows to groups by default, etc.
  • The Key, Screen, Drag, and Click objects have moved from the manager module to the config module.
  • The Match object has moved from the dgroups module to the config module.
  • The addgroup hook now takes two parameters: the qtile object and the name of the group added:
@hook.subscribe
def addgroup_hook(qtile, name):
    pass
  • The nextgroup and prevgroup commands are now on Screen instead of Group.

For most people, you should be able to just:

sed -i -e 's/libqtile.manager/libqtile.config' config.py

...dgroups users will need to go to a bit more work, but hopefully configuration will be much simpler now for new users.

  • ✨ features
    • New widgets: task list,
    • New layout: Matrix
    • Added ability to drag and drop groups on GroupBox
    • added "next urgent window" command
    • added font shadowing on widgets
    • maildir widget supports multiple folders
    • new config option log_level to set logging level (any of logging.{DEBUG, INFO, WARNING, ERROR, CRITICAL})
    • add option to battery widget to hide while level is above a certain amount
    • vastly simplify configuration of dynamic groups
    • MPD widget now supports lots of metadata options
  • 🐛 bugfixes
    • don't crash on restart when the config has errors
    • save layout and selected group state on restart
    • varous EWMH properties implemented correctly
    • fix non-black systray icon backgrounds
    • drastically reduce the number of timeout_add calls in most widgets
    • restart on RandR attach events to allow for new screens
    • log level defaults to ERROR
    • default config options are no longer initialized when users define their corresponding option (preventing duplicate widgets, etc.)
    • don't try to load config in qsh (not used)
    • fix font alignment across Textbox based widgets

🦋 Qtile 0.5, released 2012-11-11

(Note, this is not complete! Many, many changes have gone in to 0.5, by a large number of contributors. Thanks to everyone who reported a bug or fixed one!)
  • ✨ features
    • Test framework is now nose
    • Documentation is now in sphinx
    • Several install guides for various OSes
    • New widgets: battery based icon, MPRIS1, canto, current layout, yahoo weather, sensors, screen brightness, notify, pacman, windowtabs, she, crashme, wifi.
    • Several improvements to old widgets (e.g. battery widget displays low battery in red, GroupBox now has a better indication of which screen has focus in multi-screen setups, improvements to Prompt, etc.)
    • Desktop notification service.
    • More sane way to handle configuration files
    • Promote dgroups to a first class entity in libqtile
    • Allow layouts to be named on an instance level, so you can:
layouts = [
    # a layout just for gimp
    layout.Slice('left', 192, name='gimp', role='gimp-toolbox',
        fallback=layout.Slice('right', 256, role='gimp-dock',
        fallback=layout.Stack(stacks=1, **border_args)))
]
...

dynamic_groups = { 'gimp': {'layout': 'gimp'} }

Dgroups(..., dynamic_groups, ...)
  • New Layout: Zoomy
  • Add a session manager to re-exec qtile if things go south
  • Support for WM_TAKE_FOCUS protocol
  • Basic .desktop file for support in login managers
  • Qsh reconnects after qtile is restarted from within it
  • Textbox supports pango markup
  • Examples moved to qtile-examples repository.
  • 🐛 bugfixes
    • Fix several classes of X races in a more sane way
    • Minor typo fixes to most widgets
    • Fix several crashes when drawing systray icons too early
    • Create directories for qtile socket as necessary
    • PEP8 formatting updates (though we're not totally there yet)
    • All unit tests pass
    • Lots of bugfixes to MonadTall
    • Create IPC socket directory if necessary
    • Better error if two widgets have STRETCH length
    • Autofloat window classes can now be overridden
    • xkeysyms updated

And of course, the writers left us the most iconic little breadcrumb ever at the bottom of the changelog file:

# vim :set ts=4 sw=4 sts=4 et :

✍️ Author

Author name not set

no cap, truly nobody signed this one. ghost author vibes 👻

aaaand that's the whole page bestie!! 🥳 you just read the entirety of QTILE(1) — source/version Qtile page rendered as QTILE(1), dated Jun 15, 2026 — and honestly? iconic. you now know more about a window manager than most people know about themselves 💅✨ go forth, configure, slay 💖🦋👑

📦 where this page comes from