oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git

added tests, overlay, keychords. path to 100% compliance is en route.

Commit
2bbe4276bfcd504288c54b25f503d2a30a6ce8db
Parent
a8a8323
Author
tonybanters <tonybanters@gmail.com>
Date
2026-02-04 07:50:37

Diff

diff --git a/build.zig b/build.zig
index 30e883d..7b6237e 100644
--- a/build.zig
+++ b/build.zig
@@ -40,6 +40,22 @@ pub fn build(b: *std.Build) void {
     });
     test_step.dependOn(&b.addRunArtifact(unit_tests).step);
 
+    const lua_config_tests = b.addTest(.{
+        .root_module = b.createModule(.{
+            .root_source_file = b.path("tests/lua_config_tests.zig"),
+            .target = target,
+            .optimize = optimize,
+        }),
+    });
+    lua_config_tests.root_module.addImport("lua", b.createModule(.{
+        .root_source_file = b.path("src/config/lua.zig"),
+        .target = target,
+        .optimize = optimize,
+    }));
+    lua_config_tests.linkSystemLibrary("lua");
+    lua_config_tests.linkLibC();
+    test_step.dependOn(&b.addRunArtifact(lua_config_tests).step);
+
     const xephyr_step = b.step("xephyr", "Run in Xephyr (1280x800 on :2)");
     xephyr_step.dependOn(&add_xephyr_run(b, exe, false).step);
 
diff --git a/resources/test-config.lua b/resources/test-config.lua
index c52da7f..a99dd82 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -107,50 +107,50 @@ oxwm.key.bind({ modkey, "Shift" }, "J", oxwm.client.move_stack(1))
 oxwm.key.bind({ modkey, "Shift" }, "K", oxwm.client.move_stack(-1))
 
 -- View tag (switch workspace)
-oxwm.key.bind({ modkey }, "1", oxwm.tag.view(1))
-oxwm.key.bind({ modkey }, "2", oxwm.tag.view(2))
-oxwm.key.bind({ modkey }, "3", oxwm.tag.view(3))
-oxwm.key.bind({ modkey }, "4", oxwm.tag.view(4))
-oxwm.key.bind({ modkey }, "5", oxwm.tag.view(5))
-oxwm.key.bind({ modkey }, "6", oxwm.tag.view(6))
-oxwm.key.bind({ modkey }, "7", oxwm.tag.view(7))
-oxwm.key.bind({ modkey }, "8", oxwm.tag.view(8))
-oxwm.key.bind({ modkey }, "9", oxwm.tag.view(9))
+oxwm.key.bind({ modkey }, "1", oxwm.tag.view(0))
+oxwm.key.bind({ modkey }, "2", oxwm.tag.view(1))
+oxwm.key.bind({ modkey }, "3", oxwm.tag.view(2))
+oxwm.key.bind({ modkey }, "4", oxwm.tag.view(3))
+oxwm.key.bind({ modkey }, "5", oxwm.tag.view(4))
+oxwm.key.bind({ modkey }, "6", oxwm.tag.view(5))
+oxwm.key.bind({ modkey }, "7", oxwm.tag.view(6))
+oxwm.key.bind({ modkey }, "8", oxwm.tag.view(7))
+oxwm.key.bind({ modkey }, "9", oxwm.tag.view(8))
 
 -- Move window to tag
-oxwm.key.bind({ modkey, "Shift" }, "1", oxwm.tag.move_to(1))
-oxwm.key.bind({ modkey, "Shift" }, "2", oxwm.tag.move_to(2))
-oxwm.key.bind({ modkey, "Shift" }, "3", oxwm.tag.move_to(3))
-oxwm.key.bind({ modkey, "Shift" }, "4", oxwm.tag.move_to(4))
-oxwm.key.bind({ modkey, "Shift" }, "5", oxwm.tag.move_to(5))
-oxwm.key.bind({ modkey, "Shift" }, "6", oxwm.tag.move_to(6))
-oxwm.key.bind({ modkey, "Shift" }, "7", oxwm.tag.move_to(7))
-oxwm.key.bind({ modkey, "Shift" }, "8", oxwm.tag.move_to(8))
-oxwm.key.bind({ modkey, "Shift" }, "9", oxwm.tag.move_to(9))
+oxwm.key.bind({ modkey, "Shift" }, "1", oxwm.tag.move_to(0))
+oxwm.key.bind({ modkey, "Shift" }, "2", oxwm.tag.move_to(1))
+oxwm.key.bind({ modkey, "Shift" }, "3", oxwm.tag.move_to(2))
+oxwm.key.bind({ modkey, "Shift" }, "4", oxwm.tag.move_to(3))
+oxwm.key.bind({ modkey, "Shift" }, "5", oxwm.tag.move_to(4))
+oxwm.key.bind({ modkey, "Shift" }, "6", oxwm.tag.move_to(5))
+oxwm.key.bind({ modkey, "Shift" }, "7", oxwm.tag.move_to(6))
+oxwm.key.bind({ modkey, "Shift" }, "8", oxwm.tag.move_to(7))
+oxwm.key.bind({ modkey, "Shift" }, "9", oxwm.tag.move_to(8))
 
 -- Toggle view (view multiple tags at once) - dwm-style multi-tag viewing
 -- Example: Mod+Ctrl+2 while on tag 1 will show BOTH tags 1 and 2
-oxwm.key.bind({ modkey, "Control" }, "1", oxwm.tag.toggleview(1))
-oxwm.key.bind({ modkey, "Control" }, "2", oxwm.tag.toggleview(2))
-oxwm.key.bind({ modkey, "Control" }, "3", oxwm.tag.toggleview(3))
-oxwm.key.bind({ modkey, "Control" }, "4", oxwm.tag.toggleview(4))
-oxwm.key.bind({ modkey, "Control" }, "5", oxwm.tag.toggleview(5))
-oxwm.key.bind({ modkey, "Control" }, "6", oxwm.tag.toggleview(6))
-oxwm.key.bind({ modkey, "Control" }, "7", oxwm.tag.toggleview(7))
-oxwm.key.bind({ modkey, "Control" }, "8", oxwm.tag.toggleview(8))
-oxwm.key.bind({ modkey, "Control" }, "9", oxwm.tag.toggleview(9))
+oxwm.key.bind({ modkey, "Control" }, "1", oxwm.tag.toggleview(0))
+oxwm.key.bind({ modkey, "Control" }, "2", oxwm.tag.toggleview(1))
+oxwm.key.bind({ modkey, "Control" }, "3", oxwm.tag.toggleview(2))
+oxwm.key.bind({ modkey, "Control" }, "4", oxwm.tag.toggleview(3))
+oxwm.key.bind({ modkey, "Control" }, "5", oxwm.tag.toggleview(4))
+oxwm.key.bind({ modkey, "Control" }, "6", oxwm.tag.toggleview(5))
+oxwm.key.bind({ modkey, "Control" }, "7", oxwm.tag.toggleview(6))
+oxwm.key.bind({ modkey, "Control" }, "8", oxwm.tag.toggleview(7))
+oxwm.key.bind({ modkey, "Control" }, "9", oxwm.tag.toggleview(8))
 
 -- Toggle tag (window on multiple tags) - dwm-style sticky windows
 -- Example: Mod+Ctrl+Shift+2 puts focused window on BOTH current tag and tag 2
-oxwm.key.bind({ modkey, "Control", "Shift" }, "1", oxwm.tag.toggletag(1))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "2", oxwm.tag.toggletag(2))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "3", oxwm.tag.toggletag(3))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "4", oxwm.tag.toggletag(4))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "5", oxwm.tag.toggletag(5))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "6", oxwm.tag.toggletag(6))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "7", oxwm.tag.toggletag(7))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "8", oxwm.tag.toggletag(8))
-oxwm.key.bind({ modkey, "Control", "Shift" }, "9", oxwm.tag.toggletag(9))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "1", oxwm.tag.toggletag(0))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "2", oxwm.tag.toggletag(1))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "3", oxwm.tag.toggletag(2))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "4", oxwm.tag.toggletag(3))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "5", oxwm.tag.toggletag(4))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "6", oxwm.tag.toggletag(5))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "7", oxwm.tag.toggletag(6))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "8", oxwm.tag.toggletag(7))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "9", oxwm.tag.toggletag(8))
 
 oxwm.key.bind({ modkey }, "Tab", oxwm.tag.view_next())
 oxwm.key.bind({ modkey, "Shift" }, "Tab", oxwm.tag.view_previous())
diff --git a/src/config/config.zig b/src/config/config.zig
index 775e1e0..7c7f918 100644
--- a/src/config/config.zig
+++ b/src/config/config.zig
@@ -36,9 +36,14 @@ pub const Action = enum {
     scroll_right,
 };
 
+pub const Key_Press = struct {
+    mod_mask: u32 = 0,
+    keysym: u64 = 0,
+};
+
 pub const Keybind = struct {
-    mod_mask: u32,
-    keysym: u64,
+    keys: [4]Key_Press = [_]Key_Press{.{}} ** 4,
+    key_count: u8 = 1,
     action: Action,
     int_arg: i32 = 0,
     str_arg: ?[]const u8 = null,
diff --git a/src/config/lua.zig b/src/config/lua.zig
index 6296639..2969749 100644
--- a/src/config/lua.zig
+++ b/src/config/lua.zig
@@ -1,5 +1,5 @@
 const std = @import("std");
-const config_mod = @import("config.zig");
+pub const config_mod = @import("config.zig");
 const Config = config_mod.Config;
 const Keybind = config_mod.Keybind;
 const Action = config_mod.Action;
@@ -392,19 +392,85 @@ fn lua_key_bind(state: ?*c.lua_State) callconv(.c) c_int {
     }
     c.lua_settop(s, -2);
 
-    cfg.add_keybind(.{
-        .mod_mask = mod_mask,
-        .keysym = keysym,
+    var keybind: config_mod.Keybind = .{
         .action = action,
         .int_arg = int_arg,
         .str_arg = str_arg,
-    }) catch return 0;
+    };
+    keybind.keys[0] = .{ .mod_mask = mod_mask, .keysym = keysym };
+    keybind.key_count = 1;
+
+    cfg.add_keybind(keybind) catch return 0;
 
     return 0;
 }
 
 fn lua_key_chord(state: ?*c.lua_State) callconv(.c) c_int {
-    _ = state;
+    const s = state orelse return 0;
+    const cfg = config orelse return 0;
+
+    if (c.lua_type(s, 1) != c.LUA_TTABLE) return 0;
+    if (c.lua_type(s, 2) != c.LUA_TTABLE) return 0;
+
+    var keybind: config_mod.Keybind = .{
+        .action = .quit,
+        .int_arg = 0,
+        .str_arg = null,
+    };
+    keybind.key_count = 0;
+
+    const num_keys = c.lua_rawlen(s, 1);
+    if (num_keys == 0 or num_keys > 4) return 0;
+
+    var i: usize = 1;
+    while (i <= num_keys) : (i += 1) {
+        _ = c.lua_rawgeti(s, 1, @intCast(i));
+        if (c.lua_type(s, -1) != c.LUA_TTABLE) {
+            c.lua_settop(s, -2);
+            return 0;
+        }
+
+        _ = c.lua_rawgeti(s, -1, 1);
+        const mod_mask = parse_modifiers_at_top(s);
+        c.lua_settop(s, -2);
+
+        _ = c.lua_rawgeti(s, -1, 2);
+        const key_str = get_lua_string(s, -1) orelse {
+            c.lua_settop(s, -3);
+            return 0;
+        };
+        c.lua_settop(s, -2);
+
+        const keysym = key_name_to_keysym(key_str) orelse {
+            c.lua_settop(s, -2);
+            return 0;
+        };
+
+        keybind.keys[keybind.key_count] = .{ .mod_mask = mod_mask, .keysym = keysym };
+        keybind.key_count += 1;
+
+        c.lua_settop(s, -2);
+    }
+
+    _ = c.lua_getfield(s, 2, "__action");
+    const action_str = get_lua_string(s, -1) orelse {
+        c.lua_settop(s, -2);
+        return 0;
+    };
+    c.lua_settop(s, -2);
+
+    keybind.action = parse_action(action_str) orelse return 0;
+
+    _ = c.lua_getfield(s, 2, "__arg");
+    if (c.lua_type(s, -1) == c.LUA_TNUMBER) {
+        keybind.int_arg = @intCast(c.lua_tointegerx(s, -1, null));
+    } else if (c.lua_type(s, -1) == c.LUA_TSTRING) {
+        keybind.str_arg = get_lua_string(s, -1);
+    }
+    c.lua_settop(s, -2);
+
+    cfg.add_keybind(keybind) catch return 0;
+
     return 0;
 }
 
@@ -532,7 +598,7 @@ fn lua_layout_scroll_right(state: ?*c.lua_State) callconv(.c) c_int {
 fn lua_tag_view(state: ?*c.lua_State) callconv(.c) c_int {
     const s = state orelse return 0;
     const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
-    create_action_table_with_int(s, "ViewTag", idx - 1);
+    create_action_table_with_int(s, "ViewTag", idx);
     return 1;
 }
 
@@ -563,21 +629,21 @@ fn lua_tag_view_previous_nonempty(state: ?*c.lua_State) callconv(.c) c_int {
 fn lua_tag_toggleview(state: ?*c.lua_State) callconv(.c) c_int {
     const s = state orelse return 0;
     const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
-    create_action_table_with_int(s, "ToggleView", idx - 1);
+    create_action_table_with_int(s, "ToggleView", idx);
     return 1;
 }
 
 fn lua_tag_move_to(state: ?*c.lua_State) callconv(.c) c_int {
     const s = state orelse return 0;
     const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
-    create_action_table_with_int(s, "MoveToTag", idx - 1);
+    create_action_table_with_int(s, "MoveToTag", idx);
     return 1;
 }
 
 fn lua_tag_toggletag(state: ?*c.lua_State) callconv(.c) c_int {
     const s = state orelse return 0;
     const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
-    create_action_table_with_int(s, "ToggleTag", idx - 1);
+    create_action_table_with_int(s, "ToggleTag", idx);
     return 1;
 }
 
@@ -1081,6 +1147,10 @@ fn parse_modifiers(state: *c.lua_State, idx: c_int) u32 {
     return mod_mask;
 }
 
+fn parse_modifiers_at_top(state: *c.lua_State) u32 {
+    return parse_modifiers(state, -1);
+}
+
 fn parse_single_modifier(name: []const u8) u32 {
     if (std.mem.eql(u8, name, "Mod4") or std.mem.eql(u8, name, "mod4") or std.mem.eql(u8, name, "super")) {
         return (1 << 6);
diff --git a/src/main.zig b/src/main.zig
index 21c5fdc..6563ceb 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -13,6 +13,7 @@ const bar_mod = @import("bar/bar.zig");
 const blocks_mod = @import("bar/blocks/blocks.zig");
 const config_mod = @import("config/config.zig");
 const lua = @import("config/lua.zig");
+const overlay_mod = @import("overlay.zig");
 
 const Display = display_mod.Display;
 const Client = client_mod.Client;
@@ -67,6 +68,87 @@ var config_path_global: ?[]const u8 = null;
 var scroll_animation: animations.Scroll_Animation = .{};
 var animation_config: animations.Animation_Config = .{ .duration_ms = 150, .easing = .ease_out };
 
+var chord_keys: [4]config_mod.Key_Press = [_]config_mod.Key_Press{.{}} ** 4;
+var chord_index: u8 = 0;
+var chord_timestamp: i64 = 0;
+const chord_timeout_ms: i64 = 1000;
+var keyboard_grabbed: bool = false;
+
+var keybind_overlay: ?*overlay_mod.Keybind_Overlay = null;
+
+fn print_help() void {
+    std.debug.print(
+        \\oxwm - A window manager
+        \\
+        \\USAGE:
+        \\    oxwm [OPTIONS]
+        \\
+        \\OPTIONS:
+        \\    --init              Create default config in ~/.config/oxwm/config.lua
+        \\    --config <PATH>     Use custom config file
+        \\    --version           Print version information
+        \\    --help              Print this help message
+        \\
+        \\CONFIG:
+        \\    Location: ~/.config/oxwm/config.lua
+        \\    Edit the config file and use Mod+Shift+R to reload
+        \\    No compilation needed - instant hot-reload!
+        \\
+        \\FIRST RUN:
+        \\    Run 'oxwm --init' to create a config file
+        \\    Or just start oxwm and it will create one automatically
+        \\
+    , .{});
+}
+
+fn init_config(allocator: std.mem.Allocator) void {
+    const home = std.posix.getenv("HOME") orelse {
+        std.debug.print("error: HOME environment variable not set\n", .{});
+        return;
+    };
+
+    var path_buf: [512]u8 = undefined;
+    const config_dir = std.fmt.bufPrint(&path_buf, "{s}/.config/oxwm", .{home}) catch {
+        std.debug.print("error: path too long\n", .{});
+        return;
+    };
+
+    std.fs.makeDirAbsolute(config_dir) catch |err| {
+        if (err != error.PathAlreadyExists) {
+            std.debug.print("error: could not create config directory: {}\n", .{err});
+            return;
+        }
+    };
+
+    var config_path_buf: [512]u8 = undefined;
+    const config_path = std.fmt.bufPrint(&config_path_buf, "{s}/config.lua", .{config_dir}) catch {
+        std.debug.print("error: path too long\n", .{});
+        return;
+    };
+
+    const template = std.fs.cwd().readFileAlloc(allocator, "templates/config.lua", 64 * 1024) catch |err| {
+        std.debug.print("error: could not read template (templates/config.lua): {}\n", .{err});
+        std.debug.print("hint: run from the oxwm source directory, or copy templates/config.lua manually\n", .{});
+        return;
+    };
+    defer allocator.free(template);
+
+    const file = std.fs.createFileAbsolute(config_path, .{}) catch |err| {
+        std.debug.print("error: could not create config file: {}\n", .{err});
+        return;
+    };
+    defer file.close();
+
+    _ = file.writeAll(template) catch |err| {
+        std.debug.print("error: could not write config file: {}\n", .{err});
+        return;
+    };
+
+    std.debug.print("Config created at {s}\n", .{config_path});
+    std.debug.print("Edit the file and reload with Mod+Shift+R\n", .{});
+    std.debug.print("No compilation needed - changes take effect immediately!\n", .{});
+}
+
 pub fn main() !void {
     const allocator = gpa.allocator();
     defer _ = gpa.deinit();
@@ -80,7 +162,13 @@ pub fn main() !void {
         if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--config")) {
             config_path = args.next();
         } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
-            std.debug.print("usage: oxwm [-c config.lua]\n", .{});
+            print_help();
+            return;
+        } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--version")) {
+            std.debug.print("oxwm 0.1.0\n", .{});
+            return;
+        } else if (std.mem.eql(u8, arg, "--init")) {
+            init_config(allocator);
             return;
         }
     }
@@ -140,6 +228,7 @@ pub fn main() !void {
 
     setup_monitors(&display);
     setup_bars(allocator, &display);
+    setup_overlay(allocator, &display);
     grab_keybinds(&display);
     scan_existing_windows(&display);
 
@@ -222,6 +311,10 @@ fn setup_bars(allocator: std.mem.Allocator, display: *Display) void {
     }
 }
 
+fn setup_overlay(allocator: std.mem.Allocator, display: *Display) void {
+    keybind_overlay = overlay_mod.Keybind_Overlay.init(display.handle, display.screen, display.root, config.font, allocator);
+}
+
 fn config_block_to_bar_block(cfg: config_mod.Block) blocks_mod.Block {
     return switch (cfg.block_type) {
         .static => blocks_mod.Block.init_static(cfg.format, cfg.color, cfg.underline),
@@ -335,41 +428,60 @@ fn apply_config_values() void {
     tags = config.tags;
 }
 
+fn make_keybind(mod: u32, key: u64, action: config_mod.Action) config_mod.Keybind {
+    var kb: config_mod.Keybind = .{ .action = action };
+    kb.keys[0] = .{ .mod_mask = mod, .keysym = key };
+    kb.key_count = 1;
+    return kb;
+}
+
+fn make_keybind_int(mod: u32, key: u64, action: config_mod.Action, int_arg: i32) config_mod.Keybind {
+    var kb = make_keybind(mod, key, action);
+    kb.int_arg = int_arg;
+    return kb;
+}
+
+fn make_keybind_str(mod: u32, key: u64, action: config_mod.Action, str_arg: []const u8) config_mod.Keybind {
+    var kb = make_keybind(mod, key, action);
+    kb.str_arg = str_arg;
+    return kb;
+}
+
 fn setup_default_keybinds() void {
     const mod_key: u32 = 1 << 6;
     const shift_key: u32 = 1 << 0;
     const control_key: u32 = 1 << 2;
 
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0xff0d, .action = .spawn_terminal }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'd', .action = .spawn, .str_arg = "rofi -show drun" }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 's', .action = .spawn, .str_arg = "maim -s | xclip -selection clipboard -t image/png" }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'q', .action = .kill_client }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'q', .action = .quit }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'r', .action = .reload_config }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'j', .action = .focus_next }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'k', .action = .focus_prev }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'j', .action = .move_next }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'k', .action = .move_prev }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'h', .action = .resize_master, .int_arg = -50 }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'l', .action = .resize_master, .int_arg = 50 }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'i', .action = .inc_master }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'p', .action = .dec_master }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'a', .action = .toggle_gaps }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'f', .action = .toggle_fullscreen }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0x0020, .action = .toggle_floating }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'n', .action = .cycle_layout }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0x002c, .action = .focus_monitor, .int_arg = -1 }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0x002e, .action = .focus_monitor, .int_arg = 1 }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 0x002c, .action = .send_to_monitor, .int_arg = -1 }) catch {};
-    config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 0x002e, .action = .send_to_monitor, .int_arg = 1 }) catch {};
+    config.add_keybind(make_keybind(mod_key, 0xff0d, .spawn_terminal)) catch {};
+    config.add_keybind(make_keybind_str(mod_key, 'd', .spawn, "rofi -show drun")) catch {};
+    config.add_keybind(make_keybind_str(mod_key, 's', .spawn, "maim -s | xclip -selection clipboard -t image/png")) catch {};
+    config.add_keybind(make_keybind(mod_key, 'q', .kill_client)) catch {};
+    config.add_keybind(make_keybind(mod_key | shift_key, 'q', .quit)) catch {};
+    config.add_keybind(make_keybind(mod_key | shift_key, 'r', .reload_config)) catch {};
+    config.add_keybind(make_keybind(mod_key, 'j', .focus_next)) catch {};
+    config.add_keybind(make_keybind(mod_key, 'k', .focus_prev)) catch {};
+    config.add_keybind(make_keybind(mod_key | shift_key, 'j', .move_next)) catch {};
+    config.add_keybind(make_keybind(mod_key | shift_key, 'k', .move_prev)) catch {};
+    config.add_keybind(make_keybind_int(mod_key, 'h', .resize_master, -50)) catch {};
+    config.add_keybind(make_keybind_int(mod_key, 'l', .resize_master, 50)) catch {};
+    config.add_keybind(make_keybind(mod_key, 'i', .inc_master)) catch {};
+    config.add_keybind(make_keybind(mod_key, 'p', .dec_master)) catch {};
+    config.add_keybind(make_keybind(mod_key, 'a', .toggle_gaps)) catch {};
+    config.add_keybind(make_keybind(mod_key, 'f', .toggle_fullscreen)) catch {};
+    config.add_keybind(make_keybind(mod_key, 0x0020, .toggle_floating)) catch {};
+    config.add_keybind(make_keybind(mod_key, 'n', .cycle_layout)) catch {};
+    config.add_keybind(make_keybind_int(mod_key, 0x002c, .focus_monitor, -1)) catch {};
+    config.add_keybind(make_keybind_int(mod_key, 0x002e, .focus_monitor, 1)) catch {};
+    config.add_keybind(make_keybind_int(mod_key | shift_key, 0x002c, .send_to_monitor, -1)) catch {};
+    config.add_keybind(make_keybind_int(mod_key | shift_key, 0x002e, .send_to_monitor, 1)) catch {};
 
     var tag_index: i32 = 0;
     while (tag_index < 9) : (tag_index += 1) {
         const keysym: u64 = @as(u64, '1') + @as(u64, @intCast(tag_index));
-        config.add_keybind(.{ .mod_mask = mod_key, .keysym = keysym, .action = .view_tag, .int_arg = tag_index }) catch {};
-        config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = keysym, .action = .move_to_tag, .int_arg = tag_index }) catch {};
-        config.add_keybind(.{ .mod_mask = mod_key | control_key, .keysym = keysym, .action = .toggle_view_tag, .int_arg = tag_index }) catch {};
-        config.add_keybind(.{ .mod_mask = mod_key | control_key | shift_key, .keysym = keysym, .action = .toggle_tag, .int_arg = tag_index }) catch {};
+        config.add_keybind(make_keybind_int(mod_key, keysym, .view_tag, tag_index)) catch {};
+        config.add_keybind(make_keybind_int(mod_key | shift_key, keysym, .move_to_tag, tag_index)) catch {};
+        config.add_keybind(make_keybind_int(mod_key | control_key, keysym, .toggle_view_tag, tag_index)) catch {};
+        config.add_keybind(make_keybind_int(mod_key | control_key | shift_key, keysym, .toggle_tag, tag_index)) catch {};
     }
 }
 
@@ -380,13 +492,15 @@ fn grab_keybinds(display: *Display) void {
     _ = xlib.XUngrabKey(display.handle, xlib.AnyKey, xlib.AnyModifier, display.root);
 
     for (config.keybinds.items) |keybind| {
-        const keycode = xlib.XKeysymToKeycode(display.handle, @intCast(keybind.keysym));
+        if (keybind.key_count == 0) continue;
+        const first_key = keybind.keys[0];
+        const keycode = xlib.XKeysymToKeycode(display.handle, @intCast(first_key.keysym));
         if (keycode != 0) {
             for (modifiers) |modifier| {
                 _ = xlib.XGrabKey(
                     display.handle,
                     keycode,
-                    keybind.mod_mask | modifier,
+                    first_key.mod_mask | modifier,
                     display.root,
                     xlib.True,
                     xlib.GrabModeAsync,
@@ -708,15 +822,94 @@ fn handle_configure_request(display: *Display, event: *xlib.XConfigureRequestEve
     _ = xlib.XSync(display.handle, xlib.False);
 }
 
+fn reset_chord_state() void {
+    chord_index = 0;
+    chord_keys = [_]config_mod.Key_Press{.{}} ** 4;
+    chord_timestamp = 0;
+    if (keyboard_grabbed) {
+        if (display_global) |dsp| {
+            _ = xlib.XUngrabKeyboard(dsp.handle, xlib.CurrentTime);
+        }
+        keyboard_grabbed = false;
+    }
+}
+
 fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
     const keysym = xlib.XKeycodeToKeysym(display.handle, @intCast(event.keycode), 0);
+
+    if (keybind_overlay) |overlay| {
+        if (overlay.handle_key(keysym)) {
+            return;
+        }
+    }
+
     const clean_state = event.state & ~@as(c_uint, xlib.LockMask | xlib.Mod2Mask);
+    const current_time = std.time.milliTimestamp();
+
+    if (chord_index > 0 and (current_time - chord_timestamp) > chord_timeout_ms) {
+        reset_chord_state();
+    }
+
+    chord_keys[chord_index] = .{ .mod_mask = clean_state, .keysym = keysym };
+    chord_index += 1;
+    chord_timestamp = current_time;
 
     for (config.keybinds.items) |keybind| {
-        if (keysym == keybind.keysym and clean_state == keybind.mod_mask) {
-            execute_action(display, keybind.action, keybind.int_arg, keybind.str_arg);
-            return;
+        if (keybind.key_count == 0) continue;
+
+        if (keybind.key_count == chord_index) {
+            var matches = true;
+            var i: u8 = 0;
+            while (i < keybind.key_count) : (i += 1) {
+                if (chord_keys[i].keysym != keybind.keys[i].keysym or
+                    chord_keys[i].mod_mask != keybind.keys[i].mod_mask)
+                {
+                    matches = false;
+                    break;
+                }
+            }
+            if (matches) {
+                execute_action(display, keybind.action, keybind.int_arg, keybind.str_arg);
+                reset_chord_state();
+                return;
+            }
+        }
+    }
+
+    var has_partial_match = false;
+    for (config.keybinds.items) |keybind| {
+        if (keybind.key_count > chord_index) {
+            var matches = true;
+            var i: u8 = 0;
+            while (i < chord_index) : (i += 1) {
+                if (chord_keys[i].keysym != keybind.keys[i].keysym or
+                    chord_keys[i].mod_mask != keybind.keys[i].mod_mask)
+                {
+                    matches = false;
+                    break;
+                }
+            }
+            if (matches) {
+                has_partial_match = true;
+                break;
+            }
+        }
+    }
+
+    if (has_partial_match and !keyboard_grabbed) {
+        const grab_result = xlib.XGrabKeyboard(
+            display.handle,
+            display.root,
+            xlib.True,
+            xlib.GrabModeAsync,
+            xlib.GrabModeAsync,
+            xlib.CurrentTime,
+        );
+        if (grab_result == xlib.GrabSuccess) {
+            keyboard_grabbed = true;
         }
+    } else if (!has_partial_match) {
+        reset_chord_state();
     }
 }
 
@@ -734,8 +927,15 @@ fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, st
             running = false;
         },
         .reload_config => reload_config(display),
-        .restart => {},
-        .show_keybinds => {},
+        .restart => reload_config(display),
+        .show_keybinds => {
+            if (keybind_overlay) |overlay| {
+                const mon = monitor_mod.selected_monitor orelse monitor_mod.monitors;
+                if (mon) |m| {
+                    overlay.toggle(m.mon_x, m.mon_y, m.mon_w, m.mon_h);
+                }
+            }
+        },
         .focus_next => focusstack(display, 1),
         .focus_prev => focusstack(display, -1),
         .move_next => movestack(display, 1),
@@ -748,8 +948,8 @@ fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, st
         .toggle_gaps => toggle_gaps(),
         .cycle_layout => cycle_layout(),
         .set_layout => set_layout(str_arg),
-        .set_layout_tiling => {},
-        .set_layout_floating => {},
+        .set_layout_tiling => set_layout_index(0),
+        .set_layout_floating => set_layout_index(2),
         .view_tag => {
             const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
             view(display, tag_mask);
@@ -1323,6 +1523,20 @@ fn set_layout(layout_name: ?[]const u8) void {
     }
 }
 
+fn set_layout_index(index: u32) void {
+    const monitor = monitor_mod.selected_monitor orelse return;
+    monitor.sel_lt = index;
+    monitor.pertag.sellts[monitor.pertag.curtag] = index;
+    if (index != 3) {
+        monitor.scroll_offset = 0;
+    }
+    arrange(monitor);
+    bar_mod.invalidate_bars();
+    if (monitor.lt[monitor.sel_lt]) |layout| {
+        std.debug.print("set_layout_index: {s}\n", .{layout.symbol});
+    }
+}
+
 fn focusmon(display: *Display, direction: i32) void {
     const selmon = monitor_mod.selected_monitor orelse return;
     const target = monitor_mod.dir_to_monitor(direction) orelse return;
diff --git a/src/overlay.zig b/src/overlay.zig
new file mode 100644
index 0000000..82b2164
--- /dev/null
+++ b/src/overlay.zig
@@ -0,0 +1,371 @@
+const std = @import("std");
+const xlib = @import("x11/xlib.zig");
+const config_mod = @import("config/config.zig");
+
+const padding: i32 = 32;
+const line_spacing: i32 = 12;
+const key_action_spacing: i32 = 32;
+const border_width: i32 = 4;
+const border_color: c_ulong = 0x7fccff;
+const bg_color: c_ulong = 0x1a1a1a;
+const fg_color: c_ulong = 0xffffff;
+const key_bg_color: c_ulong = 0x2a2a2a;
+
+const max_lines: usize = 12;
+
+pub const Keybind_Overlay = struct {
+    window: xlib.Window = 0,
+    pixmap: xlib.Pixmap = 0,
+    gc: xlib.GC = null,
+    xft_draw: ?*xlib.XftDraw = null,
+    font: ?*xlib.XftFont = null,
+    font_height: i32 = 0,
+    width: i32 = 0,
+    height: i32 = 0,
+    visible: bool = false,
+    display: ?*xlib.Display = null,
+    root: xlib.Window = 0,
+    screen: c_int = 0,
+
+    key_bufs: [max_lines][64]u8 = undefined,
+    key_lens: [max_lines]usize = undefined,
+    descs: [max_lines][]const u8 = undefined,
+    line_count: usize = 0,
+
+    pub fn init(display: *xlib.Display, screen: c_int, root: xlib.Window, font_name: []const u8, allocator: std.mem.Allocator) ?*Keybind_Overlay {
+        const overlay = allocator.create(Keybind_Overlay) catch return null;
+
+        const font_name_z = allocator.dupeZ(u8, font_name) catch {
+            allocator.destroy(overlay);
+            return null;
+        };
+        defer allocator.free(font_name_z);
+
+        const font = xlib.XftFontOpenName(display, screen, font_name_z);
+        if (font == null) {
+            allocator.destroy(overlay);
+            return null;
+        }
+
+        const font_height = font.*.ascent + font.*.descent;
+
+        overlay.* = .{
+            .display = display,
+            .root = root,
+            .font = font,
+            .font_height = font_height,
+            .screen = screen,
+        };
+
+        return overlay;
+    }
+
+    pub fn deinit(self: *Keybind_Overlay, allocator: std.mem.Allocator) void {
+        if (self.display) |display| {
+            self.destroy_window(display);
+            if (self.font) |font| {
+                xlib.XftFontClose(display, font);
+            }
+        }
+        allocator.destroy(self);
+    }
+
+    fn destroy_window(self: *Keybind_Overlay, display: *xlib.Display) void {
+        if (self.xft_draw) |xft_draw| {
+            xlib.XftDrawDestroy(xft_draw);
+            self.xft_draw = null;
+        }
+        if (self.gc) |gc| {
+            _ = xlib.XFreeGC(display, gc);
+            self.gc = null;
+        }
+        if (self.pixmap != 0) {
+            _ = xlib.XFreePixmap(display, self.pixmap);
+            self.pixmap = 0;
+        }
+        if (self.window != 0) {
+            _ = xlib.c.XDestroyWindow(display, self.window);
+            self.window = 0;
+        }
+    }
+
+    pub fn toggle(self: *Keybind_Overlay, mon_x: i32, mon_y: i32, mon_w: i32, mon_h: i32) void {
+        if (self.visible) {
+            self.hide();
+        } else {
+            self.show(mon_x, mon_y, mon_w, mon_h);
+        }
+    }
+
+    pub fn show(self: *Keybind_Overlay, mon_x: i32, mon_y: i32, mon_w: i32, mon_h: i32) void {
+        const display = self.display orelse return;
+        const cfg = config_mod.get_config() orelse return;
+
+        self.collect_keybinds(cfg);
+        if (self.line_count == 0) return;
+
+        var max_key_width: i32 = 0;
+        var max_desc_width: i32 = 0;
+
+        for (0..self.line_count) |i| {
+            const key_slice = self.key_bufs[i][0..self.key_lens[i]];
+            const key_w = self.text_width(display, key_slice);
+            const desc_w = self.text_width(display, self.descs[i]);
+            if (key_w > max_key_width) max_key_width = key_w;
+            if (desc_w > max_desc_width) max_desc_width = desc_w;
+        }
+
+        const title = "Keybindings";
+        const title_width = self.text_width(display, title);
+
+        const content_width = max_key_width + key_action_spacing + max_desc_width;
+        const min_width = @max(title_width, content_width);
+
+        self.width = min_width + padding * 2;
+        const line_height = self.font_height + line_spacing;
+        const title_height = self.font_height + 20;
+        self.height = title_height + @as(i32, @intCast(self.line_count)) * line_height + padding * 2;
+
+        self.destroy_window(display);
+
+        const x: i32 = mon_x + @divTrunc(mon_w - self.width, 2);
+        const y: i32 = mon_y + @divTrunc(mon_h - self.height, 2);
+
+        const visual = xlib.XDefaultVisual(display, self.screen);
+        const colormap = xlib.XDefaultColormap(display, self.screen);
+        const depth = xlib.XDefaultDepth(display, self.screen);
+
+        self.window = xlib.c.XCreateSimpleWindow(
+            display,
+            self.root,
+            x,
+            y,
+            @intCast(self.width),
+            @intCast(self.height),
+            @intCast(border_width),
+            border_color,
+            bg_color,
+        );
+
+        var attrs: xlib.c.XSetWindowAttributes = undefined;
+        attrs.override_redirect = xlib.True;
+        attrs.event_mask = xlib.c.ExposureMask | xlib.c.KeyPressMask | xlib.c.ButtonPressMask;
+        _ = xlib.c.XChangeWindowAttributes(display, self.window, xlib.c.CWOverrideRedirect | xlib.c.CWEventMask, &attrs);
+
+        self.pixmap = xlib.XCreatePixmap(display, self.window, @intCast(self.width), @intCast(self.height), @intCast(depth));
+        self.gc = xlib.XCreateGC(display, self.pixmap, 0, null);
+        self.xft_draw = xlib.XftDrawCreate(display, self.pixmap, visual, colormap);
+
+        _ = xlib.XMapWindow(display, self.window);
+        _ = xlib.XRaiseWindow(display, self.window);
+
+        self.draw(display, max_key_width, title);
+
+        _ = xlib.XGrabKeyboard(display, self.window, xlib.True, xlib.GrabModeAsync, xlib.GrabModeAsync, xlib.CurrentTime);
+
+        _ = xlib.XSync(display, xlib.False);
+
+        self.visible = true;
+    }
+
+    pub fn hide(self: *Keybind_Overlay) void {
+        if (!self.visible) return;
+        if (self.display) |display| {
+            _ = xlib.XUngrabKeyboard(display, xlib.CurrentTime);
+            if (self.window != 0) {
+                _ = xlib.c.XUnmapWindow(display, self.window);
+            }
+        }
+        self.visible = false;
+    }
+
+    pub fn handle_key(self: *Keybind_Overlay, keysym: u64) bool {
+        if (!self.visible) return false;
+        if (keysym == 0xff1b or keysym == 'q' or keysym == 'Q') {
+            self.hide();
+            return true;
+        }
+        return false;
+    }
+
+    pub fn is_overlay_window(self: *Keybind_Overlay, win: xlib.Window) bool {
+        return self.visible and self.window != 0 and self.window == win;
+    }
+
+    fn draw(self: *Keybind_Overlay, display: *xlib.Display, max_key_width: i32, title: []const u8) void {
+        self.fill_rect(display, 0, 0, self.width, self.height, bg_color);
+
+        const title_x = @divTrunc(self.width - self.text_width(display, title), 2);
+        const title_y = padding + self.font.?.*.ascent;
+        self.draw_text(display, title_x, title_y, title, fg_color);
+
+        const line_height = self.font_height + line_spacing;
+        var y = padding + self.font_height + 20 + self.font.?.*.ascent;
+
+        for (0..self.line_count) |i| {
+            const key_slice = self.key_bufs[i][0..self.key_lens[i]];
+            const key_w = self.text_width(display, key_slice);
+            self.fill_rect(display, padding - 4, y - self.font.?.*.ascent - 2, key_w + 8, self.font_height + 4, key_bg_color);
+            self.draw_text(display, padding, y, key_slice, fg_color);
+
+            const desc_x = padding + max_key_width + key_action_spacing;
+            self.draw_text(display, desc_x, y, self.descs[i], fg_color);
+
+            y += line_height;
+        }
+
+        _ = xlib.c.XCopyArea(display, self.pixmap, self.window, self.gc, 0, 0, @intCast(self.width), @intCast(self.height), 0, 0);
+        _ = xlib.c.XFlush(display);
+    }
+
+    fn fill_rect(self: *Keybind_Overlay, display: *xlib.Display, x: i32, y: i32, w: i32, h: i32, color: c_ulong) void {
+        _ = xlib.XSetForeground(display, self.gc, color);
+        _ = xlib.XFillRectangle(display, self.pixmap, self.gc, x, y, @intCast(w), @intCast(h));
+    }
+
+    fn draw_text(self: *Keybind_Overlay, display: *xlib.Display, x: i32, y: i32, text: []const u8, color: c_ulong) void {
+        if (self.xft_draw == null or self.font == null) return;
+        if (text.len == 0) return;
+
+        var xft_color: xlib.XftColor = undefined;
+        var render_color: xlib.XRenderColor = undefined;
+        render_color.red = @intCast((color >> 16 & 0xff) * 257);
+        render_color.green = @intCast((color >> 8 & 0xff) * 257);
+        render_color.blue = @intCast((color & 0xff) * 257);
+        render_color.alpha = 0xffff;
+
+        const visual = xlib.XDefaultVisual(display, self.screen);
+        const colormap = xlib.XDefaultColormap(display, self.screen);
+
+        _ = xlib.XftColorAllocValue(display, visual, colormap, &render_color, &xft_color);
+        xlib.XftDrawStringUtf8(self.xft_draw, &xft_color, self.font, x, y, text.ptr, @intCast(text.len));
+        xlib.XftColorFree(display, visual, colormap, &xft_color);
+    }
+
+    fn text_width(self: *Keybind_Overlay, display: *xlib.Display, text: []const u8) i32 {
+        if (self.font == null or text.len == 0) return 0;
+        var extents: xlib.XGlyphInfo = undefined;
+        xlib.XftTextExtentsUtf8(display, self.font, text.ptr, @intCast(text.len), &extents);
+        return extents.xOff;
+    }
+
+    fn collect_keybinds(self: *Keybind_Overlay, cfg: *config_mod.Config) void {
+        const priority_actions = [_]config_mod.Action{
+            .show_keybinds,
+            .quit,
+            .reload_config,
+            .kill_client,
+            .spawn_terminal,
+            .toggle_fullscreen,
+            .toggle_floating,
+            .cycle_layout,
+            .focus_next,
+            .focus_prev,
+            .view_tag,
+            .move_to_tag,
+        };
+
+        self.line_count = 0;
+
+        for (priority_actions) |action| {
+            if (self.line_count >= max_lines) break;
+
+            for (cfg.keybinds.items) |kb| {
+                if (kb.action == action and kb.key_count > 0) {
+                    self.format_key_to_buf(self.line_count, &kb.keys[0]);
+                    self.descs[self.line_count] = action_desc(action);
+                    self.line_count += 1;
+                    break;
+                }
+            }
+        }
+    }
+
+    fn format_key_to_buf(self: *Keybind_Overlay, idx: usize, key: *const config_mod.Key_Press) void {
+        var len: usize = 0;
+        var buf = &self.key_bufs[idx];
+
+        if (key.mod_mask & (1 << 6) != 0) {
+            const s = "Mod + ";
+            @memcpy(buf[len .. len + s.len], s);
+            len += s.len;
+        }
+        if (key.mod_mask & (1 << 0) != 0) {
+            const s = "Shift + ";
+            @memcpy(buf[len .. len + s.len], s);
+            len += s.len;
+        }
+        if (key.mod_mask & (1 << 2) != 0) {
+            const s = "Ctrl + ";
+            @memcpy(buf[len .. len + s.len], s);
+            len += s.len;
+        }
+
+        const key_name = keysym_to_name(key.keysym);
+        if (len + key_name.len < buf.len) {
+            @memcpy(buf[len .. len + key_name.len], key_name);
+            len += key_name.len;
+        }
+
+        self.key_lens[idx] = len;
+    }
+
+    fn keysym_to_name(keysym: u64) []const u8 {
+        return switch (keysym) {
+            0xff0d => "Return",
+            0x0020 => "Space",
+            0xff1b => "Escape",
+            0xff08 => "BackSpace",
+            0xff09 => "Tab",
+            0xffbe => "F1",
+            0xffbf => "F2",
+            0xffc0 => "F3",
+            0xffc1 => "F4",
+            0xffc2 => "F5",
+            0xffc3 => "F6",
+            0xffc4 => "F7",
+            0xffc5 => "F8",
+            0xffc6 => "F9",
+            0xffc7 => "F10",
+            0xffc8 => "F11",
+            0xffc9 => "F12",
+            0xff51 => "Left",
+            0xff52 => "Up",
+            0xff53 => "Right",
+            0xff54 => "Down",
+            0x002c => ",",
+            0x002e => ".",
+            0x002f => "/",
+            'a'...'z' => |c| &[_]u8{@intCast(c - 32)},
+            'A'...'Z' => |c| &[_]u8{@intCast(c)},
+            '0'...'9' => |c| &[_]u8{@intCast(c)},
+            else => "?",
+        };
+    }
+
+    fn action_desc(action: config_mod.Action) []const u8 {
+        return switch (action) {
+            .show_keybinds => "Show Keybinds",
+            .quit => "Quit WM",
+            .reload_config => "Reload Config",
+            .restart => "Restart WM",
+            .kill_client => "Close Window",
+            .spawn_terminal => "Open Terminal",
+            .spawn => "Launch Program",
+            .toggle_fullscreen => "Toggle Fullscreen",
+            .toggle_floating => "Toggle Floating",
+            .toggle_gaps => "Toggle Gaps",
+            .cycle_layout => "Cycle Layout",
+            .set_layout => "Set Layout",
+            .focus_next => "Focus Next",
+            .focus_prev => "Focus Previous",
+            .move_next => "Move Next",
+            .move_prev => "Move Previous",
+            .view_tag => "View Tag",
+            .move_to_tag => "Move to Tag",
+            .focus_monitor => "Focus Monitor",
+            .send_to_monitor => "Send to Monitor",
+            else => "Action",
+        };
+    }
+};
diff --git a/src/x11/xlib.zig b/src/x11/xlib.zig
index 50065d1..9de9d74 100644
--- a/src/x11/xlib.zig
+++ b/src/x11/xlib.zig
@@ -252,6 +252,8 @@ pub const XGrabServer = c.XGrabServer;
 pub const XUngrabServer = c.XUngrabServer;
 pub const XUngrabButton = c.XUngrabButton;
 pub const XUngrabKey = c.XUngrabKey;
+pub const XGrabKeyboard = c.XGrabKeyboard;
+pub const XUngrabKeyboard = c.XUngrabKeyboard;
 pub const AnyKey = c.AnyKey;
 pub const AnyModifier = c.AnyModifier;
 
diff --git a/tests/lua_config_tests.zig b/tests/lua_config_tests.zig
new file mode 100644
index 0000000..f1faa18
--- /dev/null
+++ b/tests/lua_config_tests.zig
@@ -0,0 +1,18 @@
+const std = @import("std");
+const testing = std.testing;
+const lua = @import("lua");
+const Config = lua.config_mod.Config;
+
+test "test-config.lua loads without errors" {
+    var cfg = Config.init(testing.allocator);
+    defer cfg.deinit();
+
+    const initialized = lua.init(&cfg);
+    if (!initialized) {
+        return error.LuaInitFailed;
+    }
+    defer lua.deinit();
+
+    const loaded = lua.load_file("resources/test-config.lua");
+    try testing.expect(loaded);
+}