diff --git a/example/zig-formatter/src/main.zig b/example/zig-formatter/src/main.zig index 085b6d116..ad101dbf1 100644 --- a/example/zig-formatter/src/main.zig +++ b/example/zig-formatter/src/main.zig @@ -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(); } diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 70cdd347b..baa6b61c1 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -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 = "
"; - 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) {