aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/synthetic/cli/Styled.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/synthetic/cli/Styled.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/synthetic/cli/Styled.zig')
-rw-r--r--vendor/libghostty-vt/src/synthetic/cli/Styled.zig410
1 files changed, 410 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/synthetic/cli/Styled.zig b/vendor/libghostty-vt/src/synthetic/cli/Styled.zig
new file mode 100644
index 00000000..c437a6ab
--- /dev/null
+++ b/vendor/libghostty-vt/src/synthetic/cli/Styled.zig
@@ -0,0 +1,410 @@
+//! Generates styled terminal content: lines of printable text
+//! interspersed with SGR sequences, optional multi-byte UTF-8
+//! codepoints, combining marks (multi-codepoint graphemes), and
+//! OSC 8 hyperlinks.
+//!
+//! This exists primarily to build corpora for benchmarking code that
+//! consumes terminal *contents* (e.g. the terminal formatter used for
+//! clipboard copy and dumps), where we want workloads with controlled
+//! amounts of styling and Unicode rather than raw random bytes.
+//!
+//! Examples:
+//!
+//! # Plain ASCII lines (equivalent to `ascii` with lines)
+//! ghostty-gen styled --seed=42
+//!
+//! # Heavily styled ASCII
+//! ghostty-gen styled --seed=42 --style-rate=0.8
+//!
+//! # Unicode-heavy, no styling
+//! ghostty-gen styled --seed=42 --weight-two=1 --weight-three=1 \
+//! --weight-four=0.5 --grapheme-rate=0.1
+//!
+//! # Mixed: styles, Unicode, and hyperlinks
+//! ghostty-gen styled --seed=42 --style-rate=0.3 --weight-two=0.5 \
+//! --weight-three=0.25 --weight-four=0.1 --grapheme-rate=0.05 \
+//! --osc8-rate=0.1
+const Styled = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+
+pub const Options = struct {
+ /// Seed to use for deterministic generation. If unset, a time-based
+ /// seed is used by the generic synthetic CLI.
+ seed: ?u64 = null,
+
+ /// Emit lines whose printable length (in characters, not columns)
+ /// is uniformly distributed in `[line-min, line-max]`. Each line is
+ /// terminated by CR LF.
+ @"line-min": usize = 40,
+ @"line-max": usize = 80,
+
+ /// Probability that a style change (SGR sequence) is emitted at
+ /// each run boundary. Zero (default) produces unstyled output.
+ @"style-rate": f64 = 0.0,
+
+ /// The length in characters of a "run": a sequence of characters
+ /// that share styling. Style/hyperlink changes only happen on run
+ /// boundaries.
+ @"run-min": usize = 4,
+ @"run-max": usize = 16,
+
+ /// Relative weights for choosing the UTF-8 encoding length of each
+ /// generated character. Unlike the raw `utf8` generator, characters
+ /// are drawn from curated printable ranges so the output never
+ /// contains control characters:
+ ///
+ /// one: printable ASCII (with occasional spaces)
+ /// two: Latin-1 Supplement / Latin Extended (narrow)
+ /// three: Hiragana/Katakana and CJK ideographs (wide)
+ /// four: emoji (wide)
+ @"weight-one": f64 = 1.0,
+ @"weight-two": f64 = 0.0,
+ @"weight-three": f64 = 0.0,
+ @"weight-four": f64 = 0.0,
+
+ /// Probability that a generated character is followed by a
+ /// combining diacritical mark, producing a multi-codepoint
+ /// grapheme cluster.
+ @"grapheme-rate": f64 = 0.0,
+
+ /// Probability that an OSC 8 hyperlink is toggled (opened or
+ /// closed) at each run boundary.
+ @"osc8-rate": f64 = 0.0,
+};
+
+opts: Options,
+
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*Styled {
+ for ([_]f64{
+ opts.@"style-rate",
+ opts.@"grapheme-rate",
+ opts.@"osc8-rate",
+ }) |rate| {
+ if (rate < 0 or rate > 1) return error.InvalidValue;
+ }
+
+ const weights = [_]f64{
+ opts.@"weight-one",
+ opts.@"weight-two",
+ opts.@"weight-three",
+ opts.@"weight-four",
+ };
+ var weight_sum: f64 = 0;
+ for (weights) |weight| {
+ if (weight < 0) return error.InvalidValue;
+ weight_sum += weight;
+ }
+ if (weight_sum <= 0) return error.InvalidValue;
+
+ if (opts.@"line-min" == 0) return error.InvalidValue;
+ if (opts.@"run-min" == 0) return error.InvalidValue;
+
+ const ptr = try alloc.create(Styled);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{ .opts = opts };
+ return ptr;
+}
+
+pub fn destroy(self: *Styled, alloc: Allocator) void {
+ alloc.destroy(self);
+}
+
+pub fn run(self: *Styled, writer: *std.Io.Writer, rand: std.Random) !void {
+ var prng: ?std.Random.DefaultPrng = null;
+ var gen_rand = rand;
+ if (self.opts.seed) |seed| {
+ prng = std.Random.DefaultPrng.init(seed);
+ gen_rand = prng.?.random();
+ }
+
+ var state: State = .{};
+ while (true) {
+ self.writeLine(writer, gen_rand, &state) catch |err| {
+ const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err);
+ switch (@as(Error, err)) {
+ error.BrokenPipe => return, // stdout closed
+ error.WriteFailed => return, // fixed buffer full
+ }
+ };
+ }
+}
+
+const State = struct {
+ /// True when a non-default SGR style is currently active.
+ styled: bool = false,
+
+ /// True when an OSC 8 hyperlink is currently open.
+ link: bool = false,
+};
+
+fn writeLine(
+ self: *Styled,
+ writer: *std.Io.Writer,
+ rand: std.Random,
+ state: *State,
+) std.Io.Writer.Error!void {
+ const line_min = self.opts.@"line-min";
+ const line_max = @max(line_min, self.opts.@"line-max");
+ const run_min = self.opts.@"run-min";
+ const run_max = @max(run_min, self.opts.@"run-max");
+
+ const line_len = rand.intRangeAtMostBiased(usize, line_min, line_max);
+ var remaining = line_len;
+ while (remaining > 0) {
+ if (self.opts.@"style-rate" > 0 and
+ rand.float(f64) < self.opts.@"style-rate")
+ {
+ try self.writeSgr(writer, rand, state);
+ }
+
+ if (self.opts.@"osc8-rate" > 0 and
+ rand.float(f64) < self.opts.@"osc8-rate")
+ {
+ try self.toggleLink(writer, rand, state);
+ }
+
+ const run_len = @min(
+ remaining,
+ rand.intRangeAtMostBiased(usize, run_min, run_max),
+ );
+ for (0..run_len) |_| try self.writeChar(writer, rand);
+ remaining -= run_len;
+ }
+
+ // Close any open styling/hyperlink so state never bleeds across
+ // lines. This mirrors what well-behaved programs do.
+ if (state.styled) {
+ try writer.writeAll("\x1b[0m");
+ state.styled = false;
+ }
+ if (state.link) {
+ try writer.writeAll("\x1b]8;;\x1b\\");
+ state.link = false;
+ }
+
+ try writer.writeAll("\r\n");
+}
+
+fn writeChar(
+ self: *Styled,
+ writer: *std.Io.Writer,
+ rand: std.Random,
+) std.Io.Writer.Error!void {
+ const weights = [_]f64{
+ self.opts.@"weight-one",
+ self.opts.@"weight-two",
+ self.opts.@"weight-three",
+ self.opts.@"weight-four",
+ };
+
+ const cp: u21 = switch (rand.weightedIndex(f64, &weights)) {
+ // Printable ASCII with occasional word-ish spacing.
+ 0 => if (rand.float(f64) < 0.15)
+ ' '
+ else
+ rand.intRangeAtMostBiased(u21, 0x21, 0x7E),
+
+ // Latin-1 Supplement and Latin Extended-A/B (narrow).
+ 1 => rand.intRangeAtMostBiased(u21, 0xC0, 0x24F),
+
+ // Kana or CJK ideographs (wide).
+ 2 => if (rand.boolean())
+ rand.intRangeAtMostBiased(u21, 0x3041, 0x30FE)
+ else
+ rand.intRangeAtMostBiased(u21, 0x4E00, 0x9FFF),
+
+ // Emoji (wide).
+ 3 => rand.intRangeAtMostBiased(u21, 0x1F600, 0x1F64F),
+
+ else => unreachable,
+ };
+
+ try writer.print("{u}", .{cp});
+
+ // Optionally follow with a combining diacritical mark to form a
+ // multi-codepoint grapheme cluster.
+ if (self.opts.@"grapheme-rate" > 0 and
+ rand.float(f64) < self.opts.@"grapheme-rate")
+ {
+ const mark = rand.intRangeAtMostBiased(u21, 0x300, 0x36F);
+ try writer.print("{u}", .{mark});
+ }
+}
+
+fn writeSgr(
+ self: *Styled,
+ writer: *std.Io.Writer,
+ rand: std.Random,
+ state: *State,
+) std.Io.Writer.Error!void {
+ _ = self;
+
+ switch (rand.uintLessThan(u8, 8)) {
+ // Reset.
+ 0 => {
+ try writer.writeAll("\x1b[0m");
+ state.styled = false;
+ return;
+ },
+
+ // 16-color foreground.
+ 1 => {
+ const base: u8 = if (rand.boolean()) 30 else 90;
+ try writer.print("\x1b[{d}m", .{base + rand.uintLessThan(u8, 8)});
+ },
+
+ // 16-color background.
+ 2 => {
+ const base: u8 = if (rand.boolean()) 40 else 100;
+ try writer.print("\x1b[{d}m", .{base + rand.uintLessThan(u8, 8)});
+ },
+
+ // 256-color foreground/background.
+ 3 => try writer.print("\x1b[38;5;{d}m", .{rand.int(u8)}),
+ 4 => try writer.print("\x1b[48;5;{d}m", .{rand.int(u8)}),
+
+ // RGB foreground/background. Components are quantized so the
+ // number of unique styles stays bounded (6^3 per channel pair)
+ // rather than pathologically unique per cell.
+ 5, 6 => |kind| {
+ const layer: u8 = if (kind == 5) 38 else 48;
+ try writer.print("\x1b[{d};2;{d};{d};{d}m", .{
+ layer,
+ rand.uintLessThan(u8, 6) * 51,
+ rand.uintLessThan(u8, 6) * 51,
+ rand.uintLessThan(u8, 6) * 51,
+ });
+ },
+
+ // Attributes: bold, italic, underline, inverse, strikethrough.
+ 7 => {
+ const attrs = [_]u8{ 1, 3, 4, 7, 9 };
+ try writer.print("\x1b[{d}m", .{
+ attrs[rand.uintLessThan(usize, attrs.len)],
+ });
+ },
+
+ else => unreachable,
+ }
+
+ state.styled = true;
+}
+
+fn toggleLink(
+ self: *Styled,
+ writer: *std.Io.Writer,
+ rand: std.Random,
+ state: *State,
+) std.Io.Writer.Error!void {
+ _ = self;
+
+ if (state.link) {
+ try writer.writeAll("\x1b]8;;\x1b\\");
+ state.link = false;
+ return;
+ }
+
+ try writer.print(
+ "\x1b]8;;http://example.com/{d}\x1b\\",
+ .{rand.uintLessThan(u16, 1024)},
+ );
+ state.link = true;
+}
+
+test Styled {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *Styled = try .create(alloc, .{ .seed = 1 });
+ defer impl.destroy(alloc);
+
+ var prng = std.Random.DefaultPrng.init(1);
+ const rand = prng.random();
+
+ var buf: [4096]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ try impl.run(&writer, rand);
+ const output = writer.buffered();
+ try testing.expect(output.len > 0);
+
+ // Default options: plain printable ASCII lines only.
+ for (output) |byte| {
+ try testing.expect(byte == '\r' or byte == '\n' or
+ (byte >= 0x20 and byte < 0x7F));
+ }
+}
+
+test "Styled styled output" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *Styled = try .create(alloc, .{
+ .seed = 1,
+ .@"style-rate" = 0.5,
+ .@"osc8-rate" = 0.2,
+ });
+ defer impl.destroy(alloc);
+
+ var prng = std.Random.DefaultPrng.init(1);
+ const rand = prng.random();
+
+ var buf: [16384]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ try impl.run(&writer, rand);
+ const output = writer.buffered();
+
+ // Must contain SGR and OSC 8 sequences.
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[") != null);
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]8;;") != null);
+}
+
+test "Styled unicode output" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *Styled = try .create(alloc, .{
+ .seed = 1,
+ .@"weight-two" = 1.0,
+ .@"weight-three" = 1.0,
+ .@"weight-four" = 1.0,
+ .@"grapheme-rate" = 0.25,
+ });
+ defer impl.destroy(alloc);
+
+ var prng = std.Random.DefaultPrng.init(1);
+ const rand = prng.random();
+
+ var buf: [16384]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ try impl.run(&writer, rand);
+ const output = writer.buffered();
+
+ // No escapes, valid UTF-8 (modulo a possibly truncated tail from
+ // the fixed buffer filling up mid-sequence).
+ try testing.expect(std.mem.indexOfScalar(u8, output, 0x1B) == null);
+ var end = output.len;
+ while (end > 0 and output[end - 1] & 0xC0 == 0x80) end -= 1;
+ if (end > 0 and output[end - 1] >= 0xC0) end -= 1;
+ try testing.expect(std.unicode.utf8ValidateSlice(output[0..end]));
+}
+
+test "Styled invalid options" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ try testing.expectError(error.InvalidValue, Styled.create(alloc, .{
+ .@"style-rate" = 1.5,
+ }));
+ try testing.expectError(error.InvalidValue, Styled.create(alloc, .{
+ .@"weight-one" = 0,
+ }));
+ try testing.expectError(error.InvalidValue, Styled.create(alloc, .{
+ .@"line-min" = 0,
+ }));
+}