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.

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).

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.

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.

_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.

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.

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

This loop runs in the event loop so it is important that the poll method does not call some blocking function. If this is required, widgets should inherit the base.ThreadPoolText class (see below).

ThreadPoolText

The base.ThreadPoolText class is very similar to the base.InLoopPollText class. The key difference is that the poll method is run asynchronously and triggers a callback once the function completes. This allows widgets to get text from long-running functions without blocking Qtile.

Mixins

As well as inheriting from one of the base classes above, widgets can also inherit one or more mixins to provide some additional functionality to the widget.

PaddingMixin

This provides the padding(_x|_y|) attributes which can be used to change the appearance of the widget.

If you use this mixin in your widget, you need to add the following line to your __init__ method:

self.add_defaults(base.PaddingMixin.defaults)

MarginMixin

The MarginMixin is essentially effectively exactly the same as the PaddingMixin but, instead, it provides the margin(_x|_y|) attributes.

As above, if you use this mixin in your widget, you need to add the following line to your __init__ method:

self.add_defaults(base.MarginMixin.defaults)

Configuration

Now you know which class to base your widget on, you need to know how the widget gets configured.

Defining Parameters

Each widget will likely have a number of parameters that users can change to customise the look and feel and/or behaviour of the widget for their own needs.

The widget should therefore provide the default values of these 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")
]

Users can override the default value when creating their config.py file.

MyCustomWidget(parameter_name=updated_value)

Once the widget is initialised, these parameters are available at self.parameter_name.

The __init__ method

Parameters that should not be changed by users can be defined in the __init__ method.

This method is run when the widgets are initially created. This happens before the qtile object is available.

The _configure method

The _configure method is called by the bar object and sets the self.bar and self.qtile attributes of the widget. It also creates the self.drawer attribute which is necessary for displaying any content.

Once this method has been run, your widget should be ready to display content as the bar will draw once it has finished its configuration.

Calls to methods required to prepare the content for your widget should therefore be made from this method rather than __init__.

Displaying output

A Qtile widget is just a drawing that is displayed at a certain location the user's bar. The widget's job is therefore to create a small drawing surface that can be placed in the appropriate location on the bar.

The "draw" method

The draw method is called when the widget needs to update its appearance. This can be triggered by the widget itself (e.g. if the content has changed) or by the bar (e.g. if the bar needs to redraw its entire contents).

This method therefore needs to contain all the relevant code to draw the various components that make up the widget. Examples of displaying text, icons and drawings are set out below.

It is important to note that the bar controls the placing of the widget by assigning the offsetx value (for horizontal positioning) and offsety value (for vertical positioning). Widgets should use this at the end of the draw method. Both offsetx and offsety are required as both values will be set if the bar is drawing a border.

self.drawer.draw(offsetx=self.offsetx, offsety=self.offsety, width=self.width)

Note

If you need to trigger a redrawing of your widget, you should call self.draw() if the width of your widget is unchanged. Otherwise you need to call self.bar.draw() as this method means the bar recalculates the position of all widgets.

Displaying text

Text is displayed by using a drawer.TextLayout object. If all you are doing is displaying text then it's highly recommended that you use the `base._TextBox superclass as this simplifies adding and updating text.

If you wish to implement this manually then you can create a your own drawer.TextLayout by using the self.drawer.textlayout method of the widget (only available after the _configure method has been run). object to include in your widget.

Some additional formatting of Text can be displayed using pango markup and ensuring the markup parameter is set to True.

self.textlayout = self.drawer.textlayout(
                     "Text",
                     "fffff",       # 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)
                     )

Displaying icons and images

Qtile provides a helper library to convert images to a surface that can be drawn by the widget. If the images are static then you should only load them once when the widget is configured. Given the small size of the bar, this is most commonly used to draw icons but the same method applies to other images.

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

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

Drawing the image is then 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.drawer.draw(offsetx=self.offset, width=self.length)

Drawing shapes

It is possible to 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.

In addition, you can access the Cairo context drawing functions via self.drawer.ctx.

For example, the following 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.height - 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()

This creates something looking like this: wifi_image.

Background

At the start of the draw method, the widget should clear the drawer by drawing the background. Usually this is done by including the following line at the 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 which will result in a linear gradient from top to bottom.

Updating the widget

Widgets will usually need to update their content periodically. There are numerous ways that this can be done. Some of the most common ones are 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 ThreadPoolText superclass where you are calling a function repeatedly and displaying its output as text.

Hooks

Qtile has a number of hooks built in which are triggered on certain events.

The WindowCount widget is a good example of using hooks to trigger updates. It includes the following method which is run 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 page for details of which hooks are available and which arguments are passed to the callback function.

Using dbus

Qtile uses dbus-next 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-next 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-next can also be used to query properties, call methods etc. on dbus interfaces. Refer to the dbus-next documentation 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 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

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


@pytest.mark.parametrize(
    "screenshot_manager",
    [
        {"location": {"London": "Home"}}
    ],
    indirect=True
)
def ss_wttr(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.

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)

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.