DEV Community

S.Morteza Hosseini
S.Morteza Hosseini

Posted on

Writing a window manager in zig with xcb - part 2

From the first post, I have been trying to understand how xcb library works and what are all those commands that was used previously. Today, I will explain how to get key press event in an event loop with xcb and zig.

This time, let's try to go step by step and review the code:

const std = @import("std");
const print = std.debug.print;
const c = @cImport({
    @cInclude("xcb/xcb.h");
    @cInclude("stdlib.h");
});
Enter fullscreen mode Exit fullscreen mode

This code does

  • importing standard library in Zig and assigning it to std,
  • renaming the std.debug.print function to print for easier use,
  • and importing two important C libraries. stdlib.h has free function which is good to free event pointers at the end of each event loop.

Then, we start the main function as

pub fn main() !void {
    const conn: ?*c.xcb_connection_t = c.xcb_connect(null, null);
    defer c.xcb_disconnect(conn);

    if (conn == null or c.xcb_connection_has_error(conn) != 0) {
        print("Failed to connect to X server\n", .{});
        return error.XConnectionFailed;
    }

    const xcbsetup = c.xcb_get_setup(conn);
    if (xcbsetup == null) {
        print("Failed to get XCB setup\n", .{});
        return error.XCBSetupFailed;
    }

    const screen_iter = c.xcb_setup_roots_iterator(xcbsetup);
    if (screen_iter.rem == 0) {
        print("No screens found\n", .{});
        return error.NoScreens;
    }

    const screen: *c.xcb_screen_t = screen_iter.data;

    const win = c.xcb_generate_id(conn);

Enter fullscreen mode Exit fullscreen mode
  • xcb_connection establishes connection to the xorg server.
  • The rest of the lines help us access our screen and generate a window in it.
    //  Event masks: capturing all possible events
    const event_mask = c.XCB_EVENT_MASK_EXPOSURE |
        c.XCB_EVENT_MASK_KEY_PRESS |
        c.XCB_EVENT_MASK_KEY_RELEASE |
        c.XCB_EVENT_MASK_BUTTON_PRESS |
        c.XCB_EVENT_MASK_BUTTON_RELEASE |
        c.XCB_EVENT_MASK_POINTER_MOTION |
        c.XCB_EVENT_MASK_ENTER_WINDOW |
        c.XCB_EVENT_MASK_LEAVE_WINDOW |
        c.XCB_EVENT_MASK_FOCUS_CHANGE |
        c.XCB_EVENT_MASK_STRUCTURE_NOTIFY;

    const value_list = [_]u32{event_mask};

    _ = c.xcb_create_window(
        conn,
        c.XCB_COPY_FROM_PARENT,
        win,
        screen.root,
        0,
        0, // x, y
        400,
        300, // width, height
        10, // border width
        c.XCB_WINDOW_CLASS_INPUT_OUTPUT,
        screen.root_visual,
        c.XCB_CW_EVENT_MASK,
        &value_list,
    );

    _ = c.xcb_map_window(conn, win);
    _ = c.xcb_flush(conn);
Enter fullscreen mode Exit fullscreen mode
  • We first define all the events that we are waiting for! xcb library is not only to be used for window manager and it can be used by other applications as well. Those applications do not need to listen every even! We specify the events that we want by defining the event mask. Then we pass it to xcb to create a window for us.

  • This window is not automatically shown to users, so we have to use the map window function to show it.

  • Finally, we use flush to send all the info to the xord server with our connection so that we can be ready to get events. apparently if we dont do this, we will receive those info instead of events (not completely understood it yet!).

    while (true) {
        const event = c.xcb_wait_for_event(conn);
        defer c.free(event);

        if (event == null) {
            print("Error waiting for event\nBreaking from the event loop!\n", .{});
            break;
        }

        switch (event.*.response_type) {
            c.XCB_KEY_PRESS => {
                const key_event: *c.xcb_key_press_event_t = @ptrCast(event);
                print("Key pressed: code = {}, state = {}\n", .{ key_event.detail, key_event.state });
            },
            else => {
                print("Unhandled event of type {}.\n", .{event.*.response_type});
            },
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This is our event loop at the end of the main function. event is a pointer to struct type and in C we get its field by event->response_type. In Zig this is done by .* instead of ->. In our switch we first check what is the type of the event passed to us and then just type cast it to the corresponding type by @ptrCast().

Now that this is done, we just have to add exe.linklibC(); in our build.zig file so that our compiler links libc when compiling this code and we are good to go. Here is our build.zig file:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "blakewm",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.linkSystemLibrary("xcb");
    exe.linkLibC();
    b.installArtifact(exe);
}
Enter fullscreen mode Exit fullscreen mode

As usual, I built this code with zig build and ran it using

Xephyr -ac -br -noreset -screen 800x600 :1 & sleep 1; DISPLAY=:1 ./zig-out/bin/blakewm
Enter fullscreen mode Exit fullscreen mode

Some references to learn xcb, Zig:

Top comments (0)