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 => {},
}
}
}