1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
//! Deterministic tests for the terminal-pane pipeline, exercised without a real
//! tty/vaxis: pty -> ghostty-vt parsing -> RenderState grid, plus the input
//! encoders used to send keys/mouse back to the child.
const std = @import("std");
const posix = std.posix;
const testing = std.testing;
const ghostty_vt = @import("ghostty-vt");
extern "c" fn forkpty(amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const posix.winsize) c_int;
extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
extern "c" fn _exit(status: c_int) noreturn;
/// Read all of a cell row's codepoints into `out`, returning the slice used.
fn rowText(rs: *const ghostty_vt.RenderState, y: usize, out: []u8) []const u8 {
const cells = rs.row_data.items(.cells)[y];
var n: usize = 0;
var x: usize = 0;
while (x < rs.cols and n < out.len) : (x += 1) {
const cp = cells.get(x).raw.codepoint();
out[n] = if (cp == 0 or cp > 0x7f) ' ' else @intCast(cp);
n += 1;
}
// trim trailing spaces for convenience
while (n > 0 and out[n - 1] == ' ') n -= 1;
return out[0..n];
}
test "pty -> ghostty: child output lands in the grid with color" {
const alloc = testing.allocator;
var t: ghostty_vt.Terminal = try .init(alloc, .{ .cols = 40, .rows = 5 });
defer t.deinit(alloc);
var stream = t.vtStream();
defer stream.deinit();
// A one-shot child that prints bold-green "GREEN" then a newline.
const argv = [_:null]?[*:0]const u8{
"/bin/sh", "-c", "printf '\\033[1;32mGREEN\\033[0m\\n'", null,
};
var master: c_int = undefined;
const ws = posix.winsize{ .row = 5, .col = 40, .xpixel = 0, .ypixel = 0 };
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {
_ = execvp(argv[0].?, &argv);
_exit(127);
}
defer posix.kill(pid, posix.SIG.HUP) catch {};
// Drain the child until EOF, feeding bytes into ghostty as they arrive
// (exactly what the app's reader task does).
var buf: [1024]u8 = undefined;
while (true) {
const n = posix.read(master, &buf) catch break;
if (n == 0) break;
stream.nextSlice(buf[0..n]);
}
var rs: ghostty_vt.RenderState = .empty;
defer rs.deinit(alloc);
try rs.update(alloc, &t);
var text_buf: [40]u8 = undefined;
try testing.expectEqualStrings("GREEN", rowText(&rs, 0, &text_buf));
// The 'G' cell should be bold with a green (palette index 2) foreground.
const g = rs.row_data.items(.cells)[0].get(0);
try testing.expect(g.raw.hasStyling());
try testing.expect(g.style.flags.bold);
try testing.expectEqual(ghostty_vt.Style.Color{ .palette = 2 }, g.style.fg_color);
}
test "encodeKey: ctrl+c -> 0x03" {
// Mirrors how main.zig builds a KeyEvent from a vaxis ctrl+c.
const ev: ghostty_vt.input.KeyEvent = .{
.action = .press,
.key = .key_c,
.mods = .{ .ctrl = true },
.unshifted_codepoint = 'c',
};
var buf: [16]u8 = undefined;
var w = std.Io.Writer.fixed(&buf);
try ghostty_vt.input.encodeKey(&w, ev, .{});
try testing.expectEqualStrings("\x03", w.buffered());
}
test "encodeKey: plain 'a' -> a" {
const ev: ghostty_vt.input.KeyEvent = .{
.action = .press,
.key = .key_a,
.unshifted_codepoint = 'a',
.utf8 = "a",
};
var buf: [16]u8 = undefined;
var w = std.Io.Writer.fixed(&buf);
try ghostty_vt.input.encodeKey(&w, ev, .{});
try testing.expectEqualStrings("a", w.buffered());
}
test "encodeKey: up arrow -> CSI A" {
const ev: ghostty_vt.input.KeyEvent = .{ .action = .press, .key = .arrow_up };
var buf: [16]u8 = undefined;
var w = std.Io.Writer.fixed(&buf);
try ghostty_vt.input.encodeKey(&w, ev, .{});
try testing.expectEqualStrings("\x1b[A", w.buffered());
}
test "encodeMouse: SGR left press at cell (0,0)" {
const alloc = testing.allocator;
var t: ghostty_vt.Terminal = try .init(alloc, .{ .cols = 40, .rows = 5 });
defer t.deinit(alloc);
// Enable normal mouse tracking (1000) in SGR mode (1006), as a child app
// would, so fromTerminal picks up the right reporting mode/format.
var stream = t.vtStream();
defer stream.deinit();
stream.nextSlice("\x1b[?1000h\x1b[?1006h");
const Size = @FieldType(ghostty_vt.input.MouseEncodeOptions, "size");
var size: Size = std.mem.zeroes(Size);
size.screen = .{ .width = t.cols, .height = t.rows };
size.cell = .{ .width = 1, .height = 1 };
var opts = ghostty_vt.input.MouseEncodeOptions.fromTerminal(&t, size);
opts.any_button_pressed = true;
const ev: ghostty_vt.input.MouseEncodeEvent = .{
.action = .press,
.button = .left,
.pos = .{ .x = 0, .y = 0 },
};
var buf: [32]u8 = undefined;
var w = std.Io.Writer.fixed(&buf);
try ghostty_vt.input.encodeMouse(&w, ev, opts);
try testing.expectEqualStrings("\x1b[<0;1;1M", w.buffered());
}
|