terminal: formatters now emit background/foreground information (#9414)
This allows the full terminal style to be copied, except for the cursor: <img width="1136" height="660" alt="image" src="https://github.com/user-attachments/assets/448d7125-d4fd-477b-9a9b-96176d7fae56" />pull/9415/head
commit
038cdde334
|
|
@ -38,4 +38,5 @@ pub fn main() !void {
|
|||
var stdout_writer = std.fs.File.stdout().writer(&buf);
|
||||
const stdout = &stdout_writer.interface;
|
||||
try stdout.print("{f}", .{formatter});
|
||||
try stdout.flush();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ pub const Options = struct {
|
|||
/// is currently only space characters (0x20).
|
||||
trim: bool = true,
|
||||
|
||||
/// Set a background and foreground color to use for the "screen".
|
||||
/// For styled formats, this will emit the proper sequences or styles.
|
||||
background: ?color.RGB = null,
|
||||
foreground: ?color.RGB = null,
|
||||
|
||||
/// If set, then styled formats in `emit` will use this palette to
|
||||
/// emit colors directly as RGB. If this is null, styled formats will
|
||||
/// still work but will use deferred palette styling (e.g. CSS variables
|
||||
|
|
@ -902,14 +907,66 @@ pub const PageFormatter = struct {
|
|||
}
|
||||
|
||||
// Wrap HTML output in monospace font styling
|
||||
if (self.opts.emit == .html) {
|
||||
const monospace = "<div style=\"font-family: monospace; white-space: pre;\">";
|
||||
try writer.writeAll(monospace);
|
||||
if (self.point_map) |*map| map.map.appendNTimes(
|
||||
map.alloc,
|
||||
.{ .x = 0, .y = 0 },
|
||||
monospace.len,
|
||||
) catch return error.WriteFailed;
|
||||
switch (self.opts.emit) {
|
||||
.plain => {},
|
||||
|
||||
.html => {
|
||||
// Setup our div. We use a buffer here that should always
|
||||
// fit the stuff we need, in order to make counting bytes easier.
|
||||
var buf: [1024]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&buf);
|
||||
const buf_writer = stream.writer();
|
||||
|
||||
// Monospace and whitespace preserving
|
||||
buf_writer.writeAll("<div style=\"font-family: monospace; white-space: pre;") catch return error.WriteFailed;
|
||||
|
||||
// Background/foreground colors
|
||||
if (self.opts.background) |bg| buf_writer.print(
|
||||
"background-color: #{x:0>2}{x:0>2}{x:0>2};",
|
||||
.{ bg.r, bg.g, bg.b },
|
||||
) catch return error.WriteFailed;
|
||||
if (self.opts.foreground) |fg| buf_writer.print(
|
||||
"color: #{x:0>2}{x:0>2}{x:0>2};",
|
||||
.{ fg.r, fg.g, fg.b },
|
||||
) catch return error.WriteFailed;
|
||||
|
||||
buf_writer.writeAll("\">") catch return error.WriteFailed;
|
||||
|
||||
const header = stream.getWritten();
|
||||
try writer.writeAll(header);
|
||||
if (self.point_map) |*map| map.map.appendNTimes(
|
||||
map.alloc,
|
||||
.{ .x = 0, .y = 0 },
|
||||
header.len,
|
||||
) catch return error.WriteFailed;
|
||||
},
|
||||
|
||||
.vt => {
|
||||
// OSC 10 sets foreground color, OSC 11 sets background color
|
||||
var buf: [512]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&buf);
|
||||
const buf_writer = stream.writer();
|
||||
if (self.opts.foreground) |fg| {
|
||||
buf_writer.print(
|
||||
"\x1b]10;rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\",
|
||||
.{ fg.r, fg.g, fg.b },
|
||||
) catch return error.WriteFailed;
|
||||
}
|
||||
if (self.opts.background) |bg| {
|
||||
buf_writer.print(
|
||||
"\x1b]11;rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\",
|
||||
.{ bg.r, bg.g, bg.b },
|
||||
) catch return error.WriteFailed;
|
||||
}
|
||||
|
||||
const header = stream.getWritten();
|
||||
try writer.writeAll(header);
|
||||
if (self.point_map) |*map| map.map.appendNTimes(
|
||||
map.alloc,
|
||||
.{ .x = 0, .y = 0 },
|
||||
header.len,
|
||||
) catch return error.WriteFailed;
|
||||
},
|
||||
}
|
||||
|
||||
// Our style for non-plain formats
|
||||
|
|
@ -3073,6 +3130,43 @@ test "Page VT with foreground color" {
|
|||
);
|
||||
}
|
||||
|
||||
test "Page VT with background and foreground colors" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var builder: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
var t = try Terminal.init(alloc, .{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
});
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
|
||||
try s.nextSlice("hello");
|
||||
|
||||
const pages = &t.screen.pages;
|
||||
const page = &pages.pages.last.?.data;
|
||||
|
||||
var formatter: PageFormatter = .init(page, .{
|
||||
.emit = .vt,
|
||||
.background = .{ .r = 0x12, .g = 0x34, .b = 0x56 },
|
||||
.foreground = .{ .r = 0xab, .g = 0xcd, .b = 0xef },
|
||||
});
|
||||
|
||||
try formatter.format(&builder.writer);
|
||||
const output = builder.writer.buffered();
|
||||
|
||||
// Should emit OSC 10 for foreground, OSC 11 for background, then the text
|
||||
try testing.expectEqualStrings(
|
||||
"\x1b]10;rgb:ab/cd/ef\x1b\\\x1b]11;rgb:12/34/56\x1b\\hello",
|
||||
output,
|
||||
);
|
||||
}
|
||||
|
||||
test "Page VT multi-line with styles" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
@ -4866,6 +4960,41 @@ test "TerminalFormatter html with palette" {
|
|||
try testing.expect(std.mem.indexOf(u8, output, "test") != null);
|
||||
}
|
||||
|
||||
test "Page html with background and foreground colors" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var builder: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
var t = try Terminal.init(alloc, .{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
});
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
|
||||
try s.nextSlice("hello");
|
||||
|
||||
const pages = &t.screen.pages;
|
||||
const page = &pages.pages.last.?.data;
|
||||
var formatter: PageFormatter = .init(page, .{
|
||||
.emit = .html,
|
||||
.background = .{ .r = 0x12, .g = 0x34, .b = 0x56 },
|
||||
.foreground = .{ .r = 0xab, .g = 0xcd, .b = 0xef },
|
||||
});
|
||||
|
||||
try formatter.format(&builder.writer);
|
||||
const output = builder.writer.buffered();
|
||||
|
||||
try testing.expectEqualStrings(
|
||||
"<div style=\"font-family: monospace; white-space: pre;background-color: #123456;color: #abcdef;\">hello</div>",
|
||||
output,
|
||||
);
|
||||
}
|
||||
|
||||
test "Page html with escaping" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ const std = @import("std");
|
|||
const testing = std.testing;
|
||||
const stream = @import("stream.zig");
|
||||
const Action = stream.Action;
|
||||
const CursorStyle = @import("Screen.zig").CursorStyle;
|
||||
const Mode = @import("modes.zig").Mode;
|
||||
const Screen = @import("Screen.zig");
|
||||
const modes = @import("modes.zig");
|
||||
const osc_color = @import("osc/color.zig");
|
||||
const kitty_color = @import("kitty/color.zig");
|
||||
const Terminal = @import("Terminal.zig");
|
||||
|
||||
/// This is a Stream implementation that processes actions against
|
||||
|
|
@ -76,7 +78,7 @@ pub const Handler = struct {
|
|||
.default, .steady_block, .steady_bar, .steady_underline => false,
|
||||
.blinking_block, .blinking_bar, .blinking_underline => true,
|
||||
};
|
||||
const style: CursorStyle = switch (value) {
|
||||
const style: Screen.CursorStyle = switch (value) {
|
||||
.default, .blinking_block, .steady_block => .block,
|
||||
.blinking_bar, .steady_bar => .bar,
|
||||
.blinking_underline, .steady_underline => .underline,
|
||||
|
|
@ -214,7 +216,7 @@ pub const Handler = struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn setMode(self: *Handler, mode: Mode, enabled: bool) !void {
|
||||
fn setMode(self: *Handler, mode: modes.Mode, enabled: bool) !void {
|
||||
// Set the mode on the terminal
|
||||
self.terminal.modes.set(mode, enabled);
|
||||
|
||||
|
|
@ -294,8 +296,8 @@ pub const Handler = struct {
|
|||
|
||||
fn colorOperation(
|
||||
self: *Handler,
|
||||
op: @import("osc/color.zig").Operation,
|
||||
requests: *const @import("osc/color.zig").List,
|
||||
op: osc_color.Operation,
|
||||
requests: *const osc_color.List,
|
||||
) !void {
|
||||
_ = op;
|
||||
if (requests.count() == 0) return;
|
||||
|
|
@ -366,7 +368,7 @@ pub const Handler = struct {
|
|||
|
||||
fn kittyColorOperation(
|
||||
self: *Handler,
|
||||
request: @import("kitty/color.zig").OSC,
|
||||
request: kitty_color.OSC,
|
||||
) !void {
|
||||
for (request.list.items) |item| {
|
||||
switch (item) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue