A keyboard-oriented system tray for X11 tiling window managers
Table of Contents
Introduction
Typically, tiling window managers like XMonad do not have a system tray. If you want a system tray, you can use a standalone implementation like stalonetray or a status bar implementation with built-in system tray support, such as polybar.
However, neither of these implementations can't operate the system tray by the keyboard. I think the need for a keyboard-oriented system tray implementation that haves a high affinity with tiling window managers. This idea was inspired by cereja an old Windows shell replacement.
So, I created the yet another system tray implementation named "GeekTray" by Rust. This article will how GeekTray was implemented.
How to implement a system tray for X11
The system tray for X11 is documented by freedesktop.org as the System Tray Protocol Specification. According to this specification, the system tray is a window that own a special manager selection. And, the tray icon is a window to be embedded in that window. The tray icon is embedded in the system tray in the following steps:
- The system tray make the manager window that becomes the selection owner aquire
_NET_SYSTEM_TRAY_S{SCREEN_NUM}
. This window can be different one that is used to embed tray icons. - If the system tray acquires the selection owner, it should broadcast a
MANAGER
client message. Otherwise, it wait for current selection owner to be destroyed. - When the tray icon receives a
MANAGER
client message, it requests dock viaSYSTEM_TRAY_REQUEST_DOCK
to the system tray's manager window. - If the system tray receives a dock request from the tray icon, it will embed the tray icon into the embedder window, then it reply
XEMBED_EMBEDDED_NOTIFY
to the tray icon.
I implemented the system tray in Rust using the x11rb, which provides a low-level API based on XCB (XCB is designed as a replacement for Xlib). You can see the implementation of the System Tray Protocol in tray_manager.rs.
Rendering the system tray window
In x11rb, you can use the XRender extension, but its API is very low-level and difficult to use directly. Text rendering is particularly difficult because you need to do the rasterization and layout yourself.
Therefore, for graphics and text rendering, it is recommended to combine x11rb with other libraries. In this case, I created a small wrapper named RenderContext
to handle drawing by using cairo-sys and pangocairo-sys. Here is its initialization part:
So let's explain the actual drawing of the tray window. Before drawing, we need to determine the drawing dimension that represents position and size. This process is called layout. The layout is executed in the following timings:
- When the window size is changed.
- When tray icons are added or removed.
The layout is executed by TrayEmbedder::layout()
. This function updates the dimensions of the tray items to be drawn inside the window, and returns the size of the entire window.
When the layout is completed, The RenderContext
is created based on the size returned by the layout. As a result, the RenderContext
will be recreated whenever the layout is executed.
Next, the drawing is executed by TrayEmbedder::draw()
. This function draws the content of the window, updates the positions of the embedded tray icons, and requests a redraw embedded tray icons. This function is called at the following timings:
- When a redraw is requested by an
Expose
event. - When the title of a tray icon is changed.
- When the selected tray icon is changed.
The rendering pipeline, consisting of layout and drawing, is executed within the event loop before starting the next polling when the event queue becomes empty. The following App::handle_tick()
does this:
Great! With this, I was able to render the UI without using GUI toolkits like GTK+.
Handling key events
GeekTray is a keyboard-oriented system tray. When a user inputs a key, it needs to execute the corresponding action. However, with X11, only the physical number (Keycode) of the key is obtained, and it is not clear what the key (Keysym) it is assigned to.
In Xlib, there is XKeycodeToKeysym()
to convert keycode to keysym, but in x11rb (XCB), such a function does not exist. I used xkbcommon for this implementation (I think the implementation based on the X Keyboard Extension is too complex and I don't want to implement it myself).
I generated bindings to use xkbcommon for Rust and created a simple wrapper for it. Those implementations are taken inspiration from druid-shell.
Conclusion
Through the implementation of GeekTray, I learned how to create a simple GUI application without using any frameworks or toolkits. Of course, for applications that have complex UI, some kind of framework will be necessary.
In the past, I worked on a declarative UI framework called YuiUI for that purpose. The experience gained from developing on YuiUI has been applied to the GeekTray. In the future, I might write an article explaining YuiUI and its development process.