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