";
- 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("
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(
+ "
hello
",
+ output,
+ );
+}
+
test "Page html with escaping" {
const testing = std.testing;
const alloc = testing.allocator;
diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig
index e762fdf86..907c48762 100644
--- a/src/terminal/stream_readonly.zig
+++ b/src/terminal/stream_readonly.zig
@@ -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) {