diff options
Diffstat (limited to 'src/tui_app.zig')
| -rw-r--r-- | src/tui_app.zig | 62 |
1 files changed, 57 insertions, 5 deletions
diff --git a/src/tui_app.zig b/src/tui_app.zig index 03b32c5..788595a 100644 --- a/src/tui_app.zig +++ b/src/tui_app.zig @@ -480,10 +480,29 @@ pub const RunOptions = struct { /// 3. Decode buffered bytes -> keys -> the focused input box. /// 4. On a submitted line: drive a turn (or dispatch a slash command), /// pumping the stream's events into component state. +/// Keyboard-protocol handshake state for one session. Resolved from the +/// terminal's replies to the startup `negotiate_query`. +const Handshake = struct { + /// Kitty keyboard protocol confirmed active (non-zero flags reply seen). + kitty: bool = false, + /// We enabled the modifyOtherKeys fallback (must reset it on teardown). + mok_enabled: bool = false, + /// Handshake has resolved (a DA sentinel reply was seen). + resolved: bool = false, +}; + pub fn runLoop(app: *App, term: *Terminal, opts: RunOptions) !void { - // Negotiate bracketed paste (+ opportunistic Kitty). Teardown on exit. - term.writeAll(input_mod.negotiate_setup); - defer term.writeAll(input_mod.negotiate_teardown); + var hs: Handshake = .{}; + // Start the keyboard-protocol handshake: enable bracketed paste, push the + // Kitty flags we want, then query (Kitty flags + DA sentinel). The replies + // are consumed in `handleBytes`, which enables the modifyOtherKeys fallback + // iff the terminal turns out not to support Kitty. + term.writeAll(input_mod.negotiate_query); + defer { + input_mod.setKittyActive(false); + if (hs.mok_enabled) term.writeAll(input_mod.disable_modify_other_keys); + term.writeAll(input_mod.negotiate_teardown); + } term.hideCursor(); defer term.showCursor(); @@ -522,7 +541,7 @@ pub fn runLoop(app: *App, term: *Terminal, opts: RunOptions) !void { // 3. Decode. Prepend any retained tail, decode all complete sequences, // retain the unconsumed tail for the next read. try tail.appendSlice(app.alloc, read_buf[0..n]); - const consumed = try handleBytes(app, tail.items, opts); + const consumed = try handleBytes(app, term, &hs, tail.items, opts); // Keep the unconsumed tail. const leftover = tail.items.len - consumed; std.mem.copyForwards(u8, tail.items[0..leftover], tail.items[consumed..]); @@ -537,7 +556,7 @@ pub fn runLoop(app: *App, term: *Terminal, opts: RunOptions) !void { /// level, feed the rest to the focused input box, and act on any submitted /// line. Returns the number of bytes consumed (the unconsumed partial tail is /// retained by the caller). -fn handleBytes(app: *App, bytes: []const u8, opts: RunOptions) !usize { +fn handleBytes(app: *App, term: *Terminal, hs: *Handshake, bytes: []const u8, opts: RunOptions) !usize { var off: usize = 0; while (off < bytes.len) { const step = input_mod.decodeOne(bytes[off..]) orelse break; // partial tail @@ -555,6 +574,11 @@ fn handleBytes(app: *App, bytes: []const u8, opts: RunOptions) !usize { .paste => { app.input_box.comp().handleInput(bytes[off .. off + step.consumed]); }, + .negotiation => |neg| { + handleNegotiation(term, hs, neg); + off += step.consumed; + continue; // not a keypress; no render / submit check + }, } off += step.consumed; app.scheduler.requestRender(); @@ -570,6 +594,34 @@ fn handleBytes(app: *App, bytes: []const u8, opts: RunOptions) !usize { return off; } +/// React to a keyboard-protocol negotiation reply from the terminal. +/// +/// - A non-zero Kitty flags reply confirms the Kitty protocol: mark it active +/// and do NOT enable modifyOtherKeys (they can conflict). +/// - The DA reply is the handshake sentinel: it always arrives. If we reach it +/// without having confirmed Kitty, the terminal lacks Kitty support, so we +/// enable the modifyOtherKeys fallback (e.g. for tmux/xterm). +fn handleNegotiation(term: *Terminal, hs: *Handshake, neg: input_mod.Negotiation) void { + switch (neg) { + .kitty_flags => |flags| { + if (flags != 0 and !hs.kitty) { + hs.kitty = true; + input_mod.setKittyActive(true); + term.caps.kitty_keyboard = true; + } + }, + .device_attributes => { + if (hs.resolved) return; + hs.resolved = true; + if (!hs.kitty and !hs.mok_enabled) { + term.writeAll(input_mod.enable_modify_other_keys); + hs.mok_enabled = true; + term.caps.kitty_keyboard = false; + } + }, + } +} + /// Handle a submitted input line: slash command vs. model turn. fn handleSubmittedLine(app: *App, line: []const u8, opts: RunOptions) !void { if (line.len == 0) return; |
