/// Generates random terminal OSC requests. const Osc = @This(); const std = @import("std"); const assert = std.debug.assert; const Generator = @import("Generator.zig"); const Bytes = @import("Bytes.zig"); /// Valid OSC request kinds that can be generated. pub const ValidKind = enum { change_window_title, prompt_start, prompt_end, }; /// Invalid OSC request kinds that can be generated. pub const InvalidKind = enum { /// Literally random bytes. Might even be valid, but probably not. random, /// A good prefix, but ultimately invalid format. good_prefix, }; /// Random number generator. rand: std.Random, /// Probability of a valid OSC sequence being generated. p_valid: f64 = 1.0, /// Probabilities of specific valid or invalid OSC request kinds. /// The probabilities are weighted relative to each other, so they /// can sum greater than 1.0. A kind of weight 1.0 and a kind of /// weight 2.0 will have a 2:1 chance of the latter being selected. p_valid_kind: std.enums.EnumArray(ValidKind, f64) = .initFill(1.0), p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0), /// The alphabet for random bytes (omitting 0x1B and 0x07). const bytes_alphabet: []const u8 = alphabet: { var alphabet: [256]u8 = undefined; for (0..alphabet.len) |i| { if (i == 0x1B or i == 0x07) { alphabet[i] = @intCast(i + 1); } else { alphabet[i] = @intCast(i); } } const result = alphabet; break :alphabet &result; }; pub fn generator(self: *Osc) Generator { return .init(self, next); } /// Get the next OSC request in bytes. The generated OSC request will /// have the prefix `ESC ]` and the terminator `BEL` (0x07). /// /// This will generate both valid and invalid OSC requests (based on /// the `p_valid` probability value). Invalid requests still have the /// prefix and terminator, but the content in between is not a valid /// OSC request. /// /// The buffer must be at least 3 bytes long to accommodate the /// prefix and terminator. pub fn next(self: *Osc, buf: []u8) Generator.Error![]const u8 { if (buf.len < 3) return error.NoSpaceLeft; const unwrapped = try self.nextUnwrapped(buf[2 .. buf.len - 1]); buf[0] = 0x1B; // ESC buf[1] = ']'; buf[unwrapped.len + 2] = 0x07; // BEL return buf[0 .. unwrapped.len + 3]; } fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { return switch (self.chooseValidity()) { .valid => valid: { const Indexer = @TypeOf(self.p_valid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_valid_kind.values); break :valid try self.nextUnwrappedValidExact( buf, Indexer.keyForIndex(idx), ); }, .invalid => invalid: { const Indexer = @TypeOf(self.p_invalid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_invalid_kind.values); break :invalid try self.nextUnwrappedInvalidExact( buf, Indexer.keyForIndex(idx), ); }, }; } fn nextUnwrappedValidExact(self: *const Osc, buf: []u8, k: ValidKind) Generator.Error![]const u8 { var fbs = std.io.fixedBufferStream(buf); switch (k) { .change_window_title => { try fbs.writer().writeAll("0;"); // Set window title var bytes_gen = self.bytes(); const title = try bytes_gen.next(fbs.buffer[fbs.pos..]); try fbs.seekBy(@intCast(title.len)); }, .prompt_start => { try fbs.writer().writeAll("133;A"); // Start prompt // aid if (self.rand.boolean()) { var bytes_gen = self.bytes(); bytes_gen.max_len = 16; try fbs.writer().writeAll(";aid="); const aid = try bytes_gen.next(fbs.buffer[fbs.pos..]); try fbs.seekBy(@intCast(aid.len)); } // redraw if (self.rand.boolean()) { try fbs.writer().writeAll(";redraw="); if (self.rand.boolean()) { try fbs.writer().writeAll("1"); } else { try fbs.writer().writeAll("0"); } } }, .prompt_end => try fbs.writer().writeAll("133;B"), // End prompt } return fbs.getWritten(); } fn nextUnwrappedInvalidExact( self: *const Osc, buf: []u8, k: InvalidKind, ) Generator.Error![]const u8 { switch (k) { .random => { var bytes_gen = self.bytes(); return try bytes_gen.next(buf); }, .good_prefix => { var fbs = std.io.fixedBufferStream(buf); try fbs.writer().writeAll("133;"); var bytes_gen = self.bytes(); const data = try bytes_gen.next(fbs.buffer[fbs.pos..]); try fbs.seekBy(@intCast(data.len)); return fbs.getWritten(); }, } } fn bytes(self: *const Osc) Bytes { return .{ .rand = self.rand, .alphabet = bytes_alphabet, }; } /// Choose whether to generate a valid or invalid OSC request based /// on the validity probability. fn chooseValidity(self: *const Osc) Validity { return if (self.rand.float(f64) > self.p_valid) .invalid else .valid; } const Validity = enum { valid, invalid }; /// A fixed seed we can use for our tests to avoid flakes. const test_seed = 0xC0FFEEEEEEEEEEEE; test "OSC generator" { var prng = std.Random.DefaultPrng.init(test_seed); var buf: [4096]u8 = undefined; var v: Osc = .{ .rand = prng.random() }; const gen = v.generator(); for (0..50) |_| _ = try gen.next(&buf); } test "OSC generator valid" { const testing = std.testing; const terminal = @import("../terminal/main.zig"); var prng = std.Random.DefaultPrng.init(test_seed); var buf: [256]u8 = undefined; var gen: Osc = .{ .rand = prng.random(), .p_valid = 1.0, }; for (0..50) |_| { const seq = try gen.next(&buf); var parser: terminal.osc.Parser = .{}; for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) != null); } } test "OSC generator invalid" { const testing = std.testing; const terminal = @import("../terminal/main.zig"); var prng = std.Random.DefaultPrng.init(test_seed); var buf: [256]u8 = undefined; var gen: Osc = .{ .rand = prng.random(), .p_valid = 0.0, }; for (0..50) |_| { const seq = try gen.next(&buf); var parser: terminal.osc.Parser = .{}; for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) == null); } }