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¶
To create a minimal, functioning layout your layout must include the methods listed below:
__init__
configure
add_client
remove
focus_first
focus_last
focus_next
focus_previous
next
previous
As noted above, if you create a layout based on the _SimpleLayoutBase
class, you will only
need to define configure
(and _init__
, if you have custom parameters). However, for the
purposes of this document, we will show examples of all required methods.
__init__
¶
Initialise your layout's variables here. The main use of this method will be to load
any default parameters defined by layout. These are defined in a class attribute called
defaults
. The format of this attribute is a list of tuples.
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 the layouts is initialised, these parameters are available at
self.border_width
etc.
configure
¶
This is where the magic happens! This method is responsible for determining how to position a window on the screen.
This method should therefore configure the dimensions and borders of a window using the
window's .place()
method. The layout can also call either hide()
or .unhide()
on the window.
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 method is called whenever a window is added to the group, regardless of whether the layout is current or not. The layout should just add the window to its internal datastructures, without mapping or configuring/displaying.
def add_client(self, client: Window) -> None:
# Assumes self.clients is simple list
self.clients.insert(0, client)
self.current_client = client
remove
¶
This method is called whenever a window is removed from the group, regardless of whether the layout is current or not. The layout should just de-register the window from its data structures, without unmapping the window.
The method must also return the "next" window that should gain focus or None
if there are no other windows.
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
¶
This method is called when the first client in the layout should be focused.
This method should just return the first client in the layout, if any. NB the method should not focus the client itself, this is done by caller.
def focus_first(self) -> Window | None:
if not self.clients:
return None
return self.client[0]
focus_last
¶
This method is called when the last client in the layout should be focused.
This method should just return the last client in the layout, if any. NB the method should not focus the client itself, this is done by caller.
def focus_last(self) -> Window | None:
if not self.clients:
return None
return self.client[-1]
focus_next
¶
This method is called the next client in the layout should be focused.
This method should return the next client in the layout, if any. NB the layout should not cycle clients when reaching the end of the list as there are other method in the group for cycling windows which focus floating windows once the the end of the tiled client list is reached.
In addition, 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
¶
This method is called the previous client in the layout should be focused.
This method should return the previous client in the layout, if any. NB the layout should not cycle clients when reaching the end of the list as there are other method in the group for cycling windows which focus floating windows once the the end of the tiled client list is reached.
In addition, 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 can cycle back to the beginning of the list.
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
¶
This method focuses the previous tiled window and 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¶
While not essential to implement, the following methods can also be defined:
clone
show
hide
swap
focus
blur
clone
¶
Each group gets a copy of the layout. The clone
method is used to create this copy. The default
implementation in Layout
is as follows:
def clone(self, group: _Group) -> Self:
c = copy.copy(self)
c._group = group
return c
show
¶
This method can be used to run code when the layout is being displayed. The method receives one argument,
the ScreenRect
for the screen showing the layout.
The default implementation is a no-op:
def show(self, screen_rect: ScreenRect) -> None:
pass
hide
¶
This method can be used to run code when the layout is being hidden.
The default implementation is a no-op:
def hide(self) -> None:
pass
swap
¶
This method is used to change the position of two windows in the layout.
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 method is called 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
¶
This method is called when the layout loses focus.
def blur(self) -> None:
self.current_client = None
Adding commands¶
Adding commands allows users to modify the behaviour of the layout. To make commands
available via the command interface (e.g. via lazy.layout
calls), the layout must
include the following import:
from libqtile.command.base import expose_command
Commands are then decorated with @expose_command
. For example:
@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()
The info
command¶
Layouts should also implement an info
method to provide information about the layout.
As a minimum, the test suite (see below) will expect a layout to return the following information:
- Its name
- Its group
- The clients managed by the layout
NB the last item is not included in Layout
's implementation of the method so it should be added
when defining a class that inherits that base.
@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 think your layout is amazing and you want to share with other users by including it in the main repo then there are a couple of extra steps that you need to take.
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
e.g.
from libqtile.layout.twobytwo import TwoByTwo
Add tests¶
Basic functionality for all layouts is handled automatically by the core test suite. However, you
should create tests for any custom functionality of your layout (e.g. testing the rotate
command
defined above).
Full example¶
The full code for the example layout is as follows:
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:
Getting help¶
If you still need help with developing your widget then please submit a question in the qtile-dev group or submit an issue on the GitHub page if you believe there's an error in the codebase.