From 0e9d395a55b16da1d5bf7bfb59e2cfa59a7f2630 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 12 Jan 2024 21:39:57 -0600 Subject: [PATCH] first --- .gitignore | 3 + build.zig | 25 ++ build.zig.zon | 14 ++ flake.lock | 76 ++++++ flake.nix | 61 +++++ src/main.zig | 638 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 817 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..835d42d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/result* +/zig-cache +/zig-out diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..a39bab8 --- /dev/null +++ b/build.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + _ = b.addModule( + "anzi", + .{ + .root_source_file = .{ + .path = "src/main.zig", + }, + }, + ); + + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..4bdecff --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = "ansi", + .version = "0.0.0", + + .paths = .{ + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..67a2a7e --- /dev/null +++ b/flake.lock @@ -0,0 +1,76 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "make-shell": { + "locked": { + "lastModified": 1634940815, + "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", + "owner": "ursi", + "repo": "nix-make-shell", + "rev": "8add91681170924e4d0591b22f294aee3f5516f9", + "type": "github" + }, + "original": { + "owner": "ursi", + "repo": "nix-make-shell", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1697059129, + "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "make-shell": "make-shell", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..87fc844 --- /dev/null +++ b/flake.nix @@ -0,0 +1,61 @@ +{ + description = "zig-ansi"; + + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-unstable"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + make-shell = { + url = "github:ursi/nix-make-shell"; + }; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + ... + } @ inputs: + flake-utils.lib.eachDefaultSystem + ( + system: let + pkgs = import nixpkgs { + inherit system; + }; + in { + devShells.default = let + project = "zig-ansi"; + make-shell = import inputs.make-shell { + inherit system; + pkgs = pkgs; + }; + in + make-shell { + packages = [ + pkgs.zon2nix + pkgs.zig_0_11 + pkgs.zls + ]; + env = { + NIX_PROJECT = project; + }; + }; + packages = { + zig-ansi = pkgs.stdenv.mkDerivation { + name = "zig-ansi"; + src = ./.; + nativeBuildInputs = [ + pkgs.zig_0_11.hook + ]; + postPatch = '' + ln -s ${pkgs.callPackage ./deps.nix {}} $ZIG_GLOBAL_CACHE_DIR/p + ''; + dontStrip = true; + }; + }; + } + ); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..53705bb --- /dev/null +++ b/src/main.zig @@ -0,0 +1,638 @@ +const std = @import("std"); + +pub const Mode = enum { + C0, + C1, + Bash, +}; + +pub const Options = struct { + mode: Mode = .C0, + wrap: bool = false, +}; + +pub const ColorName = enum(u8) { + BLACK = 0, + RED = 1, + GREEN = 2, + YELLOW = 3, + BLUE = 4, + MAGENTA = 5, + CYAN = 6, + WHITE = 7, + DEFAULT = 9, +}; + +pub const Layer = enum { + foreground, + background, +}; + +pub const Color8 = struct { + color: ColorName, + bright: bool = false, +}; + +pub const Color256 = u8; + +pub const ColorRGB = struct { + red: u8, + green: u8, + blue: u8, +}; + +pub const Style = enum(u8) { + /// enable bold mode + BOLD = 1, + + /// enable faint mode + FAINT = 2, + + /// enable italic mode + ITALIC = 3, + + /// enable underline mode + UNDERLINE = 4, + + /// enable blinking mode + BLINKING = 5, + + /// enable inverse mode + INVERSE = 7, + + /// enable hidden mode + HIDDEN = 8, + + /// enable strikethrough mode + STRIKETHROUGH = 9, + + /// double underline mode, not supported by many terminals + DOUBLE_UNDERLINE = 21, + + /// set dim mode + pub const DIM = Style.FAINT; + + /// set reverse mode + pub const REVERSE = Style.INVERSE; + + /// set invisible mode + pub const INVISIBLE = Style.HIDDEN; +}; + +pub fn ANSI(comptime options: Options) type { + return struct { + const mode = options.mode; + const wrap = options.wrap; + + const Self = @This(); + + pub const SOH: []const u8 = switch (options.mode) { + .C0, .C1 => &.{std.ascii.control_code.soh}, + .Bash => "\\[", + }; + + pub const STX: []const u8 = switch (options.mode) { + .C0, .C1 => &.{std.ascii.control_code.stx}, + .Bash => "\\]", + }; + + pub const RL_PROMPT_START_IGNORE: []const u8 = if (options.wrap) Self.SOH else ""; + pub const RL_PROMPT_END_IGNORE: []const u8 = if (options.wrap) Self.STX else ""; + + pub fn readlinePromptStartIgnore(writer: anytype) !void { + if (comptime options.wrap) try writer.writeAll(Self.SOH); + } + + pub fn readlinePromptEndIgnore(writer: anytype) !void { + if (comptime options.wrap) try writer.writeAll(Self.STX); + } + + pub const BEL: []const u8 = switch (options.mode) { + .C0, .C1 => &.{std.ascii.control_code.bel}, + .Bash => "\\a", + }; + + pub const BS: []const u8 = &.{std.ascii.control_code.bs}; + pub const HT: []const u8 = &.{std.ascii.control_code.ht}; + pub const LF: []const u8 = &.{std.ascii.control_code.lf}; + pub const VT: []const u8 = &.{std.ascii.control_code.vt}; + pub const FF: []const u8 = &.{std.ascii.control_code.ff}; + pub const CR: []const u8 = &.{std.ascii.control_code.cr}; + + pub const ESC: []const u8 = switch (options.mode) { + .C0, .C1 => &.{std.ascii.control_code.esc}, + .Bash => "\\e", + }; + + pub const DEL: []const u8 = "\x7f"; + + /// Device Control String + pub const DCS: []const u8 = switch (options.mode) { + .C0, .Bash => Self.ESC ++ "P", + .C1 => "\x8d", + }; + + /// Control Sequence Introducer + pub const CSI: []const u8 = switch (options.mode) { + .C0, .Bash => Self.ESC ++ "[", + .C1 => "\x9b", + }; + + /// String Terminator + pub const ST: []const u8 = switch (options.mode) { + .C0 => Self.ESC ++ "\\", + .C1 => "\x9c", + .Bash => Self.BEL, + }; + + /// Operating System Command + pub const OSC: []const u8 = switch (options.mode) { + .C0, .Bash => Self.ESC ++ "]", + .C1 => "\x9d", + }; + + pub const CursorMove = union(enum) { + /// moves cursor to home position (0, 0) + home: void, + /// moves cursor to line #, column # + to: struct { + line: u8, + column: u8, + }, + /// moves cursor up # lines + up: u8, + /// moves cursor down # lines + down: u8, + /// moves cursor right # columns + right: u8, + /// moves cursor left # columns + left: u8, + /// moves cursor to beginning of next line, # lines down + downBOL: u8, + /// moves cursor to beginning of previous line, # lines up + upBOL: u8, + /// moves cursor to column # + toColumn: u8, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.CSI); + switch (self) { + .home => try writer.writeAll("H"), + .to => |value| { + try std.fmt.formatInt(value.line, 10, .lower, .{}, writer); + try writer.writeAll(";"); + try std.fmt.formatInt(value.column, 10, .lower, .{}, writer); + try writer.writeAll("H"); // also could use "f" instead of "H" + }, + .up, .down, .left, .right, .downBOL, .upBOL, .toColumn => |value| { + try std.fmt.formatInt(value, 10, .lower, .{}, writer); + const v = switch (self) { + .up => "A", + .down => "B", + .right => "C", + .left => "D", + .downBOL => "E", + .upBOL => "F", + .toColumn => "G", + }; + try writer.writeAll(v); + }, + } + try writer.writeAll(Self.RL_PROMPT_END_IGNORE); + } + }; + + /// request cursor position (reports as ESC[#;#R) + pub fn requestCursorPosition(writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.CSI ++ "6n" ++ Self.RL_PROMPT_END_IGNORE); + } + + /// moves cursor one line up, scrolling if needed + pub fn moveCursorUpOne(writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.CSI ++ "7" ++ Self.RL_PROMPT_END_IGNORE); + } + + /// save cursor position + pub const SaveCursorPosition = enum { + DEC, + SCO, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ + switch (self) { + .DEC => Self.ESC ++ "7", + .SCO => Self.CSI ++ "s", + } ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const RestoreCursorPosition = enum { + DEC, + SCO, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ + switch (self) { + .DEC => Self.ESC ++ "8", + .SCO => Self.CSI ++ "u", + } ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const Erase = enum { + fromCursorToEndOfScreen, + fromStartOfScreenToCursor, + entireScreen, + savedLines, + fromCursorToEndOfLine, + fromStartOfLineToCursor, + entireLine, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ + switch (self) { + .fromCursorToEndOfScreen => "0J", + .fromStartOfScreenToCursor => "1J", + .entireScreen => "2J", + .savedLines => "3J", + .fromCursorToEndOfLine => "0K", + .fromStartOfLineToCursor => "1K", + .entireLine => "2K", + } ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + const ColorType = union(enum) { + color8: Color8, + color256: Color256, + colorRGB: ColorRGB, + }; + + const GraphicsRenditions = struct { + const GraphicsRendition = union(enum) { + reset: void, + style: struct { + mode: enum { enable, disable }, + style: Style, + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self.mode) { + .enable => try std.fmt.formatInt(@intFromEnum(self.style), 10, .lower, .{}, writer), + .disable => switch (self.style) { + .BOLD, .FAINT => try writer.writeAll("22"), + else => try std.fmt.formatInt(@intFromEnum(self.style) + 20, 10, .lower, .{}, writer), + }, + } + } + }, + color: struct { + layer: Layer, + color: ColorType, + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self.color) { + .color8 => |v| { + var offset: u8 = 30; + if (v.bright) offset = 100; + if (self.layer == .background) offset += 10; + try std.fmt.formatInt(@intFromEnum(v.color) + offset, 10, .lower, .{}, writer); + }, + .color256 => |v| { + switch (self.layer) { + .foreground => try writer.writeAll("38;5;"), + .background => try writer.writeAll("48;5;"), + } + try std.fmt.formatInt(v, 10, .lower, .{}, writer); + }, + .colorRGB => |v| { + switch (self.layer) { + .foreground => try writer.writeAll("38;2;"), + .background => try writer.writeAll("48;2;"), + } + try std.fmt.formatInt(v.red, 10, .lower, .{}, writer); + try writer.writeAll(";"); + try std.fmt.formatInt(v.green, 10, .lower, .{}, writer); + try writer.writeAll(";"); + try std.fmt.formatInt(v.blue, 10, .lower, .{}, writer); + }, + } + } + }, + }; + + graphics: []const GraphicsRendition, + + pub fn format(self: @This(), comptime fmt: []const u8, opt: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.CSI); + + for (self.graphics, 0..) |graphic, index| { + if (index > 0) try writer.writeAll(";"); + switch (graphic) { + .reset => try writer.writeAll("0"), + .style => |s| try s.format(fmt, opt, writer), + .color => |c| try c.format(fmt, opt, writer), + } + } + + try writer.writeAll("m" ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + /// Reset all styles and colors to the default + const reset = GraphicsRenditions{ + .graphics = &.{ + .{ + .reset = {}, + }, + }, + }; + + /// Set the color using the 8 standard colors. + pub fn color8(layer: Layer, color: ColorName, bright: bool) GraphicsRenditions { + return GraphicsRenditions{ + .graphics = &.{ + .{ + .color = .{ + .layer = layer, + .color = .{ + .color8 = .{ + .color = color, + .bright = bright, + }, + }, + }, + }, + }, + }; + } + + /// Set the color using the 256 color palette. + pub fn color256(layer: Layer, color: Color256) GraphicsRenditions { + return GraphicsRenditions{ + .graphics = &.{ + .{ + .color = .{ + .layer = layer, + .color = .{ + .color256 = color, + }, + }, + }, + }, + }; + } + + /// Set the color using a 24 bit RGB color. + pub fn colorRGB(layer: Layer, red: u8, green: u8, blue: u8) GraphicsRenditions { + return GraphicsRenditions{ + .graphics = &.{ + .{ + .color = .{ + .layer = layer, + .color = .{ + .colorRGB = .{ + .red = red, + .green = green, + .blue = blue, + }, + }, + }, + }, + }, + }; + } + + pub const IconNameAndWindowTitle = struct { + icon_name: ?[]const u8 = null, + window_title: ?[]const u8 = null, + + fn write(comptime control: []const u8, parameter: []const u8, writer: anytype) !void { + try writer.writeAll(Self.OSC ++ control ++ ";"); + try writer.writeAll(parameter); + try writer.writeAll(Self.ST); + } + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE); + if (self.icon_name) |icon_name| { + if (self.window_title) |window_title| { + if (std.mem.eql(u8, icon_name, window_title)) { + try write("0", icon_name, writer); + } else { + try write("1", icon_name, writer); + try write("2", window_title, writer); + } + } else { + try write("1", icon_name, writer); + } + } else { + if (self.window_title) |window_title| { + try write("2", window_title, writer); + } + } + try writer.writeAll(Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const SetProperty = struct { + property: []const u8, + value: ?[]const u8, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ "3;"); + try writer.writeAll(self.property); + if (self.value) |v| { + try writer.writeAll("="); + try writer.writeAll(v); + } + try writer.writeAll(Self.ST ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const Hyperlink = struct { + link: []const u8, + text: []const u8, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.OSC ++ "8;;"); + try writer.writeAll(self.link); + try writer.writeAll(Self.ST ++ Self.RL_PROMPT_END_IGNORE); + try writer.writeAll(self.text); + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.OSC ++ "8;;" ++ Self.ST ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const Notification = struct { + text: []const u8, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.OSC ++ "9;"); + try writer.writeAll(self.text); + try writer.writeAll(Self.ST ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const DesktopNotification = struct { + identifier: []const u8, + encoded: bool = false, + title: []const u8, + body: []const u8, + report: bool = false, + focus: bool = false, + + fn formatMetadata(self: @This(), part: enum { title, body }, done: bool, text: []const u8, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.OSC ++ "99;"); + try writer.writeAll("i="); + try writer.writeAll(self.identifier); + if (self.encoded) try writer.writeAll(";e=1") else try writer.writeAll(";e=0"); + try writer.writeAll(";a="); + if (self.report) try writer.writeAll("report") else try writer.writeAll("-report"); + try writer.writeAll(","); + if (self.focus) try writer.writeAll("focus") else try writer.writeAll("-focus"); + switch (part) { + .title => try writer.writeAll(";p=title"), + .body => try writer.writeAll(";p=body"), + } + if (done) try writer.writeAll(";d=1;") else try writer.writeAll(";d=0;"); + try writer.writeAll(text); + try writer.writeAll(Self.ST ++ Self.RL_PROMPT_END_IGNORE); + } + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + _ = writer; + _ = self; + } + }; + + pub const CurrentDirectory = struct { + text: []const u8, + style: enum { + OSC1337, + } = .OSC1337, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self.style) { + .OSC1337 => { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.OSC ++ "1337;CurrentDir="); + try writer.writeAll(self.text); + try writer.writeAll(Self.ST ++ Self.RL_PROMPT_END_IGNORE); + }, + } + } + }; + + pub const ActiveStatusDisplay = enum(u8) { + MAIN_DISPLAY = 0, + STATUS_LINE = 1, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_START_IGNORE ++ Self.CSI); + try std.fmt.formatInt(@intFromEnum(self), 10, .lower, .{}, writer); + try writer.writeAll("$}" ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const StatusLineType = enum(u8) { + NONE = 0, + INDICATOR = 1, + HOST_WRITABLE = 2, + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(Self.RL_PROMPT_END_IGNORE_START_IGNORE ++ Self.CSI); + try std.fmt.formatInt(@intFromEnum(self), 10, .lower, .{}, writer); + try writer.writeAll("$~" ++ Self.RL_PROMPT_END_IGNORE); + } + }; + + pub const ScreenMode = struct {}; + }; +} + +test "osc-c0" { + const a = ANSI(.{ .mode = .C0 }); + try std.testing.expect(std.mem.eql(u8, a.OSC, @as([]const u8, "\x1b]"))); +} + +test "osc-c1" { + const a = ANSI(.{ .mode = .C1 }); + try std.testing.expect(std.mem.eql(u8, a.OSC, @as([]const u8, "\x9d"))); +} + +test "osc-bash" { + const a = ANSI(.{ .mode = .Bash }); + try std.testing.expect(std.mem.eql(u8, a.OSC, @as([]const u8, "\\e]"))); +} + +test "colorRGB-bash" { + const a = ANSI(.{ .mode = .Bash }); + const g = a.GraphicsRenditions{ + .graphics = &.{ + .{ + .color = .{ + .layer = .foreground, + .color = .{ + .colorRGB = .{ + .red = 1, + .green = 2, + .blue = 3, + }, + }, + }, + }, + }, + }; + const result = try std.fmt.allocPrint(std.testing.allocator, "{}", .{g}); + try std.testing.expectEqualSlices(u8, result, "\\e[38;2;1;2;3m"); + std.testing.allocator.free(result); +} + +test "colorRGB-c0" { + const a = ANSI(.{ .mode = .C0 }); + const g = a.GraphicsRenditions{ + .graphics = &.{ + .{ + .reset = {}, + }, + .{ + .color = .{ + .layer = .foreground, + .color = .{ + .color8 = .{ + .color = .RED, + .bright = false, + }, + }, + }, + }, + }, + }; + const result = try std.fmt.allocPrint(std.testing.allocator, "{}", .{g}); + try std.testing.expectEqualSlices(u8, result, "\x1b[0;31m"); + std.testing.allocator.free(result); +} + +test "reset" { + const a = ANSI(.{ .mode = .C0 }); + const g = a.reset; + const result = try std.fmt.allocPrint(std.testing.allocator, "{}", .{g}); + try std.testing.expectEqualSlices(u8, result, "\x1b[0m"); + std.testing.allocator.free(result); +} + +test "color8" { + const a = ANSI(.{ .mode = .C0 }); + const g = a.color8(.foreground, .BLUE, false); + const result = try std.fmt.allocPrint(std.testing.allocator, "{}", .{g}); + try std.testing.expectEqualSlices(u8, result, "\x1b[34m"); + std.testing.allocator.free(result); +} + +test "hyperlink-c0" { + const a = ANSI(.{ .mode = .C0, .wrap = false }); + var buffer: [128]u8 = undefined; + const written = try std.fmt.bufPrint(&buffer, "{}", .{ + a.Hyperlink{ + .link = "https://www.example.com", + .text = "Example", + }, + }); + try std.testing.expectEqualSlices(u8, "\x1b]8;;https://www.example.com\x1b\\Example\x1b]8;;\x1b\\", written); +}