i3 is my favorite tile window manager. But quite recently, having taken up the regular repainting of my desktop, I came across one most unpleasant thing: the functionality of the native panel is not enough at all to embody all of my fantasies. In particular, it does not know how to change the size or change the color of the borders. And what does a linuksoid do when the software does not suit him and there are no alternatives (and there are none)? That's right, patch existing, or write your own. I have no desire to deal with xcb, on which the standard panel is written, so I followed the second path. C ++ was chosen as the language. About the framework ask KO
Foreword
What is the panel? Wikipedia on this subject tells us:
“Taskbar (eng. Taskbar) is an application that is used to launch other programs or control already running, and is a toolbar. Particularly used to manage windows applications. "
But in tile window managers, things are not that simple. Windows in them do not overlap each other. The manager allocates to each window its own space (tile) on which this window is stretched. For example:

See you All windows in sight. So the question of the need for the taskbar in such managers disappears by itself. But on one screen, many applications do not fit. What to do? And here comes the concept of multiple desktops. It's damn convenient! There is no need to change the focus of windows, their size and position every minute. I switched to the first desktop - here you have a set of applications for development, switched to the second - keep the file manager and player next to it. And so on. Theoretically, there can be infinitely many desktops, but it can be quite problematic to use more than 10 at a time.
So, our panel will not manage windows, but desktops. Therefore, I prefer to use just the word “panel”, rather than the phrase “taskbar”.
')
We start the development
As a friendship begins with a smile, most Qt applications start with a widget. So we will not depart from the traditions and first of all we will write a widget for our future panel. For now, it will be just a class inherited from QWidget. With the designer and destructor.
class Q3Panel : public QWidget { Q_OBJECT public: Q3Panel(QWidget *parent = 0); ~Q3Panel(); };
If you now display this widget, you will see a blank window to the painfully familiar # D4D0C8 color. To turn this into a panel, we need a little magic and change one flag.
setAttribute(Qt::WA_X11NetWmWindowTypeDock, true);
Just add this to our widget's constructor. Thus, you set the property of our window _NET_WM_WINDOW_TYPE X11 to the value of _NET_WM_WINDOW_TYPE_DOCK. Now it's not just a window, but a dock! We'll see:

Here it is, the panel of my dreams, hefty! Too hefty, actually. Control of the size and position, we would not interfere.
void Q3Panel::setup(int height) { QRect screen = QApplication::desktop()->screenGeometry(); resize(screen.width(), height); int x, y; x = screen.left(); y = position() == top ? screen.top() : screen.bottom(); move(x, y); }
In this method, in the screen variable we get the geometry of the so-called
root window . Calling the resize () method makes the width of our panel equal to the width of the screen, and the height equal to the passed parameter. In the variable x, we write the left coordinate of our screen, and in y, the upper or lower coordinate, depending on the position. Then we move our panel along the (x, y) coordinates. position (), as you may have guessed, is one of two methods for working with a hidden property:
public: Position position() { return _position; }; void setPosition(Position position) { _position = position; }; private: Position _position;
Now we can control the size and position of our panel. It's time to move on to the next step.
Chat with i3
So we got to the most interesting part. To manage our window manager, you need to somehow communicate with it. Fortunately, i3 out of the box supports such an
IPC method as
unix socket 's. And even more fortunately, Qt has a very convenient class for working with them - QLocalSocket.
But first, a brief description of the protocol. The message is as follows:
< > < > < > <>
Now about everything in order. The magic string is “i3-ipc”. Its sole purpose is to control protocol versions. It is followed by a 32-bit number, which stores the size of the message. Then exactly the same 32-bit message type, followed by the message itself. The message type can have the following values:
- COMMAND (0) - the message contains the command
- GET_WORKSPACES (1) - get a list of desktops
- SUBSCRIBE (2) - subscribe to a specific event
- GET_OUTPUTS (3) - get a list of output devices
- GET_TREE (4) - get a list of all windows of all desktops
- GET_MARKS (5) - get a list of container identifiers
- GET_BAR_CONFIG (6) - get the panel configuration from the i3 config
Using all this functionality, you can create a panel with a bunch of features, but in this article we will limit ourselves only to the display and switching of desktops. For this we need 3 types of messages: COMMAND, GET_WORKSPACES and SUBSCRIBE. Now more about each. Oh yeah, I almost forgot. The data format that i3 receives and sends is json.
COMMAND : I will not describe all the possible commands - there are too many of them. To switch desktops, we need only one: “workspace number X”, where X is the desktop number. The response to the command is an associative array, which contains only one property - “success”, which can be true or false. Sample answer:
{ "success": true }
GET_WORKSPACES : the message body is empty, the answer is a list of desktops, each of which contains the following properties:
- num (integer) - logical number of the desktop
- name (string) - the name of the desktop in UTF-8
- visible (boolean) - whether the desktop is displayed. In the case of multiple output devices, multiple desktops can be displayed simultaneously.
- focused (boolean) - whether the desktop is in focus. Only one desktop can be in focus at the same time.
- urgent (boolean) - is there a window on the desktop requiring user attention (here I can be wrong, but I understood that way)
- rect (map) - desktop geometry, containing x and y coordinates, width and height
- output (string) - the output device on which the desktop is displayed
Sample answer:
[ { "num": 0, "name": "1", "visible": true, "focused": true, "urgent": false, "rect": { "x": 0, "y": 0, "width": 1280, "height": 800 }, "output": "LVDS1" }, { "num": 1, "name": "2", "visible": false, "focused": false, "urgent": false, "rect": { "x": 0, "y": 0, "width": 1280, "height": 800 }, "output": "LVDS1" } ]
SUBSCRIBE : i3 allows you to subscribe to events. In total there are 2 types of events:
- workspace (0) - events related to desktops. The body contains only one single property - “change”, which can take values:
- focus - when focus shifts to another desktop
- init - when creating a new desktop
- empty - when deleting an empty desktop
- output (1) - events associated with output devices.
The event message is completely identical to the standard message with the only difference that the most significant bit of the message type is set to 1. So, of the two types of events, we are only interested in the first one. His body is an associative array with one string property “changed”, which can take the values ​​“focus”, “init”, “empty” and “urgent”. Sample answer:
{ "change": "focus" }
Armed with knowledge of the protocol, you can begin to implement our client. We will use 2 sockets: one will be subscribed to events, with the help of the other we will send commands and receive a list of desktops. The general algorithm is as follows:
- Connect
- Update the list of desktops
- Subscribing to the workspace event
- We wait
- When the event arrives, we update the list of desktops
- goto 4
Let me remind you that the
unix socket 's identifier is a file in the file system. You can get its name either by reading the “I3_SOCKET_PATH” property of the
root window , or by calling i3 --get-socketpath. I took the path of least resistance:
QString I3Ipc::getSocketPath() { QProcess i3process; i3process.start("i3 --get-socketpath", QIODevice::ReadOnly); if (!i3process.waitForFinished()) { qDebug() << i3process.errorString(); exit(EXIT_FAILURE); } return QString(i3process.readAllStandardOutput()).remove(QChar('\n')); }
Now, knowing the path to the socket file, you can join the server:
void I3Ipc::reconnect() { mainSocket->abort(); eventSocket->abort(); QString socketPath = getSocketPath(); mainSocket->connectToServer(socketPath); eventSocket->connectToServer(socketPath); if (!mainSocket->waitForConnected() || !eventSocket->waitForConnected()) { qDebug() << "Connection timeout!"; exit(EXIT_FAILURE); } subscribe(); }
And send data:
QByteArray I3Ipc::pack(int type, QString payload) { QByteArray b; QDataStream s(&b, QIODevice::WriteOnly); s.setByteOrder(QDataStream::LittleEndian); s.writeRawData(I3_IPC_MAGIC, qstrlen(I3_IPC_MAGIC)); s << (quint32) payload.size(); s << (quint32) type; s.writeRawData(payload.toAscii().data(), payload.size()); return b; } void I3Ipc::send(int type, QString payload) { send(type, payload, mainSocket); } void I3Ipc::send(int type, QString payload, QLocalSocket* socket) { socket->write(pack(type, payload)); }
If you carefully read the part about the protocol, then this code should be clear to you. We pack the data and write to the socket. I3_IPC_MAGIC is a constant from the <i3 / ipc.h> header file describing the protocol.
Regarding setByteOrder (): the standard for QDataStream is BigEndian byte order, and i3 is waiting for data in the native, therefore either such a crutch or you will have to abandon QDataStream in favor of char arrays and memcpy (). We learned to send data, now we learn to accept answers:
void I3Ipc::read() { QLocalSocket *socket = (QLocalSocket*)sender(); if (socket->bytesAvailable() < (int) (qstrlen(I3_IPC_MAGIC) + sizeof(quint32) * 2)) return; QDataStream s(socket); s.setByteOrder(QDataStream::LittleEndian); quint32 msgType, payloadSize; s.skipRawData(qstrlen(I3_IPC_MAGIC)); s >> payloadSize; s >> msgType; while (socket->bytesAvailable() < payloadSize) { if (!socket->waitForReadyRead()) { qDebug() << "Reading timeout!"; exit(EXIT_FAILURE); } } char buf[payloadSize]; s.readRawData(buf, payloadSize); QByteArray jsonPayload(buf, payloadSize); if (msgType >> 31) { if (msgType == I3_IPC_EVENT_WORKSPACE) { emit workspaceEvent(); } } else { if (msgType == I3_IPC_REPLY_TYPE_WORKSPACES) { emit workspaceReply(jsonPayload); } } }
Here, too, everything is extremely simple: we wait until 14 bytes have accumulated (6 is the magic line and two 4-byte numbers), we miss 6 bytes and read the message size and its type into variables. It remains only to wait for the message itself and, depending on the type, to send the appropriate signal.
Now, after each event, we will receive the current list of desktops in json-format. To bring it to a “normal” state, we will use the QJson library. In many distributions it is in the repositories, and if not, no one bothers to build it yourself. So, we connect:
LIBS += -lqjson
In the .pro file and
#include <qjson/parser.h>
In the header file. QJson is extremely easy to use.
void Q3Panel::workspaceReplySlot(const QByteArray jsonPayload) { bool ok; QList<QVariant> workspacesList = jsonParser->parse(jsonPayload, &ok).toList(); if (!ok) { qDebug() << "Parser error: " << jsonParser->errorString(); return; } workspaces->clear(); for (int i = 0; i < workspacesList.size(); ++i) { QMap<QString, QVariant> w = workspacesList.at(i).toMap(); workspaces->insert(w.value("num").toUInt(), workspaceInfo(w.value("name").toString(), w.value("focused").toBool(), w.value("urgent").toBool())); } emit updateWorkspacesWidget(workspaces); }
workspaces is a hash table in which we store information about desktops. Its key is the quint16 number of the desktop, and the value is such a structure:
struct workspaceInfo { QString name; bool focused; bool urgent; workspaceInfo(QString _n, bool _f = 0, bool _u = 0) { name = _n; focused = _f; urgent = _u; } };
Everything is simple and clear. We are able to receive information about desktops, to store too. A trifle remains: to display it and by clicking on a specific desktop send a command to the window manager. You can go different ways, I decided to write my widget based on QHBoxLayout. Nothing complicated, store a hash table, where the key is the number of the desktop, and the value is a link to the WorkspaceButton button. WorkspaceButton is inherited from QToolButton and does not represent anything new, except for its policy of changing the size and style. After each update of the list of desktops, it is necessary to update the widget. It would be possible to simply delete all the buttons and create them, but we will go in a slightly different way:
void WorkspacesWidget::updateWorkspacesWidgetSlot(const QHash<qint16, workspaceInfo> *workspaces) { clearLayout(); QHash<qint16, workspaceInfo>::const_iterator wi = workspaces->constBegin(); while (wi != workspaces->constEnd()) { if (buttons->contains(wi.key())) { buttons->value(wi.key())->setFocused(wi.value().focused); } else { addButton(wi.key(), wi.value().focused, wi.value().name); } ++wi; } QHash<qint16, WorkspaceButton*>::const_iterator bi = buttons->constBegin(); QList<qint16> toDelete; while (bi != buttons->constEnd()) { if (!workspaces->contains(bi.key())) { delete bi.value(); toDelete << bi.key(); } else { mainLayout->addWidget(bi.value()); } ++bi; } for (int i = 0; i < toDelete.size(); ++i) { buttons->remove(toDelete.at(i)); } } void WorkspacesWidget::clearLayout() { while (mainLayout->takeAt(0)); } void WorkspacesWidget::addButton(quint16 num, bool focused, QString name) { WorkspaceButton* newButton = new WorkspaceButton(name, focused); connect(newButton, SIGNAL(clicked()), this, SLOT(buttonClickedSlot())); buttons->insert(num, newButton); }
First, we clear QHBoxLayout and go through the workspaces elements and, if there is already a button for the current desktop, update the focused property. If there is no button, add it. Then we check all the elements of buttons to see if the desktop still exists or not. And, if exists, we add on QHBoxLayout. If not, delete. I do not know how optimal this method is, but it seemed to me much better than to delete and re-create all the buttons every time.
Everything! Here it is, our panel:

Works fine, displays displays, switches. But this is just a basic functionality, something to catch up and overtake the standard panel in terms of functionality, it remains to add the settings, the tray, the menu and the clock. About this in the next article, if, of course, the topic will be interesting.
Source Code Repository