//! 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()); }