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
|
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
/// One text selection (block-shaped), in pane-local coords; the row carries the
/// scrollback offset. .none == nothing selected.
pub const Sel = struct {
state: enum { none, dragging, done } = .none,
c0: i32 = 0,
c1: i32 = 0,
r0: i32 = 0,
r1: i32 = 0,
};
pub const EDIT_CAP = 128; // max chars in one insertion run
pub const MAX_EDITS = 16; // max concurrent insertion runs per window
/// A file pane's backing state (see the `file` field below).
pub const File = struct {
path: []const u8, // gpa-owned absolute path
content: []u8, // gpa-owned file bytes
scroll: usize = 0, // top visible file line
};
/// A run of inserted ("char mode") text. `col` is the ORIGINAL tty column it is
/// spliced in *before* — the run shifts the underlying row right, it doesn't
/// overwrite. `row`/`col` are pane-local (row carries the BOX_H tag offset).
pub const Edit = struct {
row: i32 = 0,
col: i32 = 0,
len: u16 = 0,
text: [EDIT_CAP]u8 = undefined,
};
/// Persistent per-terminal state: the pty handle, the ghostty emulator and its
/// (stateful) VT parser, the background reader task, plus its layout weight and
/// text selection.
///
/// Heap-allocate these and never move them: `stream`'s handler holds a pointer
/// back into `term`, so the address must stay stable for parser state split
/// across pty reads to keep working.
pub const Term = @This();
pty: std.Io.File,
pid: std.posix.pid_t, // the shell child; used to read /proc/<pid>/cwd for the tag
term: ghostty_vt.Terminal,
stream: ghostty_vt.TerminalStream,
reader: std.Io.Future(anyerror!void),
/// When set, this pane is a (read/editable) text FILE view, not a terminal: its
/// body comes from `content` instead of the emulator, and all the tty-specific
/// handling (pty io, modes, prompt hiding) is skipped. Everything else — layout,
/// selection, char-mode editing — is shared. pty.handle is -1, reader is a no-op.
file: ?File = null,
/// current emulator grid size; we only push a resize when it actually changes.
cols: u16,
rows: u16,
/// vertical weight within the column (siblings split the column height by this).
vweight: f32 = 1,
/// One independent selection per mouse button: 0=left, 1=middle, 2=right.
sel: [3]Sel = .{ .{}, .{}, .{} },
/// modal input. true = "text"/char mode (DEFAULT; keys insert text at the
/// cursor, shell prompts hidden), false = "tty"/sym mode (keys -> pty as
/// terminal sequences, prompts shown). Esc toggles to tty; a left-click returns
/// to text mode and anchors the cursor.
///
/// The cursor and edits are anchored at ABSOLUTE body rows (visual row + scroll
/// offset) so they ride the scrollback, not the screen. `cur_off` is the offset
/// into the run sitting at (cur_row, cur_col). `cur_pinned` is false until the
/// user clicks or types, while which the cursor tracks the shell's cursor.
editing: bool = true,
cur_pinned: bool = false,
cur_row: i32 = 0,
cur_col: i32 = 0,
cur_off: u16 = 0,
nedits: usize = 0,
edits: [MAX_EDITS]Edit = @splat(.{}),
pub fn deinit(self: *Term, io: std.Io, gpa: std.mem.Allocator) void {
if (self.file) |f| {
gpa.free(f.path);
gpa.free(f.content);
} else {
_ = self.reader.cancel(io) catch {};
self.pty.close(io);
}
self.stream.deinit();
self.term.deinit(gpa);
}
|