diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-24 10:56:05 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 09:42:07 -0300 |
| commit | c3d0b84b7961ae26d2d654e7120821cc2d83d20d (patch) | |
| tree | 0b9a060a8ff0ee7d83c8df5ca797a99e0d66c53e /src/term_pane.zig | |
| parent | 70bde600793ea70bd68832018a154671c6bf1512 (diff) | |
| download | pardes-c3d0b84b7961ae26d2d654e7120821cc2d83d20d.tar.gz pardes-c3d0b84b7961ae26d2d654e7120821cc2d83d20d.zip | |
host: the core owns the event loop; every platform becomes a vtable of optional methods
Diffstat (limited to 'src/term_pane.zig')
| -rw-r--r-- | src/term_pane.zig | 99 |
1 files changed, 77 insertions, 22 deletions
diff --git a/src/term_pane.zig b/src/term_pane.zig index cc0cc24e..7662292e 100644 --- a/src/term_pane.zig +++ b/src/term_pane.zig @@ -954,14 +954,12 @@ fn fillBody(dst: ?[]u8, pane: *Pane, viewport: []const []const u8) usize { return written; } -/// 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 { +/// tty colors: recolor each visible body cell from the emulator's own style so +/// raw output keeps its ansi colors. Runs ONLY in tty mode (the caller gates +/// it) and reads the live viewport row for row: normal/insert editing shows +/// plain text, so nothing an edit does can move a shell row's colour. +pub fn recolorAnsi(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, body_h: u16) void { const s = &p.surface; - const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; var filtered_storage: FilteredColors = undefined; const filtered: ?*FilteredColors = if (pane.tty_filter) blk: { @@ -970,25 +968,15 @@ pub fn recolorAnsi(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bo } else null; 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; - // bodyText already gave a wide glyph's empty surface tail the - // head's body style. Ordinary ANSI projection historically leaves - // that alone; Filter must replace it too or one half of the cell - // can retain a host/default colour (Ghostty gives the tail the - // same style id as its wide head). + const ci = pane.vt.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(c), .y = @intCast(vr) } }) orelse continue; + // Ghostty gives a wide glyph's spacer tail the head's style id; + // ordinary projection leaves it, a filter must repaint it too. if (ci.cell.wide == .spacer_tail and filtered == null) continue; const cell = s.at(tx + c, body_y + vr); - // In ordinary ANSI mode retain the old sparse projection: bodyText - // already painted every visible glyph. A filter paints the whole - // VT viewport so even empty/default and background-only cells are - // RGB-keyed by the theme rather than leaking the host defaults. + // sparse projection: bodyText already painted every glyph, so only + // a filter (which theme-keys blank/default cells too) touches these. if (cell.default and filtered == null) continue; cell.default = false; cell.style = cellStyle(p, ci, filtered); @@ -1340,6 +1328,73 @@ test "terminal Filter preserves exact palette-null light theme default roles" { try testing.expectEqual(pardes.Color{ .rgb = light.bg.? }, reversed.at(tx, body_y).style.fg); try testing.expectEqual(pardes.Color{ .rgb = light.fg.? }, reversed.at(tx, body_y).style.bg); } +test "tty ansi colors render only in tty mode, never in normal mode" { + const testing = std.testing; + const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 6 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const pane = p.panes[0].?; + pane.tty_filter = false; + + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;A\x1b\\\x1b[32mPP\x1b]133;B\x1b\\\x1b[31mR\x1b[34mB\x1b[0m out" } }); + + var frame = std.heap.ArenaAllocator.init(testing.allocator); + defer frame.deinit(); + const r = p.rects[0]; + const tx = r.x + config.GUTTER; + const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const red: pardes.Color = .{ .index = 1 }; + const blue: pardes.Color = .{ .index = 4 }; + + // tty mode projects the emulator's ansi colours cell for cell. + pane.mode = .tty; + p.shell_rows.stale = true; + const tty = try p.render(frame.allocator()); + try testing.expectEqual(red, tty.at(tx + 2, body_y).style.fg); + try testing.expectEqual(blue, tty.at(tx + 3, body_y).style.fg); + + // normal mode is a plain editing view: no ansi projection at all, so an + // edit made here cannot change what tty mode renders. + pane.mode = .normal; + p.shell_rows.stale = true; + _ = frame.reset(.retain_capacity); + const norm = try p.render(frame.allocator()); + try testing.expectEqualStrings("R", norm.at(tx, body_y).grapheme()); + try testing.expect(!std.meta.eql(red, norm.at(tx, body_y).style.fg)); + try testing.expect(!std.meta.eql(blue, norm.at(tx + 1, body_y).style.fg)); +} + +test "an edit overlay never changes tty-mode ansi colors" { + const testing = std.testing; + const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 6 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const pane = p.panes[0].?; + pane.tty_filter = false; + pane.mode = .tty; + + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[31mAAA\x1b[0m\r\n\x1b[32mBBB\x1b[0m\r\n\x1b[34mCCC\x1b[0m" } }); + + var frame = std.heap.ArenaAllocator.init(testing.allocator); + defer frame.deinit(); + const r = p.rects[0]; + const tx = r.x + config.GUTTER; + const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const blue: pardes.Color = .{ .index = 4 }; + + p.shell_rows.stale = true; + const before = try p.render(frame.allocator()); + try testing.expectEqualStrings("C", before.at(tx, body_y + 2).grapheme()); + try testing.expectEqual(blue, before.at(tx, body_y + 2).style.fg); + + // A lingering multi-line edit overlay must not move any shell row's colour. + pane.ovl = .{ .row = 0, .rows = 1, .text = try p.gpa.dupe(u8, "e\nd\ni\nt") }; + p.shell_rows.stale = true; + _ = frame.reset(.retain_capacity); + const after = try p.render(frame.allocator()); + try testing.expectEqualStrings("C", after.at(tx, body_y + 2).grapheme()); + try testing.expectEqual(blue, after.at(tx, body_y + 2).style.fg); +} pub fn palColor(p: *Pardes, idx: u8) pardes.Color { if (p.theme().palette) |pal| if (idx < 16) return .{ .rgb = pal[idx] }; |
