summaryrefslogtreecommitdiff
path: root/src/tty/tty.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 17:14:21 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit3c981a1b9f3e1328e8ee4b0181d90b7c87f5061a (patch)
treed0d37e173941fbc9944d2546d92ae0b3ed26d0f0 /src/tty/tty.zig
parent9babe3a63ca06b4b0248d1d21ccb74687c5b188d (diff)
downloadpardes-3c981a1b9f3e1328e8ee4b0181d90b7c87f5061a.tar.gz
pardes-3c981a1b9f3e1328e8ee4b0181d90b7c87f5061a.zip
A lone ESC read by itself is the Escape key at once, not after a 50 ms wait
Diffstat (limited to 'src/tty/tty.zig')
-rw-r--r--src/tty/tty.zig70
1 files changed, 63 insertions, 7 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 01d2f08e..84552ddc 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -148,14 +148,19 @@ fn readInput(loop: *Loop, tty: anytype, cache: *vaxis.GraphemeCache) !void {
if (scalar_len > end - (lead - 1)) parse_end = lead - 1;
}
var consumed: usize = 0;
- var waited = false;
+ // A read that is one ESC and nothing else is the Escape key: a
+ // terminal writes each key's sequence whole and ssh carries each of
+ // its writes whole, so no sequence arrives cut right after its ESC.
+ // Waiting on it cost every Escape press 50 ms on a legacy terminal
+ // (tmux, most ssh sessions), and 1 s before kitty's flags were on.
+ var waited = carried == 0 and received == 1;
while (consumed < parse_end) {
- // A lone ESC that ends the read is the Escape key, or the first
- // byte of a sequence whose rest the read boundary held back (an
- // ssh packet, a busy link), which parsed now left the rest to be
- // typed as text. Wait a moment for more. Under the kitty keyboard
- // protocol Escape is `CSI 27 u`, so there the ESC can only start
- // a sequence and the wait can be long.
+ // A lone ESC that ends a longer read is the Escape key, or the
+ // first byte of a sequence whose rest the read boundary held
+ // back (an ssh packet, a busy link), which parsed now left the
+ // rest to be typed as text. Wait a moment for more. Under the
+ // kitty keyboard protocol Escape is `CSI 27 u`, so there the ESC
+ // can only start a sequence and the wait can be long.
const rest = buf[consumed..parse_end];
if (!waited and parse_end == end and rest[0] == 0x1b and (rest.len == 1 or (rest.len == 2 and rest[1] == 0x1b))) {
waited = true;
@@ -455,6 +460,57 @@ test "terminal input split at every byte with a gap reads as it does whole" {
vx.caps.kitty_keyboard = false;
}
+test "an ESC read by itself is the Escape key at once" {
+ if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const io = std.testing.io;
+ var env = try std.testing.environ.createMap(gpa);
+ defer env.deinit();
+ var vx = try vaxis.init(io, gpa, &env, .{});
+ var output: std.Io.Writer.Allocating = .init(gpa);
+ defer output.deinit();
+ defer vx.deinit(gpa, &output.writer);
+ var tty: vaxis.Tty = undefined;
+ var loop: Loop = .init(io, &tty, &vx);
+ var cache: vaxis.GraphemeCache = .{};
+ // More always follows at once: a reader that waited on the ESC would
+ // take `j` for the rest of its sequence (Alt-j).
+ const Reader = struct {
+ parts: []const []const u8,
+ next: usize = 0,
+ asked: bool = false,
+ fn getWinsize(_: *@This()) !vaxis.Winsize {
+ return .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 };
+ }
+ fn inputFollows(self: *@This(), _: u32) bool {
+ self.asked = true;
+ return self.next < self.parts.len;
+ }
+ fn read(self: *@This(), buf: []u8) !usize {
+ if (self.next == self.parts.len) return 0;
+ const part = self.parts[self.next];
+ self.next += 1;
+ @memcpy(buf[0..part.len], part);
+ return part.len;
+ }
+ };
+ for ([_]bool{ false, true }) |kitty| {
+ vx.caps.kitty_keyboard = kitty;
+ var reader: Reader = .{ .parts = &.{ "\x1b", "j" } };
+ try readInput(&loop, &reader, &cache);
+ try std.testing.expect(!reader.asked);
+ try std.testing.expectEqual(.winsize, std.meta.activeTag((try loop.tryEvent()).?));
+ const escape = (try loop.tryEvent()).?.key_press;
+ try std.testing.expectEqual(vaxis.Key.escape, escape.codepoint);
+ try std.testing.expect(!escape.mods.alt);
+ const j = (try loop.tryEvent()).?.key_press;
+ try std.testing.expectEqual(@as(u21, 'j'), j.codepoint);
+ try std.testing.expect(!j.mods.alt);
+ try std.testing.expect(try loop.tryEvent() == null);
+ }
+ vx.caps.kitty_keyboard = false;
+}
+
test "terminal input cancellation joins blocked reads and queued EOF" {
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
const gpa = std.testing.allocator;