Application Launcher written in Zig
Files | Log | Commits | Refs | README
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 => {}, } } }