repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
08a06c0
parent
d0e3ed1
author
Eric Bower
date
2026-04-04 10:22:43 -0400 EDT
refactor: kitty key press logic to be more generic

fix: src/util.zig tests were not being run :sad:
3 files changed,  +30, -13
M build.zig
+15, -1
 1@@ -64,8 +64,22 @@ pub fn build(b: *std.Build) void {
 2     // Test
 3     {
 4         const test_step = b.step("test", "Run unit tests");
 5+        const test_module = b.addModule("test", .{
 6+            .root_source_file = b.path("src/test.zig"),
 7+            .target = target,
 8+            .optimize = optimize,
 9+        });
10+        if (b.lazyDependency("ghostty", .{
11+            .target = target,
12+            .optimize = optimize,
13+        })) |dep| {
14+            test_module.addImport(
15+                "ghostty-vt",
16+                dep.module("ghostty-vt"),
17+            );
18+        }
19         const exe_unit_tests = b.addTest(.{
20-            .root_module = exe_mod,
21+            .root_module = test_module,
22         });
23         const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
24         test_step.dependOn(&run_exe_unit_tests.step);
A src/test.zig
+5, -0
1@@ -0,0 +1,5 @@
2+comptime {
3+    _ = @import("main.zig");
4+    _ = @import("util.zig");
5+    _ = @import("socket.zig");
6+}
M src/util.zig
+10, -12
 1@@ -220,24 +220,22 @@ pub fn findTaskExitMarker(output: []const u8) ?u8 {
 2 /// and alternate key sub-fields from the kitty protocol's progressive
 3 /// enhancement flags.
 4 pub fn isKittyCtrlBackslash(buf: []const u8) bool {
 5-    return isKittyCtrlKey(buf, 92);
 6+    return detectCsiKeyPress(buf, 92, 0b100);
 7 }
 8 
 9-fn isKittyCtrlKey(buf: []const u8, key_code: u32) bool {
10+fn detectCsiKeyPress(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
11     // Scan for any CSI u sequence encoding the given Ctrl+key in the buffer.
12     // The sequence can appear at any offset (e.g. preceded by other input).
13     var i: usize = 0;
14     while (i + 2 < buf.len) : (i += 1) {
15         if (buf[i] == 0x1b and buf[i + 1] == '[') {
16-            if (parseKittyCtrlKey(buf[i + 2 ..], key_code)) return true;
17+            if (isKeyPressed(buf[i + 2 ..], expected_key, expected_mods)) return true;
18         }
19     }
20     return false;
21 }
22 
23-/// Parse a CSI u sequence (after the `\x1b[` prefix) and return true if it
24-/// encodes a Ctrl+key press or repeat event for the given key code.
25-fn parseKittyCtrlKey(buf: []const u8, expected_key: u32) bool {
26+fn isKeyPressed(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
27     var pos: usize = 0;
28 
29     // 1. Parse key code.
30@@ -259,11 +257,11 @@ fn parseKittyCtrlKey(buf: []const u8, expected_key: u32) bool {
31     if (mod_encoded < 1) return false;
32     const mod_raw = mod_encoded - 1;
33 
34-    // 5. Ctrl must be the only intentional modifier. Lock modifiers
35+    // 5. Only accept intentional modifiers. Lock modifiers
36     //    (caps_lock=0b1000000, num_lock=0b10000000) are tolerated because
37     //    they are ambient state, not deliberate key combinations.
38     const intentional_mods = mod_raw & 0b00111111;
39-    if (intentional_mods != 0b100) return false;
40+    if (intentional_mods != expected_mods) return false;
41 
42     // 6. Parse optional event type after ':'.
43     if (pos < buf.len and buf[pos] == ':') {
44@@ -735,9 +733,9 @@ test "serializeTerminalState excludes synchronized output replay" {
45     var stream = term.vtStream();
46     defer stream.deinit();
47 
48-    stream.nextSlice("\x1b[?2004h"); // Bracketed paste
49-    stream.nextSlice("\x1b[?2026h"); // Synchronized output
50-    stream.nextSlice("hello");
51+    try stream.nextSlice("\x1b[?2004h"); // Bracketed paste
52+    try stream.nextSlice("\x1b[?2026h"); // Synchronized output
53+    try stream.nextSlice("hello");
54 
55     try std.testing.expect(term.modes.get(.bracketed_paste));
56     try std.testing.expect(term.modes.get(.synchronized_output));
57@@ -755,7 +753,7 @@ test "serializeTerminalState excludes synchronized output replay" {
58 
59     var restored_stream = restored.vtStream();
60     defer restored_stream.deinit();
61-    restored_stream.nextSlice(output);
62+    try restored_stream.nextSlice(output);
63 
64     try std.testing.expect(restored.modes.get(.bracketed_paste));
65     try std.testing.expect(!restored.modes.get(.synchronized_output));