summaryrefslogtreecommitdiff
path: root/term.zig
blob: 13b4ff85c6fd278eff1f17016b419cac60846c4d (plain) (blame)
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
const image_mod = @import("image.zig");

/// 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,
};

/// Modal input mode. normal/insert are the helix-style text-editing modes; tty
/// forwards keys to the shell (terminal panes only — file panes never use it).
///   normal  -> helix NORMAL: hjkl/w/b/e/... move a block cursor; i/a/o -> insert;
///              v/x select; Esc collapses the selection; Ctrl-b -> tty (terminals).
///   insert  -> keys insert text at the cursor (acme click-and-type); Esc -> normal.
///   tty     -> keys -> pty as terminal sequences. The mouse stays usable here (it
///              selects/copies without dropping you back to normal); Ctrl-b exits.
pub const Mode = enum { normal, insert, tty };

/// A modal line selection (helix `x` extends the selection down line by line).
/// r0/r1 are ABSOLUTE body rows (visual + scroll off). active == false means the
/// cursor is a plain 1-char selection (normal mode default).
pub const LineSel = struct {
    active: bool = false,
    r0: i32 = 0,
    r1: i32 = 0,
};

/// A modal character-range selection (helix `v` select mode). The anchor is here
/// (row/col in the same coords as the cursor: ABSOLUTE body row; content col for
/// files, visual col for terminals); the head is the cursor. Motions extend it;
/// d/c/y act on [anchor, cursor] inclusive. Mutually exclusive with LineSel.
pub const CharSel = struct {
    active: bool = false,
    row: i32 = 0,
    col: 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
    /// tree-sitter syntax styles, one byte per `content` byte (0 = none). Empty
    /// when the file's language isn't highlighted. gpa-owned. See main.highlightFile.
    highlights: []u8 = &.{},
    /// dumb whole-content undo/redo: each entry is a gpa-owned snapshot of
    /// `content`. main.pushUndo snapshots before an edit (one per insert session /
    /// delete / paste); `u`/`U` move between them. Bounded (oldest dropped).
    undo: std.ArrayList([]u8) = .empty,
    redo: std.ArrayList([]u8) = .empty,
};

/// 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,
};

/// A dumb whole-state snapshot of a terminal pane's char-mode insertions, for
/// undo/redo (the terminal analogue of File's content snapshots). Plain value —
/// the edits array has no pointers — so it copies by assignment, no allocation.
pub const EditSnap = struct {
    edits: [MAX_EDITS]Edit,
    nedits: usize,
    cur_row: i32,
    cur_col: i32,
    cur_off: u16,
};

/// 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,

/// When set, this pane displays an image (kitty graphics) instead of text. Like a
/// file pane it has no pty/reader (handle -1) and a stub emulator; the render loop
/// transmits + draws it. See image.zig. Mutually exclusive with `file`.
image: ?image_mod.View = null,

/// A freshly forked terminal greets with `ls` once it has its real geometry. Set
/// at fork; the resize loop fires the `ls` after sizing the pty, so the listing
/// wraps to the right width (sending it at fork time uses the wrong, full size).
greet: bool = false,

/// 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 mode (see Mode). DEFAULT is normal (helix): a block cursor that
/// moves with hjkl; i/a/o enter insert; Esc reaches tty on terminals.
mode: Mode = .normal,
/// one pending prefix codepoint (0 = none): `g` (gg/ge/...) and `z` (zt/zz/zb).
pending: u21 = 0,
/// modal line selection from `x` (helix extend-line). In absolute body rows.
msel: LineSel = .{},
/// modal character-range selection from `v` (helix select). Anchor is here, head
/// is the cursor. Mutually exclusive with `msel`.
vsel: CharSel = .{},
/// 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.
///
/// For terminals in normal mode cur_col is a VISUAL column; in insert mode it is
/// the original-tty column the run is anchored at (renderedCol maps it to visual).
/// For file panes cur_col is a content column (no edit runs; edits stay empty).
cur_pinned: bool = false,
cur_row: i32 = 0,
cur_col: i32 = 0,
cur_off: u16 = 0,
nedits: usize = 0,
edits: [MAX_EDITS]Edit = @splat(.{}),
/// terminal char-mode undo/redo: snapshots of the edit state, one per edit op
/// (insert session / delete / paste). File panes use File.undo/redo instead;
/// these stay empty for them. See main.pushUndo/doUndo.
ed_undo: std.ArrayList(EditSnap) = .empty,
ed_redo: std.ArrayList(EditSnap) = .empty,

/// A real shell pane (has a pty/emulator), as opposed to a file or image view.
pub fn isTerminal(self: *const Term) bool {
    return self.file == null and self.image == null;
}

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);
        if (f.highlights.len > 0) gpa.free(f.highlights);
        for (f.undo.items) |s| gpa.free(s);
        for (f.redo.items) |s| gpa.free(s);
        f.undo.deinit(gpa);
        f.redo.deinit(gpa);
    } else if (self.image) |*iv| {
        gpa.free(iv.path);
        image_mod.deinitView(iv, gpa);
    } else {
        _ = self.reader.cancel(io) catch {};
        self.pty.close(io);
    }
    self.ed_undo.deinit(gpa);
    self.ed_redo.deinit(gpa);
    self.stream.deinit();
    self.term.deinit(gpa);
}