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