diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 14:11:05 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | f2c0c6f1aa90a567dc9d37c5eb8f53ece58cff88 (patch) | |
| tree | 8d758a9da26c495df9076c8a473225087667ebbe /src | |
| parent | 4e224b7c7e67fafcbe7359fcf534048e254b701d (diff) | |
| download | pardes-f2c0c6f1aa90a567dc9d37c5eb8f53ece58cff88.tar.gz pardes-f2c0c6f1aa90a567dc9d37c5eb8f53ece58cff88.zip | |
Terminal replies and malformed escape sequences never become typed text
Diffstat (limited to 'src')
| -rw-r--r-- | src/tty/tty.zig | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig index a6f6bdfc..8992b51b 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -243,6 +243,85 @@ test "terminal input preserves fragmented keys queries paste and text after EOF" try std.testing.expect(resized and up and pasted and clipboard and escape); } +test "terminal replies and malformed sequences never become typed text" { + 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, .{ .system_clipboard_allocator = gpa }); + var output: std.Io.Writer.Allocating = .init(gpa); + defer output.deinit(); + defer vx.deinit(gpa, &output.writer); + vx.queries_done.store(false, .unordered); + var tty: vaxis.Tty = undefined; + var loop: Loop = .init(io, &tty, &vx); + var cache: vaxis.GraphemeCache = .{}; + const Reader = struct { + parts: []const []const u8, + next: usize = 0, + fn getWinsize(_: *@This()) !vaxis.Winsize { + return .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }; + } + 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; + } + }; + var reader: Reader = .{ + .parts = &.{ + // The startup width queries' answers where the terminal has no + // explicit width: both used to arrive as a bare F3, typed as text. + "a\x1b[1;1R\x1b[1;1R", + // A late report somewhere else on the screen, and DECXCPR. + "\x1b[12;40R\x1b[?3;7;1R", + // rxvt's Alt+Up: ESC before the sequence, not Alt+Esc then `[A`. + "b\x1b\x1b[A", + // A kitty graphics reply cut inside its ST sliced past its input. + "\x1b_Gi=1;OK\x1b", + "\\", + // An X10 mouse report cut after `CSI M` left three bytes as text. + "\x1b[M", + " !!", + // Malformed color and clipboard replies failed the parse, which + // ended input altogether; and a reply ended by BEL ran on to the + // next sequence's ESC, leaving what lay between as text. + "\x1b]11;rgb:zz/zz/zz\x1b\\\x1b]52;c;!!!!\x07\x1b]52\x07", + // Device reports whose fields end early. + "\x1b[?1;y\x1b[?997;n\x1b[c", + "\x1b[?62;c", + "c", + }, + }; + try readInput(&loop, &reader, &cache); + try std.testing.expect(vx.queries_done.load(.unordered)); + try std.testing.expect(vx.caps.kitty_graphics); + var text: std.ArrayList(u8) = .empty; + defer text.deinit(gpa); + var alt_up = false; + var clicked = false; + while (try loop.tryEvent()) |event| switch (event) { + .winsize => {}, + .key_press => |key| { + if (key.codepoint == vaxis.Key.up and key.mods.alt) { + alt_up = true; + } else if (key.text) |bytes| { + try text.appendSlice(gpa, bytes); + } else return error.UnexpectedInputKey; + }, + .mouse => |mouse| { + try std.testing.expectEqual(@as(i16, 0), mouse.col); + clicked = true; + }, + else => return error.UnexpectedInputEvent, + }; + try std.testing.expectEqualStrings("abc", text.items); + try std.testing.expect(alt_up and clicked); +} + test "terminal input cancellation joins blocked reads and queued EOF" { if (comptime builtin.os.tag == .windows) return error.SkipZigTest; const gpa = std.testing.allocator; |
