mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 11:35:48 +00:00
terminal: formatting feedback
This commit is contained in:
@@ -1120,18 +1120,27 @@ pub const PageFormatter = struct {
|
||||
|
||||
// If we have a zero value, then we accumulate a counter. We
|
||||
// only want to turn zero values into spaces if we have a non-zero
|
||||
// char sometime later. However, for styled formats (VT, HTML), if
|
||||
// the cell has styling (e.g., background color), we must emit it
|
||||
// to preserve the visual appearance.
|
||||
const dominated_by_style = self.opts.emit.styled() and
|
||||
(!cell.isEmpty() or cell.hasStyling());
|
||||
if (!dominated_by_style and !cell.hasText()) {
|
||||
blank_cells += 1;
|
||||
continue;
|
||||
}
|
||||
if (cell.codepoint() == ' ' and self.opts.trim and !dominated_by_style) {
|
||||
blank_cells += 1;
|
||||
continue;
|
||||
// char sometime later.
|
||||
blank: {
|
||||
// If we're emitting styled output (not plaintext) and
|
||||
// the cell has some kind of styling or is not empty
|
||||
// then this isn't blank.
|
||||
if (self.opts.emit.styled() and
|
||||
(!cell.isEmpty() or cell.hasStyling())) break :blank;
|
||||
|
||||
// Cells with no text are blank
|
||||
if (!cell.hasText()) {
|
||||
blank_cells += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trailing spaces are blank. We know it is trailing
|
||||
// because if we get a non-empty cell later we'll
|
||||
// fill the blanks.
|
||||
if (cell.codepoint() == ' ' and self.opts.trim) {
|
||||
blank_cells += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// This cell is not blank. If we have accumulated blank cells
|
||||
@@ -1169,79 +1178,72 @@ pub const PageFormatter = struct {
|
||||
blank_cells = 0;
|
||||
}
|
||||
|
||||
style: {
|
||||
// If we aren't emitting styled output then we don't
|
||||
// have to worry about styles.
|
||||
if (!self.opts.emit.styled()) break :style;
|
||||
|
||||
// Get our cell style.
|
||||
const cell_style = self.cellStyle(cell);
|
||||
|
||||
// If the style hasn't changed, don't bloat output.
|
||||
if (cell_style.eql(style)) break :style;
|
||||
|
||||
// If we had a previous style, we need to close it,
|
||||
// because we've confirmed we have some new style
|
||||
// (which is maybe default).
|
||||
if (!style.default()) switch (self.opts.emit) {
|
||||
.html => try self.formatStyleClose(writer),
|
||||
|
||||
// For VT, we only close if we're switching to a default
|
||||
// style because any non-default style will emit
|
||||
// a \x1b[0m as the start of a VT coloring sequence.
|
||||
.vt => if (cell_style.default()) try self.formatStyleClose(writer),
|
||||
|
||||
// Unreachable because of the styled() check at the
|
||||
// top of this block.
|
||||
.plain => unreachable,
|
||||
};
|
||||
|
||||
// At this point, we can copy our style over
|
||||
style = cell_style;
|
||||
|
||||
// If we're just the default style now, we're done.
|
||||
if (cell_style.default()) break :style;
|
||||
|
||||
// New style, emit it.
|
||||
try self.formatStyleOpen(
|
||||
writer,
|
||||
&style,
|
||||
);
|
||||
|
||||
// If we have a point map, we map the style to
|
||||
// this cell.
|
||||
if (self.point_map) |*map| {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
try self.formatStyleOpen(
|
||||
&discarding.writer,
|
||||
&style,
|
||||
);
|
||||
for (0..discarding.count) |_| map.map.append(map.alloc, .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
}) catch return error.WriteFailed;
|
||||
}
|
||||
}
|
||||
|
||||
switch (cell.content_tag) {
|
||||
// We combine codepoint and graphemes because both have
|
||||
// shared style handling. We use comptime to dup it.
|
||||
inline .codepoint, .codepoint_grapheme => |tag| {
|
||||
// Handle closing our styling if we go back to unstyled
|
||||
// content.
|
||||
if (self.opts.emit.styled() and
|
||||
!cell.hasStyling() and
|
||||
!style.default())
|
||||
{
|
||||
try self.formatStyleClose(writer);
|
||||
style = .{};
|
||||
}
|
||||
|
||||
// If we're emitting styling and we have styles, then
|
||||
// we need to load the style and emit any sequences
|
||||
// as necessary.
|
||||
if (self.opts.emit.styled() and cell.hasStyling()) style: {
|
||||
// Get the style.
|
||||
const cell_style = self.page.styles.get(
|
||||
self.page.memory,
|
||||
cell.style_id,
|
||||
);
|
||||
|
||||
// If the style hasn't changed since our last
|
||||
// emitted style, don't bloat the output.
|
||||
if (cell_style.eql(style)) break :style;
|
||||
|
||||
// We need to emit a closing tag if the style
|
||||
// was non-default before, which means we set
|
||||
// styles once.
|
||||
const closing = !style.default();
|
||||
|
||||
// New style, emit it.
|
||||
style = cell_style.*;
|
||||
try self.formatStyleOpen(
|
||||
writer,
|
||||
&style,
|
||||
closing,
|
||||
);
|
||||
|
||||
// If we have a point map, we map the style to
|
||||
// this cell.
|
||||
if (self.point_map) |*map| {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
try self.formatStyleOpen(
|
||||
&discarding.writer,
|
||||
&style,
|
||||
closing,
|
||||
);
|
||||
for (0..discarding.count) |_| map.map.append(map.alloc, .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
}) catch return error.WriteFailed;
|
||||
}
|
||||
}
|
||||
|
||||
// For styled cells without text, emit a space to carry the styling
|
||||
if (cell.hasText()) {
|
||||
try self.writeCell(tag, writer, cell);
|
||||
} else {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
try self.writeCell(tag, writer, cell);
|
||||
|
||||
// If we have a point map, all codepoints map to this
|
||||
// cell.
|
||||
if (self.point_map) |*map| {
|
||||
const byte_count: usize = if (cell.hasText()) count: {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
try self.writeCell(tag, &discarding.writer, cell);
|
||||
break :count discarding.count;
|
||||
} else 1;
|
||||
for (0..byte_count) |_| map.map.append(map.alloc, .{
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
try self.writeCell(tag, &discarding.writer, cell);
|
||||
for (0..discarding.count) |_| map.map.append(map.alloc, .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
}) catch return error.WriteFailed;
|
||||
@@ -1250,21 +1252,12 @@ pub const PageFormatter = struct {
|
||||
|
||||
// Cells with only background color (no text). Emit a space
|
||||
// with the appropriate background color SGR sequence.
|
||||
.bg_color_palette => {
|
||||
const index = cell.content.color_palette;
|
||||
try self.emitBgColorSgr(writer, index, null, &style);
|
||||
.bg_color_palette, .bg_color_rgb => {
|
||||
try writer.writeByte(' ');
|
||||
if (self.point_map) |*map| {
|
||||
map.map.append(map.alloc, .{ .x = x, .y = y }) catch return error.WriteFailed;
|
||||
}
|
||||
},
|
||||
.bg_color_rgb => {
|
||||
const rgb = cell.content.color_rgb;
|
||||
try self.emitBgColorSgr(writer, null, rgb, &style);
|
||||
try writer.writeByte(' ');
|
||||
if (self.point_map) |*map| {
|
||||
map.map.append(map.alloc, .{ .x = x, .y = y }) catch return error.WriteFailed;
|
||||
}
|
||||
if (self.point_map) |*map| map.map.append(
|
||||
map.alloc,
|
||||
.{ .x = x, .y = y },
|
||||
) catch return error.WriteFailed;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1298,6 +1291,14 @@ pub const PageFormatter = struct {
|
||||
writer: *std.Io.Writer,
|
||||
cell: *const Cell,
|
||||
) !void {
|
||||
// Blank cells get an empty space that isn't replaced by anything
|
||||
// because it isn't really a space. We do this so that formatting
|
||||
// is preserved if we're emitting styles.
|
||||
if (!cell.hasText()) {
|
||||
try writer.writeByte(' ');
|
||||
return;
|
||||
}
|
||||
|
||||
try self.writeCodepointWithReplacement(writer, cell.content.codepoint);
|
||||
if (comptime tag == .codepoint_grapheme) {
|
||||
for (self.page.lookupGrapheme(cell).?) |cp| {
|
||||
@@ -1381,67 +1382,47 @@ pub const PageFormatter = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit background color SGR sequence for bg_color_* content tags.
|
||||
/// Updates the style tracking to reflect the emitted background.
|
||||
fn emitBgColorSgr(
|
||||
self: PageFormatter,
|
||||
writer: *std.Io.Writer,
|
||||
palette_index: ?u8,
|
||||
rgb: ?Cell.RGB,
|
||||
style: *Style,
|
||||
) std.Io.Writer.Error!void {
|
||||
switch (self.opts.emit) {
|
||||
.plain => {},
|
||||
.vt => {
|
||||
// Close previous style if non-default
|
||||
if (!style.default()) try writer.writeAll("\x1b[0m");
|
||||
// Emit background color
|
||||
if (palette_index) |idx| {
|
||||
try writer.print("\x1b[48;5;{d}m", .{idx});
|
||||
} else if (rgb) |c| {
|
||||
try writer.print("\x1b[48;2;{d};{d};{d}m", .{ c.r, c.g, c.b });
|
||||
}
|
||||
// Update style tracking - set bg_color so we know to reset later
|
||||
style.* = .{};
|
||||
style.bg_color = if (palette_index) |idx|
|
||||
.{ .palette = idx }
|
||||
else if (rgb) |c|
|
||||
.{ .rgb = .{ .r = c.r, .g = c.g, .b = c.b } }
|
||||
else
|
||||
.none;
|
||||
/// Returns the style for the given cell. If there is no styling this
|
||||
/// will return the default style.
|
||||
fn cellStyle(
|
||||
self: *const PageFormatter,
|
||||
cell: *const Cell,
|
||||
) Style {
|
||||
return switch (cell.content_tag) {
|
||||
inline .codepoint, .codepoint_grapheme => if (!cell.hasStyling())
|
||||
.{}
|
||||
else
|
||||
self.page.styles.get(
|
||||
self.page.memory,
|
||||
cell.style_id,
|
||||
).*,
|
||||
|
||||
.bg_color_palette => .{
|
||||
.bg_color = .{
|
||||
.palette = cell.content.color_palette,
|
||||
},
|
||||
},
|
||||
.html => {
|
||||
// Close previous tag if needed
|
||||
if (!style.default()) try writer.writeAll("</div>");
|
||||
// Emit background color as inline style
|
||||
if (palette_index) |idx| {
|
||||
try writer.print("<div style=\"display: inline;background-color: var(--vt-palette-{d});\">", .{idx});
|
||||
} else if (rgb) |c| {
|
||||
try writer.print("<div style=\"display: inline;background-color: #{x:0>2}{x:0>2}{x:0>2};\">", .{ c.r, c.g, c.b });
|
||||
}
|
||||
style.* = .{};
|
||||
style.bg_color = if (palette_index) |idx|
|
||||
.{ .palette = idx }
|
||||
else if (rgb) |c|
|
||||
.{ .rgb = .{ .r = c.r, .g = c.g, .b = c.b } }
|
||||
else
|
||||
.none;
|
||||
|
||||
.bg_color_rgb => .{
|
||||
.bg_color = .{
|
||||
.rgb = .{
|
||||
.r = cell.content.color_rgb.r,
|
||||
.g = cell.content.color_rgb.g,
|
||||
.b = cell.content.color_rgb.b,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn formatStyleOpen(
|
||||
self: PageFormatter,
|
||||
writer: *std.Io.Writer,
|
||||
style: *const Style,
|
||||
closing: bool,
|
||||
) std.Io.Writer.Error!void {
|
||||
switch (self.opts.emit) {
|
||||
.plain => unreachable,
|
||||
|
||||
// Note: we don't use closing on purpose because VT sequences
|
||||
// always reset the prior style. Our formatter always emits a
|
||||
// \x1b[0m before emitting a new style if necessary.
|
||||
.vt => {
|
||||
var formatter = style.formatterVt();
|
||||
formatter.palette = self.opts.palette;
|
||||
@@ -1451,7 +1432,6 @@ pub const PageFormatter = struct {
|
||||
// We use `display: inline` so that the div doesn't impact
|
||||
// layout since we're primarily using it as a CSS wrapper.
|
||||
.html => {
|
||||
if (closing) try writer.writeAll("</div>");
|
||||
var formatter = style.formatterHtml();
|
||||
formatter.palette = self.opts.palette;
|
||||
try writer.print(
|
||||
|
||||
Reference in New Issue
Block a user