oxwm

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

refactor: create `ChordState` type and remove chord state globals

Commit
78f29dc49b5b5a82ec6db7959e1540c1ca37e463
Parent
2141216
Author
emzywastaken <amiamemetoo@gmail.com>
Date
2026-02-20 23:16:51

Diff

diff --git a/src/keyboard/chord.zig b/src/keyboard/chord.zig
new file mode 100644
index 0000000..a23766c
--- /dev/null
+++ b/src/keyboard/chord.zig
@@ -0,0 +1,69 @@
+const std = @import("std");
+const xlib = @import("../x11/xlib.zig");
+const config = @import("../config/config.zig");
+
+pub const max_chord_len: u8 = 4;
+/// How long (in milliseconds) the user has between key presses within
+/// a chord before the sequence is abandoned and state is reset.
+pub const timeout_ms: i64 = 1000;
+
+/// Tracks the in-progress key-chord sequence.
+///
+/// Owned by `WindowManager`. Call `update` on every key press; it returns
+/// whether the sequence is still live. Call `reset` to abandon the
+/// current sequence and release the keyboard grab if one is held.
+pub const ChordState = struct {
+    keys: [max_chord_len]config.Key_Press = .{config.Key_Press{}} ** max_chord_len,
+    index: u8 = 0,
+    last_timestamp: i64 = 0,
+    keyboard_grabbed: bool = false,
+
+    /// Push a new key press onto the sequence and update the timestamp.
+    ///
+    /// Returns `false` if the sequence is already at maximum length, in which
+    /// case the caller should call `reset` before retrying.
+    pub fn push(self: *ChordState, key: config.Key_Press) bool {
+        if (self.index >= max_chord_len) return false;
+        self.keys[self.index] = key;
+        self.index += 1;
+        self.last_timestamp = std.time.milliTimestamp();
+
+        return true;
+    }
+
+    /// Returns true if the sequence has timed out and should be reset.
+    pub fn is_timed_out(self: *const ChordState) bool {
+        if (self.index == 0) return false;
+        return (std.time.milliTimestamp() - self.last_timestamp) >= timeout_ms;
+    }
+
+    /// Clears the sequence and releases the keyboard grab if one is held.
+    ///
+    /// `display` may be null only during early startup before the connection
+    /// is open, in normal operation it should always be provided.
+    pub fn reset(self: *ChordState, display: ?*xlib.Display) void {
+        self.keys = .{config.Key_Press{}} ** max_chord_len;
+        self.index = 0;
+        self.last_timestamp = 0;
+
+        if (self.keyboard_grabbed) {
+            if (display) |dpy| {
+                _ = xlib.XUngrabKeyboard(dpy, xlib.CurrentTime);
+            }
+            self.keyboard_grabbed = false;
+        }
+    }
+
+    /// Try to grab the keyboard for exclusive input during a partial match.
+    ///
+    /// Sets `keyboard_grabbed` on success.  Safe to call repeatedly,
+    /// does nothing if already grabbed.
+    pub fn grab_keyboard(self: *ChordState, display: *xlib.Display, root: xlib.Window) void {
+        if (self.keyboard_grabbed) return;
+
+        const result = xlib.XGrabKeyboard(display, root, xlib.True, xlib.GrabModeAsync, xlib.GrabModeAsync, xlib.CurrentTime);
+        if (result == xlib.GrabSuccess) {
+            self.keyboard_grabbed = true;
+        }
+    }
+};
diff --git a/src/main.zig b/src/main.zig
index 84f2f38..c61accd 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,5 +1,6 @@
 const std = @import("std");
 const VERSION = "v0.11.2";
+const kchord = @import("keyboard/chord.zig");
 const display_mod = @import("x11/display.zig");
 const events = @import("x11/events.zig");
 const xlib = @import("x11/xlib.zig");
@@ -70,11 +71,7 @@ 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 chord = kchord.ChordState{};
 
 var keybind_overlay: ?*overlay_mod.Keybind_Overlay = null;
 
@@ -900,16 +897,8 @@ 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 reset_chord_state(display_handle: *xlib.Display) void {
+    chord.reset(display_handle);
 }
 
 fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
@@ -924,31 +913,26 @@ fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
     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();
+    if (chord.index > 0 and (current_time - chord.last_timestamp) > kchord.timeout_ms) {
+        reset_chord_state(display.handle);
     }
 
-    chord_keys[chord_index] = .{ .mod_mask = clean_state, .keysym = keysym };
-    chord_index += 1;
-    chord_timestamp = current_time;
+    _ = chord.push(.{ .mod_mask = clean_state, .keysym = keysym });
 
     for (config.keybinds.items) |keybind| {
         if (keybind.key_count == 0) continue;
 
-        if (keybind.key_count == chord_index) {
+        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)
-                {
+            for (0..keybind.key_count) |i| {
+                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();
+                reset_chord_state(display.handle);
                 return;
             }
         }
@@ -956,13 +940,10 @@ fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
 
     var has_partial_match = false;
     for (config.keybinds.items) |keybind| {
-        if (keybind.key_count > chord_index) {
+        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)
-                {
+            for (0..chord.index) |i| {
+                if (chord.keys[i].keysym != keybind.keys[i].keysym or chord.keys[i].mod_mask != keybind.keys[i].mod_mask) {
                     matches = false;
                     break;
                 }
@@ -974,20 +955,10 @@ fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
         }
     }
 
-    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;
-        }
+    if (has_partial_match) {
+        chord.grab_keyboard(display.handle, display.root);
     } else if (!has_partial_match) {
-        reset_chord_state();
+        reset_chord_state(display.handle);
     }
 }