summaryrefslogtreecommitdiff
path: root/src/term_pane.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/term_pane.zig')
-rw-r--r--src/term_pane.zig76
1 files changed, 70 insertions, 6 deletions
diff --git a/src/term_pane.zig b/src/term_pane.zig
index 9f13832f..1243dfa1 100644
--- a/src/term_pane.zig
+++ b/src/term_pane.zig
@@ -18,6 +18,7 @@ const Pane = pardes.Pane;
const EditSnap = pardes.EditSnap;
const Ovl = pardes.Ovl;
const modal = @import("modal.zig");
+const config = @import("config.zig");
/// The memo behind `shellRows`. ONE entry for the editor, because the motion
/// surface is built for the pane the cursor is in and a second pane asking
@@ -78,6 +79,65 @@ const Rows = struct {
rows: [][]const u8,
};
+/// What LEAVING raw tty mode does to one prompt row, decided from its cells
+/// alone. See config.tty_blank for why any of this happens.
+pub const PromptCut = union(enum) {
+ /// show the row exactly as ghostty dumped it
+ keep,
+ /// show nothing at all
+ blank,
+ /// drop this many leading COLUMNS — the prompt — and keep the rest, which
+ /// is what was typed at it
+ cut: usize,
+};
+
+/// The prompt and the command typed at it share a grid row, and OSC 133 marks
+/// them apart CELL by cell (`Cell.semantic_content` is output / input /
+/// prompt). The row flag every caller tests first is only ghostty's "some cell
+/// in here is a prompt cell" index; taking the row on that flag alone is what
+/// used to throw the command away with the prompt.
+pub fn promptCut(pin: ghostty_vt.Pin) PromptCut {
+ if (config.tty_blank == .prompt_and_input) return .blank;
+ const cells = pin.cells(.all);
+ var cols: usize = 0;
+ while (cols < cells.len and cells[cols].semantic_content == .prompt) cols += 1;
+ // Flagged, but with no prompt cells at the FRONT: a right-side prompt, or
+ // a repaint that has moved on. Nothing here is the prompt, so hide nothing.
+ if (cols == 0) return .keep;
+ // ...and all prompt, nothing typed yet: the row is chrome end to end.
+ if (cols >= cells.len) return .blank;
+ return .{ .cut = cols };
+}
+
+/// That decision applied to `raw`, the line ghostty dumped for `pin`'s row.
+/// Always a slice OF `raw` — dropping the prompt is a left-hug, so the command
+/// starts at column 0 with no run of blanks in front of it where the prompt
+/// used to be, and there is nothing to allocate or copy anywhere.
+///
+/// Walking the dump rather than rebuilding the row out of cells keeps ghostty
+/// the single authority on how a cell spells itself — wide glyphs, combining
+/// marks and all. One non-spacer cell is one dumped grapheme, and that is what
+/// makes the cell walk and the byte walk stay in step.
+pub fn promptRow(pin: ghostty_vt.Pin, raw: []const u8) []const u8 {
+ const cols = switch (promptCut(pin)) {
+ .keep => return raw,
+ .blank => return "",
+ .cut => |n| n,
+ };
+ const cells = pin.cells(.all);
+ var at: usize = 0;
+ var col: usize = 0;
+ while (col < cols and at < raw.len) {
+ const cell = &cells[col];
+ var cps: usize = 1;
+ if (pin.grapheme(cell)) |extra| cps += extra.len;
+ for (0..cps) |_| at = modal.nextGrapheme(raw, at);
+ // the tail cell of a wide glyph spells nothing of its own
+ col += if (cell.wide == .wide) @as(usize, 2) else 1;
+ }
+ return std.mem.trimEnd(u8, raw[at..], " \t");
+}
+
/// 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
@@ -130,8 +190,10 @@ fn buildRows(alloc: std.mem.Allocator, p: *Pardes, pane: *Pane) !Rows {
var n: usize = 0;
var it = std.mem.splitScalar(u8, full, '\n');
while (it.next()) |raw| {
- const is_prompt = if (pit.next()) |pin| pin.rowAndCell().row.semantic_prompt != .none else false;
- const shown = if (is_prompt) "" else raw;
+ const shown = if (pit.next()) |pin|
+ if (pin.rowAndCell().row.semantic_prompt != .none) promptRow(pin, raw) else raw
+ else
+ raw;
if (n > 0) {
text[at] = '\n';
at += 1;
@@ -159,11 +221,13 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
var lines = std.mem.splitScalar(u8, raw, '\n');
var n: usize = 0;
while (lines.next()) |ln| {
- const is_prompt = if (pane.mode != .tty)
- if (prompts.next()) |pin| pin.rowAndCell().row.semantic_prompt != .none else false
+ vp[n] = if (pane.mode != .tty)
+ if (prompts.next()) |pin|
+ if (pin.rowAndCell().row.semantic_prompt != .none) promptRow(pin, ln) else ln
+ else
+ ln
else
- false;
- vp[n] = if (is_prompt) "" else ln;
+ ln;
n += 1;
}
std.debug.assert(n == vp.len);