zmen
Owner: IIIlllIIIllI URL: git@git.0x00nyx.xyz:seb/zmen.git
src/main.zig
//! zmen: 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 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,
commands: std.ArrayList([]const u8),
current_completion: ?[]const u8 = null,
space_width: f64 = 0.0,
pub fn init() !zmen {
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 = config.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_window_type_dock_cookie = c.xcb_intern_atom(conn, 0, 24, "_NET_WM_WINDOW_TYPE_DOCK");
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_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);
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);
_ = 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) {
return error.VisualNotFound;
}
const surface = c.cairo_xcb_surface_create(conn, window, visual, width, height);
if (surface == null) {
return error.CairoSurfaceCreationFailed;
}
const cr = c.cairo_create(surface);
if (cr == null) {
return error.CairoContextCreationFailed;
}
const key_symbols = c.xcb_key_symbols_alloc(conn);
if (key_symbols == null) {
return error.KeySymbolsAllocationFailed;
}
_ = c.xcb_map_window(conn, window);
_ = c.xcb_flush(conn);
try loadCommands(&commands);
var app = zmen{
.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: *zmen) 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: *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);
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: *zmen, 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: [config.max_text_len]u8 = undefined;
var temp_len: usize = 0;
for (text) |char| {
if (char != ' ' and temp_len < config.max_text_len) {
temp_buf[temp_len] = char;
temp_len += 1;
}
}
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 total_width = text_extents.width + @as(f64, @floatFromInt(space_count)) * self.space_width;
return total_width;
}
pub fn calculateCursorX(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.calculateTextWidth(text_slice);
return prompt_width + width;
}
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);
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;
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));
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, 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));
}
}
const cursor_x = self.calculateCursorX(prompt_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);
c.cairo_surface_flush(self.surface);
_ = c.xcb_flush(self.conn);
}
pub fn handleKeyPress(self: *zmen, keycode: c.xcb_keycode_t) void {
const keysym = c.xcb_key_symbols_get_keysym(self.key_symbols, keycode, 0);
if (keysym == c.XK_space) {
if (self.input_len < config.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, config.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.run();
},
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 < config.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;
}
fn updateCompletion(self: *zmen) 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 run(self: *zmen) void {
var cmd_buf: [config.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;
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 cmd_buf[i] == ' ') : (i += 1) {}
if (i >= self.input_len) break;
if (argc + 1 >= argv.len) break;
argv[argc] = @ptrCast(&cmd_buf[i]);
argc += 1;
while (i < self.input_len and cmd_buf[i] != ' ') : (i += 1) {}
if (i < self.input_len) {
cmd_buf[i] = 0;
i += 1;
}
}
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);
}
};
pub fn execvp(file: [*:0]const u8, argv: *const [64:null]?[*:0]const u8) noreturn {
const name = std.mem.span(file);
if (std.mem.indexOfScalar(u8, name, '/')) |_| {
std.posix.execveZ(file, argv, std.c.environ) catch std.process.exit(1);
unreachable;
}
const path_z: [:0]const u8 = std.posix.getenvZ("PATH") orelse "/bin:/usr/bin";
const path: []const u8 = path_z[0..path_z.len];
var it = std.mem.splitScalar(u8, path, ':');
var buf: [std.posix.PATH_MAX:0]u8 = undefined;
while (it.next()) |dir| {
if (dir.len == 0) continue;
const need = dir.len + 1 + name.len;
if (need >= buf.len) continue;
@memcpy(buf[0..dir.len], dir);
buf[dir.len] = '/';
@memcpy(buf[dir.len + 1 .. need], name);
buf[need] = 0;
std.posix.accessZ(&buf, std.posix.X_OK) catch |err| {
if (err == error.FileNotFound) continue;
std.process.exit(1);
};
switch (std.posix.execveZ(&buf, argv, std.c.environ)) {
error.FileNotFound,
error.AccessDenied,
=> continue,
else => std.process.exit(1),
}
}
std.process.exit(1);
}
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 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 scanDir(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 scanDir(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.draw();
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 => {},
}
}
}