- commit
- c6956f8
- parent
- a65e500
- author
- Eric Bower
- date
- 2026-04-04 22:22:24 -0400 EDT
chore(leader): up arrow can claim client leader
2 files changed,
+63,
-56
+5,
-2
1@@ -586,6 +586,7 @@ const Daemon = struct {
2 }
3
4 pub fn handleInput(self: *Daemon, client: *Client, payload: []const u8) !void {
5+ std.log.debug("buffering pty input data={x}", .{payload});
6 // client is leader, send entire payload (ansi escape codes + text)
7 if (self.leader_client_fd == client.socket_fd) {
8 self.queuePtyInput(payload);
9@@ -594,7 +595,9 @@ const Daemon = struct {
10
11 // quick check to see if a newline happened so we can set that client to leader
12 // without creating a ghostty vt
13- if (std.mem.indexOfScalar(u8, payload, '\r')) |_| {
14+ const isNewline = std.mem.indexOfScalar(u8, payload, '\r') != null;
15+ const isUpArrow = std.mem.eql(u8, payload, "\x1b[A") or util.isUpArrow(payload);
16+ if (isNewline or isUpArrow) {
17 std.log.info(
18 "setting new leader session={s} client_fd={d}",
19 .{ self.session_name, client.socket_fd },
20@@ -1486,7 +1489,7 @@ fn clientLoop(client_sock_fd: i32) !void {
21 if (n_opt) |n| {
22 if (n > 0) {
23 // Check for detach sequences (ctrl+\ as first byte or Kitty escape sequence)
24- if (buf[0] == 0x1C or util.isKittyCtrlBackslash(buf[0..n])) {
25+ if (util.isCtrlBackslash(buf[0..n])) {
26 try ipc.appendMessage(alloc, &sock_write_buf, .Detach, "");
27 } else {
28 try ipc.appendMessage(alloc, &sock_write_buf, .Input, buf[0..n]);
+58,
-54
1@@ -211,31 +211,35 @@ pub fn findTaskExitMarker(output: []const u8) ?u8 {
2 }
3
4 /// Detects Kitty keyboard protocol escape sequence for Ctrl+\.
5-/// Parses the general CSI u form:
6-/// CSI key-code[:alternates] ; modifiers[:event-type] [; text-codepoints] u
7-///
8-/// Matches when key-code is 92 (backslash), ctrl bit is set in modifiers,
9-/// and event type is press (1 or absent) or repeat (2). Rejects release (3).
10-/// Tolerates additional modifiers (caps_lock, num_lock)
11-/// and alternate key sub-fields from the kitty protocol's progressive
12-/// enhancement flags.
13-pub fn isKittyCtrlBackslash(buf: []const u8) bool {
14- return detectCsiKeyPress(buf, 92, 0b100);
15+pub fn isCtrlBackslash(buf: []const u8) bool {
16+ if (buf.len == 0) return false;
17+ return buf[0] == 0x1C or isKeyPressed(buf, 0x5c, 0b100);
18+}
19+
20+/// Detects vt100 or kitty keyboard protocol escape sequence for up arrow.
21+pub fn isUpArrow(buf: []const u8) bool {
22+ return std.mem.eql(u8, buf, "\x1b[A") or std.mem.eql(u8, buf, "\x1b[26;1u");
23 }
24
25-fn detectCsiKeyPress(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
26- // Scan for any CSI u sequence encoding the given Ctrl+key in the buffer.
27- // The sequence can appear at any offset (e.g. preceded by other input).
28+fn isKeyPressed(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
29+ // Scan for any CSI u sequence encoding in the buffer.
30 var i: usize = 0;
31 while (i + 2 < buf.len) : (i += 1) {
32 if (buf[i] == 0x1b and buf[i + 1] == '[') {
33- if (isKeyPressed(buf[i + 2 ..], expected_key, expected_mods)) return true;
34+ if (keypressWithMod(buf[i + 2 ..], expected_key, expected_mods)) return true;
35 }
36 }
37 return false;
38 }
39
40-fn isKeyPressed(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
41+/// Parses the general CSI u form:
42+/// CSI key-code[:alternates] ; modifiers[:event-type] [; text-codepoints] u
43+///
44+/// Event type is press (1 or absent) or repeat (2). Rejects release (3).
45+/// Tolerates additional modifiers (caps_lock, num_lock)
46+/// and alternate key sub-fields from the kitty protocol's progressive
47+/// enhancement flags.
48+fn keypressWithMod(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
49 var pos: usize = 0;
50
51 // 1. Parse key code.
52@@ -261,7 +265,7 @@ fn isKeyPressed(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
53 // (caps_lock=0b1000000, num_lock=0b10000000) are tolerated because
54 // they are ambient state, not deliberate key combinations.
55 const intentional_mods = mod_raw & 0b00111111;
56- if (intentional_mods != expected_mods) return false;
57+ if (expected_mods > 0 and expected_mods != intentional_mods) return false;
58
59 // 6. Parse optional event type after ':'.
60 if (pos < buf.len and buf[pos] == ':') {
61@@ -634,95 +638,95 @@ test "shellQuote" {
62 try std.testing.expectEqualStrings("'hello'", plain);
63 }
64
65-test "isKittyCtrlBackslash" {
66+test "isCtrlBackslash" {
67 const expect = std.testing.expect;
68
69 // Basic: ctrl only (modifier 5 = 1 + 4)
70- try expect(isKittyCtrlBackslash("\x1b[92;5u"));
71+ try expect(isCtrlBackslash("\x1b[92;5u"));
72
73 // Explicit press event type (:1)
74- try expect(isKittyCtrlBackslash("\x1b[92;5:1u"));
75+ try expect(isCtrlBackslash("\x1b[92;5:1u"));
76
77 // Repeat event (:2) -- user holding Ctrl+\
78- try expect(isKittyCtrlBackslash("\x1b[92;5:2u"));
79+ try expect(isCtrlBackslash("\x1b[92;5:2u"));
80
81 // Release event (:3) -- must NOT trigger detach
82- try expect(!isKittyCtrlBackslash("\x1b[92;5:3u"));
83+ try expect(!isCtrlBackslash("\x1b[92;5:3u"));
84
85 // Lock modifiers: caps_lock (bit 6) changes modifier value
86 // ctrl + caps_lock = 1 + (4 + 64) = 69
87- try expect(isKittyCtrlBackslash("\x1b[92;69u"));
88- try expect(isKittyCtrlBackslash("\x1b[92;69:1u"));
89- try expect(!isKittyCtrlBackslash("\x1b[92;69:3u"));
90+ try expect(isCtrlBackslash("\x1b[92;69u"));
91+ try expect(isCtrlBackslash("\x1b[92;69:1u"));
92+ try expect(!isCtrlBackslash("\x1b[92;69:3u"));
93
94 // ctrl + num_lock = 1 + (4 + 128) = 133
95- try expect(isKittyCtrlBackslash("\x1b[92;133u"));
96+ try expect(isCtrlBackslash("\x1b[92;133u"));
97
98 // ctrl + caps_lock + num_lock = 1 + (4 + 64 + 128) = 197
99- try expect(isKittyCtrlBackslash("\x1b[92;197u"));
100+ try expect(isCtrlBackslash("\x1b[92;197u"));
101
102 // Combined intentional modifiers -- must NOT match (ctrl+\ is the
103 // detach key, not ctrl+shift+\ or ctrl+alt+\)
104 // ctrl + shift = 1 + (4 + 1) = 6
105- try expect(!isKittyCtrlBackslash("\x1b[92;6u"));
106+ try expect(!isCtrlBackslash("\x1b[92;6u"));
107
108 // ctrl + alt = 1 + (4 + 2) = 7
109- try expect(!isKittyCtrlBackslash("\x1b[92;7u"));
110+ try expect(!isCtrlBackslash("\x1b[92;7u"));
111
112 // ctrl + super = 1 + (4 + 8) = 13
113- try expect(!isKittyCtrlBackslash("\x1b[92;13u"));
114+ try expect(!isCtrlBackslash("\x1b[92;13u"));
115
116 // ctrl + shift + caps_lock = 1 + (1 + 4 + 64) = 70 -- shift is intentional
117- try expect(!isKittyCtrlBackslash("\x1b[92;70u"));
118+ try expect(!isCtrlBackslash("\x1b[92;70u"));
119
120 // ctrl + shift + num_lock = 1 + (1 + 4 + 128) = 134 -- shift is intentional
121- try expect(!isKittyCtrlBackslash("\x1b[92;134u"));
122+ try expect(!isCtrlBackslash("\x1b[92;134u"));
123
124 // Modifier without ctrl bit -- must NOT match
125 // shift only = 1 + 1 = 2
126- try expect(!isKittyCtrlBackslash("\x1b[92;1u"));
127- try expect(!isKittyCtrlBackslash("\x1b[92;2u"));
128+ try expect(!isCtrlBackslash("\x1b[92;1u"));
129+ try expect(!isCtrlBackslash("\x1b[92;2u"));
130
131 // Alternate key sub-fields (report_alternates flag)
132 // shifted key | (124): \x1b[92:124;5u
133- try expect(isKittyCtrlBackslash("\x1b[92:124;5u"));
134+ try expect(isCtrlBackslash("\x1b[92:124;5u"));
135
136 // base layout key only (non-US keyboard): \x1b[92::92;5u
137- try expect(isKittyCtrlBackslash("\x1b[92::92;5u"));
138+ try expect(isCtrlBackslash("\x1b[92::92;5u"));
139
140 // both shifted and base layout: \x1b[92:124:92;5u
141- try expect(isKittyCtrlBackslash("\x1b[92:124:92;5u"));
142+ try expect(isCtrlBackslash("\x1b[92:124:92;5u"));
143
144 // Alternate keys + lock modifiers + event type
145- try expect(isKittyCtrlBackslash("\x1b[92:124;69:1u"));
146- try expect(!isKittyCtrlBackslash("\x1b[92:124;69:3u"));
147+ try expect(isCtrlBackslash("\x1b[92:124;69:1u"));
148+ try expect(!isCtrlBackslash("\x1b[92:124;69:3u"));
149
150 // Text codepoints section (flag 0b10000) -- tolerated and skipped
151 // Even though ctrl+\ text is typically empty, terminals may vary
152- try expect(isKittyCtrlBackslash("\x1b[92;5;28u"));
153- try expect(isKittyCtrlBackslash("\x1b[92;5;28:92u"));
154+ try expect(isCtrlBackslash("\x1b[92;5;28u"));
155+ try expect(isCtrlBackslash("\x1b[92;5;28:92u"));
156
157 // Wrong key code -- must NOT match
158- try expect(!isKittyCtrlBackslash("\x1b[91;5u"));
159- try expect(!isKittyCtrlBackslash("\x1b[93;5u"));
160- try expect(!isKittyCtrlBackslash("\x1b[9;5u"));
161- try expect(!isKittyCtrlBackslash("\x1b[920;5u"));
162+ try expect(!isCtrlBackslash("\x1b[91;5u"));
163+ try expect(!isCtrlBackslash("\x1b[93;5u"));
164+ try expect(!isCtrlBackslash("\x1b[9;5u"));
165+ try expect(!isCtrlBackslash("\x1b[920;5u"));
166
167 // Sequence embedded in larger buffer (e.g., preceded by other input)
168- try expect(isKittyCtrlBackslash("abc\x1b[92;5u"));
169- try expect(isKittyCtrlBackslash("\x1b[A\x1b[92;5u"));
170+ try expect(isCtrlBackslash("abc\x1b[92;5u"));
171+ try expect(isCtrlBackslash("\x1b[A\x1b[92;5u"));
172
173 // Garbage / malformed inputs
174- try expect(!isKittyCtrlBackslash("garbage"));
175- try expect(!isKittyCtrlBackslash(""));
176- try expect(!isKittyCtrlBackslash("\x1b["));
177- try expect(!isKittyCtrlBackslash("\x1b[92"));
178- try expect(!isKittyCtrlBackslash("\x1b[92;"));
179- try expect(!isKittyCtrlBackslash("\x1b[92;u"));
180- try expect(!isKittyCtrlBackslash("\x1b[;5u"));
181+ try expect(!isCtrlBackslash("garbage"));
182+ try expect(!isCtrlBackslash(""));
183+ try expect(!isCtrlBackslash("\x1b["));
184+ try expect(!isCtrlBackslash("\x1b[92"));
185+ try expect(!isCtrlBackslash("\x1b[92;"));
186+ try expect(!isCtrlBackslash("\x1b[92;u"));
187+ try expect(!isCtrlBackslash("\x1b[;5u"));
188
189 // Other CSI u sequences that happen to contain '92' elsewhere
190- try expect(!isKittyCtrlBackslash("\x1b[65;92u"));
191+ try expect(!isCtrlBackslash("\x1b[65;92u"));
192 }
193
194 test "serializeTerminalState excludes synchronized output replay" {