diff --git a/src/bench/stream.zig b/src/bench/stream.zig index 8af4ffff3..3e6262014 100644 --- a/src/bench/stream.zig +++ b/src/bench/stream.zig @@ -15,7 +15,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const ziglyph = @import("ziglyph"); const cli = @import("../cli.zig"); const terminal = @import("../terminal/main.zig"); -const terminalnew = @import("../terminal/new/main.zig"); +const terminalnew = @import("../terminal2/main.zig"); const Args = struct { mode: Mode = .noop, diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 73e771a7c..4e99001c6 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -309,6 +309,7 @@ test { _ = @import("segmented_pool.zig"); _ = @import("inspector/main.zig"); _ = @import("terminal/main.zig"); + _ = @import("terminal2/main.zig"); _ = @import("terminfo/main.zig"); _ = @import("simd/main.zig"); _ = @import("unicode/main.zig"); diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 55625f7a6..5c2a64e20 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -25,7 +25,6 @@ pub const CSI = Parser.Action.CSI; pub const DCS = Parser.Action.DCS; pub const MouseShape = @import("mouse_shape.zig").MouseShape; pub const Terminal = @import("Terminal.zig"); -//pub const Terminal = new.Terminal; pub const Parser = @import("Parser.zig"); pub const Selection = @import("Selection.zig"); pub const Screen = @import("Screen.zig"); @@ -49,8 +48,8 @@ pub usingnamespace if (builtin.target.isWasm()) struct { pub usingnamespace @import("wasm.zig"); } else struct {}; -/// The new stuff. TODO: remove this before merge. -pub const new = @import("new/main.zig"); +// TODO(paged-terminal) remove before merge +pub const new = @import("../terminal2/main.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/new/PageList.zig b/src/terminal2/PageList.zig similarity index 100% rename from src/terminal/new/PageList.zig rename to src/terminal2/PageList.zig diff --git a/src/terminal/new/Screen.zig b/src/terminal2/Screen.zig similarity index 99% rename from src/terminal/new/Screen.zig rename to src/terminal2/Screen.zig index f4581b722..5326fb7e8 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal2/Screen.zig @@ -3,12 +3,12 @@ const Screen = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const ansi = @import("../ansi.zig"); -const charsets = @import("../charsets.zig"); -const kitty = @import("../kitty.zig"); -const sgr = @import("../sgr.zig"); -const unicode = @import("../../unicode/main.zig"); -const Selection = @import("../Selection.zig"); +const ansi = @import("ansi.zig"); +const charsets = @import("charsets.zig"); +const kitty = @import("kitty.zig"); +const sgr = @import("sgr.zig"); +const unicode = @import("../unicode/main.zig"); +//const Selection = @import("../Selection.zig"); const PageList = @import("PageList.zig"); const pagepkg = @import("page.zig"); const point = @import("point.zig"); @@ -37,7 +37,8 @@ cursor: Cursor, saved_cursor: ?SavedCursor = null, /// The selection for this screen (if any). -selection: ?Selection = null, +//selection: ?Selection = null, +selection: ?void = null, /// The charset state charset: CharsetState = .{}, @@ -826,18 +827,18 @@ pub fn manualStyleUpdate(self: *Screen) !void { /// Returns the raw text associated with a selection. This will unwrap /// soft-wrapped edges. The returned slice is owned by the caller and allocated /// using alloc, not the allocator associated with the screen (unless they match). -pub fn selectionString( - self: *Screen, - alloc: Allocator, - sel: Selection, - trim: bool, -) ![:0]const u8 { - _ = self; - _ = alloc; - _ = sel; - _ = trim; - @panic("TODO"); -} +// pub fn selectionString( +// self: *Screen, +// alloc: Allocator, +// sel: Selection, +// trim: bool, +// ) ![:0]const u8 { +// _ = self; +// _ = alloc; +// _ = sel; +// _ = trim; +// @panic("TODO"); +// } /// Dump the screen to a string. The writer given should be buffered; /// this function does not attempt to efficiently write and generally writes diff --git a/src/terminal2/Tabstops.zig b/src/terminal2/Tabstops.zig new file mode 100644 index 000000000..5a54fb28b --- /dev/null +++ b/src/terminal2/Tabstops.zig @@ -0,0 +1,231 @@ +//! Keep track of the location of tabstops. +//! +//! This is implemented as a bit set. There is a preallocation segment that +//! is used for almost all screen sizes. Then there is a dynamically allocated +//! segment if the screen is larger than the preallocation amount. +//! +//! In reality, tabstops don't need to be the most performant in any metric. +//! This implementation tries to balance denser memory usage (by using a bitset) +//! and minimizing unnecessary allocations. +const Tabstops = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const testing = std.testing; +const assert = std.debug.assert; +const fastmem = @import("../fastmem.zig"); + +/// Unit is the type we use per tabstop unit (see file docs). +const Unit = u8; +const unit_bits = @bitSizeOf(Unit); + +/// The number of columns we preallocate for. This is kind of high which +/// costs us some memory, but this is more columns than my 6k monitor at +/// 12-point font size, so this should prevent allocation in almost all +/// real world scenarios for the price of wasting at most +/// (columns / sizeOf(Unit)) bytes. +const prealloc_columns = 512; + +/// The number of entries we need for our preallocation. +const prealloc_count = prealloc_columns / unit_bits; + +/// We precompute all the possible masks since we never use a huge bit size. +const masks = blk: { + var res: [unit_bits]Unit = undefined; + for (res, 0..) |_, i| { + res[i] = @shlExact(@as(Unit, 1), @as(u3, @intCast(i))); + } + + break :blk res; +}; + +/// The number of columns this tabstop is set to manage. Use resize() +/// to change this number. +cols: usize = 0, + +/// Preallocated tab stops. +prealloc_stops: [prealloc_count]Unit = [1]Unit{0} ** prealloc_count, + +/// Dynamically expanded stops above prealloc stops. +dynamic_stops: []Unit = &[0]Unit{}, + +/// Returns the entry in the stops array that would contain this column. +inline fn entry(col: usize) usize { + return col / unit_bits; +} + +inline fn index(col: usize) usize { + return @mod(col, unit_bits); +} + +pub fn init(alloc: Allocator, cols: usize, interval: usize) !Tabstops { + var res: Tabstops = .{}; + try res.resize(alloc, cols); + res.reset(interval); + return res; +} + +pub fn deinit(self: *Tabstops, alloc: Allocator) void { + if (self.dynamic_stops.len > 0) alloc.free(self.dynamic_stops); + self.* = undefined; +} + +/// Set the tabstop at a certain column. The columns are 0-indexed. +pub fn set(self: *Tabstops, col: usize) void { + const i = entry(col); + const idx = index(col); + if (i < prealloc_count) { + self.prealloc_stops[i] |= masks[idx]; + return; + } + + const dynamic_i = i - prealloc_count; + assert(dynamic_i < self.dynamic_stops.len); + self.dynamic_stops[dynamic_i] |= masks[idx]; +} + +/// Unset the tabstop at a certain column. The columns are 0-indexed. +pub fn unset(self: *Tabstops, col: usize) void { + const i = entry(col); + const idx = index(col); + if (i < prealloc_count) { + self.prealloc_stops[i] ^= masks[idx]; + return; + } + + const dynamic_i = i - prealloc_count; + assert(dynamic_i < self.dynamic_stops.len); + self.dynamic_stops[dynamic_i] ^= masks[idx]; +} + +/// Get the value of a tabstop at a specific column. The columns are 0-indexed. +pub fn get(self: Tabstops, col: usize) bool { + const i = entry(col); + const idx = index(col); + const mask = masks[idx]; + const unit = if (i < prealloc_count) + self.prealloc_stops[i] + else unit: { + const dynamic_i = i - prealloc_count; + assert(dynamic_i < self.dynamic_stops.len); + break :unit self.dynamic_stops[dynamic_i]; + }; + + return unit & mask == mask; +} + +/// Resize this to support up to cols columns. +// TODO: needs interval to set new tabstops +pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void { + // Set our new value + self.cols = cols; + + // Do nothing if it fits. + if (cols <= prealloc_columns) return; + + // What we need in the dynamic size + const size = cols - prealloc_columns; + if (size < self.dynamic_stops.len) return; + + // Note: we can probably try to realloc here but I'm not sure it matters. + const new = try alloc.alloc(Unit, size); + if (self.dynamic_stops.len > 0) { + fastmem.copy(Unit, new, self.dynamic_stops); + alloc.free(self.dynamic_stops); + } + + self.dynamic_stops = new; +} + +/// Return the maximum number of columns this can support currently. +pub fn capacity(self: Tabstops) usize { + return (prealloc_count + self.dynamic_stops.len) * unit_bits; +} + +/// Unset all tabstops and then reset the initial tabstops to the given +/// interval. An interval of 0 sets no tabstops. +pub fn reset(self: *Tabstops, interval: usize) void { + @memset(&self.prealloc_stops, 0); + @memset(self.dynamic_stops, 0); + + if (interval > 0) { + var i: usize = interval; + while (i < self.cols - 1) : (i += interval) { + self.set(i); + } + } +} + +test "Tabstops: basic" { + var t: Tabstops = .{}; + defer t.deinit(testing.allocator); + try testing.expectEqual(@as(usize, 0), entry(4)); + try testing.expectEqual(@as(usize, 1), entry(8)); + try testing.expectEqual(@as(usize, 0), index(0)); + try testing.expectEqual(@as(usize, 1), index(1)); + try testing.expectEqual(@as(usize, 1), index(9)); + + try testing.expectEqual(@as(Unit, 0b00001000), masks[3]); + try testing.expectEqual(@as(Unit, 0b00010000), masks[4]); + + try testing.expect(!t.get(4)); + t.set(4); + try testing.expect(t.get(4)); + try testing.expect(!t.get(3)); + + t.reset(0); + try testing.expect(!t.get(4)); + + t.set(4); + try testing.expect(t.get(4)); + t.unset(4); + try testing.expect(!t.get(4)); +} + +test "Tabstops: dynamic allocations" { + var t: Tabstops = .{}; + defer t.deinit(testing.allocator); + + // Grow the capacity by 2. + const cap = t.capacity(); + try t.resize(testing.allocator, cap * 2); + + // Set something that was out of range of the first + t.set(cap + 5); + try testing.expect(t.get(cap + 5)); + try testing.expect(!t.get(cap + 4)); + + // Prealloc still works + try testing.expect(!t.get(5)); +} + +test "Tabstops: interval" { + var t: Tabstops = try init(testing.allocator, 80, 4); + defer t.deinit(testing.allocator); + try testing.expect(!t.get(0)); + try testing.expect(t.get(4)); + try testing.expect(!t.get(5)); + try testing.expect(t.get(8)); +} + +test "Tabstops: count on 80" { + // https://superuser.com/questions/710019/why-there-are-11-tabstops-on-a-80-column-console + + var t: Tabstops = try init(testing.allocator, 80, 8); + defer t.deinit(testing.allocator); + + // Count the tabstops + const count: usize = count: { + var v: usize = 0; + var i: usize = 0; + while (i < 80) : (i += 1) { + if (t.get(i)) { + v += 1; + } + } + + break :count v; + }; + + try testing.expectEqual(@as(usize, 9), count); +} diff --git a/src/terminal/new/Terminal.zig b/src/terminal2/Terminal.zig similarity index 99% rename from src/terminal/new/Terminal.zig rename to src/terminal2/Terminal.zig index 51f5ddf58..8afa666f3 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal2/Terminal.zig @@ -12,17 +12,17 @@ const builtin = @import("builtin"); const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; -const unicode = @import("../../unicode/main.zig"); +const unicode = @import("../unicode/main.zig"); -const ansi = @import("../ansi.zig"); -const modes = @import("../modes.zig"); -const charsets = @import("../charsets.zig"); -const csi = @import("../csi.zig"); -const kitty = @import("../kitty.zig"); -const sgr = @import("../sgr.zig"); -const Tabstops = @import("../Tabstops.zig"); -const color = @import("../color.zig"); -const mouse_shape = @import("../mouse_shape.zig"); +const ansi = @import("ansi.zig"); +const modes = @import("modes.zig"); +const charsets = @import("charsets.zig"); +const csi = @import("csi.zig"); +const kitty = @import("kitty.zig"); +const sgr = @import("sgr.zig"); +const Tabstops = @import("Tabstops.zig"); +const color = @import("color.zig"); +const mouse_shape = @import("mouse_shape.zig"); const size = @import("size.zig"); const pagepkg = @import("page.zig"); diff --git a/src/terminal2/ansi.zig b/src/terminal2/ansi.zig new file mode 100644 index 000000000..43c2a9a1c --- /dev/null +++ b/src/terminal2/ansi.zig @@ -0,0 +1,114 @@ +/// C0 (7-bit) control characters from ANSI. +/// +/// This is not complete, control characters are only added to this +/// as the terminal emulator handles them. +pub const C0 = enum(u7) { + /// Null + NUL = 0x00, + /// Start of heading + SOH = 0x01, + /// Start of text + STX = 0x02, + /// Enquiry + ENQ = 0x05, + /// Bell + BEL = 0x07, + /// Backspace + BS = 0x08, + // Horizontal tab + HT = 0x09, + /// Line feed + LF = 0x0A, + /// Vertical Tab + VT = 0x0B, + /// Form feed + FF = 0x0C, + /// Carriage return + CR = 0x0D, + /// Shift out + SO = 0x0E, + /// Shift in + SI = 0x0F, + + // Non-exhaustive so that @intToEnum never fails since the inputs are + // user-generated. + _, +}; + +/// The SGR rendition aspects that can be set, sometimes known as attributes. +/// The value corresponds to the parameter value for the SGR command (ESC [ m). +pub const RenditionAspect = enum(u16) { + default = 0, + bold = 1, + default_fg = 39, + default_bg = 49, + + // Non-exhaustive so that @intToEnum never fails since the inputs are + // user-generated. + _, +}; + +/// The device attribute request type (ESC [ c). +pub const DeviceAttributeReq = enum { + primary, // Blank + secondary, // > + tertiary, // = +}; + +/// Possible cursor styles (ESC [ q) +pub const CursorStyle = enum(u16) { + default = 0, + blinking_block = 1, + steady_block = 2, + blinking_underline = 3, + steady_underline = 4, + blinking_bar = 5, + steady_bar = 6, + + // Non-exhaustive so that @intToEnum never fails for unsupported modes. + _, + + /// True if the cursor should blink. + pub fn blinking(self: CursorStyle) bool { + return switch (self) { + .blinking_block, .blinking_underline, .blinking_bar => true, + else => false, + }; + } +}; + +/// The status line type for DECSSDT. +pub const StatusLineType = enum(u16) { + none = 0, + indicator = 1, + host_writable = 2, + + // Non-exhaustive so that @intToEnum never fails for unsupported values. + _, +}; + +/// The display to target for status updates (DECSASD). +pub const StatusDisplay = enum(u16) { + main = 0, + status_line = 1, + + // Non-exhaustive so that @intToEnum never fails for unsupported values. + _, +}; + +/// The possible modify key formats to ESC[>{a};{b}m +/// Note: this is not complete, we should add more as we support more +pub const ModifyKeyFormat = union(enum) { + legacy: void, + cursor_keys: void, + function_keys: void, + other_keys: enum { none, numeric_except, numeric }, +}; + +/// The protection modes that can be set for the terminal. See DECSCA and +/// ESC V, W. +pub const ProtectedMode = enum { + off, + iso, // ESC V, W + dec, // CSI Ps " q +}; diff --git a/src/terminal/new/bitmap_allocator.zig b/src/terminal2/bitmap_allocator.zig similarity index 100% rename from src/terminal/new/bitmap_allocator.zig rename to src/terminal2/bitmap_allocator.zig diff --git a/src/terminal2/charsets.zig b/src/terminal2/charsets.zig new file mode 100644 index 000000000..316238458 --- /dev/null +++ b/src/terminal2/charsets.zig @@ -0,0 +1,114 @@ +const std = @import("std"); +const assert = std.debug.assert; + +/// The available charset slots for a terminal. +pub const Slots = enum(u3) { + G0 = 0, + G1 = 1, + G2 = 2, + G3 = 3, +}; + +/// The name of the active slots. +pub const ActiveSlot = enum { GL, GR }; + +/// The list of supported character sets and their associated tables. +pub const Charset = enum { + utf8, + ascii, + british, + dec_special, + + /// The table for the given charset. This returns a pointer to a + /// slice that is guaranteed to be 255 chars that can be used to map + /// ASCII to the given charset. + pub fn table(set: Charset) []const u16 { + return switch (set) { + .british => &british, + .dec_special => &dec_special, + + // utf8 is not a table, callers should double-check if the + // charset is utf8 and NOT use tables. + .utf8 => unreachable, + + // recommended that callers just map ascii directly but we can + // support a table + .ascii => &ascii, + }; + } +}; + +/// Just a basic c => c ascii table +const ascii = initTable(); + +/// https://vt100.net/docs/vt220-rm/chapter2.html +const british = british: { + var table = initTable(); + table[0x23] = 0x00a3; + break :british table; +}; + +/// https://en.wikipedia.org/wiki/DEC_Special_Graphics +const dec_special = tech: { + var table = initTable(); + table[0x60] = 0x25C6; + table[0x61] = 0x2592; + table[0x62] = 0x2409; + table[0x63] = 0x240C; + table[0x64] = 0x240D; + table[0x65] = 0x240A; + table[0x66] = 0x00B0; + table[0x67] = 0x00B1; + table[0x68] = 0x2424; + table[0x69] = 0x240B; + table[0x6a] = 0x2518; + table[0x6b] = 0x2510; + table[0x6c] = 0x250C; + table[0x6d] = 0x2514; + table[0x6e] = 0x253C; + table[0x6f] = 0x23BA; + table[0x70] = 0x23BB; + table[0x71] = 0x2500; + table[0x72] = 0x23BC; + table[0x73] = 0x23BD; + table[0x74] = 0x251C; + table[0x75] = 0x2524; + table[0x76] = 0x2534; + table[0x77] = 0x252C; + table[0x78] = 0x2502; + table[0x79] = 0x2264; + table[0x7a] = 0x2265; + table[0x7b] = 0x03C0; + table[0x7c] = 0x2260; + table[0x7d] = 0x00A3; + table[0x7e] = 0x00B7; + break :tech table; +}; + +/// Our table length is 256 so we can contain all ASCII chars. +const table_len = std.math.maxInt(u8) + 1; + +/// Creates a table that maps ASCII to ASCII as a getting started point. +fn initTable() [table_len]u16 { + var result: [table_len]u16 = undefined; + var i: usize = 0; + while (i < table_len) : (i += 1) result[i] = @intCast(i); + assert(i == table_len); + return result; +} + +test { + const testing = std.testing; + const info = @typeInfo(Charset).Enum; + inline for (info.fields) |field| { + // utf8 has no table + if (@field(Charset, field.name) == .utf8) continue; + + const table = @field(Charset, field.name).table(); + + // Yes, I could use `table_len` here, but I want to explicitly use a + // hardcoded constant so that if there are miscompilations or a comptime + // issue, we catch it. + try testing.expectEqual(@as(usize, 256), table.len); + } +} diff --git a/src/terminal2/color.zig b/src/terminal2/color.zig new file mode 100644 index 000000000..194cee8b1 --- /dev/null +++ b/src/terminal2/color.zig @@ -0,0 +1,339 @@ +const std = @import("std"); +const assert = std.debug.assert; +const x11_color = @import("x11_color.zig"); + +/// The default palette. +pub const default: Palette = default: { + var result: Palette = undefined; + + // Named values + var i: u8 = 0; + while (i < 16) : (i += 1) { + result[i] = Name.default(@enumFromInt(i)) catch unreachable; + } + + // Cube + assert(i == 16); + var r: u8 = 0; + while (r < 6) : (r += 1) { + var g: u8 = 0; + while (g < 6) : (g += 1) { + var b: u8 = 0; + while (b < 6) : (b += 1) { + result[i] = .{ + .r = if (r == 0) 0 else (r * 40 + 55), + .g = if (g == 0) 0 else (g * 40 + 55), + .b = if (b == 0) 0 else (b * 40 + 55), + }; + + i += 1; + } + } + } + + // Grey ramp + assert(i == 232); + assert(@TypeOf(i) == u8); + while (i > 0) : (i +%= 1) { + const value = ((i - 232) * 10) + 8; + result[i] = .{ .r = value, .g = value, .b = value }; + } + + break :default result; +}; + +/// Palette is the 256 color palette. +pub const Palette = [256]RGB; + +/// Color names in the standard 8 or 16 color palette. +pub const Name = enum(u8) { + black = 0, + red = 1, + green = 2, + yellow = 3, + blue = 4, + magenta = 5, + cyan = 6, + white = 7, + + bright_black = 8, + bright_red = 9, + bright_green = 10, + bright_yellow = 11, + bright_blue = 12, + bright_magenta = 13, + bright_cyan = 14, + bright_white = 15, + + // Remainders are valid unnamed values in the 256 color palette. + _, + + /// Default colors for tagged values. + pub fn default(self: Name) !RGB { + return switch (self) { + .black => RGB{ .r = 0x1D, .g = 0x1F, .b = 0x21 }, + .red => RGB{ .r = 0xCC, .g = 0x66, .b = 0x66 }, + .green => RGB{ .r = 0xB5, .g = 0xBD, .b = 0x68 }, + .yellow => RGB{ .r = 0xF0, .g = 0xC6, .b = 0x74 }, + .blue => RGB{ .r = 0x81, .g = 0xA2, .b = 0xBE }, + .magenta => RGB{ .r = 0xB2, .g = 0x94, .b = 0xBB }, + .cyan => RGB{ .r = 0x8A, .g = 0xBE, .b = 0xB7 }, + .white => RGB{ .r = 0xC5, .g = 0xC8, .b = 0xC6 }, + + .bright_black => RGB{ .r = 0x66, .g = 0x66, .b = 0x66 }, + .bright_red => RGB{ .r = 0xD5, .g = 0x4E, .b = 0x53 }, + .bright_green => RGB{ .r = 0xB9, .g = 0xCA, .b = 0x4A }, + .bright_yellow => RGB{ .r = 0xE7, .g = 0xC5, .b = 0x47 }, + .bright_blue => RGB{ .r = 0x7A, .g = 0xA6, .b = 0xDA }, + .bright_magenta => RGB{ .r = 0xC3, .g = 0x97, .b = 0xD8 }, + .bright_cyan => RGB{ .r = 0x70, .g = 0xC0, .b = 0xB1 }, + .bright_white => RGB{ .r = 0xEA, .g = 0xEA, .b = 0xEA }, + + else => error.NoDefaultValue, + }; + } +}; + +/// RGB +pub const RGB = struct { + r: u8 = 0, + g: u8 = 0, + b: u8 = 0, + + pub fn eql(self: RGB, other: RGB) bool { + return self.r == other.r and self.g == other.g and self.b == other.b; + } + + /// Calculates the contrast ratio between two colors. The contrast + /// ration is a value between 1 and 21 where 1 is the lowest contrast + /// and 21 is the highest contrast. + /// + /// https://www.w3.org/TR/WCAG20/#contrast-ratiodef + pub fn contrast(self: RGB, other: RGB) f64 { + // pair[0] = lighter, pair[1] = darker + const pair: [2]f64 = pair: { + const self_lum = self.luminance(); + const other_lum = other.luminance(); + if (self_lum > other_lum) break :pair .{ self_lum, other_lum }; + break :pair .{ other_lum, self_lum }; + }; + + return (pair[0] + 0.05) / (pair[1] + 0.05); + } + + /// Calculates luminance based on the W3C formula. This returns a + /// normalized value between 0 and 1 where 0 is black and 1 is white. + /// + /// https://www.w3.org/TR/WCAG20/#relativeluminancedef + pub fn luminance(self: RGB) f64 { + const r_lum = componentLuminance(self.r); + const g_lum = componentLuminance(self.g); + const b_lum = componentLuminance(self.b); + return 0.2126 * r_lum + 0.7152 * g_lum + 0.0722 * b_lum; + } + + /// Calculates single-component luminance based on the W3C formula. + /// + /// Expects sRGB color space which at the time of writing we don't + /// generally use but it's a good enough approximation until we fix that. + /// https://www.w3.org/TR/WCAG20/#relativeluminancedef + fn componentLuminance(c: u8) f64 { + const c_f64: f64 = @floatFromInt(c); + const normalized: f64 = c_f64 / 255; + if (normalized <= 0.03928) return normalized / 12.92; + return std.math.pow(f64, (normalized + 0.055) / 1.055, 2.4); + } + + /// Calculates "perceived luminance" which is better for determining + /// light vs dark. + /// + /// Source: https://www.w3.org/TR/AERT/#color-contrast + pub fn perceivedLuminance(self: RGB) f64 { + const r_f64: f64 = @floatFromInt(self.r); + const g_f64: f64 = @floatFromInt(self.g); + const b_f64: f64 = @floatFromInt(self.b); + return 0.299 * (r_f64 / 255) + 0.587 * (g_f64 / 255) + 0.114 * (b_f64 / 255); + } + + test "size" { + try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB)); + try std.testing.expectEqual(@as(usize, 3), @sizeOf(RGB)); + } + + /// Parse a color from a floating point intensity value. + /// + /// The value should be between 0.0 and 1.0, inclusive. + fn fromIntensity(value: []const u8) !u8 { + const i = std.fmt.parseFloat(f64, value) catch return error.InvalidFormat; + if (i < 0.0 or i > 1.0) { + return error.InvalidFormat; + } + + return @intFromFloat(i * std.math.maxInt(u8)); + } + + /// Parse a color from a string of hexadecimal digits + /// + /// The string can contain 1, 2, 3, or 4 characters and represents the color + /// value scaled in 4, 8, 12, or 16 bits, respectively. + fn fromHex(value: []const u8) !u8 { + if (value.len == 0 or value.len > 4) { + return error.InvalidFormat; + } + + const color = std.fmt.parseUnsigned(u16, value, 16) catch return error.InvalidFormat; + const divisor: usize = switch (value.len) { + 1 => std.math.maxInt(u4), + 2 => std.math.maxInt(u8), + 3 => std.math.maxInt(u12), + 4 => std.math.maxInt(u16), + else => unreachable, + }; + + return @intCast(@as(usize, color) * std.math.maxInt(u8) / divisor); + } + + /// Parse a color specification. + /// + /// Any of the following forms are accepted: + /// + /// 1. rgb:// + /// + /// , , := h | hh | hhh | hhhh + /// + /// where `h` is a single hexadecimal digit. + /// + /// 2. rgbi:// + /// + /// where , , and are floating point values between + /// 0.0 and 1.0 (inclusive). + /// + /// 3. #hhhhhh + /// + /// where `h` is a single hexadecimal digit. + pub fn parse(value: []const u8) !RGB { + if (value.len == 0) { + return error.InvalidFormat; + } + + if (value[0] == '#') { + if (value.len != 7) { + return error.InvalidFormat; + } + + return RGB{ + .r = try RGB.fromHex(value[1..3]), + .g = try RGB.fromHex(value[3..5]), + .b = try RGB.fromHex(value[5..7]), + }; + } + + // Check for X11 named colors. We allow whitespace around the edges + // of the color because Kitty allows whitespace. This is not part of + // any spec I could find. + if (x11_color.map.get(std.mem.trim(u8, value, " "))) |rgb| return rgb; + + if (value.len < "rgb:a/a/a".len or !std.mem.eql(u8, value[0..3], "rgb")) { + return error.InvalidFormat; + } + + var i: usize = 3; + + const use_intensity = if (value[i] == 'i') blk: { + i += 1; + break :blk true; + } else false; + + if (value[i] != ':') { + return error.InvalidFormat; + } + + i += 1; + + const r = r: { + const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| + value[i..end] + else + return error.InvalidFormat; + + i += slice.len + 1; + + break :r if (use_intensity) + try RGB.fromIntensity(slice) + else + try RGB.fromHex(slice); + }; + + const g = g: { + const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| + value[i..end] + else + return error.InvalidFormat; + + i += slice.len + 1; + + break :g if (use_intensity) + try RGB.fromIntensity(slice) + else + try RGB.fromHex(slice); + }; + + const b = if (use_intensity) + try RGB.fromIntensity(value[i..]) + else + try RGB.fromHex(value[i..]); + + return RGB{ + .r = r, + .g = g, + .b = b, + }; + } +}; + +test "palette: default" { + const testing = std.testing; + + // Safety check + var i: u8 = 0; + while (i < 16) : (i += 1) { + try testing.expectEqual(Name.default(@as(Name, @enumFromInt(i))), default[i]); + } +} + +test "RGB.parse" { + const testing = std.testing; + + try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("rgbi:1.0/0/0")); + try testing.expectEqual(RGB{ .r = 127, .g = 160, .b = 0 }, try RGB.parse("rgb:7f/a0a0/0")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("rgb:f/ff/fff")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffff")); + try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010")); + + try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black")); + try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("red")); + try testing.expectEqual(RGB{ .r = 0, .g = 255, .b = 0 }, try RGB.parse("green")); + try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 255 }, try RGB.parse("blue")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("white")); + + try testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, try RGB.parse("LawnGreen")); + try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, try RGB.parse("medium spring green")); + try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, try RGB.parse(" Forest Green ")); + + // Invalid format + try testing.expectError(error.InvalidFormat, RGB.parse("rgb;")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:")); + try testing.expectError(error.InvalidFormat, RGB.parse(":a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:a/a/a/")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:00000///")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:000/")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgbi:a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:0.5/0.0/1.0")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:not/hex/zz")); + try testing.expectError(error.InvalidFormat, RGB.parse("#")); + try testing.expectError(error.InvalidFormat, RGB.parse("#ff")); + try testing.expectError(error.InvalidFormat, RGB.parse("#ffff")); + try testing.expectError(error.InvalidFormat, RGB.parse("#fffff")); + try testing.expectError(error.InvalidFormat, RGB.parse("#gggggg")); +} diff --git a/src/terminal2/csi.zig b/src/terminal2/csi.zig new file mode 100644 index 000000000..877f5986e --- /dev/null +++ b/src/terminal2/csi.zig @@ -0,0 +1,33 @@ +// Modes for the ED CSI command. +pub const EraseDisplay = enum(u8) { + below = 0, + above = 1, + complete = 2, + scrollback = 3, + + /// This is an extension added by Kitty to move the viewport into the + /// scrollback and then erase the display. + scroll_complete = 22, +}; + +// Modes for the EL CSI command. +pub const EraseLine = enum(u8) { + right = 0, + left = 1, + complete = 2, + right_unless_pending_wrap = 4, + + // Non-exhaustive so that @intToEnum never fails since the inputs are + // user-generated. + _, +}; + +// Modes for the TBC (tab clear) command. +pub const TabClear = enum(u8) { + current = 0, + all = 3, + + // Non-exhaustive so that @intToEnum never fails since the inputs are + // user-generated. + _, +}; diff --git a/src/terminal/new/hash_map.zig b/src/terminal2/hash_map.zig similarity index 100% rename from src/terminal/new/hash_map.zig rename to src/terminal2/hash_map.zig diff --git a/src/terminal2/kitty.zig b/src/terminal2/kitty.zig new file mode 100644 index 000000000..6b86a3280 --- /dev/null +++ b/src/terminal2/kitty.zig @@ -0,0 +1,9 @@ +//! Types and functions related to Kitty protocols. + +// TODO: migrate to terminal2 +pub const graphics = @import("../terminal/kitty/graphics.zig"); +pub usingnamespace @import("../terminal/kitty/key.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/terminal/new/main.zig b/src/terminal2/main.zig similarity index 100% rename from src/terminal/new/main.zig rename to src/terminal2/main.zig diff --git a/src/terminal2/modes.zig b/src/terminal2/modes.zig new file mode 100644 index 000000000..e42efa16e --- /dev/null +++ b/src/terminal2/modes.zig @@ -0,0 +1,247 @@ +//! This file contains all the terminal modes that we support +//! and various support types for them: an enum of supported modes, +//! a packed struct to store mode values, a more generalized state +//! struct to store values plus handle save/restore, and much more. +//! +//! There is pretty heavy comptime usage and type generation here. +//! I don't love to have this sort of complexity but its a good way +//! to ensure all our various types and logic remain in sync. + +const std = @import("std"); +const testing = std.testing; + +/// A struct that maintains the state of all the settable modes. +pub const ModeState = struct { + /// The values of the current modes. + values: ModePacked = .{}, + + /// The saved values. We only allow saving each mode once. + /// This is in line with other terminals that implement XTSAVE + /// and XTRESTORE. We can improve this in the future if it becomes + /// a real-world issue but we need to be aware of a DoS vector. + saved: ModePacked = .{}, + + /// Set a mode to a value. + pub fn set(self: *ModeState, mode: Mode, value: bool) void { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + @field(self.values, entry.name) = value; + }, + } + } + + /// Get the value of a mode. + pub fn get(self: *ModeState, mode: Mode) bool { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + return @field(self.values, entry.name); + }, + } + } + + /// Save the state of the given mode. This can then be restored + /// with restore. This will only be accurate if the previous + /// mode was saved exactly once and not restored. Otherwise this + /// will just keep restoring the last stored value in memory. + pub fn save(self: *ModeState, mode: Mode) void { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + @field(self.saved, entry.name) = @field(self.values, entry.name); + }, + } + } + + /// See save. This will return the restored value. + pub fn restore(self: *ModeState, mode: Mode) bool { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + @field(self.values, entry.name) = @field(self.saved, entry.name); + return @field(self.values, entry.name); + }, + } + } + + test { + // We have this here so that we explicitly fail when we change the + // size of modes. The size of modes is NOT particularly important, + // we just want to be mentally aware when it happens. + try std.testing.expectEqual(8, @sizeOf(ModePacked)); + } +}; + +/// A packed struct of all the settable modes. This shouldn't +/// be used directly but rather through the ModeState struct. +pub const ModePacked = packed_struct: { + const StructField = std.builtin.Type.StructField; + var fields: [entries.len]StructField = undefined; + for (entries, 0..) |entry, i| { + fields[i] = .{ + .name = entry.name, + .type = bool, + .default_value = &entry.default, + .is_comptime = false, + .alignment = 0, + }; + } + + break :packed_struct @Type(.{ .Struct = .{ + .layout = .Packed, + .fields = &fields, + .decls = &.{}, + .is_tuple = false, + } }); +}; + +/// An enum(u16) of the available modes. See entries for available values. +pub const Mode = mode_enum: { + const EnumField = std.builtin.Type.EnumField; + var fields: [entries.len]EnumField = undefined; + for (entries, 0..) |entry, i| { + fields[i] = .{ + .name = entry.name, + .value = @as(ModeTag.Backing, @bitCast(ModeTag{ + .value = entry.value, + .ansi = entry.ansi, + })), + }; + } + + break :mode_enum @Type(.{ .Enum = .{ + .tag_type = ModeTag.Backing, + .fields = &fields, + .decls = &.{}, + .is_exhaustive = true, + } }); +}; + +/// The tag type for our enum is a u16 but we use a packed struct +/// in order to pack the ansi bit into the tag. +pub const ModeTag = packed struct(u16) { + pub const Backing = @typeInfo(@This()).Struct.backing_integer.?; + value: u15, + ansi: bool = false, + + test "order" { + const t: ModeTag = .{ .value = 1 }; + const int: Backing = @bitCast(t); + try std.testing.expectEqual(@as(Backing, 1), int); + } +}; + +pub fn modeFromInt(v: u16, ansi: bool) ?Mode { + inline for (entries) |entry| { + if (comptime !entry.disabled) { + if (entry.value == v and entry.ansi == ansi) { + const tag: ModeTag = .{ .ansi = ansi, .value = entry.value }; + const int: ModeTag.Backing = @bitCast(tag); + return @enumFromInt(int); + } + } + } + + return null; +} + +fn entryForMode(comptime mode: Mode) ModeEntry { + @setEvalBranchQuota(10_000); + const name = @tagName(mode); + for (entries) |entry| { + if (std.mem.eql(u8, entry.name, name)) return entry; + } + + unreachable; +} + +/// A single entry of a possible mode we support. This is used to +/// dynamically define the enum and other tables. +const ModeEntry = struct { + name: [:0]const u8, + value: comptime_int, + default: bool = false, + + /// True if this is an ANSI mode, false if its a DEC mode (?-prefixed). + ansi: bool = false, + + /// If true, this mode is disabled and Ghostty will not allow it to be + /// set or queried. The mode enum still has it, allowing Ghostty developers + /// to develop a mode without exposing it to real users. + disabled: bool = false, +}; + +/// The full list of available entries. For documentation see how +/// they're used within Ghostty or google their values. It is not +/// valuable to redocument them all here. +const entries: []const ModeEntry = &.{ + // ANSI + .{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM + .{ .name = "insert", .value = 4, .ansi = true }, + .{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM + .{ .name = "linefeed", .value = 20, .ansi = true }, + + // DEC + .{ .name = "cursor_keys", .value = 1 }, // DECCKM + .{ .name = "132_column", .value = 3 }, + .{ .name = "slow_scroll", .value = 4 }, + .{ .name = "reverse_colors", .value = 5 }, + .{ .name = "origin", .value = 6 }, + .{ .name = "wraparound", .value = 7, .default = true }, + .{ .name = "autorepeat", .value = 8 }, + .{ .name = "mouse_event_x10", .value = 9 }, + .{ .name = "cursor_blinking", .value = 12 }, + .{ .name = "cursor_visible", .value = 25, .default = true }, + .{ .name = "enable_mode_3", .value = 40 }, + .{ .name = "reverse_wrap", .value = 45 }, + .{ .name = "keypad_keys", .value = 66 }, + .{ .name = "enable_left_and_right_margin", .value = 69 }, + .{ .name = "mouse_event_normal", .value = 1000 }, + .{ .name = "mouse_event_button", .value = 1002 }, + .{ .name = "mouse_event_any", .value = 1003 }, + .{ .name = "focus_event", .value = 1004 }, + .{ .name = "mouse_format_utf8", .value = 1005 }, + .{ .name = "mouse_format_sgr", .value = 1006 }, + .{ .name = "mouse_alternate_scroll", .value = 1007, .default = true }, + .{ .name = "mouse_format_urxvt", .value = 1015 }, + .{ .name = "mouse_format_sgr_pixels", .value = 1016 }, + .{ .name = "ignore_keypad_with_numlock", .value = 1035, .default = true }, + .{ .name = "alt_esc_prefix", .value = 1036, .default = true }, + .{ .name = "alt_sends_escape", .value = 1039 }, + .{ .name = "reverse_wrap_extended", .value = 1045 }, + .{ .name = "alt_screen", .value = 1047 }, + .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 }, + .{ .name = "bracketed_paste", .value = 2004 }, + .{ .name = "synchronized_output", .value = 2026 }, + .{ .name = "grapheme_cluster", .value = 2027 }, + .{ .name = "report_color_scheme", .value = 2031 }, +}; + +test { + _ = Mode; + _ = ModePacked; +} + +test modeFromInt { + try testing.expect(modeFromInt(4, true).? == .insert); + try testing.expect(modeFromInt(9, true) == null); + try testing.expect(modeFromInt(9, false).? == .mouse_event_x10); + try testing.expect(modeFromInt(14, true) == null); +} + +test ModeState { + var state: ModeState = .{}; + + // Normal set/get + try testing.expect(!state.get(.cursor_keys)); + state.set(.cursor_keys, true); + try testing.expect(state.get(.cursor_keys)); + + // Save/restore + state.save(.cursor_keys); + state.set(.cursor_keys, false); + try testing.expect(!state.get(.cursor_keys)); + try testing.expect(state.restore(.cursor_keys)); + try testing.expect(state.get(.cursor_keys)); +} diff --git a/src/terminal2/mouse_shape.zig b/src/terminal2/mouse_shape.zig new file mode 100644 index 000000000..cf8f42c4b --- /dev/null +++ b/src/terminal2/mouse_shape.zig @@ -0,0 +1,115 @@ +const std = @import("std"); + +/// The possible cursor shapes. Not all app runtimes support these shapes. +/// The shapes are always based on the W3C supported cursor styles so we +/// can have a cross platform list. +// +// Must be kept in sync with ghostty_cursor_shape_e +pub const MouseShape = enum(c_int) { + default, + context_menu, + help, + pointer, + progress, + wait, + cell, + crosshair, + text, + vertical_text, + alias, + copy, + move, + no_drop, + not_allowed, + grab, + grabbing, + all_scroll, + col_resize, + row_resize, + n_resize, + e_resize, + s_resize, + w_resize, + ne_resize, + nw_resize, + se_resize, + sw_resize, + ew_resize, + ns_resize, + nesw_resize, + nwse_resize, + zoom_in, + zoom_out, + + /// Build cursor shape from string or null if its unknown. + pub fn fromString(v: []const u8) ?MouseShape { + return string_map.get(v); + } +}; + +const string_map = std.ComptimeStringMap(MouseShape, .{ + // W3C + .{ "default", .default }, + .{ "context-menu", .context_menu }, + .{ "help", .help }, + .{ "pointer", .pointer }, + .{ "progress", .progress }, + .{ "wait", .wait }, + .{ "cell", .cell }, + .{ "crosshair", .crosshair }, + .{ "text", .text }, + .{ "vertical-text", .vertical_text }, + .{ "alias", .alias }, + .{ "copy", .copy }, + .{ "move", .move }, + .{ "no-drop", .no_drop }, + .{ "not-allowed", .not_allowed }, + .{ "grab", .grab }, + .{ "grabbing", .grabbing }, + .{ "all-scroll", .all_scroll }, + .{ "col-resize", .col_resize }, + .{ "row-resize", .row_resize }, + .{ "n-resize", .n_resize }, + .{ "e-resize", .e_resize }, + .{ "s-resize", .s_resize }, + .{ "w-resize", .w_resize }, + .{ "ne-resize", .ne_resize }, + .{ "nw-resize", .nw_resize }, + .{ "se-resize", .se_resize }, + .{ "sw-resize", .sw_resize }, + .{ "ew-resize", .ew_resize }, + .{ "ns-resize", .ns_resize }, + .{ "nesw-resize", .nesw_resize }, + .{ "nwse-resize", .nwse_resize }, + .{ "zoom-in", .zoom_in }, + .{ "zoom-out", .zoom_out }, + + // xterm/foot + .{ "left_ptr", .default }, + .{ "question_arrow", .help }, + .{ "hand", .pointer }, + .{ "left_ptr_watch", .progress }, + .{ "watch", .wait }, + .{ "cross", .crosshair }, + .{ "xterm", .text }, + .{ "dnd-link", .alias }, + .{ "dnd-copy", .copy }, + .{ "dnd-move", .move }, + .{ "dnd-no-drop", .no_drop }, + .{ "crossed_circle", .not_allowed }, + .{ "hand1", .grab }, + .{ "right_side", .e_resize }, + .{ "top_side", .n_resize }, + .{ "top_right_corner", .ne_resize }, + .{ "top_left_corner", .nw_resize }, + .{ "bottom_side", .s_resize }, + .{ "bottom_right_corner", .se_resize }, + .{ "bottom_left_corner", .sw_resize }, + .{ "left_side", .w_resize }, + .{ "fleur", .all_scroll }, +}); + +test "cursor shape from string" { + const testing = std.testing; + try testing.expectEqual(MouseShape.default, MouseShape.fromString("default").?); +} diff --git a/src/terminal/new/page.zig b/src/terminal2/page.zig similarity index 99% rename from src/terminal/new/page.zig rename to src/terminal2/page.zig index 045ad29af..b9bd9b993 100644 --- a/src/terminal/new/page.zig +++ b/src/terminal2/page.zig @@ -2,9 +2,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const testing = std.testing; -const fastmem = @import("../../fastmem.zig"); -const color = @import("../color.zig"); -const sgr = @import("../sgr.zig"); +const fastmem = @import("../fastmem.zig"); +const color = @import("color.zig"); +const sgr = @import("sgr.zig"); const style = @import("style.zig"); const size = @import("size.zig"); const getOffset = size.getOffset; diff --git a/src/terminal/new/point.zig b/src/terminal2/point.zig similarity index 95% rename from src/terminal/new/point.zig rename to src/terminal2/point.zig index 00350f265..4f1d7836b 100644 --- a/src/terminal/new/point.zig +++ b/src/terminal2/point.zig @@ -77,11 +77,6 @@ pub const Viewport = struct { pub fn eql(self: Viewport, other: Viewport) bool { return self.x == other.x and self.y == other.y; } - - const TerminalScreen = @import("Screen.zig"); - pub fn toScreen(_: Viewport, _: *const TerminalScreen) Screen { - @panic("TODO"); - } }; /// A point in the terminal that is in relation to the entire screen. diff --git a/src/terminal2/res/rgb.txt b/src/terminal2/res/rgb.txt new file mode 100644 index 000000000..709664376 --- /dev/null +++ b/src/terminal2/res/rgb.txt @@ -0,0 +1,782 @@ +255 250 250 snow +248 248 255 ghost white +248 248 255 GhostWhite +245 245 245 white smoke +245 245 245 WhiteSmoke +220 220 220 gainsboro +255 250 240 floral white +255 250 240 FloralWhite +253 245 230 old lace +253 245 230 OldLace +250 240 230 linen +250 235 215 antique white +250 235 215 AntiqueWhite +255 239 213 papaya whip +255 239 213 PapayaWhip +255 235 205 blanched almond +255 235 205 BlanchedAlmond +255 228 196 bisque +255 218 185 peach puff +255 218 185 PeachPuff +255 222 173 navajo white +255 222 173 NavajoWhite +255 228 181 moccasin +255 248 220 cornsilk +255 255 240 ivory +255 250 205 lemon chiffon +255 250 205 LemonChiffon +255 245 238 seashell +240 255 240 honeydew +245 255 250 mint cream +245 255 250 MintCream +240 255 255 azure +240 248 255 alice blue +240 248 255 AliceBlue +230 230 250 lavender +255 240 245 lavender blush +255 240 245 LavenderBlush +255 228 225 misty rose +255 228 225 MistyRose +255 255 255 white + 0 0 0 black + 47 79 79 dark slate gray + 47 79 79 DarkSlateGray + 47 79 79 dark slate grey + 47 79 79 DarkSlateGrey +105 105 105 dim gray +105 105 105 DimGray +105 105 105 dim grey +105 105 105 DimGrey +112 128 144 slate gray +112 128 144 SlateGray +112 128 144 slate grey +112 128 144 SlateGrey +119 136 153 light slate gray +119 136 153 LightSlateGray +119 136 153 light slate grey +119 136 153 LightSlateGrey +190 190 190 gray +190 190 190 grey +190 190 190 x11 gray +190 190 190 X11Gray +190 190 190 x11 grey +190 190 190 X11Grey +128 128 128 web gray +128 128 128 WebGray +128 128 128 web grey +128 128 128 WebGrey +211 211 211 light grey +211 211 211 LightGrey +211 211 211 light gray +211 211 211 LightGray + 25 25 112 midnight blue + 25 25 112 MidnightBlue + 0 0 128 navy + 0 0 128 navy blue + 0 0 128 NavyBlue +100 149 237 cornflower blue +100 149 237 CornflowerBlue + 72 61 139 dark slate blue + 72 61 139 DarkSlateBlue +106 90 205 slate blue +106 90 205 SlateBlue +123 104 238 medium slate blue +123 104 238 MediumSlateBlue +132 112 255 light slate blue +132 112 255 LightSlateBlue + 0 0 205 medium blue + 0 0 205 MediumBlue + 65 105 225 royal blue + 65 105 225 RoyalBlue + 0 0 255 blue + 30 144 255 dodger blue + 30 144 255 DodgerBlue + 0 191 255 deep sky blue + 0 191 255 DeepSkyBlue +135 206 235 sky blue +135 206 235 SkyBlue +135 206 250 light sky blue +135 206 250 LightSkyBlue + 70 130 180 steel blue + 70 130 180 SteelBlue +176 196 222 light steel blue +176 196 222 LightSteelBlue +173 216 230 light blue +173 216 230 LightBlue +176 224 230 powder blue +176 224 230 PowderBlue +175 238 238 pale turquoise +175 238 238 PaleTurquoise + 0 206 209 dark turquoise + 0 206 209 DarkTurquoise + 72 209 204 medium turquoise + 72 209 204 MediumTurquoise + 64 224 208 turquoise + 0 255 255 cyan + 0 255 255 aqua +224 255 255 light cyan +224 255 255 LightCyan + 95 158 160 cadet blue + 95 158 160 CadetBlue +102 205 170 medium aquamarine +102 205 170 MediumAquamarine +127 255 212 aquamarine + 0 100 0 dark green + 0 100 0 DarkGreen + 85 107 47 dark olive green + 85 107 47 DarkOliveGreen +143 188 143 dark sea green +143 188 143 DarkSeaGreen + 46 139 87 sea green + 46 139 87 SeaGreen + 60 179 113 medium sea green + 60 179 113 MediumSeaGreen + 32 178 170 light sea green + 32 178 170 LightSeaGreen +152 251 152 pale green +152 251 152 PaleGreen + 0 255 127 spring green + 0 255 127 SpringGreen +124 252 0 lawn green +124 252 0 LawnGreen + 0 255 0 green + 0 255 0 lime + 0 255 0 x11 green + 0 255 0 X11Green + 0 128 0 web green + 0 128 0 WebGreen +127 255 0 chartreuse + 0 250 154 medium spring green + 0 250 154 MediumSpringGreen +173 255 47 green yellow +173 255 47 GreenYellow + 50 205 50 lime green + 50 205 50 LimeGreen +154 205 50 yellow green +154 205 50 YellowGreen + 34 139 34 forest green + 34 139 34 ForestGreen +107 142 35 olive drab +107 142 35 OliveDrab +189 183 107 dark khaki +189 183 107 DarkKhaki +240 230 140 khaki +238 232 170 pale goldenrod +238 232 170 PaleGoldenrod +250 250 210 light goldenrod yellow +250 250 210 LightGoldenrodYellow +255 255 224 light yellow +255 255 224 LightYellow +255 255 0 yellow +255 215 0 gold +238 221 130 light goldenrod +238 221 130 LightGoldenrod +218 165 32 goldenrod +184 134 11 dark goldenrod +184 134 11 DarkGoldenrod +188 143 143 rosy brown +188 143 143 RosyBrown +205 92 92 indian red +205 92 92 IndianRed +139 69 19 saddle brown +139 69 19 SaddleBrown +160 82 45 sienna +205 133 63 peru +222 184 135 burlywood +245 245 220 beige +245 222 179 wheat +244 164 96 sandy brown +244 164 96 SandyBrown +210 180 140 tan +210 105 30 chocolate +178 34 34 firebrick +165 42 42 brown +233 150 122 dark salmon +233 150 122 DarkSalmon +250 128 114 salmon +255 160 122 light salmon +255 160 122 LightSalmon +255 165 0 orange +255 140 0 dark orange +255 140 0 DarkOrange +255 127 80 coral +240 128 128 light coral +240 128 128 LightCoral +255 99 71 tomato +255 69 0 orange red +255 69 0 OrangeRed +255 0 0 red +255 105 180 hot pink +255 105 180 HotPink +255 20 147 deep pink +255 20 147 DeepPink +255 192 203 pink +255 182 193 light pink +255 182 193 LightPink +219 112 147 pale violet red +219 112 147 PaleVioletRed +176 48 96 maroon +176 48 96 x11 maroon +176 48 96 X11Maroon +128 0 0 web maroon +128 0 0 WebMaroon +199 21 133 medium violet red +199 21 133 MediumVioletRed +208 32 144 violet red +208 32 144 VioletRed +255 0 255 magenta +255 0 255 fuchsia +238 130 238 violet +221 160 221 plum +218 112 214 orchid +186 85 211 medium orchid +186 85 211 MediumOrchid +153 50 204 dark orchid +153 50 204 DarkOrchid +148 0 211 dark violet +148 0 211 DarkViolet +138 43 226 blue violet +138 43 226 BlueViolet +160 32 240 purple +160 32 240 x11 purple +160 32 240 X11Purple +128 0 128 web purple +128 0 128 WebPurple +147 112 219 medium purple +147 112 219 MediumPurple +216 191 216 thistle +255 250 250 snow1 +238 233 233 snow2 +205 201 201 snow3 +139 137 137 snow4 +255 245 238 seashell1 +238 229 222 seashell2 +205 197 191 seashell3 +139 134 130 seashell4 +255 239 219 AntiqueWhite1 +238 223 204 AntiqueWhite2 +205 192 176 AntiqueWhite3 +139 131 120 AntiqueWhite4 +255 228 196 bisque1 +238 213 183 bisque2 +205 183 158 bisque3 +139 125 107 bisque4 +255 218 185 PeachPuff1 +238 203 173 PeachPuff2 +205 175 149 PeachPuff3 +139 119 101 PeachPuff4 +255 222 173 NavajoWhite1 +238 207 161 NavajoWhite2 +205 179 139 NavajoWhite3 +139 121 94 NavajoWhite4 +255 250 205 LemonChiffon1 +238 233 191 LemonChiffon2 +205 201 165 LemonChiffon3 +139 137 112 LemonChiffon4 +255 248 220 cornsilk1 +238 232 205 cornsilk2 +205 200 177 cornsilk3 +139 136 120 cornsilk4 +255 255 240 ivory1 +238 238 224 ivory2 +205 205 193 ivory3 +139 139 131 ivory4 +240 255 240 honeydew1 +224 238 224 honeydew2 +193 205 193 honeydew3 +131 139 131 honeydew4 +255 240 245 LavenderBlush1 +238 224 229 LavenderBlush2 +205 193 197 LavenderBlush3 +139 131 134 LavenderBlush4 +255 228 225 MistyRose1 +238 213 210 MistyRose2 +205 183 181 MistyRose3 +139 125 123 MistyRose4 +240 255 255 azure1 +224 238 238 azure2 +193 205 205 azure3 +131 139 139 azure4 +131 111 255 SlateBlue1 +122 103 238 SlateBlue2 +105 89 205 SlateBlue3 + 71 60 139 SlateBlue4 + 72 118 255 RoyalBlue1 + 67 110 238 RoyalBlue2 + 58 95 205 RoyalBlue3 + 39 64 139 RoyalBlue4 + 0 0 255 blue1 + 0 0 238 blue2 + 0 0 205 blue3 + 0 0 139 blue4 + 30 144 255 DodgerBlue1 + 28 134 238 DodgerBlue2 + 24 116 205 DodgerBlue3 + 16 78 139 DodgerBlue4 + 99 184 255 SteelBlue1 + 92 172 238 SteelBlue2 + 79 148 205 SteelBlue3 + 54 100 139 SteelBlue4 + 0 191 255 DeepSkyBlue1 + 0 178 238 DeepSkyBlue2 + 0 154 205 DeepSkyBlue3 + 0 104 139 DeepSkyBlue4 +135 206 255 SkyBlue1 +126 192 238 SkyBlue2 +108 166 205 SkyBlue3 + 74 112 139 SkyBlue4 +176 226 255 LightSkyBlue1 +164 211 238 LightSkyBlue2 +141 182 205 LightSkyBlue3 + 96 123 139 LightSkyBlue4 +198 226 255 SlateGray1 +185 211 238 SlateGray2 +159 182 205 SlateGray3 +108 123 139 SlateGray4 +202 225 255 LightSteelBlue1 +188 210 238 LightSteelBlue2 +162 181 205 LightSteelBlue3 +110 123 139 LightSteelBlue4 +191 239 255 LightBlue1 +178 223 238 LightBlue2 +154 192 205 LightBlue3 +104 131 139 LightBlue4 +224 255 255 LightCyan1 +209 238 238 LightCyan2 +180 205 205 LightCyan3 +122 139 139 LightCyan4 +187 255 255 PaleTurquoise1 +174 238 238 PaleTurquoise2 +150 205 205 PaleTurquoise3 +102 139 139 PaleTurquoise4 +152 245 255 CadetBlue1 +142 229 238 CadetBlue2 +122 197 205 CadetBlue3 + 83 134 139 CadetBlue4 + 0 245 255 turquoise1 + 0 229 238 turquoise2 + 0 197 205 turquoise3 + 0 134 139 turquoise4 + 0 255 255 cyan1 + 0 238 238 cyan2 + 0 205 205 cyan3 + 0 139 139 cyan4 +151 255 255 DarkSlateGray1 +141 238 238 DarkSlateGray2 +121 205 205 DarkSlateGray3 + 82 139 139 DarkSlateGray4 +127 255 212 aquamarine1 +118 238 198 aquamarine2 +102 205 170 aquamarine3 + 69 139 116 aquamarine4 +193 255 193 DarkSeaGreen1 +180 238 180 DarkSeaGreen2 +155 205 155 DarkSeaGreen3 +105 139 105 DarkSeaGreen4 + 84 255 159 SeaGreen1 + 78 238 148 SeaGreen2 + 67 205 128 SeaGreen3 + 46 139 87 SeaGreen4 +154 255 154 PaleGreen1 +144 238 144 PaleGreen2 +124 205 124 PaleGreen3 + 84 139 84 PaleGreen4 + 0 255 127 SpringGreen1 + 0 238 118 SpringGreen2 + 0 205 102 SpringGreen3 + 0 139 69 SpringGreen4 + 0 255 0 green1 + 0 238 0 green2 + 0 205 0 green3 + 0 139 0 green4 +127 255 0 chartreuse1 +118 238 0 chartreuse2 +102 205 0 chartreuse3 + 69 139 0 chartreuse4 +192 255 62 OliveDrab1 +179 238 58 OliveDrab2 +154 205 50 OliveDrab3 +105 139 34 OliveDrab4 +202 255 112 DarkOliveGreen1 +188 238 104 DarkOliveGreen2 +162 205 90 DarkOliveGreen3 +110 139 61 DarkOliveGreen4 +255 246 143 khaki1 +238 230 133 khaki2 +205 198 115 khaki3 +139 134 78 khaki4 +255 236 139 LightGoldenrod1 +238 220 130 LightGoldenrod2 +205 190 112 LightGoldenrod3 +139 129 76 LightGoldenrod4 +255 255 224 LightYellow1 +238 238 209 LightYellow2 +205 205 180 LightYellow3 +139 139 122 LightYellow4 +255 255 0 yellow1 +238 238 0 yellow2 +205 205 0 yellow3 +139 139 0 yellow4 +255 215 0 gold1 +238 201 0 gold2 +205 173 0 gold3 +139 117 0 gold4 +255 193 37 goldenrod1 +238 180 34 goldenrod2 +205 155 29 goldenrod3 +139 105 20 goldenrod4 +255 185 15 DarkGoldenrod1 +238 173 14 DarkGoldenrod2 +205 149 12 DarkGoldenrod3 +139 101 8 DarkGoldenrod4 +255 193 193 RosyBrown1 +238 180 180 RosyBrown2 +205 155 155 RosyBrown3 +139 105 105 RosyBrown4 +255 106 106 IndianRed1 +238 99 99 IndianRed2 +205 85 85 IndianRed3 +139 58 58 IndianRed4 +255 130 71 sienna1 +238 121 66 sienna2 +205 104 57 sienna3 +139 71 38 sienna4 +255 211 155 burlywood1 +238 197 145 burlywood2 +205 170 125 burlywood3 +139 115 85 burlywood4 +255 231 186 wheat1 +238 216 174 wheat2 +205 186 150 wheat3 +139 126 102 wheat4 +255 165 79 tan1 +238 154 73 tan2 +205 133 63 tan3 +139 90 43 tan4 +255 127 36 chocolate1 +238 118 33 chocolate2 +205 102 29 chocolate3 +139 69 19 chocolate4 +255 48 48 firebrick1 +238 44 44 firebrick2 +205 38 38 firebrick3 +139 26 26 firebrick4 +255 64 64 brown1 +238 59 59 brown2 +205 51 51 brown3 +139 35 35 brown4 +255 140 105 salmon1 +238 130 98 salmon2 +205 112 84 salmon3 +139 76 57 salmon4 +255 160 122 LightSalmon1 +238 149 114 LightSalmon2 +205 129 98 LightSalmon3 +139 87 66 LightSalmon4 +255 165 0 orange1 +238 154 0 orange2 +205 133 0 orange3 +139 90 0 orange4 +255 127 0 DarkOrange1 +238 118 0 DarkOrange2 +205 102 0 DarkOrange3 +139 69 0 DarkOrange4 +255 114 86 coral1 +238 106 80 coral2 +205 91 69 coral3 +139 62 47 coral4 +255 99 71 tomato1 +238 92 66 tomato2 +205 79 57 tomato3 +139 54 38 tomato4 +255 69 0 OrangeRed1 +238 64 0 OrangeRed2 +205 55 0 OrangeRed3 +139 37 0 OrangeRed4 +255 0 0 red1 +238 0 0 red2 +205 0 0 red3 +139 0 0 red4 +255 20 147 DeepPink1 +238 18 137 DeepPink2 +205 16 118 DeepPink3 +139 10 80 DeepPink4 +255 110 180 HotPink1 +238 106 167 HotPink2 +205 96 144 HotPink3 +139 58 98 HotPink4 +255 181 197 pink1 +238 169 184 pink2 +205 145 158 pink3 +139 99 108 pink4 +255 174 185 LightPink1 +238 162 173 LightPink2 +205 140 149 LightPink3 +139 95 101 LightPink4 +255 130 171 PaleVioletRed1 +238 121 159 PaleVioletRed2 +205 104 137 PaleVioletRed3 +139 71 93 PaleVioletRed4 +255 52 179 maroon1 +238 48 167 maroon2 +205 41 144 maroon3 +139 28 98 maroon4 +255 62 150 VioletRed1 +238 58 140 VioletRed2 +205 50 120 VioletRed3 +139 34 82 VioletRed4 +255 0 255 magenta1 +238 0 238 magenta2 +205 0 205 magenta3 +139 0 139 magenta4 +255 131 250 orchid1 +238 122 233 orchid2 +205 105 201 orchid3 +139 71 137 orchid4 +255 187 255 plum1 +238 174 238 plum2 +205 150 205 plum3 +139 102 139 plum4 +224 102 255 MediumOrchid1 +209 95 238 MediumOrchid2 +180 82 205 MediumOrchid3 +122 55 139 MediumOrchid4 +191 62 255 DarkOrchid1 +178 58 238 DarkOrchid2 +154 50 205 DarkOrchid3 +104 34 139 DarkOrchid4 +155 48 255 purple1 +145 44 238 purple2 +125 38 205 purple3 + 85 26 139 purple4 +171 130 255 MediumPurple1 +159 121 238 MediumPurple2 +137 104 205 MediumPurple3 + 93 71 139 MediumPurple4 +255 225 255 thistle1 +238 210 238 thistle2 +205 181 205 thistle3 +139 123 139 thistle4 + 0 0 0 gray0 + 0 0 0 grey0 + 3 3 3 gray1 + 3 3 3 grey1 + 5 5 5 gray2 + 5 5 5 grey2 + 8 8 8 gray3 + 8 8 8 grey3 + 10 10 10 gray4 + 10 10 10 grey4 + 13 13 13 gray5 + 13 13 13 grey5 + 15 15 15 gray6 + 15 15 15 grey6 + 18 18 18 gray7 + 18 18 18 grey7 + 20 20 20 gray8 + 20 20 20 grey8 + 23 23 23 gray9 + 23 23 23 grey9 + 26 26 26 gray10 + 26 26 26 grey10 + 28 28 28 gray11 + 28 28 28 grey11 + 31 31 31 gray12 + 31 31 31 grey12 + 33 33 33 gray13 + 33 33 33 grey13 + 36 36 36 gray14 + 36 36 36 grey14 + 38 38 38 gray15 + 38 38 38 grey15 + 41 41 41 gray16 + 41 41 41 grey16 + 43 43 43 gray17 + 43 43 43 grey17 + 46 46 46 gray18 + 46 46 46 grey18 + 48 48 48 gray19 + 48 48 48 grey19 + 51 51 51 gray20 + 51 51 51 grey20 + 54 54 54 gray21 + 54 54 54 grey21 + 56 56 56 gray22 + 56 56 56 grey22 + 59 59 59 gray23 + 59 59 59 grey23 + 61 61 61 gray24 + 61 61 61 grey24 + 64 64 64 gray25 + 64 64 64 grey25 + 66 66 66 gray26 + 66 66 66 grey26 + 69 69 69 gray27 + 69 69 69 grey27 + 71 71 71 gray28 + 71 71 71 grey28 + 74 74 74 gray29 + 74 74 74 grey29 + 77 77 77 gray30 + 77 77 77 grey30 + 79 79 79 gray31 + 79 79 79 grey31 + 82 82 82 gray32 + 82 82 82 grey32 + 84 84 84 gray33 + 84 84 84 grey33 + 87 87 87 gray34 + 87 87 87 grey34 + 89 89 89 gray35 + 89 89 89 grey35 + 92 92 92 gray36 + 92 92 92 grey36 + 94 94 94 gray37 + 94 94 94 grey37 + 97 97 97 gray38 + 97 97 97 grey38 + 99 99 99 gray39 + 99 99 99 grey39 +102 102 102 gray40 +102 102 102 grey40 +105 105 105 gray41 +105 105 105 grey41 +107 107 107 gray42 +107 107 107 grey42 +110 110 110 gray43 +110 110 110 grey43 +112 112 112 gray44 +112 112 112 grey44 +115 115 115 gray45 +115 115 115 grey45 +117 117 117 gray46 +117 117 117 grey46 +120 120 120 gray47 +120 120 120 grey47 +122 122 122 gray48 +122 122 122 grey48 +125 125 125 gray49 +125 125 125 grey49 +127 127 127 gray50 +127 127 127 grey50 +130 130 130 gray51 +130 130 130 grey51 +133 133 133 gray52 +133 133 133 grey52 +135 135 135 gray53 +135 135 135 grey53 +138 138 138 gray54 +138 138 138 grey54 +140 140 140 gray55 +140 140 140 grey55 +143 143 143 gray56 +143 143 143 grey56 +145 145 145 gray57 +145 145 145 grey57 +148 148 148 gray58 +148 148 148 grey58 +150 150 150 gray59 +150 150 150 grey59 +153 153 153 gray60 +153 153 153 grey60 +156 156 156 gray61 +156 156 156 grey61 +158 158 158 gray62 +158 158 158 grey62 +161 161 161 gray63 +161 161 161 grey63 +163 163 163 gray64 +163 163 163 grey64 +166 166 166 gray65 +166 166 166 grey65 +168 168 168 gray66 +168 168 168 grey66 +171 171 171 gray67 +171 171 171 grey67 +173 173 173 gray68 +173 173 173 grey68 +176 176 176 gray69 +176 176 176 grey69 +179 179 179 gray70 +179 179 179 grey70 +181 181 181 gray71 +181 181 181 grey71 +184 184 184 gray72 +184 184 184 grey72 +186 186 186 gray73 +186 186 186 grey73 +189 189 189 gray74 +189 189 189 grey74 +191 191 191 gray75 +191 191 191 grey75 +194 194 194 gray76 +194 194 194 grey76 +196 196 196 gray77 +196 196 196 grey77 +199 199 199 gray78 +199 199 199 grey78 +201 201 201 gray79 +201 201 201 grey79 +204 204 204 gray80 +204 204 204 grey80 +207 207 207 gray81 +207 207 207 grey81 +209 209 209 gray82 +209 209 209 grey82 +212 212 212 gray83 +212 212 212 grey83 +214 214 214 gray84 +214 214 214 grey84 +217 217 217 gray85 +217 217 217 grey85 +219 219 219 gray86 +219 219 219 grey86 +222 222 222 gray87 +222 222 222 grey87 +224 224 224 gray88 +224 224 224 grey88 +227 227 227 gray89 +227 227 227 grey89 +229 229 229 gray90 +229 229 229 grey90 +232 232 232 gray91 +232 232 232 grey91 +235 235 235 gray92 +235 235 235 grey92 +237 237 237 gray93 +237 237 237 grey93 +240 240 240 gray94 +240 240 240 grey94 +242 242 242 gray95 +242 242 242 grey95 +245 245 245 gray96 +245 245 245 grey96 +247 247 247 gray97 +247 247 247 grey97 +250 250 250 gray98 +250 250 250 grey98 +252 252 252 gray99 +252 252 252 grey99 +255 255 255 gray100 +255 255 255 grey100 +169 169 169 dark grey +169 169 169 DarkGrey +169 169 169 dark gray +169 169 169 DarkGray + 0 0 139 dark blue + 0 0 139 DarkBlue + 0 139 139 dark cyan + 0 139 139 DarkCyan +139 0 139 dark magenta +139 0 139 DarkMagenta +139 0 0 dark red +139 0 0 DarkRed +144 238 144 light green +144 238 144 LightGreen +220 20 60 crimson + 75 0 130 indigo +128 128 0 olive +102 51 153 rebecca purple +102 51 153 RebeccaPurple +192 192 192 silver + 0 128 128 teal diff --git a/src/terminal2/sgr.zig b/src/terminal2/sgr.zig new file mode 100644 index 000000000..b23bd1514 --- /dev/null +++ b/src/terminal2/sgr.zig @@ -0,0 +1,559 @@ +//! SGR (Select Graphic Rendition) attrinvbute parsing and types. + +const std = @import("std"); +const testing = std.testing; +const color = @import("color.zig"); + +/// Attribute type for SGR +pub const Attribute = union(enum) { + /// Unset all attributes + unset: void, + + /// Unknown attribute, the raw CSI command parameters are here. + unknown: struct { + /// Full is the full SGR input. + full: []const u16, + + /// Partial is the remaining, where we got hung up. + partial: []const u16, + }, + + /// Bold the text. + bold: void, + reset_bold: void, + + /// Italic text. + italic: void, + reset_italic: void, + + /// Faint/dim text. + /// Note: reset faint is the same SGR code as reset bold + faint: void, + + /// Underline the text + underline: Underline, + reset_underline: void, + underline_color: color.RGB, + @"256_underline_color": u8, + reset_underline_color: void, + + /// Blink the text + blink: void, + reset_blink: void, + + /// Invert fg/bg colors. + inverse: void, + reset_inverse: void, + + /// Invisible + invisible: void, + reset_invisible: void, + + /// Strikethrough the text. + strikethrough: void, + reset_strikethrough: void, + + /// Set foreground color as RGB values. + direct_color_fg: color.RGB, + + /// Set background color as RGB values. + direct_color_bg: color.RGB, + + /// Set the background/foreground as a named color attribute. + @"8_bg": color.Name, + @"8_fg": color.Name, + + /// Reset the fg/bg to their default values. + reset_fg: void, + reset_bg: void, + + /// Set the background/foreground as a named bright color attribute. + @"8_bright_bg": color.Name, + @"8_bright_fg": color.Name, + + /// Set background color as 256-color palette. + @"256_bg": u8, + + /// Set foreground color as 256-color palette. + @"256_fg": u8, + + pub const Underline = enum(u3) { + none = 0, + single = 1, + double = 2, + curly = 3, + dotted = 4, + dashed = 5, + }; +}; + +/// Parser parses the attributes from a list of SGR parameters. +pub const Parser = struct { + params: []const u16, + idx: usize = 0, + + /// True if the separator is a colon + colon: bool = false, + + /// Next returns the next attribute or null if there are no more attributes. + pub fn next(self: *Parser) ?Attribute { + if (self.idx > self.params.len) return null; + + // Implicitly means unset + if (self.params.len == 0) { + self.idx += 1; + return Attribute{ .unset = {} }; + } + + const slice = self.params[self.idx..self.params.len]; + self.idx += 1; + + // Our last one will have an idx be the last value. + if (slice.len == 0) return null; + + switch (slice[0]) { + 0 => return Attribute{ .unset = {} }, + + 1 => return Attribute{ .bold = {} }, + + 2 => return Attribute{ .faint = {} }, + + 3 => return Attribute{ .italic = {} }, + + 4 => blk: { + if (self.colon) { + switch (slice.len) { + // 0 is unreachable because we're here and we read + // an element to get here. + 0 => unreachable, + + // 1 is possible if underline is the last element. + 1 => return Attribute{ .underline = .single }, + + // 2 means we have a specific underline style. + 2 => { + self.idx += 1; + switch (slice[1]) { + 0 => return Attribute{ .reset_underline = {} }, + 1 => return Attribute{ .underline = .single }, + 2 => return Attribute{ .underline = .double }, + 3 => return Attribute{ .underline = .curly }, + 4 => return Attribute{ .underline = .dotted }, + 5 => return Attribute{ .underline = .dashed }, + + // For unknown underline styles, just render + // a single underline. + else => return Attribute{ .underline = .single }, + } + }, + + // Colon-separated must only be 2. + else => break :blk, + } + } + + return Attribute{ .underline = .single }; + }, + + 5 => return Attribute{ .blink = {} }, + + 6 => return Attribute{ .blink = {} }, + + 7 => return Attribute{ .inverse = {} }, + + 8 => return Attribute{ .invisible = {} }, + + 9 => return Attribute{ .strikethrough = {} }, + + 22 => return Attribute{ .reset_bold = {} }, + + 23 => return Attribute{ .reset_italic = {} }, + + 24 => return Attribute{ .reset_underline = {} }, + + 25 => return Attribute{ .reset_blink = {} }, + + 27 => return Attribute{ .reset_inverse = {} }, + + 28 => return Attribute{ .reset_invisible = {} }, + + 29 => return Attribute{ .reset_strikethrough = {} }, + + 30...37 => return Attribute{ + .@"8_fg" = @enumFromInt(slice[0] - 30), + }, + + 38 => if (slice.len >= 5 and slice[1] == 2) { + self.idx += 4; + + // In the 6-len form, ignore the 3rd param. + const rgb = slice[2..5]; + + // We use @truncate because the value should be 0 to 255. If + // it isn't, the behavior is undefined so we just... truncate it. + return Attribute{ + .direct_color_fg = .{ + .r = @truncate(rgb[0]), + .g = @truncate(rgb[1]), + .b = @truncate(rgb[2]), + }, + }; + } else if (slice.len >= 3 and slice[1] == 5) { + self.idx += 2; + return Attribute{ + .@"256_fg" = @truncate(slice[2]), + }; + }, + + 39 => return Attribute{ .reset_fg = {} }, + + 40...47 => return Attribute{ + .@"8_bg" = @enumFromInt(slice[0] - 40), + }, + + 48 => if (slice.len >= 5 and slice[1] == 2) { + self.idx += 4; + + // We only support the 5-len form. + const rgb = slice[2..5]; + + // We use @truncate because the value should be 0 to 255. If + // it isn't, the behavior is undefined so we just... truncate it. + return Attribute{ + .direct_color_bg = .{ + .r = @truncate(rgb[0]), + .g = @truncate(rgb[1]), + .b = @truncate(rgb[2]), + }, + }; + } else if (slice.len >= 3 and slice[1] == 5) { + self.idx += 2; + return Attribute{ + .@"256_bg" = @truncate(slice[2]), + }; + }, + + 49 => return Attribute{ .reset_bg = {} }, + + 58 => if (slice.len >= 5 and slice[1] == 2) { + self.idx += 4; + + // In the 6-len form, ignore the 3rd param. Otherwise, use it. + const rgb = if (slice.len == 5) slice[2..5] else rgb: { + // Consume one more element + self.idx += 1; + break :rgb slice[3..6]; + }; + + // We use @truncate because the value should be 0 to 255. If + // it isn't, the behavior is undefined so we just... truncate it. + return Attribute{ + .underline_color = .{ + .r = @truncate(rgb[0]), + .g = @truncate(rgb[1]), + .b = @truncate(rgb[2]), + }, + }; + } else if (slice.len >= 3 and slice[1] == 5) { + self.idx += 2; + return Attribute{ + .@"256_underline_color" = @truncate(slice[2]), + }; + }, + + 59 => return Attribute{ .reset_underline_color = {} }, + + 90...97 => return Attribute{ + // 82 instead of 90 to offset to "bright" colors + .@"8_bright_fg" = @enumFromInt(slice[0] - 82), + }, + + 100...107 => return Attribute{ + .@"8_bright_bg" = @enumFromInt(slice[0] - 92), + }, + + else => {}, + } + + return Attribute{ .unknown = .{ .full = self.params, .partial = slice } }; + } +}; + +fn testParse(params: []const u16) Attribute { + var p: Parser = .{ .params = params }; + return p.next().?; +} + +fn testParseColon(params: []const u16) Attribute { + var p: Parser = .{ .params = params, .colon = true }; + return p.next().?; +} + +test "sgr: Parser" { + try testing.expect(testParse(&[_]u16{}) == .unset); + try testing.expect(testParse(&[_]u16{0}) == .unset); + + { + const v = testParse(&[_]u16{ 38, 2, 40, 44, 52 }); + try testing.expect(v == .direct_color_fg); + try testing.expectEqual(@as(u8, 40), v.direct_color_fg.r); + try testing.expectEqual(@as(u8, 44), v.direct_color_fg.g); + try testing.expectEqual(@as(u8, 52), v.direct_color_fg.b); + } + + try testing.expect(testParse(&[_]u16{ 38, 2, 44, 52 }) == .unknown); + + { + const v = testParse(&[_]u16{ 48, 2, 40, 44, 52 }); + try testing.expect(v == .direct_color_bg); + try testing.expectEqual(@as(u8, 40), v.direct_color_bg.r); + try testing.expectEqual(@as(u8, 44), v.direct_color_bg.g); + try testing.expectEqual(@as(u8, 52), v.direct_color_bg.b); + } + + try testing.expect(testParse(&[_]u16{ 48, 2, 44, 52 }) == .unknown); +} + +test "sgr: Parser multiple" { + var p: Parser = .{ .params = &[_]u16{ 0, 38, 2, 40, 44, 52 } }; + try testing.expect(p.next().? == .unset); + try testing.expect(p.next().? == .direct_color_fg); + try testing.expect(p.next() == null); + try testing.expect(p.next() == null); +} + +test "sgr: bold" { + { + const v = testParse(&[_]u16{1}); + try testing.expect(v == .bold); + } + + { + const v = testParse(&[_]u16{22}); + try testing.expect(v == .reset_bold); + } +} + +test "sgr: italic" { + { + const v = testParse(&[_]u16{3}); + try testing.expect(v == .italic); + } + + { + const v = testParse(&[_]u16{23}); + try testing.expect(v == .reset_italic); + } +} + +test "sgr: underline" { + { + const v = testParse(&[_]u16{4}); + try testing.expect(v == .underline); + } + + { + const v = testParse(&[_]u16{24}); + try testing.expect(v == .reset_underline); + } +} + +test "sgr: underline styles" { + { + const v = testParseColon(&[_]u16{ 4, 2 }); + try testing.expect(v == .underline); + try testing.expect(v.underline == .double); + } + + { + const v = testParseColon(&[_]u16{ 4, 0 }); + try testing.expect(v == .reset_underline); + } + + { + const v = testParseColon(&[_]u16{ 4, 1 }); + try testing.expect(v == .underline); + try testing.expect(v.underline == .single); + } + + { + const v = testParseColon(&[_]u16{ 4, 3 }); + try testing.expect(v == .underline); + try testing.expect(v.underline == .curly); + } + + { + const v = testParseColon(&[_]u16{ 4, 4 }); + try testing.expect(v == .underline); + try testing.expect(v.underline == .dotted); + } + + { + const v = testParseColon(&[_]u16{ 4, 5 }); + try testing.expect(v == .underline); + try testing.expect(v.underline == .dashed); + } +} + +test "sgr: blink" { + { + const v = testParse(&[_]u16{5}); + try testing.expect(v == .blink); + } + + { + const v = testParse(&[_]u16{6}); + try testing.expect(v == .blink); + } + + { + const v = testParse(&[_]u16{25}); + try testing.expect(v == .reset_blink); + } +} + +test "sgr: inverse" { + { + const v = testParse(&[_]u16{7}); + try testing.expect(v == .inverse); + } + + { + const v = testParse(&[_]u16{27}); + try testing.expect(v == .reset_inverse); + } +} + +test "sgr: strikethrough" { + { + const v = testParse(&[_]u16{9}); + try testing.expect(v == .strikethrough); + } + + { + const v = testParse(&[_]u16{29}); + try testing.expect(v == .reset_strikethrough); + } +} + +test "sgr: 8 color" { + var p: Parser = .{ .params = &[_]u16{ 31, 43, 90, 103 } }; + + { + const v = p.next().?; + try testing.expect(v == .@"8_fg"); + try testing.expect(v.@"8_fg" == .red); + } + + { + const v = p.next().?; + try testing.expect(v == .@"8_bg"); + try testing.expect(v.@"8_bg" == .yellow); + } + + { + const v = p.next().?; + try testing.expect(v == .@"8_bright_fg"); + try testing.expect(v.@"8_bright_fg" == .bright_black); + } + + { + const v = p.next().?; + try testing.expect(v == .@"8_bright_bg"); + try testing.expect(v.@"8_bright_bg" == .bright_yellow); + } +} + +test "sgr: 256 color" { + var p: Parser = .{ .params = &[_]u16{ 38, 5, 161, 48, 5, 236 } }; + try testing.expect(p.next().? == .@"256_fg"); + try testing.expect(p.next().? == .@"256_bg"); + try testing.expect(p.next() == null); +} + +test "sgr: 256 color underline" { + var p: Parser = .{ .params = &[_]u16{ 58, 5, 9 } }; + try testing.expect(p.next().? == .@"256_underline_color"); + try testing.expect(p.next() == null); +} + +test "sgr: 24-bit bg color" { + { + const v = testParseColon(&[_]u16{ 48, 2, 1, 2, 3 }); + try testing.expect(v == .direct_color_bg); + try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r); + try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g); + try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b); + } +} + +test "sgr: underline color" { + { + const v = testParseColon(&[_]u16{ 58, 2, 1, 2, 3 }); + try testing.expect(v == .underline_color); + try testing.expectEqual(@as(u8, 1), v.underline_color.r); + try testing.expectEqual(@as(u8, 2), v.underline_color.g); + try testing.expectEqual(@as(u8, 3), v.underline_color.b); + } + + { + const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 }); + try testing.expect(v == .underline_color); + try testing.expectEqual(@as(u8, 1), v.underline_color.r); + try testing.expectEqual(@as(u8, 2), v.underline_color.g); + try testing.expectEqual(@as(u8, 3), v.underline_color.b); + } +} + +test "sgr: reset underline color" { + var p: Parser = .{ .params = &[_]u16{59} }; + try testing.expect(p.next().? == .reset_underline_color); +} + +test "sgr: invisible" { + var p: Parser = .{ .params = &[_]u16{ 8, 28 } }; + try testing.expect(p.next().? == .invisible); + try testing.expect(p.next().? == .reset_invisible); +} + +test "sgr: underline, bg, and fg" { + var p: Parser = .{ + .params = &[_]u16{ 4, 38, 2, 255, 247, 219, 48, 2, 242, 93, 147, 4 }, + }; + { + const v = p.next().?; + try testing.expect(v == .underline); + try testing.expectEqual(Attribute.Underline.single, v.underline); + } + { + const v = p.next().?; + try testing.expect(v == .direct_color_fg); + try testing.expectEqual(@as(u8, 255), v.direct_color_fg.r); + try testing.expectEqual(@as(u8, 247), v.direct_color_fg.g); + try testing.expectEqual(@as(u8, 219), v.direct_color_fg.b); + } + { + const v = p.next().?; + try testing.expect(v == .direct_color_bg); + try testing.expectEqual(@as(u8, 242), v.direct_color_bg.r); + try testing.expectEqual(@as(u8, 93), v.direct_color_bg.g); + try testing.expectEqual(@as(u8, 147), v.direct_color_bg.b); + } + { + const v = p.next().?; + try testing.expect(v == .underline); + try testing.expectEqual(Attribute.Underline.single, v.underline); + } +} + +test "sgr: direct color fg missing color" { + // This used to crash + var p: Parser = .{ .params = &[_]u16{ 38, 5 }, .colon = false }; + while (p.next()) |_| {} +} + +test "sgr: direct color bg missing color" { + // This used to crash + var p: Parser = .{ .params = &[_]u16{ 48, 5 }, .colon = false }; + while (p.next()) |_| {} +} diff --git a/src/terminal/new/size.zig b/src/terminal2/size.zig similarity index 100% rename from src/terminal/new/size.zig rename to src/terminal2/size.zig diff --git a/src/terminal/new/style.zig b/src/terminal2/style.zig similarity index 99% rename from src/terminal/new/style.zig rename to src/terminal2/style.zig index 56c2e936a..e48630711 100644 --- a/src/terminal/new/style.zig +++ b/src/terminal2/style.zig @@ -1,7 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; -const color = @import("../color.zig"); -const sgr = @import("../sgr.zig"); +const color = @import("color.zig"); +const sgr = @import("sgr.zig"); const page = @import("page.zig"); const size = @import("size.zig"); const Offset = size.Offset; diff --git a/src/terminal2/x11_color.zig b/src/terminal2/x11_color.zig new file mode 100644 index 000000000..9e4eda86b --- /dev/null +++ b/src/terminal2/x11_color.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const assert = std.debug.assert; +const RGB = @import("color.zig").RGB; + +/// The map of all available X11 colors. +pub const map = colorMap() catch @compileError("failed to parse rgb.txt"); + +fn colorMap() !type { + @setEvalBranchQuota(100_000); + + const KV = struct { []const u8, RGB }; + + // The length of our data is the number of lines in the rgb file. + const len = std.mem.count(u8, data, "\n"); + var kvs: [len]KV = undefined; + + // Parse the line. This is not very robust parsing, because we expect + // a very exact format for rgb.txt. However, this is all done at comptime + // so if our data is bad, we should hopefully get an error here or one + // of our unit tests will catch it. + var iter = std.mem.splitScalar(u8, data, '\n'); + var i: usize = 0; + while (iter.next()) |line| { + if (line.len == 0) continue; + const r = try std.fmt.parseInt(u8, std.mem.trim(u8, line[0..3], " "), 10); + const g = try std.fmt.parseInt(u8, std.mem.trim(u8, line[4..7], " "), 10); + const b = try std.fmt.parseInt(u8, std.mem.trim(u8, line[8..11], " "), 10); + const name = std.mem.trim(u8, line[12..], " \t\n"); + kvs[i] = .{ name, .{ .r = r, .g = g, .b = b } }; + i += 1; + } + assert(i == len); + + return std.ComptimeStringMapWithEql( + RGB, + kvs, + std.comptime_string_map.eqlAsciiIgnoreCase, + ); +} + +/// This is the rgb.txt file from the X11 project. This was last sourced +/// from this location: https://gitlab.freedesktop.org/xorg/app/rgb +/// This data is licensed under the MIT/X11 license while this Zig file is +/// licensed under the same license as Ghostty. +const data = @embedFile("res/rgb.txt"); + +test { + const testing = std.testing; + try testing.expectEqual(null, map.get("nosuchcolor")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, map.get("white").?); + try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, map.get("medium spring green")); + try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, map.get("ForestGreen")); + try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, map.get("FoReStGReen")); + try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, map.get("black")); + try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, map.get("red")); + try testing.expectEqual(RGB{ .r = 0, .g = 255, .b = 0 }, map.get("green")); + try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 255 }, map.get("blue")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, map.get("white")); + try testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, map.get("lawngreen")); + try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, map.get("mediumspringgreen")); + try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, map.get("forestgreen")); +}