This commit is contained in:
Jeffrey C. Ollie 2024-01-12 21:39:57 -06:00
commit 0e9d395a55
Signed by: jeff
GPG key ID: 6F86035A6D97044E
6 changed files with 817 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/result*
/zig-cache
/zig-out

25
build.zig Normal file
View file

@ -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);
}

14
build.zig.zon Normal file
View file

@ -0,0 +1,14 @@
.{
.name = "ansi",
.version = "0.0.0",
.paths = .{
"",
// For example...
//"build.zig",
//"build.zig.zon",
//"src",
//"LICENSE",
//"README.md",
},
}

76
flake.lock Normal file
View file

@ -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
}

61
flake.nix Normal file
View file

@ -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;
};
};
}
);
}

638
src/main.zig Normal file
View file

@ -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);
}