zmen

split state, remove arraylist (heap allocation) to fixed buffer instead.

285b4d415dc6e26c0c6f186e8763bfdc7cf6ab1d

IIIlllIIIllI <seb.michalk@gmail.com>

2026-05-31 16:46:36 +0000

 README.md       |   8 +-
 src/config.zig  |  10 +-
 src/libstd.a    | Bin 0 -> 4274 bytes
 src/libstd.a.o  | Bin 0 -> 4064 bytes
 src/main.zig    | 849 +++++++++++++++++++++++++++++---------------------------
 src/refcator.md | 278 -------------------
 6 files changed, 447 insertions(+), 698 deletions(-)

diff --git a/README.md b/README.md
index 27fde0c..1c34ebe 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,16 @@
 # zmen
 
-A minimal application launcher written in Zig.
+A small application launcher written in Zig.
 
 ## Description
 
 zmen is a simple, lightweight application launcher for X11 inspired by dmenu. 
 It uses XCB and Cairo for rendering with fewer than 700 lines of code.
 
-Features:
-
 - Tab completion for commands
 - Minimal memory footprint
 - Fast command scanning from standard executable paths
-- Clean Zig implementation with no external dependencies other than X11 libraries
+- Clean Zig implementation with no external dependencies other than X11 libraries and libcairo
 
 ## Installation
 
@@ -60,7 +58,7 @@ Edit the source code directly. Simple configuration options:
 const font_size = 25.0;
 const bh = 30;           // Bar height
 const font = "Iosevka";  // Font name
-const prompt = "run:";      // Prompt character
+const prompt = "run:";   // Prompt character
 ```
 
 ## LICENSE
diff --git a/src/config.zig b/src/config.zig
index de46b26..462a019 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -1,7 +1,9 @@
 // compile-time config
-pub const font_size = 50.0;
-pub const bh: u16 = 120;
+pub const font_size = 25.0;
+pub const bh: u16 = 60;
 pub const max_text_len = 256;
+pub const max_commands = 4096;
+pub const command_buffer_size = 64 * 1024;
 pub const font = "monospace";
 pub const prompt = "$";
 
@@ -23,8 +25,8 @@ pub const colors = struct {
     const yellow = [3]f64{ 1.0, 1.0, 0.33 };
     const white = [3]f64{ 1.0, 1.0, 1.0 };
 
-    pub const background = white;
-    pub const foreground = black;
+    pub const background = black;
+    pub const foreground = magenta;
     pub const selected = light_red;
     pub const ghost = dark_gray;
 };
diff --git a/src/libstd.a b/src/libstd.a
new file mode 100644
index 0000000..3b39b42
Binary files /dev/null and b/src/libstd.a differ
diff --git a/src/libstd.a.o b/src/libstd.a.o
new file mode 100644
index 0000000..905e0d7
Binary files /dev/null and b/src/libstd.a.o differ
diff --git a/src/main.zig b/src/main.zig
index 2c504c9..21a4464 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,128 +1,265 @@
-//! zmen: app-launcher using XCB and Cairo in Zig with ghost text completion
+//! zmen: app-launcher using XCB and Cairo 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
+//! zig build-exe main.zig -lc -lxcb -lcairo -lxcb-keysyms -lX11 && ./zmen
 
 const std = @import("std");
 const c = @import("c.zig");
 const config = @import("config.zig");
 
-const Zmen = 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: [config.max_text_len]u8 = [_]u8{0} ** config.max_text_len,
-    cursor_pos: usize = 0,
-    input_len: usize = 0,
-    key_symbols: *c.xcb_key_symbols_t,
+const Input = struct {
+    text: [config.max_text_len]u8 = [_]u8{0} ** config.max_text_len,
+    len: usize = 0,
+    cursor: usize = 0,
+    completion: ?[]const u8 = null,
 
-    commands: std.ArrayList([]const u8),
-    current_completion: ?[]const u8 = null,
+    fn slice(self: *const Input) []const u8 {
+        return self.text[0..self.len];
+    }
 
-    space_width: f64 = 0.0,
+    fn insert(self: *Input, char: u8) bool {
+        if (self.len >= config.max_text_len - 1) {
+            return false;
+        }
 
-    pub fn init() !Zmen {
-        var commands = std.ArrayList([]const u8).init(std.heap.page_allocator);
+        std.mem.copyBackwards(
+            u8,
+            self.text[self.cursor + 1 .. self.len + 1],
+            self.text[self.cursor..self.len],
+        );
 
-        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;
+        self.text[self.cursor] = char;
+        self.len += 1;
+        self.cursor += 1;
+        self.text[self.len] = 0;
+        return true;
+    }
+
+    fn append_completion(self: *Input) bool {
+        const completion = self.completion orelse return false;
+        if (completion.len <= self.len) {
+            return false;
         }
 
-        if (c.xcb_connection_has_error(conn) != 0) {
-            std.debug.print("Failed to connect to X server\n", .{});
-            return error.ConnectionFailed;
+        const suffix = completion[self.len..];
+        const to_copy = @min(suffix.len, config.max_text_len - 1 - self.len);
+        if (to_copy == 0) {
+            return false;
         }
 
-        const setup = c.xcb_get_setup(conn);
-        var iter = c.xcb_setup_roots_iterator(setup);
-        var i: c_int = 0;
+        @memcpy(self.text[self.len .. self.len + to_copy], suffix[0..to_copy]);
+        self.len += to_copy;
+        self.cursor = self.len;
+        self.text[self.len] = 0;
+        return true;
+    }
 
-        while (i < screen_num) : (i += 1) {
-            c.xcb_screen_next(&iter);
+    fn accept_completion_char(self: *Input) bool {
+        const completion = self.completion orelse return false;
+        if (completion.len <= self.len) {
+            return false;
         }
 
-        const screen = iter.data;
-        const screen_width = screen.*.width_in_pixels;
-        const screen_height = screen.*.height_in_pixels;
+        return self.insert(completion[self.len]);
+    }
 
-        const width: u16 = (screen_width / 4);
-        const height: u16 = config.bh;
+    fn delete_before_cursor(self: *Input) bool {
+        if (self.cursor == 0) {
+            return false;
+        }
 
-        const x: i16 = @intCast((screen_width - width) / 2);
-        const y: i16 = @intCast((screen_height - height) / 2);
+        std.mem.copyForwards(
+            u8,
+            self.text[self.cursor - 1 .. self.len - 1],
+            self.text[self.cursor..self.len],
+        );
 
-        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,
-        };
+        self.len -= 1;
+        self.cursor -= 1;
+        self.text[self.len] = 0;
+        return true;
+    }
 
-        _ = 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,
+    fn delete_at_cursor(self: *Input) bool {
+        if (self.cursor >= self.len) {
+            return false;
+        }
+
+        std.mem.copyForwards(
+            u8,
+            self.text[self.cursor .. self.len - 1],
+            self.text[self.cursor + 1 .. self.len],
         );
 
-        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_window_type_dock_cookie = c.xcb_intern_atom(conn, 0, 24, "_NET_WM_WINDOW_TYPE_DOCK");
+        self.len -= 1;
+        self.text[self.len] = 0;
+        return true;
+    }
 
-        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);
+    fn clear(self: *Input) void {
+        self.len = 0;
+        self.cursor = 0;
+        self.completion = null;
+        self.text[0] = 0;
+    }
+};
 
-        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 Commands = struct {
+    const Entry = struct {
+        off: usize,
+        len: usize,
+    };
+
+    names: [config.command_buffer_size]u8 = undefined,
+    names_len: usize = 0,
+    items: [config.max_commands]Entry = undefined,
+    len: usize = 0,
+
+    fn init() !Commands {
+        var commands = Commands{};
+        try commands.load();
+        return commands;
+    }
 
-        const atom_window_type_dock_reply = c.xcb_intern_atom_reply(conn, atom_window_type_dock_cookie, null);
-        const atom_window_type_dock = atom_window_type_dock_reply.*.atom;
-        c.free(atom_window_type_dock_reply);
+    fn deinit(_: *Commands) void {}
 
-        const window_types = [_]c.xcb_atom_t{ atom_window_type_dialog, atom_window_type_dock };
-        _ = c.xcb_change_property(conn, c.XCB_PROP_MODE_REPLACE, window, atom_window_type, c.XCB_ATOM_ATOM, 32, 2, &window_types);
+    fn complete(self: *const Commands, input: []const u8) ?[]const u8 {
+        if (input.len == 0) {
+            return null;
+        }
 
-        _ = 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);
+        for (self.items[0..self.len]) |entry| {
+            const cmd = self.entry_name(entry);
+            if (cmd.len < input.len) {
+                continue;
+            }
 
-        _ = c.xcb_set_input_focus(conn, c.XCB_INPUT_FOCUS_POINTER_ROOT, window, c.XCB_CURRENT_TIME);
-        _ = c.xcb_flush(conn);
+            var i: usize = 0;
+            while (i < input.len) : (i += 1) {
+                if (to_lower(input[i]) != to_lower(cmd[i])) {
+                    break;
+                }
+            }
 
-        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,
-        );
+            if (i == input.len) {
+                return cmd;
+            }
+        }
+
+        return null;
+    }
+
+    fn load(self: *Commands) !void {
+        const default_path: [:0]const u8 = "/bin:/usr/bin";
+        const path_z: [:0]const u8 = std.posix.getenvZ("PATH") orelse default_path;
+        const path: []const u8 = path_z[0..path_z.len];
+
+        var it = std.mem.splitScalar(u8, path, ':');
+        while (it.next()) |dir_path| {
+            if (dir_path.len == 0) continue;
+            try self.scan_dir(dir_path);
+        }
+
+        std.sort.heap(Entry, self.items[0..self.len], self, less_than);
+    }
+
+    fn scan_dir(self: *Commands, dir_path: []const u8) !void {
+        var dirz: [std.posix.PATH_MAX:0]u8 = undefined;
+        if (dir_path.len >= dirz.len) return;
+        @memcpy(dirz[0..dir_path.len], dir_path);
+        dirz[dir_path.len] = 0;
+
+        const dir = c.opendir(&dirz);
+        if (dir == null) return;
+        defer _ = c.closedir(dir);
+
+        var full: [std.posix.PATH_MAX:0]u8 = undefined;
+
+        while (true) {
+            const entry = c.readdir(dir);
+            if (entry == null) break;
+
+            const filename = std.mem.sliceTo(&entry.*.d_name, 0);
+            if (filename.len == 0 or filename[0] == '.') continue;
+
+            switch (entry.*.d_type) {
+                c.DT_DIR => continue,
+                c.DT_REG, c.DT_LNK, c.DT_UNKNOWN => {},
+                else => continue,
+            }
+
+            const need = dir_path.len + 1 + filename.len;
+            if (need >= full.len) continue;
+
+            @memcpy(full[0..dir_path.len], dir_path);
+            full[dir_path.len] = '/';
+            @memcpy(full[dir_path.len + 1 .. need], filename);
+            full[need] = 0;
 
-        const visual = get_visual(screen, screen.*.root_visual);
-        if (visual == null) {
-            return error.VisualNotFound;
+            const fullz: [:0]const u8 = full[0..need :0];
+            std.posix.access(fullz, std.posix.X_OK) catch continue;
+
+            self.append(filename);
+        }
+    }
+
+    fn append(self: *Commands, name: []const u8) void {
+        if (self.len >= self.items.len) {
+            return;
         }
 
+        const need = name.len + 1;
+        if (self.names_len + need > self.names.len) {
+            return;
+        }
+
+        const off = self.names_len;
+        @memcpy(self.names[off .. off + name.len], name);
+        self.names[off + name.len] = 0;
+        self.names_len += need;
+
+        self.items[self.len] = .{ .off = off, .len = name.len };
+        self.len += 1;
+    }
+
+    fn entry_name(self: *const Commands, entry: Entry) []const u8 {
+        return self.names[entry.off .. entry.off + entry.len];
+    }
+
+    fn less_than(self: *Commands, a: Entry, b: Entry) bool {
+        return std.mem.lessThan(u8, self.entry_name(a), self.entry_name(b));
+    }
+};
+
+const Ui = struct {
+    conn: *c.xcb_connection_t,
+    window: c.xcb_window_t,
+    surface: *c.cairo_surface_t,
+    cr: *c.cairo_t,
+    key_symbols: *c.xcb_key_symbols_t,
+    width: u16,
+    height: u16,
+    space_width: f64 = 0.0,
+
+    fn init() !Ui {
+        var screen_num: c_int = undefined;
+        const conn = c.xcb_connect(null, &screen_num);
+        if (conn == null or c.xcb_connection_has_error(conn) != 0) {
+            return error.ConnectionFailed;
+        }
+
+        const screen = get_screen(conn.?, screen_num);
+        const width: u16 = screen.*.width_in_pixels / 4;
+        const height: u16 = config.bh;
+        const x: i16 = @intCast((screen.*.width_in_pixels - width) / 2);
+        const y: i16 = @intCast((screen.*.height_in_pixels - height) / 2);
+
+        const window = create_window(conn.?, screen, x, y, width, height);
+        try set_window_properties(conn.?, screen, window);
+
+        const visual = get_visual(screen, screen.*.root_visual) orelse return error.VisualNotFound;
         const surface = c.cairo_xcb_surface_create(conn, window, visual, width, height);
         if (surface == null) {
             return error.CairoSurfaceCreationFailed;
@@ -139,352 +276,304 @@ const Zmen = struct {
         }
 
         _ = c.xcb_map_window(conn, window);
+        _ = c.xcb_set_input_focus(conn, c.XCB_INPUT_FOCUS_POINTER_ROOT, window, c.XCB_CURRENT_TIME);
         _ = c.xcb_flush(conn);
 
-        try load_commands(&commands);
-
-        var app = Zmen{
+        var ui = Ui{
             .conn = conn.?,
             .window = window,
             .surface = surface.?,
             .cr = cr.?,
+            .key_symbols = key_symbols.?,
             .width = width,
             .height = height,
-            .key_symbols = key_symbols.?,
-            .commands = commands,
-            .space_width = 0.0,
         };
-
-        app.calculate_space_width();
-
-        return app;
+        ui.measure_space_width();
+        return ui;
     }
 
-    pub fn deinit(self: *Zmen) void {
-        for (self.commands.items) |cmd| {
-            std.heap.page_allocator.free(cmd);
-        }
-        self.commands.deinit();
-
+    fn deinit(self: *Ui) void {
         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 calculate_space_width(self: *Zmen) void {
-        c.cairo_select_font_face(self.cr, config.font, c.CAIRO_FONT_SLANT_NORMAL, c.CAIRO_FONT_WEIGHT_NORMAL);
-        c.cairo_set_font_size(self.cr, config.font_size);
+    fn draw(self: *Ui, input: *const Input) void {
+        self.set_font();
 
-        var space_extents: c.cairo_text_extents_t = undefined;
-        c.cairo_text_extents(self.cr, " ", &space_extents);
-        self.space_width = space_extents.x_advance;
+        c.cairo_set_source_rgb(self.cr, config.colors.background[0], config.colors.background[1], config.colors.background[2]);
+        c.cairo_paint(self.cr);
 
-        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;
-        }
+        var prompt_extents: c.cairo_text_extents_t = undefined;
+        c.cairo_text_extents(self.cr, config.prompt, &prompt_extents);
 
-    }
+        var font_extents: c.cairo_font_extents_t = undefined;
+        c.cairo_font_extents(self.cr, &font_extents);
 
-    pub fn calculate_text_width(self: *Zmen, text: []const u8) f64 {
-        var space_count: usize = 0;
-        for (text) |char| {
-            if (char == ' ') {
-                space_count += 1;
-            }
-        }
+        const bar_height = @as(f64, @floatFromInt(self.height));
+        const y_pos = (bar_height / 2.0) + (font_extents.ascent - font_extents.descent) / 2.0;
+        const prompt_x = 8.0;
+        const prompt_width = prompt_extents.width + 16.0;
+        const input_slice = input.slice();
+        const input_width = self.text_width(input_slice);
 
-        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;
-        }
+        c.cairo_set_source_rgb(self.cr, config.colors.selected[0], config.colors.selected[1], config.colors.selected[2]);
+        c.cairo_move_to(self.cr, prompt_x, y_pos);
+        c.cairo_show_text(self.cr, config.prompt);
 
-        var temp_buf: [config.max_text_len]u8 = undefined;
-        var temp_len: usize = 0;
+        c.cairo_set_source_rgb(self.cr, config.colors.foreground[0], config.colors.foreground[1], config.colors.foreground[2]);
+        c.cairo_move_to(self.cr, prompt_width, y_pos);
+        c.cairo_show_text(self.cr, @ptrCast(input_slice));
 
-        for (text) |char| {
-            if (char != ' ' and temp_len < config.max_text_len) {
-                temp_buf[temp_len] = char;
-                temp_len += 1;
+        if (input.completion) |completion| {
+            if (completion.len > input.len) {
+                const ghost_text = completion[input.len..];
+                c.cairo_set_source_rgb(self.cr, config.colors.ghost[0], config.colors.ghost[1], config.colors.ghost[2]);
+                c.cairo_move_to(self.cr, prompt_width + input_width, y_pos);
+                c.cairo_show_text(self.cr, @ptrCast(ghost_text));
             }
         }
 
-        if (temp_len < config.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 cursor_x = prompt_width + self.text_width(input.text[0..input.cursor]);
+        c.cairo_set_source_rgb(self.cr, config.colors.foreground[0], config.colors.foreground[1], config.colors.foreground[2]);
+        c.cairo_rectangle(self.cr, cursor_x, (bar_height - font_extents.height) / 2.0, 2, font_extents.height);
+        c.cairo_fill(self.cr);
 
-        const total_width = text_extents.width + @as(f64, @floatFromInt(space_count)) * self.space_width;
-        return total_width;
+        c.cairo_surface_flush(self.surface);
+        _ = c.xcb_flush(self.conn);
     }
 
-    pub fn calculate_cursor_x(self: *Zmen, 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.calculate_text_width(text_slice);
-
-        return prompt_width + width;
+    fn keysym(self: *const Ui, keycode: c.xcb_keycode_t) c.xcb_keysym_t {
+        return c.xcb_key_symbols_get_keysym(self.key_symbols, keycode, 0);
     }
 
-    pub fn draw(self: *Zmen) void {
-        c.cairo_set_source_rgb(self.cr, config.colors.background[0], config.colors.background[1], config.colors.background[2]);
-        c.cairo_paint(self.cr);
-
+    fn set_font(self: *Ui) void {
         c.cairo_select_font_face(self.cr, config.font, c.CAIRO_FONT_SLANT_NORMAL, c.CAIRO_FONT_WEIGHT_NORMAL);
         c.cairo_set_font_size(self.cr, config.font_size);
+    }
 
-        var text_extents: c.cairo_text_extents_t = undefined;
-        c.cairo_text_extents(self.cr, config.prompt, &text_extents);
-
-        var font_extents: c.cairo_font_extents_t = undefined;
-        c.cairo_font_extents(self.cr, &font_extents);
-
-        const bar_height = @as(f64, @floatFromInt(config.bh));
-        const y_pos = (bar_height / 2.0) + (font_extents.ascent - font_extents.descent) / 2.0;
-
-        c.cairo_set_source_rgb(self.cr, config.colors.selected[0], config.colors.selected[1], config.colors.selected[2]);
-        c.cairo_move_to(self.cr, 8, y_pos);
-        c.cairo_show_text(self.cr, config.prompt);
-
-        const prompt_width = text_extents.width + 16;
+    fn measure_space_width(self: *Ui) void {
+        self.set_font();
 
-        c.cairo_set_source_rgb(self.cr, config.colors.foreground[0], config.colors.foreground[1], config.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));
+        var space_extents: c.cairo_text_extents_t = undefined;
+        c.cairo_text_extents(self.cr, " ", &space_extents);
+        self.space_width = space_extents.x_advance;
 
-        const input_width = self.calculate_text_width(input_text_slice);
+        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;
+        }
+    }
 
-        if (self.current_completion != null) {
-            const completion = self.current_completion.?;
+    fn text_width(self: *Ui, text: []const u8) f64 {
+        var spaces: usize = 0;
+        for (text) |char| {
+            if (char == ' ') spaces += 1;
+        }
 
-            if (completion.len > self.input_len) {
-                const ghost_text = completion[self.input_len..];
+        if (spaces == 0) {
+            var extents: c.cairo_text_extents_t = undefined;
+            c.cairo_text_extents(self.cr, @ptrCast(text), &extents);
+            return extents.width;
+        }
 
-                c.cairo_set_source_rgb(self.cr, config.colors.ghost[0], config.colors.ghost[1], config.colors.ghost[2]);
-                c.cairo_move_to(self.cr, prompt_width + input_width, y_pos);
-                c.cairo_show_text(self.cr, @ptrCast(ghost_text));
+        var temp: [config.max_text_len]u8 = undefined;
+        var len: usize = 0;
+        for (text) |char| {
+            if (char != ' ' and len < config.max_text_len - 1) {
+                temp[len] = char;
+                len += 1;
             }
         }
+        temp[len] = 0;
 
-        const cursor_x = self.calculate_cursor_x(prompt_width);
+        var extents: c.cairo_text_extents_t = undefined;
+        c.cairo_text_extents(self.cr, @ptrCast(&temp), &extents);
+        return extents.width + @as(f64, @floatFromInt(spaces)) * self.space_width;
+    }
+};
 
-        c.cairo_set_source_rgb(self.cr, config.colors.foreground[0], config.colors.foreground[1], config.colors.foreground[2]);
-        c.cairo_rectangle(self.cr, cursor_x, (bar_height - font_extents.height) / 2, 2, font_extents.height);
-        c.cairo_fill(self.cr);
+const Zmen = struct {
+    ui: Ui,
+    input: Input = .{},
+    commands: Commands,
+
+    fn init() !Zmen {
+        return Zmen{
+            .ui = try Ui.init(),
+            .commands = try Commands.init(),
+        };
+    }
 
-        c.cairo_surface_flush(self.surface);
-        _ = c.xcb_flush(self.conn);
+    fn deinit(self: *Zmen) void {
+        self.commands.deinit();
+        self.ui.deinit();
     }
 
-    pub fn handle_key_press(self: *Zmen, keycode: c.xcb_keycode_t) void {
-        const keysym = c.xcb_key_symbols_get_keysym(self.key_symbols, keycode, 0);
+    fn draw(self: *Zmen) void {
+        self.ui.draw(&self.input);
+    }
+
+    fn handle_key_press(self: *Zmen, keycode: c.xcb_keycode_t) void {
+        const keysym = self.ui.keysym(keycode);
         var changed = false;
 
         switch (keysym) {
-            c.XK_space => {
-                changed = self.insert_char(' ');
-            },
-            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, config.max_text_len - self.input_len);
-
-                        @memcpy(self.input_text[self.input_len .. self.input_len + to_copy], completion_part[0..to_copy]);
-
-                        if (to_copy > 0) {
-                            self.input_len += to_copy;
-                            self.cursor_pos = self.input_len;
-                            changed = true;
-                        }
-                    }
-                }
-            },
-            c.XK_BackSpace => {
-                changed = self.delete_before_cursor();
-            },
-            c.XK_Delete => {
-                changed = self.delete_at_cursor();
-            },
+            c.XK_space => changed = self.input.insert(' '),
+            c.XK_Tab => changed = self.input.append_completion(),
+            c.XK_BackSpace => changed = self.input.delete_before_cursor(),
+            c.XK_Delete => changed = self.input.delete_at_cursor(),
             c.XK_Left => {
-                if (self.cursor_pos > 0) {
-                    self.cursor_pos -= 1;
+                if (self.input.cursor > 0) {
+                    self.input.cursor -= 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) {
-                        changed = self.insert_char(completion[self.input_len]);
-                    }
+                if (self.input.cursor < self.input.len) {
+                    self.input.cursor += 1;
+                } else {
+                    changed = self.input.accept_completion_char();
                 }
             },
-            c.XK_Home => {
-                self.cursor_pos = 0;
-            },
-            c.XK_End => {
-                self.cursor_pos = self.input_len;
-            },
-            c.XK_Return => {
-                self.run();
-            },
+            c.XK_Home => self.input.cursor = 0,
+            c.XK_End => self.input.cursor = self.input.len,
+            c.XK_Return => self.run(),
             c.XK_Escape => {
-                if (self.input_len > 0) {
-                    self.input_len = 0;
-                    self.cursor_pos = 0;
-
-                    self.current_completion = null;
-                    changed = true;
-                } else {
+                if (self.input.len == 0) {
                     std.process.exit(0);
                 }
+                self.input.clear();
             },
             else => {
                 const char = keysym_to_char(keysym);
                 if (char != 0) {
-                    changed = self.insert_char(char);
+                    changed = self.input.insert(char);
                 }
             },
         }
 
         if (changed) {
-            self.input_text[self.input_len] = 0;
-            self.update_completion();
+            self.input.completion = self.commands.complete(self.input.slice());
         }
     }
 
-    fn insert_char(self: *Zmen, char: u8) bool {
-        if (self.input_len >= config.max_text_len - 1) {
-            return false;
+    fn run(self: *Zmen) void {
+        if (is_blank(self.input.slice())) {
+            return;
         }
 
-        std.mem.copyBackwards(
-            u8,
-            self.input_text[self.cursor_pos + 1 .. self.input_len + 1],
-            self.input_text[self.cursor_pos..self.input_len],
-        );
-
-        self.input_text[self.cursor_pos] = char;
-        self.input_len += 1;
-        self.cursor_pos += 1;
-        return true;
-    }
-
-    fn delete_before_cursor(self: *Zmen) bool {
-        if (self.cursor_pos == 0) {
-            return false;
+        var argv: [64:null]?[*:0]const u8 = undefined;
+        const argc = split_argv(self.input.text[0..self.input.len], &argv);
+        if (argc == 0) {
+            return;
         }
 
-        std.mem.copyForwards(
-            u8,
-            self.input_text[self.cursor_pos - 1 .. self.input_len - 1],
-            self.input_text[self.cursor_pos..self.input_len],
-        );
+        const pid = std.posix.fork() catch std.process.exit(1);
+        if (pid != 0) std.process.exit(0);
 
-        self.input_len -= 1;
-        self.cursor_pos -= 1;
-        return true;
+        execvp(argv[0].?, &argv);
     }
+};
 
-    fn delete_at_cursor(self: *Zmen) bool {
-        if (self.cursor_pos >= self.input_len) {
-            return false;
-        }
-
-        std.mem.copyForwards(
-            u8,
-            self.input_text[self.cursor_pos .. self.input_len - 1],
-            self.input_text[self.cursor_pos + 1 .. self.input_len],
-        );
-
-        self.input_len -= 1;
-        return true;
+fn get_screen(conn: *c.xcb_connection_t, screen_num: c_int) *c.xcb_screen_t {
+    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);
     }
+    return iter.data;
+}
+
+fn create_window(conn: *c.xcb_connection_t, screen: *c.xcb_screen_t, x: i16, y: i16, width: u16, height: u16) c.xcb_window_t {
+    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,
+    );
+
+    return window;
+}
 
-    fn update_completion(self: *Zmen) void {
-        self.current_completion = null;
+fn set_window_properties(conn: *c.xcb_connection_t, screen: *c.xcb_screen_t, window: c.xcb_window_t) !void {
+    const atom_window_type = try intern_atom(conn, "_NET_WM_WINDOW_TYPE");
+    const atom_window_type_dialog = try intern_atom(conn, "_NET_WM_WINDOW_TYPE_DIALOG");
+    const atom_window_type_dock = try intern_atom(conn, "_NET_WM_WINDOW_TYPE_DOCK");
+
+    const window_types = [_]c.xcb_atom_t{ atom_window_type_dialog, atom_window_type_dock };
+    _ = c.xcb_change_property(conn, c.XCB_PROP_MODE_REPLACE, window, atom_window_type, c.XCB_ATOM_ATOM, 32, 2, &window_types);
+    _ = 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 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,
+    );
+}
 
-        if (self.input_len == 0) {
-            return;
-        }
+fn intern_atom(conn: *c.xcb_connection_t, name: [:0]const u8) !c.xcb_atom_t {
+    const cookie = c.xcb_intern_atom(conn, 0, @intCast(name.len), name.ptr);
+    const reply = c.xcb_intern_atom_reply(conn, cookie, null) orelse return error.AtomLookupFailed;
+    defer c.free(reply);
+    return reply.*.atom;
+}
 
-        const input = self.input_text[0..self.input_len];
+fn split_argv(input: []u8, argv: *[64:null]?[*:0]const u8) usize {
+    var argc: usize = 0;
+    var i: usize = 0;
 
-        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 = to_lower(input[i]);
-                    const cmd_char = to_lower(cmd[i]);
+    while (i < input.len) {
+        while (i < input.len and input[i] == ' ') : (i += 1) {}
+        if (i >= input.len) break;
+        if (argc + 1 >= argv.len) break;
 
-                    if (input_char != cmd_char) {
-                        matches = false;
-                        break;
-                    }
-                }
+        argv[argc] = @ptrCast(&input[i]);
+        argc += 1;
 
-                if (matches) {
-                    self.current_completion = cmd;
-                    break;
-                }
-            }
+        while (i < input.len and input[i] != ' ') : (i += 1) {}
+        if (i < input.len) {
+            input[i] = 0;
+            i += 1;
         }
     }
 
-    pub fn run(self: *Zmen) void {
-        var has_non_space = false;
-        for (self.input_text[0..self.input_len]) |char| {
-            if (char != ' ') {
-                has_non_space = true;
-                break;
-            }
-        }
-        if (!has_non_space) {
-            return;
-        }
-
-        var argv: [64:null]?[*:0]const u8 = undefined;
-        var argc: usize = 0;
-
-        var i: usize = 0;
-        while (i < self.input_len) {
-            while (i < self.input_len and self.input_text[i] == ' ') : (i += 1) {}
-            if (i >= self.input_len) break;
-
-            if (argc + 1 >= argv.len) break;
-            argv[argc] = @ptrCast(&self.input_text[i]);
-            argc += 1;
+    argv[argc] = null;
+    return argc;
+}
 
-            while (i < self.input_len and self.input_text[i] != ' ') : (i += 1) {}
-            if (i < self.input_len) {
-                self.input_text[i] = 0;
-                i += 1;
-            }
+fn is_blank(text: []const u8) bool {
+    for (text) |char| {
+        if (char != ' ') {
+            return false;
         }
-
-        argv[argc] = null;
-        if (argc == 0) return;
-        const pid = std.posix.fork() catch std.process.exit(1);
-        if (pid != 0) std.process.exit(0);
-
-        execvp(argv[0].?, &argv);
     }
-};
+    return true;
+}
 
 pub fn execvp(file: [*:0]const u8, argv: *const [64:null]?[*:0]const u8) noreturn {
     const name = std.mem.span(file);
@@ -517,12 +606,11 @@ pub fn execvp(file: [*:0]const u8, argv: *const [64:null]?[*:0]const u8) noretur
         };
 
         switch (std.posix.execveZ(&buf, argv, std.c.environ)) {
-            error.FileNotFound,
-            error.AccessDenied,
-            => continue,
+            error.FileNotFound, error.AccessDenied => continue,
             else => std.process.exit(1),
         }
     }
+
     std.process.exit(1);
 }
 
@@ -554,83 +642,22 @@ fn get_visual(screen: *c.xcb_screen_t, visual_id: c.xcb_visualid_t) ?*c.xcb_visu
     return null;
 }
 
-fn load_commands(commands: *std.ArrayList([]const u8)) !void {
-    const default_path: [:0]const u8 = "/bin:/usr/bin";
-    const path_z: [:0]const u8 = std.posix.getenvZ("PATH") orelse default_path;
-    const path: []const u8 = path_z[0..path_z.len];
-
-    var it = std.mem.splitScalar(u8, path, ':');
-    while (it.next()) |dir_path| {
-        if (dir_path.len == 0) continue;
-        try scan_dir(commands, dir_path);
-    }
-
-    std.sort.heap([]const u8, commands.items, {}, compare_strings);
-}
-
-fn compare_strings(_: void, a: []const u8, b: []const u8) bool {
-    return std.mem.lessThan(u8, a, b);
-}
-
-fn scan_dir(commands: *std.ArrayList([]const u8), dir_path: []const u8) !void {
-    var dirz: [std.posix.PATH_MAX:0]u8 = undefined;
-    if (dir_path.len >= dirz.len) return;
-    @memcpy(dirz[0..dir_path.len], dir_path);
-    dirz[dir_path.len] = 0;
-
-    const dir = c.opendir(&dirz);
-    if (dir == null) return;
-    defer _ = c.closedir(dir);
-
-    var full: [std.posix.PATH_MAX:0]u8 = undefined;
-
-    while (true) {
-        const entry = c.readdir(dir);
-        if (entry == null) break;
-
-        const filename = std.mem.sliceTo(&entry.*.d_name, 0);
-        if (filename.len == 0 or filename[0] == '.') continue;
-
-        switch (entry.*.d_type) {
-            c.DT_DIR => continue,
-            c.DT_REG, c.DT_LNK, c.DT_UNKNOWN => {},
-            else => continue,
-        }
-
-        const need = dir_path.len + 1 + filename.len;
-        if (need >= full.len) continue;
-
-        @memcpy(full[0..dir_path.len], dir_path);
-        full[dir_path.len] = '/';
-        @memcpy(full[dir_path.len + 1 .. need], filename);
-        full[need] = 0;
-
-        const fullz: [:0]const u8 = full[0..need :0];
-        std.posix.access(fullz, std.posix.X_OK) catch continue;
-
-        const dup = try std.heap.page_allocator.dupe(u8, filename);
-        try commands.append(dup);
-    }
-}
-
 pub fn main() !void {
     var app = try Zmen.init();
     defer app.deinit();
 
+    app.input.completion = app.commands.complete(app.input.slice());
     app.draw();
 
     while (true) {
-        const event = c.xcb_wait_for_event(app.conn);
+        const event = c.xcb_wait_for_event(app.ui.conn);
         if (event == null) {
             continue;
         }
-
         defer c.free(event);
 
         switch (event.*.response_type & ~@as(u8, 0x80)) {
-            c.XCB_EXPOSE => {
-                app.draw();
-            },
+            c.XCB_EXPOSE => app.draw(),
             c.XCB_KEY_PRESS => {
                 const key_event = @as(*c.xcb_key_press_event_t, @ptrCast(event));
                 app.handle_key_press(key_event.detail);
diff --git a/src/refcator.md b/src/refcator.md
deleted file mode 100644
index 695c64c..0000000
--- a/src/refcator.md
+++ /dev/null
@@ -1,278 +0,0 @@
-# Zig Style Convention (suckless-inspired)
-
-## Philosophy
-
-- Clarity over cleverness
-- Explicit over implicit
-- No hidden control flow
-- No hidden allocations
-- Code should be auditable by reading
-
-Zig already embodies much of this. These conventions tighten it further.
-
----
-
-## Naming
-
-`snake_case` for functions and variables. `PascalCase` for types only.
-
-```zig
-const buf_size: usize = 4096;
-
-fn read_file(path: []const u8) ![]u8 { ... }
-
-const FileHandle = struct {
-    fd: i32,
-    flags: u32,
-};
-```
-
-No abbreviations unless universal (fd, buf, len, ptr, ctx).
-
----
-
-## Indentation
-
-Four spaces. No tabs.
-
-```zig
-fn foo(x: i32) i32 {
-    if (x > 0) {
-        return x;
-    } else {
-        return -x;
-    }
-}
-```
-
-Keep functions short. One screen max.
-
----
-
-## Types
-
-Prefer concrete types. Use comptime generics only when truly needed.
-
-```zig
-// yes
-fn sum(a: i32, b: i32) i32
-
-// only when necessary
-fn sum(comptime T: type, a: T, b: T) T
-```
-
-Use distinct types via structs for semantic clarity:
-
-```zig
-const Fd = struct { raw: i32 };
-const Uid = struct { raw: u32 };
-```
-
----
-
-## Memory
-
-No allocator unless required. Stack-first.
-
-```zig
-var buf: [4096]u8 = undefined;
-```
-
-When allocation is needed, pass allocator explicitly:
-
-```zig
-fn parse(allocator: std.mem.Allocator, input: []const u8) !Ast { ... }
-```
-
-Prefer arena allocators for grouped lifetimes. Free once.
-
-```zig
-var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
-defer arena.deinit();
-const alloc = arena.allocator();
-```
-
-No general-purpose allocator in library code. Caller decides.
-
----
-
-## Error Handling
-
-Use error unions. Handle errors explicitly.
-
-```zig
-fn open_file(path: []const u8) OpenError!Fd {
-    ...
-}
-
-// caller
-const fd = open_file(path) catch |err| {
-    log_err(err);
-    return err;
-};
-```
-
-Define tight error sets:
-
-```zig
-const OpenError = error{
-    NotFound,
-    AccessDenied,
-    Unexpected,
-};
-```
-
-Avoid `anyerror` in public APIs.
-
----
-
-## Control Flow
-
-No hidden paths. Avoid `orelse` chains that obscure logic.
-
-```zig
-// yes - explicit
-const val = maybe_val orelse return error.Missing;
-
-// avoid - hidden fallback
-const val = maybe_val orelse default_val orelse other_default;
-```
-
-Use `unreachable` only when provably impossible.
-
----
-
-## Imports
-
-Import at top. Alias for brevity. No unused imports.
-
-```zig
-const std = @import("std");
-const os = std.os;
-const mem = std.mem;
-```
-
-For single-use, inline:
-
-```zig
-const pid = std.os.linux.getpid();
-```
-
----
-
-## Comments
-
-Comments explain *why*, not *what*.
-
-```zig
-// EINTR may occur on slow devices; retry per POSIX
-while (retry_count < 3) : (retry_count += 1) {
-    ...
-}
-```
-
-No doc comments on trivial functions. No banners.
-
----
-
-## Comptime
-
-Use for zero-cost abstractions only. Must be auditable.
-
-```zig
-fn init_table(comptime N: usize) [N]Entry {
-    var table: [N]Entry = undefined;
-    for (&table) |*e| e.* = .{};
-    return table;
-}
-```
-
-Avoid comptime string manipulation or complex metaprogramming.
-
----
-
-## Build
-
-Use `build.zig` minimally. Prefer static linking.
-
-```zig
-// build.zig
-const exe = b.addExecutable(.{
-    .name = "prog",
-    .root_source_file = b.path("src/main.zig"),
-    .target = target,
-    .optimize = .ReleaseSmall,
-});
-exe.link_libc = false;
-```
-
-Or use a Makefile:
-
-```makefile
-prog: src/main.zig
-	zig build-exe -OReleaseSmall -fstrip src/main.zig -o prog
-```
-
----
-
-## Project Structure
-
-```
-project/
-  src/
-    main.zig
-    util.zig
-    io.zig
-  build.zig
-  Makefile      # optional, for simple builds
-```
-
-No package manager. Vendor dependencies or avoid them.
-
----
-
-## FFI
-
-Explicit and minimal:
-
-```zig
-const c = @cImport({
-    @cInclude("unistd.h");
-});
-
-fn write_all(fd: i32, buf: []const u8) !void {
-    const ret = c.write(fd, buf.ptr, buf.len);
-    if (ret < 0) return error.WriteError;
-}
-```
-
-Prefer Zig's `std.os` wrappers when they exist and are thin.
-
----
-
-## Testing
-
-Tests beside code. Keep them minimal.
-
-```zig
-test "sum basics" {
-    try std.testing.expectEqual(@as(i32, 5), sum(2, 3));
-}
-```
-
-Run with `zig test src/main.zig`. No test frameworks.
-
----
-
-## Summary
-
-| aspect       | guideline                          |
-|--------------|------------------------------------|
-| naming       | `snake_case`, `PascalCase` types   |
-| indent       | 4 spaces                           |
-| types        | concrete, wrapper structs          |
-| memory       | stack-first, explicit allocator    |
-| errors       | tight error sets, explicit handling|
-| imports      | top of file, aliased               |
-| comptime     | zero-cost only, auditable          |
-| dependencies | zero or vendored                   |
\ No newline at end of file