[root]/ src
/ main.zig
20.1KB
//! 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 main.zig -lc -lxcb -lcairo -lxcb-keysyms -lX11 && ./zmen
const std = @import("std");
const c = @import("c.zig");
const config = @import("config.zig");
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,
fn slice(self: *const Input) []const u8 {
return self.text[0..self.len];
}
fn insert(self: *Input, char: u8) bool {
if (self.len >= config.max_text_len - 1) {
return false;
}
std.mem.copyBackwards(
u8,
self.text[self.cursor + 1 .. self.len + 1],
self.text[self.cursor..self.len],
);
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;
}
const suffix = completion[self.len..];
const to_copy = @min(suffix.len, config.max_text_len - 1 - self.len);
if (to_copy == 0) {
return false;
}
@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;
}
fn accept_completion_char(self: *Input) bool {
const completion = self.completion orelse return false;
if (completion.len <= self.len) {
return false;
}
return self.insert(completion[self.len]);
}
fn delete_before_cursor(self: *Input) bool {
if (self.cursor == 0) {
return false;
}
std.mem.copyForwards(
u8,
self.text[self.cursor - 1 .. self.len - 1],
self.text[self.cursor..self.len],
);
self.len -= 1;
self.cursor -= 1;
self.text[self.len] = 0;
return true;
}
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],
);
self.len -= 1;
self.text[self.len] = 0;
return true;
}
fn clear(self: *Input) void {
self.len = 0;
self.cursor = 0;
self.completion = null;
self.text[0] = 0;
}
};
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;
}
fn deinit(_: *Commands) void {}
fn complete(self: *const Commands, input: []const u8) ?[]const u8 {
if (input.len == 0) {
return null;
}
for (self.items[0..self.len]) |entry| {
const cmd = self.entry_name(entry);
if (cmd.len < input.len) {
continue;
}
var i: usize = 0;
while (i < input.len) : (i += 1) {
if (to_lower(input[i]) != to_lower(cmd[i])) {
break;
}
}
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 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;
}
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_set_input_focus(conn, c.XCB_INPUT_FOCUS_POINTER_ROOT, window, c.XCB_CURRENT_TIME);
_ = c.xcb_flush(conn);
var ui = Ui{
.conn = conn.?,
.window = window,
.surface = surface.?,
.cr = cr.?,
.key_symbols = key_symbols.?,
.width = width,
.height = height,
};
ui.measure_space_width();
return ui;
}
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);
}
fn draw(self: *Ui, input: *const Input) void {
self.set_font();
c.cairo_set_source_rgb(self.cr, config.colors.background[0], config.colors.background[1], config.colors.background[2]);
c.cairo_paint(self.cr);
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);
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);
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);
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));
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));
}
}
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);
c.cairo_surface_flush(self.surface);
_ = c.xcb_flush(self.conn);
}
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);
}
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);
}
fn measure_space_width(self: *Ui) 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;
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;
}
}
fn text_width(self: *Ui, text: []const u8) f64 {
var spaces: usize = 0;
for (text) |char| {
if (char == ' ') spaces += 1;
}
if (spaces == 0) {
var extents: c.cairo_text_extents_t = undefined;
c.cairo_text_extents(self.cr, @ptrCast(text), &extents);
return extents.width;
}
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;
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;
}
};
const Zmen = struct {
ui: Ui,
input: Input = .{},
commands: Commands,
fn init() !Zmen {
return Zmen{
.ui = try Ui.init(),
.commands = try Commands.init(),
};
}
fn deinit(self: *Zmen) void {
self.commands.deinit();
self.ui.deinit();
}
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.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.input.cursor > 0) {
self.input.cursor -= 1;
}
},
c.XK_Right => {
if (self.input.cursor < self.input.len) {
self.input.cursor += 1;
} else {
changed = self.input.accept_completion_char();
}
},
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) {
std.process.exit(0);
}
self.input.clear();
},
else => {
const char = keysym_to_char(keysym);
if (char != 0) {
changed = self.input.insert(char);
}
},
}
if (changed) {
self.input.completion = self.commands.complete(self.input.slice());
}
}
fn run(self: *Zmen) void {
if (is_blank(self.input.slice())) {
return;
}
var argv: [64:null]?[*:0]const u8 = undefined;
const argc = split_argv(self.input.text[0..self.input.len], &argv);
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);
}
};
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 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,
);
}
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;
}
fn split_argv(input: []u8, argv: *[64:null]?[*:0]const u8) usize {
var argc: usize = 0;
var i: usize = 0;
while (i < input.len) {
while (i < input.len and input[i] == ' ') : (i += 1) {}
if (i >= input.len) break;
if (argc + 1 >= argv.len) break;
argv[argc] = @ptrCast(&input[i]);
argc += 1;
while (i < input.len and input[i] != ' ') : (i += 1) {}
if (i < input.len) {
input[i] = 0;
i += 1;
}
}
argv[argc] = null;
return argc;
}
fn is_blank(text: []const u8) bool {
for (text) |char| {
if (char != ' ') {
return false;
}
}
return true;
}
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 to_lower(char: u8) u8 {
if (char >= 'A' and char <= 'Z') {
return char + ('a' - 'A');
}
return char;
}
fn keysym_to_char(keysym: c.xcb_keysym_t) u8 {
if (keysym >= 32 and keysym <= 126) {
return @intCast(keysym);
}
return 0;
}
fn get_visual(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;
}
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.ui.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.handle_key_press(key_event.detail);
app.draw();
},
else => {},
}
}
}