zmen

Application Launcher written in Zig

Files | Log | Commits | Refs | README


src/main.zig

Size: 23081 bytes

//! App Launcher using XCB and Cairo in Zig with ghost text completion
//! Dependencies: libc, xcb, cairo, X11 keysym
//! install dependencies with:
//! sudo apt-get install libxcb1-dev libcairo2-dev libxcb-keysyms1-dev
//! build and run with:
//! zig build-exe launcher.zig -lc -lxcb -lcairo -lxcb-keysyms -lX11 && ./zmen

const std = @import("std");
const c = @import("c.zig");

const font_size = 25.0;
const bh = 30;
const max_text_len = 256;
const font = "Liberation Mono";
const prompt = "launch:";

const colors = struct {
    
    const black = [3]f64{ 0.0, 0.0, 0.0 }; // Color 0
    const blue = [3]f64{ 0.0, 0.0, 0.67 }; // Color 1
    const green = [3]f64{ 0.0, 0.67, 0.0 }; // Color 2
    const cyan = [3]f64{ 0.0, 0.67, 0.67 }; // Color 3
    const red = [3]f64{ 0.67, 0.0, 0.0 }; // Color 4
    const magenta = [3]f64{ 0.67, 0.0, 0.67 }; // Color 5
    const brown = [3]f64{ 0.67, 0.33, 0.0 }; // Color 6
    const light_gray = [3]f64{ 0.75, 0.75, 0.75 }; // Color 7
    const dark_gray = [3]f64{ 0.5, 0.5, 0.5 }; // Color 8
    const light_blue = [3]f64{ 0.33, 0.33, 1.0 }; // Color 9
    const light_green = [3]f64{ 0.33, 1.0, 0.33 }; // Color 10
    const light_cyan = [3]f64{ 0.33, 1.0, 1.0 }; // Color 11
    const light_red = [3]f64{ 1.0, 0.33, 0.33 }; // Color 12
    const light_magenta = [3]f64{ 1.0, 0.33, 1.0 }; // Color 13
    const yellow = [3]f64{ 1.0, 1.0, 0.33 }; // Color 14
    const white = [3]f64{ 1.0, 1.0, 1.0 }; // Color 15

    const background = black;
    const foreground = white;
    const selected = yellow;
    const ghost = dark_gray;
};

const App = struct {
    conn: *c.xcb_connection_t,
    window: c.xcb_window_t,
    surface: *c.cairo_surface_t,
    cr: *c.cairo_t,
    width: u16,
    height: u16,
    input_text: [max_text_len]u8 = [_]u8{0} ** max_text_len,
    cursor_pos: usize = 0,
    input_len: usize = 0,
    key_symbols: *c.xcb_key_symbols_t,

    commands: std.ArrayList([]const u8),
    current_completion: ?[]const u8 = null,

    space_width: f64 = 0.0,

    pub fn init() !App {
        var commands = std.ArrayList([]const u8).init(std.heap.page_allocator);

        var screen_num: c_int = undefined;
        const conn = c.xcb_connect(null, &screen_num);
        if (conn == null) {
            std.debug.print("error: failed to connect to X server", .{});
            return error.ConnectionFailed;
        }

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

        const setup = c.xcb_get_setup(conn);
        var iter = c.xcb_setup_roots_iterator(setup);
        var i: c_int = 0;
        while (i < screen_num) : (i += 1) {
            c.xcb_screen_next(&iter);
        }

        const screen = iter.data;

        const screen_width = screen.*.width_in_pixels;
        const screen_height = screen.*.height_in_pixels;

        const width: u16 = screen_width / 4;
        const height: u16 = bh;

        const x: i16 = @intCast((screen_width - width) / 2);
        const y: i16 = @intCast((screen_height - height) / 2);

        const window = c.xcb_generate_id(conn);
        const mask = c.XCB_CW_BACK_PIXEL | c.XCB_CW_EVENT_MASK;
        const values = [_]u32{
            screen.*.black_pixel,
            c.XCB_EVENT_MASK_EXPOSURE | c.XCB_EVENT_MASK_KEY_PRESS | c.XCB_EVENT_MASK_KEY_RELEASE,
        };

        _ = c.xcb_create_window(
            conn,
            c.XCB_COPY_FROM_PARENT,
            window,
            screen.*.root,
            x,
            y,
            width,
            height,
            0,
            c.XCB_WINDOW_CLASS_INPUT_OUTPUT,
            screen.*.root_visual,
            mask,
            &values,
        );

        const atom_window_type_cookie = c.xcb_intern_atom(conn, 0, 19, "_NET_WM_WINDOW_TYPE");
        const atom_window_type_dialog_cookie = c.xcb_intern_atom(conn, 0, 27, "_NET_WM_WINDOW_TYPE_DIALOG");
        const atom_state_cookie = c.xcb_intern_atom(conn, 0, 13, "_NET_WM_STATE");
        const atom_state_above_cookie = c.xcb_intern_atom(conn, 0, 19, "_NET_WM_STATE_ABOVE");
        const atom_state_skip_taskbar_cookie = c.xcb_intern_atom(conn, 0, 25, "_NET_WM_STATE_SKIP_TASKBAR");
        const atom_state_skip_pager_cookie = c.xcb_intern_atom(conn, 0, 23, "_NET_WM_STATE_SKIP_PAGER");

        const atom_window_type_reply = c.xcb_intern_atom_reply(conn, atom_window_type_cookie, null);
        const atom_window_type = atom_window_type_reply.*.atom;
        c.free(atom_window_type_reply);

        const atom_window_type_dialog_reply = c.xcb_intern_atom_reply(conn, atom_window_type_dialog_cookie, null);
        const atom_window_type_dialog = atom_window_type_dialog_reply.*.atom;
        c.free(atom_window_type_dialog_reply);

        const atom_state_reply = c.xcb_intern_atom_reply(conn, atom_state_cookie, null);
        const atom_state = atom_state_reply.*.atom;
        c.free(atom_state_reply);

        const atom_state_above_reply = c.xcb_intern_atom_reply(conn, atom_state_above_cookie, null);
        const atom_state_above = atom_state_above_reply.*.atom;
        c.free(atom_state_above_reply);

        const atom_state_skip_taskbar_reply = c.xcb_intern_atom_reply(conn, atom_state_skip_taskbar_cookie, null);
        const atom_state_skip_taskbar = atom_state_skip_taskbar_reply.*.atom;
        c.free(atom_state_skip_taskbar_reply);

        const atom_state_skip_pager_reply = c.xcb_intern_atom_reply(conn, atom_state_skip_pager_cookie, null);
        const atom_state_skip_pager = atom_state_skip_pager_reply.*.atom;
        c.free(atom_state_skip_pager_reply);

        _ = c.xcb_change_property(conn, c.XCB_PROP_MODE_REPLACE, window, atom_window_type, c.XCB_ATOM_ATOM, 32, 1, &atom_window_type_dialog);

        const window_states = [_]c.xcb_atom_t{ atom_state_above, atom_state_skip_taskbar, atom_state_skip_pager };
        _ = c.xcb_change_property(conn, c.XCB_PROP_MODE_REPLACE, window, atom_state, c.XCB_ATOM_ATOM, 32, 3, &window_states);

        _ = c.xcb_change_property(conn, c.XCB_PROP_MODE_REPLACE, window, c.XCB_ATOM_WM_TRANSIENT_FOR, c.XCB_ATOM_WINDOW, 32, 1, &screen.*.root);

        const wm_size_hints_cookie = c.xcb_intern_atom(conn, 0, 13, "WM_SIZE_HINTS");
        const wm_size_hints_reply = c.xcb_intern_atom_reply(conn, wm_size_hints_cookie, null);
        const wm_size_hints_atom = wm_size_hints_reply.*.atom;
        c.free(wm_size_hints_reply);

        const size_hints = [_]u32{
            0x30, // flags: PSize | PPosition | PMinSize | PMaxSize
            @intCast(x),
            @intCast(y),
            width,
            height,
            width,
            height,
            width, 
            height,
            0, 0, 0, 0, 0, 0, 0, 0
        };
        _ = c.xcb_change_property(conn, c.XCB_PROP_MODE_REPLACE, window, wm_size_hints_atom, c.XCB_ATOM_WM_SIZE_HINTS, 32, size_hints.len, &size_hints);

        const wm_hints_cookie = c.xcb_intern_atom(conn, 0, 8, "WM_HINTS");
        const wm_hints_reply = c.xcb_intern_atom_reply(conn, wm_hints_cookie, null);
        const wm_hints_atom = wm_hints_reply.*.atom;
        c.free(wm_hints_reply);

        const hints = [_]u32{
            0x3,
            1,
            1,
            0, 0, 0, 0, 0, 0
        };
        _ = c.xcb_change_property(conn, c.XCB_PROP_MODE_REPLACE, window, wm_hints_atom, c.XCB_ATOM_WM_HINTS, 32, hints.len, &hints);

        _ = c.xcb_set_input_focus(conn, c.XCB_INPUT_FOCUS_POINTER_ROOT, window, c.XCB_CURRENT_TIME);
        _ = c.xcb_flush(conn);

        const title = "zmen";
        _ = c.xcb_change_property(
            conn,
            c.XCB_PROP_MODE_REPLACE,
            window,
            c.XCB_ATOM_WM_NAME,
            c.XCB_ATOM_STRING,
            8,
            title.len,
            title,
        );

        const visual = getVisual(screen, screen.*.root_visual);
        if (visual == null) {
            std.debug.print("Failed to find visual\n", .{});
            return error.VisualNotFound;
        }

        const surface = c.cairo_xcb_surface_create(conn, window, visual, width, height);
        if (surface == null) {
            std.debug.print("Failed to create Cairo surface\n", .{});
            return error.CairoSurfaceCreationFailed;
        }

        const cr = c.cairo_create(surface);
        if (cr == null) {
            std.debug.print("Failed to create Cairo context\n", .{});
            return error.CairoContextCreationFailed;
        }

        const key_symbols = c.xcb_key_symbols_alloc(conn);
        if (key_symbols == null) {
            std.debug.print("Failed to allocate key symbols\n", .{});
            return error.KeySymbolsAllocationFailed;
        }

        _ = c.xcb_map_window(conn, window);
        _ = c.xcb_flush(conn);

        try loadCommands(&commands);

        var app = App{
            .conn = conn.?,
            .window = window,
            .surface = surface.?,
            .cr = cr.?,
            .width = width,
            .height = height,
            .key_symbols = key_symbols.?,
            .commands = commands,
            .space_width = 0.0,
        };

        app.calculateSpaceWidth();

        return app;
    }

    pub fn deinit(self: *App) void {
        for (self.commands.items) |cmd| {
            std.heap.page_allocator.free(cmd);
        }
        self.commands.deinit();

        c.cairo_destroy(self.cr);
        c.cairo_surface_destroy(self.surface);
        c.xcb_key_symbols_free(self.key_symbols);
        c.xcb_disconnect(self.conn);
    }

    pub fn calculateSpaceWidth(self: *App) void {
        c.cairo_select_font_face(self.cr, font, c.CAIRO_FONT_SLANT_NORMAL, c.CAIRO_FONT_WEIGHT_NORMAL);
        c.cairo_set_font_size(self.cr, font_size);

        var space_extents: c.cairo_text_extents_t = undefined;
        c.cairo_text_extents(self.cr, " ", &space_extents);
        self.space_width = space_extents.x_advance;

        if (self.space_width < 1.0) {
            var char_extents: c.cairo_text_extents_t = undefined;
            c.cairo_text_extents(self.cr, "n", &char_extents);
            self.space_width = char_extents.x_advance * 0.8;
        }

        std.debug.print("Space width: {d}\n", .{self.space_width});
    }

    pub fn calculateTextWidth(self: *App, text: []const u8) f64 {
        var space_count: usize = 0;
        for (text) |char| {
            if (char == ' ') {
                space_count += 1;
            }
        }

        if (space_count == 0) {
            var text_extents: c.cairo_text_extents_t = undefined;
            c.cairo_text_extents(self.cr, @ptrCast(text), &text_extents);
            return text_extents.width;
        }

        var temp_buf: [max_text_len]u8 = undefined;
        var temp_len: usize = 0;

        for (text) |char| {
            if (char != ' ' and temp_len < max_text_len) {
                temp_buf[temp_len] = char;
                temp_len += 1;
            }
        }

        if (temp_len < max_text_len) {
            temp_buf[temp_len] = 0;
        }

        var text_extents: c.cairo_text_extents_t = undefined;
        c.cairo_text_extents(self.cr, @ptrCast(&temp_buf), &text_extents);

        const total_width = text_extents.width + @as(f64, @floatFromInt(space_count)) * self.space_width;
        return total_width;
    }

    pub fn calculateCursorX(self: *App, prompt_width: f64) f64 {
        if (self.cursor_pos == 0) {
            return prompt_width;
        }

        const text_slice = self.input_text[0..self.cursor_pos];

        const width = self.calculateTextWidth(text_slice);

        return prompt_width + width;
    }

    pub fn draw(self: *App) void {
        c.cairo_set_source_rgb(self.cr, colors.background[0], colors.background[1], colors.background[2]);
        c.cairo_paint(self.cr);

        c.cairo_select_font_face(self.cr, font, c.CAIRO_FONT_SLANT_NORMAL, c.CAIRO_FONT_WEIGHT_NORMAL);
        c.cairo_set_font_size(self.cr, font_size);

        var text_extents: c.cairo_text_extents_t = undefined;
        c.cairo_text_extents(self.cr, prompt, &text_extents);

        var font_extents: c.cairo_font_extents_t = undefined;
        c.cairo_font_extents(self.cr, &font_extents);

        const y_pos = (bh / 2.0) + (font_extents.ascent - font_extents.descent) / 2.0;

        c.cairo_set_source_rgb(self.cr, colors.selected[0], colors.selected[1], colors.selected[2]);
        c.cairo_move_to(self.cr, 8, y_pos);
        c.cairo_show_text(self.cr, prompt);

        const prompt_width = text_extents.width + 16;

        c.cairo_set_source_rgb(self.cr, colors.foreground[0], colors.foreground[1], colors.foreground[2]);
        const input_text_slice = self.input_text[0..self.input_len];
        c.cairo_move_to(self.cr, prompt_width, y_pos);
        c.cairo_show_text(self.cr, @ptrCast(input_text_slice));

        const input_width = self.calculateTextWidth(input_text_slice);

        if (self.current_completion != null) {
            const completion = self.current_completion.?;

            if (completion.len > self.input_len) {
                const ghost_text = completion[self.input_len..];

                c.cairo_set_source_rgb(self.cr, colors.ghost[0], colors.ghost[1], colors.ghost[2]);
                c.cairo_move_to(self.cr, prompt_width + input_width, y_pos);
                c.cairo_show_text(self.cr, @ptrCast(ghost_text));
            }
        }

        const cursor_x = self.calculateCursorX(prompt_width);

        c.cairo_set_source_rgb(self.cr, colors.foreground[0], colors.foreground[1], colors.foreground[2]);
        c.cairo_rectangle(self.cr, cursor_x, (bh - font_extents.height) / 2, 2, font_extents.height);
        c.cairo_fill(self.cr);

        c.cairo_surface_flush(self.surface);
        _ = c.xcb_flush(self.conn);
    }

    pub fn handleKeyPress(self: *App, keycode: c.xcb_keycode_t) void {
        const keysym = c.xcb_key_symbols_get_keysym(self.key_symbols, keycode, 0);

        std.debug.print("Key pressed: keysym={}\n", .{keysym});

        if (keysym == c.XK_space) {
            std.debug.print("Space key detected\n", .{});

            if (self.input_len < max_text_len - 1) {
                var i: usize = self.input_len;
                while (i > self.cursor_pos) : (i -= 1) {
                    self.input_text[i] = self.input_text[i - 1];
                }

                self.input_text[self.cursor_pos] = ' ';
                self.input_len += 1;
                self.cursor_pos += 1;

                self.updateCompletion();
            }
            return;
        }

        switch (keysym) {
            c.XK_Tab => {
                if (self.current_completion != null) {
                    const completion = self.current_completion.?;

                    if (completion.len > self.input_len) {
                        const completion_part = completion[self.input_len..];

                        const to_copy = @min(completion_part.len, max_text_len - self.input_len);

                        @memcpy(self.input_text[self.input_len .. self.input_len + to_copy], completion_part[0..to_copy]);

                        self.input_len += to_copy;
                        self.cursor_pos = self.input_len;
                        self.input_text[self.input_len] = 0;

                        self.updateCompletion();
                    }
                }
            },
            c.XK_BackSpace => {
                if (self.cursor_pos > 0) {
                    var i: usize = self.cursor_pos - 1;
                    while (i < self.input_len - 1) : (i += 1) {
                        self.input_text[i] = self.input_text[i + 1];
                    }
                    self.input_len -= 1;
                    self.cursor_pos -= 1;

                    self.updateCompletion();
                }
            },
            c.XK_Delete => {
                if (self.cursor_pos < self.input_len) {
                    var i: usize = self.cursor_pos;
                    while (i < self.input_len - 1) : (i += 1) {
                        self.input_text[i] = self.input_text[i + 1];
                    }
                    self.input_len -= 1;

                    self.updateCompletion();
                }
            },
            c.XK_Left => {
                if (self.cursor_pos > 0) {
                    self.cursor_pos -= 1;
                }
            },
            c.XK_Right => {
                if (self.cursor_pos < self.input_len) {
                    self.cursor_pos += 1;
                } else if (self.current_completion != null) {
                    const completion = self.current_completion.?;
                    if (completion.len > self.input_len) {
                        self.input_text[self.input_len] = completion[self.input_len];
                        self.input_len += 1;
                        self.cursor_pos = self.input_len;
                        self.input_text[self.input_len] = 0;

                        self.updateCompletion();
                    }
                }
            },
            c.XK_Home => {
                self.cursor_pos = 0;
            },
            c.XK_End => {
                self.cursor_pos = self.input_len;
            },
            c.XK_Return => {
                self.launchApplication();
            },
            c.XK_Escape => {
                if (self.input_len > 0) {
                    self.input_len = 0;
                    self.cursor_pos = 0;

                    self.current_completion = null;
                } else {
                    std.process.exit(0);
                }
            },
            else => {
                const char = keysymToChar(keysym);
                if (char != 0 and self.input_len < max_text_len - 1) {
                    var i: usize = self.input_len;
                    while (i > self.cursor_pos) : (i -= 1) {
                        self.input_text[i] = self.input_text[i - 1];
                    }

                    self.input_text[self.cursor_pos] = char;
                    self.input_len += 1;
                    self.cursor_pos += 1;

                    self.updateCompletion();
                }
            },
        }

        self.input_text[self.input_len] = 0;

        std.debug.print("Input text (len={}, cursor={}): '", .{ self.input_len, self.cursor_pos });
        for (self.input_text[0..self.input_len]) |char| {
            if (char == ' ') {
                std.debug.print("ยท", .{});
            } else {
                std.debug.print("{c}", .{char});
            }
        }
        std.debug.print("'\n", .{});
    }

    fn updateCompletion(self: *App) void {
        self.current_completion = null;

        if (self.input_len == 0) {
            return;
        }

        const input = self.input_text[0..self.input_len];

        for (self.commands.items) |cmd| {
            if (cmd.len >= self.input_len) {
                var matches = true;
                var i: usize = 0;
                while (i < self.input_len) : (i += 1) {
                    const input_char = toLower(input[i]);
                    const cmd_char = toLower(cmd[i]);

                    if (input_char != cmd_char) {
                        matches = false;
                        break;
                    }
                }

                if (matches) {
                    self.current_completion = cmd;
                    break;
                }
            }
        }
    }

    pub fn launchApplication(self: *App) void {
        var cmd_buf: [max_text_len]u8 = undefined;
        @memcpy(cmd_buf[0..self.input_len], self.input_text[0..self.input_len]);
        cmd_buf[self.input_len] = 0;

        std.debug.print("Launching: {s}\n", .{cmd_buf[0..self.input_len]});

        const pid = std.posix.fork() catch |err| {
            std.debug.print("Fork failed: {}\n", .{err});
            return;
        };

        if (pid == 0) {
            c.xcb_disconnect(self.conn);

            const args = [_:null]?[*:0]const u8{
                "/bin/sh",
                "-c",
                @ptrCast(&cmd_buf),
                null,
            };

            const err = std.posix.execveZ("/bin/sh", &args, std.c.environ);
            std.debug.print("Exec failed: {}\n", .{err});
            std.process.exit(1);
        } else {
            std.process.exit(0);
        }
    }
};

fn toLower(char: u8) u8 {
    if (char >= 'A' and char <= 'Z') {
        return char + ('a' - 'A');
    }
    return char;
}

fn keysymToChar(keysym: c.xcb_keysym_t) u8 {
    if (keysym >= 32 and keysym <= 126) {
        return @intCast(keysym);
    }
    return 0;
}

fn getVisual(screen: *c.xcb_screen_t, visual_id: c.xcb_visualid_t) ?*c.xcb_visualtype_t {
    var depth_iter = c.xcb_screen_allowed_depths_iterator(screen);
    while (depth_iter.rem != 0) : (c.xcb_depth_next(&depth_iter)) {
        var visual_iter = c.xcb_depth_visuals_iterator(depth_iter.data);
        while (visual_iter.rem != 0) : (c.xcb_visualtype_next(&visual_iter)) {
            const visual = @as(*c.xcb_visualtype_t, @ptrCast(visual_iter.data));
            if (visual.visual_id == visual_id) {
                return visual;
            }
        }
    }
    return null;
}

fn loadCommands(commands: *std.ArrayList([]const u8)) !void {
    const directories = [_][]const u8{
        "/usr/bin",
        "/usr/sbin",
        "/bin",
        "/sbin",
        "/usr/local/bin",
        "/usr/local/sbin",
    };

    for (directories) |dir_path| {
        try scanDirectory(commands, dir_path);
    }

    std.sort.heap([]const u8, commands.items, {}, compareStrings);
}

fn compareStrings(_: void, a: []const u8, b: []const u8) bool {
    return std.mem.lessThan(u8, a, b);
}

fn scanDirectory(commands: *std.ArrayList([]const u8), dir_path: []const u8) !void {
    const dir = c.opendir(dir_path.ptr);
    if (dir == null) return;
    defer _ = c.closedir(dir);

    while (true) {
        const entry = c.readdir(dir);
        if (entry == null) break;

        const filename = std.mem.sliceTo(&entry.*.d_name, 0);

        if (filename.len > 0 and filename[0] == '.') continue;

        if (entry.*.d_type == c.DT_DIR) continue;

        if (entry.*.d_type == c.DT_REG or entry.*.d_type == c.DT_LNK) {
            const dup = try std.heap.page_allocator.dupe(u8, filename);
            try commands.append(dup);
        }
    }
}

pub fn main() !void {
    var app = try App.init();
    defer app.deinit();

    app.draw();

    // Event loop
    while (true) {
        const event = c.xcb_wait_for_event(app.conn);
        if (event == null) {
            continue;
        }

        defer c.free(event);
        switch (event.*.response_type & ~@as(u8, 0x80)) {
            c.XCB_EXPOSE => {
                app.draw();
            },
            c.XCB_KEY_PRESS => {
                const key_event = @as(*c.xcb_key_press_event_t, @ptrCast(event));
                app.handleKeyPress(key_event.detail);
                app.draw();
            },
            else => {},
        }
    }
}