//! Terminal panes: everything a Pane does BECAUSE it owns a ghostty-vt //! emulator — reading the grid back out as text (for motions and for the //! body), translating the emulator's own cell styles into ours, the pty //! replies ghostty hands back through a callback, and the edit-buffer undo //! snapshots that only exist because a terminal's "content" is a live grid //! rather than a []u8. //! //! What is NOT here: the edit buffer's SEMANTICS (Pane.ovl, surfRow/gridRow) //! live on Pane in pardes.zig, because the motion, insert and undo machinery //! is shared with file panes verbatim — a terminal in insert mode behaving //! exactly like a text file is the whole design, and splitting the row math //! away from the code that uses it would only hide that. const std = @import("std"); const ghostty_vt = @import("ghostty-vt"); const pardes = @import("pardes.zig"); const Pardes = pardes.Pardes; const Pane = pardes.Pane; const EditSnap = pardes.EditSnap; const Ovl = pardes.Ovl; const modal = @import("modal.zig"); /// A terminal's shell rows as the surface sees them: the WHOLE /// history+active grid, prompt rows blanked (OSC 133), absolute grid rows /// from 0. The raw material the motion surface is composed from — the /// edit buffer is NOT applied here, so it is also what seeding the buffer /// reads. Scratch-arena backed. /// ghostty's dump trims the grid's trailing blank rows; ONE of them is /// kept back, the row the cursor sits on below the last line of output. /// That row is a file's final newline: without it the surface would have /// one line fewer than the same text in a document, and every motion and /// linewise edit at the bottom would diverge. /// ponytail: O(scrollback) dump+scan per keystroke — window it around the /// viewport if a multi-MB scrollback ever makes navigation lag. pub fn shellRows(p: *Pardes, pane: *Pane) ![]const []const u8 { const arena = p.scratch.allocator(); const full = try pane.vt.screens.active.dumpStringAlloc(arena, .{ .screen = .{} }); var pit = pane.vt.screens.active.pages.rowIterator(.right_down, .{ .screen = .{} }, null); var ls: std.ArrayList([]const u8) = .empty; var it = std.mem.splitAny(u8, full, "\n"); while (it.next()) |raw| { const is_prompt = if (pit.next()) |pin| pin.rowAndCell().row.semantic_prompt != .none else false; try ls.append(arena, if (is_prompt) "" else raw); } try ls.append(arena, ""); return ls.items; } /// The body a terminal renders: the viewport's shell rows (prompt rows blanked /// outside tty mode) with the edit buffer's lines standing in for the rows it /// covers, so what you see is what the motions move over. pub fn bodyText(arena: std.mem.Allocator, pane: *Pane) ![]const u8 { const raw = try pane.vt.plainString(arena); var prompts = pane.vt.screens.active.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); var vp: std.ArrayList([]const u8) = .empty; var lines = std.mem.splitScalar(u8, raw, '\n'); while (lines.next()) |ln| { const is_prompt = if (pane.mode != .tty) if (prompts.next()) |pin| pin.rowAndCell().row.semantic_prompt != .none else false else false; try vp.append(arena, if (is_prompt) "" else ln); } const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); const off = pane.scroll(); var g: i32 = pane.gridRow(off); // the buffer can start above the viewport: drop the lines scrolled past var skip: usize = if (pane.ovl) |o| @intCast(@max(0, off - pane.surfRow(o.row))) else 0; var out: std.ArrayList(u8) = .empty; var n: usize = 0; while (n < pane.rows) { if (pane.mode != .tty) if (pane.ovl) |o| if (g == o.row) { var bit = std.mem.splitScalar(u8, o.text, '\n'); var k: usize = 0; while (bit.next()) |ln| : (k += 1) { if (k < skip) continue; if (n >= pane.rows) break; if (n > 0) try out.append(arena, '\n'); try out.appendSlice(arena, ln); n += 1; } skip = 0; g += o.rows; continue; }; if (n > 0) try out.append(arena, '\n'); const vi = g - goff; if (vi >= 0 and @as(usize, @intCast(vi)) < vp.items.len) try out.appendSlice(arena, vp.items[@intCast(vi)]); n += 1; g += 1; } return out.items; } /// tty colors: recolor each body cell from the emulator's own style so shell /// output keeps its ansi colors (gated on the Colors toggle by the caller). /// Rows the edit buffer stands in for are OURS, not the emulator's: they keep /// the plain body style, and the rows under them are read from the shell row /// the surface actually shows there. pub fn recolorAnsi(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, body_h: u16, off: i32) void { const s = &p.surface; const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); var vr: u16 = 0; while (vr < body_h and vr < pane.rows) : (vr += 1) { const grid = pane.gridRow(off + @as(i32, vr)) - goff; if (pane.mode != .tty) if (pane.ovl) |o| { if (off + @as(i32, vr) >= o.row and off + @as(i32, vr) < o.row + @as(i32, @intCast(modal.lineCount(o.text)))) continue; }; if (grid < 0) continue; var c: u16 = 0; while (c < tw) : (c += 1) { const ci = pane.vt.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(c), .y = @intCast(grid) } }) orelse continue; if (ci.cell.wide == .spacer_tail) continue; const cell = s.at(tx + c, r.y + pardes.BOX_H + vr); if (cell.default) continue; cell.style = cellStyle(p, ci); } } } pub fn cellStyle(p: *Pardes, ci: ghostty_vt.PageList.Cell) pardes.CellStyle { const style = ci.style(); var cs: pardes.CellStyle = .{ .fg = ghostColor(p, style.fg_color, false), .bg = ghostColor(p, style.bg_color, true), .bold = style.flags.bold, .dim = style.flags.faint, .italic = style.flags.italic, .blink = style.flags.blink, .reverse = style.flags.inverse, .invisible = style.flags.invisible, .strikethrough = style.flags.strikethrough, .ul = switch (style.flags.underline) { .none => .off, .single => .single, .double => .double, .curly => .curly, .dotted => .dotted, .dashed => .dashed, }, }; switch (ci.cell.content_tag) { .bg_color_palette => cs.bg = palColor(p, ci.cell.content.color_palette.data), .bg_color_rgb => { const rgb = ci.cell.content.color_rgb; cs.bg = .{ .rgb = .{ rgb.r, rgb.g, rgb.b } }; }, else => {}, } return cs; } pub fn palColor(p: *Pardes, idx: u8) pardes.Color { if (p.theme().palette) |pal| if (idx < 16) return .{ .rgb = pal[idx] }; return .{ .index = idx }; } pub fn ghostColor(p: *Pardes, color: ghostty_vt.Style.Color, is_bg: bool) pardes.Color { return switch (color) { .none => blk: { const t = if (is_bg) p.theme().bg else p.theme().fg; break :blk if (t) |c| .{ .rgb = c } else .default; }, .palette => |idx| palColor(p, idx), .rgb => |rgb| .{ .rgb = .{ rgb.r, rgb.g, rgb.b } }, }; } /// executing at a prompt with typed text below it: pad the output area /// with newlines so the command's output doesn't overwrite the buffer pub fn padOutputBelowEdits(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; const o = pane.ovl orelse return; if (!pane.isTerminal()) return; if (!pane.vt.cursorIsAtPrompt()) return; // the buffer's LAST surface row: its lines may outnumber the shell // rows it covers, and it is the bottom one output must clear const max_row = o.row + @as(i32, @intCast(modal.lineCount(o.text))) - 1; const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); const cursor_abs = pane.surfRow(goff + @as(i32, @intCast(pane.vt.screens.active.cursor.y))); const pad = std.math.clamp(max_row - cursor_abs, 0, @as(i32, pane.rows)); var i: i32 = 0; while (i < pad) : (i += 1) p.emitWrite(id, "\r"); } /// the snapshot takes ownership of a COPY of the edit buffer's text pub fn snap(p: *Pardes, pane: *Pane) ?EditSnap { var ovl: ?Ovl = null; if (pane.ovl) |o| ovl = .{ .row = o.row, .rows = o.rows, .text = p.gpa.dupe(u8, o.text) catch return null }; return .{ .ovl = ovl, .cur_row = pane.cur_row, .cur_col = pane.cur_col, .vsel = pane.vsel }; } /// undo/redo restores the selection recorded with the snapshot (helix /// keeps selections in its history transactions) pub fn restoreSnap(p: *Pardes, pane: *Pane, s: EditSnap) void { if (pane.ovl) |o| p.gpa.free(o.text); pane.ovl = s.ovl; pane.cur_row = s.cur_row; pane.cur_col = s.cur_col; pane.cur_pinned = true; pane.vsel = s.vsel; pane.msel.active = false; pane.ensureCursorVisible(); } // ghostty calls this with a reply (cursor-position report, DA, ...) to send // back to the child as if it typed it. The handler's `terminal` is our Pane.vt // field; recover the Pane and stash the bytes — sync() drains them into write // effects (the callback has no path to the effect queue). pub fn ptyReport(handler: *ghostty_vt.TerminalStream.Handler, data: [:0]const u8) void { const pane: *Pane = @fieldParentPtr("vt", handler.terminal); const room = pane.reply.len - pane.reply_len; const n = @min(room, data.len); @memcpy(pane.reply[pane.reply_len..][0..n], data[0..n]); pane.reply_len += @intCast(n); } const DeviceAttrs = @typeInfo(@typeInfo(@typeInfo( @FieldType(ghostty_vt.TerminalStream.Handler.Effects, "device_attributes"), ).optional.child).pointer.child).@"fn".return_type.?; pub fn ptyDeviceAttrs(_: *ghostty_vt.TerminalStream.Handler) DeviceAttrs { return .{}; }