A long time ago (6 years ago, according to the commit log), I took up the quixotic task of building a custom desktop environment based on Qt.

What I have in mind is still vague, but essentially I want a thin, beautiful, customizable, wrapper around i3wm. Think “KDE window manager”. Yes, I know, a window manager is not a desktop environment is not a window manager!

A necessary building block for such a project is obviously integration of i3wm’s IPC with Qt. A handful of C++ libraries already exist for interprocess communication with i3wm. However, integrating them with Qt tends to require quite a lot of boilerplate. None of them could be integrated in a way that feels clean enough to my taste, so I decided to build my own library to do just that.

qi3pc is a simple C++ library that provides idiomatic Qt bindings for i3wm’s IPC interface. If this is of interest to you, please help testing and reviewing this release candidate for the first major version.

I plan on releasing the first major version, a month after the public announcement of this release candidate. So far, no further work is planned for that release beyond updating the documentation, fixing bugs and increasing the test coverage.

qi3pc is hosted on Sourcehut.

Release resources
Release
Date 2025-12-18
Commit 04c9a7ce9fe527c0970910eb0cc72d794dded582 [1.0.0-rc1]
Build https://builds.sr.ht/~hantz/job/1633646
Artifacts https://git.sr.ht/~hantz/qi3pc/refs/1.0.0-rc1
Project
Homepage https://qi3pc.hantz.sh/
Development Homepage https://staging.qi3pc.hantz.sh/
Repository https://git.sr.ht/~hantz/qi3pc
Packages
AUR https://aur.archlinux.org/packages/qi3pc
Help add new packages here!
Contact
Discuss and send bug reports https://lists.sr.ht/~hantz/hantzdesktop-discuss
Send patches https://lists.sr.ht/~hantz/hantzdesktop-devel
Follow news https://lists.sr.ht/~hantz/hantzdesktop-announce

This is an extract from the docs.


Building

qi3pc is a modern C++ library. It uses C++20. It depends only on Qt6 and i3wm itself. cmake is necessary for building.

git clone https://git.sr.ht/~hantz/qi3pc -b 1.0.0-rc1
cd qi3pc
cmake -S . -B build
cmake --build build
cmake --install build

For more inspiration, see the CI build recipe.

Documentation

Documentation is available here. Once installed, the docs are also available in the man pages qi3pc(3): man qi3pc. Staging docs are available here.

Getting Started

The messages, replies and events that i3wm’s IPC API provides are accessible through methods and signals with obvious names. Getting information from i3 is as simple as sending a message and waiting for a reply or subscribing to events and waiting for them to be triggered. This is done with the signal/slot mechanisms offered by Qt and normal C++ functions.

The signals and methods return objects (arrays, dictionaries, strings, etc.) matching the specifications of i3wm’s IPC protocol

Code examples

The code examples below assume some familiarity with Qt development as well as i3wm’s IPC idioms.

Listening to the window manager

Below is an example that monitors window events from i3wm.

There are no free functions. All functionality is through the qi3pc class. So first, an object of that class has to be constructed.

auto ipc = qi3pc();

Events from the window manager are available as Qt signals. To monitor those events, simply subscribe to them as to any other Qt signal. Window events include creation of new window, focus change, window move, etc.

QObject::connect(&ipc,
                 &qi3pc::windowEvent,
                 [](qi3pc::WindowChange change, const QJsonObject& container){
                    qDebug() << "Window event!";

                    if (change == qi3pc::WindowChange::Focus) {  
                        qDebug() << "Window focus changed!";
                    }
                });

Next, connect to the i3wm’s IPC server.

ipc.connect();

Multiple qi3pc objects can be instantiated in the same application. Each object is only notified of the event it cares about. These events have to be explicitly subscribed to. Without this, the qi3pc::windowEvent signal will never fire on this object, and the lambda in the connection above will never run.

QStringList events("window");
ipc.subscribe(events);

Talking to the window manager

Below is an example showing how to automatically move to a newly created workspace.

MyClass::MyClass() {
    QObject::connect(&m_ipc,
                     &qip3c::workspaceEvent,
                     this,
                     &MyClass::handleWorkspaceEvent);

    m_ipc.connect();

    QStringList events("workspace");
    m_ipc.subscribe(events);
}

void
MyClass::handleWorkspaceEvent(qi3pc::WorkspaceChange change,
                              const QJsonObject& current,
                              const QJsonObject& old)
{
    Q_UNUSED(old)
    if (change == qi3pc::WorkspaceChange::Init) {
        if (auto jsonStr = current["name"]; !jsonStr.isUndefined()) {
            QString name = jsonStr.toString();

            QByteArray payload;
            QString i3Command = "workspace " + name;
            payload.append(i3Command.toStdString().c_str());
            m_ipc.sendMessage<qi3pc::IpcType::Command>(payload);
        }
    }
}

When a new workspace is created, a command is sent to switch to it. As simple as that!

A conversation with the window manager

In the example above, after sending the workspace change request to i3wm, the response is not monitored, simply because the interesting side effect is the wokspace change itself (and also because it was just a simple example). However, sometimes we are interested in the reply from the wm. For those cases, message replies are also published with Qt signals.

To actively fetch information from i3wm, the fetch methods can be used (e.g. qi3pc::fetchWorkspaces). When called, a message is sent to i3wm, and the reply is published through a reply signal. In the case of workspaces, through qi3pc::workspacesUpdated.

Additionally, the reply is cached along with its last update time. This cached data is available through another member function, in this case qi3pc::workspaces.

The following is an example of using that workflow.

MyClass::MyClass() {
    m_ipc.connect();
    
    QObject::connect(&m_ipc,
                     &qi3pc::workspacesUpdated,
                     this,
                     &MyClass::handleWorkspaceReply);
    
    // Query up-to-date workspace information from i3wm
    m_ipc.fetchWorkspaces();
}

void
MyClass::handleWorkspaceReply(const qi3pc::DataArray& data)
{
    qDebug() << "List of workspaces";
    qDebug() << data->first;

	qDebug() << "Cached list of workspaces";
	qDebug() << ipc.workspaces().first();
}

Note that in this case there was no need to subscribe to workspace events with qi3pc::subscribe.

More examples

Other examples can be found in the repository.

buffalo

qi3pc is currently used in my (unreleased) bar, buffalo. The current implementation of buffalo covers what was necessary to replace my previous polybar setup that I ditched for instability. So it’s quite barebones. But the i3 module provides the best existing usage examples.

For a quick preview of buffalo’s capabilities, check out this message I sent to the i3wm mailing list a little while back.

If you use qi3pc in a project, reach out to me to have it listed here as an example.