Commit c6956f8

Eric Bower  ·  2026-04-04 22:22:24 -0400 EDT
parent a65e500
chore(leader): up arrow can claim client leader
2 files changed,  +63, -56
+5, -2
......@@ -586,6 +586,7 @@ const Daemon = struct {
586586 }
587587
588588 pub fn handleInput(self: *Daemon, client: *Client, payload: []const u8) !void {
589+ std.log.debug("buffering pty input data={x}", .{payload});
589590 // client is leader, send entire payload (ansi escape codes + text)
590591 if (self.leader_client_fd == client.socket_fd) {
591592 self.queuePtyInput(payload);
......@@ -594,7 +595,9 @@ const Daemon = struct {
594595
595596 // quick check to see if a newline happened so we can set that client to leader
596597 // without creating a ghostty vt
597- if (std.mem.indexOfScalar(u8, payload, '\r')) |_| {
598+ const isNewline = std.mem.indexOfScalar(u8, payload, '\r') != null;
599+ const isUpArrow = std.mem.eql(u8, payload, "\x1b[A") or util.isUpArrow(payload);
600+ if (isNewline or isUpArrow) {
598601 std.log.info(
599602 "setting new leader session={s} client_fd={d}",
600603 .{ self.session_name, client.socket_fd },
......@@ -1486,7 +1489,7 @@ fn clientLoop(client_sock_fd: i32) !void {
14861489 if (n_opt) |n| {
14871490 if (n > 0) {
14881491 // Check for detach sequences (ctrl+\ as first byte or Kitty escape sequence)
1489- if (buf[0] == 0x1C or util.isKittyCtrlBackslash(buf[0..n])) {
1492+ if (util.isCtrlBackslash(buf[0..n])) {
14901493 try ipc.appendMessage(alloc, &sock_write_buf, .Detach, "");
14911494 } else {
14921495 try ipc.appendMessage(alloc, &sock_write_buf, .Input, buf[0..n]);
+58, -54
......@@ -211,31 +211,35 @@ pub fn findTaskExitMarker(output: []const u8) ?u8 {
211211 }
212212
213213 /// Detects Kitty keyboard protocol escape sequence for Ctrl+\.
214-/// Parses the general CSI u form:
215-/// CSI key-code[:alternates] ; modifiers[:event-type] [; text-codepoints] u
216-///
217-/// Matches when key-code is 92 (backslash), ctrl bit is set in modifiers,
218-/// and event type is press (1 or absent) or repeat (2). Rejects release (3).
219-/// Tolerates additional modifiers (caps_lock, num_lock)
220-/// and alternate key sub-fields from the kitty protocol's progressive
221-/// enhancement flags.
222-pub fn isKittyCtrlBackslash(buf: []const u8) bool {
223- return detectCsiKeyPress(buf, 92, 0b100);
214+pub fn isCtrlBackslash(buf: []const u8) bool {
215+ if (buf.len == 0) return false;
216+ return buf[0] == 0x1C or isKeyPressed(buf, 0x5c, 0b100);
217+}
218+
219+/// Detects vt100 or kitty keyboard protocol escape sequence for up arrow.
220+pub fn isUpArrow(buf: []const u8) bool {
221+ return std.mem.eql(u8, buf, "\x1b[A") or std.mem.eql(u8, buf, "\x1b[26;1u");
224222 }
225223
226-fn detectCsiKeyPress(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
227- // Scan for any CSI u sequence encoding the given Ctrl+key in the buffer.
228- // The sequence can appear at any offset (e.g. preceded by other input).
224+fn isKeyPressed(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
225+ // Scan for any CSI u sequence encoding in the buffer.
229226 var i: usize = 0;
230227 while (i + 2 < buf.len) : (i += 1) {
231228 if (buf[i] == 0x1b and buf[i + 1] == '[') {
232- if (isKeyPressed(buf[i + 2 ..], expected_key, expected_mods)) return true;
229+ if (keypressWithMod(buf[i + 2 ..], expected_key, expected_mods)) return true;
233230 }
234231 }
235232 return false;
236233 }
237234
238-fn isKeyPressed(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
235+/// Parses the general CSI u form:
236+/// CSI key-code[:alternates] ; modifiers[:event-type] [; text-codepoints] u
237+///
238+/// Event type is press (1 or absent) or repeat (2). Rejects release (3).
239+/// Tolerates additional modifiers (caps_lock, num_lock)
240+/// and alternate key sub-fields from the kitty protocol's progressive
241+/// enhancement flags.
242+fn keypressWithMod(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
239243 var pos: usize = 0;
240244
241245 // 1. Parse key code.
......@@ -261,7 +265,7 @@ fn isKeyPressed(buf: []const u8, expected_key: u32, expected_mods: u32) bool {
261265 // (caps_lock=0b1000000, num_lock=0b10000000) are tolerated because
262266 // they are ambient state, not deliberate key combinations.
263267 const intentional_mods = mod_raw & 0b00111111;
264- if (intentional_mods != expected_mods) return false;
268+ if (expected_mods > 0 and expected_mods != intentional_mods) return false;
265269
266270 // 6. Parse optional event type after ':'.
267271 if (pos < buf.len and buf[pos] == ':') {
......@@ -634,95 +638,95 @@ test "shellQuote" {
634638 try std.testing.expectEqualStrings("'hello'", plain);
635639 }
636640
637-test "isKittyCtrlBackslash" {
641+test "isCtrlBackslash" {
638642 const expect = std.testing.expect;
639643
640644 // Basic: ctrl only (modifier 5 = 1 + 4)
641- try expect(isKittyCtrlBackslash("\x1b[92;5u"));
645+ try expect(isCtrlBackslash("\x1b[92;5u"));
642646
643647 // Explicit press event type (:1)
644- try expect(isKittyCtrlBackslash("\x1b[92;5:1u"));
648+ try expect(isCtrlBackslash("\x1b[92;5:1u"));
645649
646650 // Repeat event (:2) -- user holding Ctrl+\
647- try expect(isKittyCtrlBackslash("\x1b[92;5:2u"));
651+ try expect(isCtrlBackslash("\x1b[92;5:2u"));
648652
649653 // Release event (:3) -- must NOT trigger detach
650- try expect(!isKittyCtrlBackslash("\x1b[92;5:3u"));
654+ try expect(!isCtrlBackslash("\x1b[92;5:3u"));
651655
652656 // Lock modifiers: caps_lock (bit 6) changes modifier value
653657 // ctrl + caps_lock = 1 + (4 + 64) = 69
654- try expect(isKittyCtrlBackslash("\x1b[92;69u"));
655- try expect(isKittyCtrlBackslash("\x1b[92;69:1u"));
656- try expect(!isKittyCtrlBackslash("\x1b[92;69:3u"));
658+ try expect(isCtrlBackslash("\x1b[92;69u"));
659+ try expect(isCtrlBackslash("\x1b[92;69:1u"));
660+ try expect(!isCtrlBackslash("\x1b[92;69:3u"));
657661
658662 // ctrl + num_lock = 1 + (4 + 128) = 133
659- try expect(isKittyCtrlBackslash("\x1b[92;133u"));
663+ try expect(isCtrlBackslash("\x1b[92;133u"));
660664
661665 // ctrl + caps_lock + num_lock = 1 + (4 + 64 + 128) = 197
662- try expect(isKittyCtrlBackslash("\x1b[92;197u"));
666+ try expect(isCtrlBackslash("\x1b[92;197u"));
663667
664668 // Combined intentional modifiers -- must NOT match (ctrl+\ is the
665669 // detach key, not ctrl+shift+\ or ctrl+alt+\)
666670 // ctrl + shift = 1 + (4 + 1) = 6
667- try expect(!isKittyCtrlBackslash("\x1b[92;6u"));
671+ try expect(!isCtrlBackslash("\x1b[92;6u"));
668672
669673 // ctrl + alt = 1 + (4 + 2) = 7
670- try expect(!isKittyCtrlBackslash("\x1b[92;7u"));
674+ try expect(!isCtrlBackslash("\x1b[92;7u"));
671675
672676 // ctrl + super = 1 + (4 + 8) = 13
673- try expect(!isKittyCtrlBackslash("\x1b[92;13u"));
677+ try expect(!isCtrlBackslash("\x1b[92;13u"));
674678
675679 // ctrl + shift + caps_lock = 1 + (1 + 4 + 64) = 70 -- shift is intentional
676- try expect(!isKittyCtrlBackslash("\x1b[92;70u"));
680+ try expect(!isCtrlBackslash("\x1b[92;70u"));
677681
678682 // ctrl + shift + num_lock = 1 + (1 + 4 + 128) = 134 -- shift is intentional
679- try expect(!isKittyCtrlBackslash("\x1b[92;134u"));
683+ try expect(!isCtrlBackslash("\x1b[92;134u"));
680684
681685 // Modifier without ctrl bit -- must NOT match
682686 // shift only = 1 + 1 = 2
683- try expect(!isKittyCtrlBackslash("\x1b[92;1u"));
684- try expect(!isKittyCtrlBackslash("\x1b[92;2u"));
687+ try expect(!isCtrlBackslash("\x1b[92;1u"));
688+ try expect(!isCtrlBackslash("\x1b[92;2u"));
685689
686690 // Alternate key sub-fields (report_alternates flag)
687691 // shifted key | (124): \x1b[92:124;5u
688- try expect(isKittyCtrlBackslash("\x1b[92:124;5u"));
692+ try expect(isCtrlBackslash("\x1b[92:124;5u"));
689693
690694 // base layout key only (non-US keyboard): \x1b[92::92;5u
691- try expect(isKittyCtrlBackslash("\x1b[92::92;5u"));
695+ try expect(isCtrlBackslash("\x1b[92::92;5u"));
692696
693697 // both shifted and base layout: \x1b[92:124:92;5u
694- try expect(isKittyCtrlBackslash("\x1b[92:124:92;5u"));
698+ try expect(isCtrlBackslash("\x1b[92:124:92;5u"));
695699
696700 // Alternate keys + lock modifiers + event type
697- try expect(isKittyCtrlBackslash("\x1b[92:124;69:1u"));
698- try expect(!isKittyCtrlBackslash("\x1b[92:124;69:3u"));
701+ try expect(isCtrlBackslash("\x1b[92:124;69:1u"));
702+ try expect(!isCtrlBackslash("\x1b[92:124;69:3u"));
699703
700704 // Text codepoints section (flag 0b10000) -- tolerated and skipped
701705 // Even though ctrl+\ text is typically empty, terminals may vary
702- try expect(isKittyCtrlBackslash("\x1b[92;5;28u"));
703- try expect(isKittyCtrlBackslash("\x1b[92;5;28:92u"));
706+ try expect(isCtrlBackslash("\x1b[92;5;28u"));
707+ try expect(isCtrlBackslash("\x1b[92;5;28:92u"));
704708
705709 // Wrong key code -- must NOT match
706- try expect(!isKittyCtrlBackslash("\x1b[91;5u"));
707- try expect(!isKittyCtrlBackslash("\x1b[93;5u"));
708- try expect(!isKittyCtrlBackslash("\x1b[9;5u"));
709- try expect(!isKittyCtrlBackslash("\x1b[920;5u"));
710+ try expect(!isCtrlBackslash("\x1b[91;5u"));
711+ try expect(!isCtrlBackslash("\x1b[93;5u"));
712+ try expect(!isCtrlBackslash("\x1b[9;5u"));
713+ try expect(!isCtrlBackslash("\x1b[920;5u"));
710714
711715 // Sequence embedded in larger buffer (e.g., preceded by other input)
712- try expect(isKittyCtrlBackslash("abc\x1b[92;5u"));
713- try expect(isKittyCtrlBackslash("\x1b[A\x1b[92;5u"));
716+ try expect(isCtrlBackslash("abc\x1b[92;5u"));
717+ try expect(isCtrlBackslash("\x1b[A\x1b[92;5u"));
714718
715719 // Garbage / malformed inputs
716- try expect(!isKittyCtrlBackslash("garbage"));
717- try expect(!isKittyCtrlBackslash(""));
718- try expect(!isKittyCtrlBackslash("\x1b["));
719- try expect(!isKittyCtrlBackslash("\x1b[92"));
720- try expect(!isKittyCtrlBackslash("\x1b[92;"));
721- try expect(!isKittyCtrlBackslash("\x1b[92;u"));
722- try expect(!isKittyCtrlBackslash("\x1b[;5u"));
720+ try expect(!isCtrlBackslash("garbage"));
721+ try expect(!isCtrlBackslash(""));
722+ try expect(!isCtrlBackslash("\x1b["));
723+ try expect(!isCtrlBackslash("\x1b[92"));
724+ try expect(!isCtrlBackslash("\x1b[92;"));
725+ try expect(!isCtrlBackslash("\x1b[92;u"));
726+ try expect(!isCtrlBackslash("\x1b[;5u"));
723727
724728 // Other CSI u sequences that happen to contain '92' elsewhere
725- try expect(!isKittyCtrlBackslash("\x1b[65;92u"));
729+ try expect(!isCtrlBackslash("\x1b[65;92u"));
726730 }
727731
728732 test "serializeTerminalState excludes synchronized output replay" {