Skip to content

Commit

Permalink
🎨✨ Added advanced formatting features to chroma-zig library
Browse files Browse the repository at this point in the history
- 🌈 Enhanced color support: ANSI, ANSI 256, and True Color
- πŸ’… Added text styling: bold, italic, underline, and more
- πŸ› οΈ Improved parser for combined color and style formats
- πŸ”„ Added support for reset and mixed formats
- πŸ”§ Graceful handling of edge cases and invalid inputs
- πŸ“š Updated documentation and examples in main.zig
  • Loading branch information
πŸ•΅οΈ Detective build.zig.zon committed Feb 17, 2024
1 parent 27e80a5 commit aeea721
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 73 deletions.
41 changes: 31 additions & 10 deletions src/ansi.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
/// The `AnsiColor` enum provides a simple and type-safe way to use ANSI color codes
/// in terminal output. It includes both foreground and background colors, as well as
/// a method to reset the color. It offers two public methods to interact with the
/// color values: `to_string`, which returns the string representation of the color,
/// and `code`, which returns the ANSI escape code associated with the color.
pub const AnsiColor = enum(u8) {
/// The `AnsiCode` enum offers a comprehensive set of ANSI escape codes for both
/// styling and coloring text in the terminal. This includes basic styles like bold
/// and italic, foreground and background colors, and special modes like blinking or
/// hidden text. It provides methods for obtaining the string name and the corresponding
/// ANSI escape code of each color or style, enabling easy and readable text formatting.
pub const AnsiCode = enum(u8) {
// Standard style codes
reset = 0,
bold,
dim,
italic,
underline,
///Not widely supported
blink,
reverse = 7,
hidden,

// Standard text colors
black = 30,
red,
Expand All @@ -23,14 +34,13 @@ pub const AnsiColor = enum(u8) {
bgMagenta,
bgCyan,
bgWhite,
reset = 0,

/// Returns the string representation of the color.
/// This method makes it easy to identify a color by its name in the source code.
///
/// Returns:
/// A slice of constant u8 bytes representing the color's name.
pub fn to_string(self: AnsiColor) []const u8 {
pub fn to_string(self: AnsiCode) []const u8 {
return @tagName(self);
}

Expand All @@ -40,8 +50,19 @@ pub const AnsiColor = enum(u8) {
///
/// Returns:
/// A slice of constant u8 bytes representing the ANSI escape code for the color.
pub fn code(self: AnsiColor) []const u8 {
pub fn code(self: AnsiCode) []const u8 {
return switch (self) {
// Standard style codes
.reset => "0",
.bold => "1",
.dim => "2",
.italic => "3",
.underline => "4",
// Not widely supported
.blink => "5",
.reverse => "7",
.hidden => "8",
// foregroond colors
.black => "30",
.red => "31",
.green => "32",
Expand All @@ -50,6 +71,7 @@ pub const AnsiColor = enum(u8) {
.magenta => "35",
.cyan => "36",
.white => "37",
// background colors
.bgBlack => "40",
.bgRed => "41",
.bgGreen => "42",
Expand All @@ -58,7 +80,6 @@ pub const AnsiColor = enum(u8) {
.bgMagenta => "45",
.bgCyan => "46",
.bgWhite => "47",
.reset => "0",
};
}
};
116 changes: 77 additions & 39 deletions src/lib.zig
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
//BUG: apparently {{}} is not reflected to {}

/// This module provides a flexible way to format strings with ANSI color codes
/// dynamically using {colorName} placeholders within the text. It supports standard
/// ANSI colors, ANSI 256 extended colors, and true color (24-bit) formats.
/// It intelligently handles color formatting by parsing placeholders and replacing
/// them with the appropriate ANSI escape codes for terminal output.
const std = @import("std");
const AnsiColor = @import("ansi.zig").AnsiColor;
const AnsiCode = @import("ansi.zig").AnsiCode;
const compileAssert = @import("utils.zig").compileAssert;

/// Formats a string with ANSI, ANSI 256 color codes, and RGB color specifications.
/// Unrecognized placeholders are output as-is, allowing for literal '{' and '}' via
/// double braces '{{' and '}}'.
///
/// Arguments:
/// - `fmt`: The format string with {colorName} placeholders.
/// Provides dynamic string formatting capabilities with ANSI escape codes for both
/// color and text styling within terminal outputs. This module supports a wide range
/// of formatting options including standard ANSI colors, ANSI 256 extended color set,
/// and true color (24-bit) specifications. It parses given format strings with embedded
/// placeholders (e.g., `{color}` or `{style}`) and replaces them with the corresponding
/// ANSI escape codes. The format function is designed to be used at compile time,
/// enhancing readability and maintainability of terminal output styling in Zig applications.
///
/// Returns:
/// A formatted string with color escape codes embedded.
/// The formatting syntax supports modifiers (`fg` for foreground and `bg` for background),
/// as well as multiple formats within a single placeholder. Unrecognized placeholders
/// are output as-is, allowing for the inclusion of literal braces by doubling them (`{{` and `}}`).
// TODO: Refactor this lol
pub fn format(comptime fmt: []const u8) []const u8 {
@setEvalBranchQuota(2000000);
Expand Down Expand Up @@ -64,28 +68,56 @@ pub fn format(comptime fmt: []const u8) []const u8 {
}

comptime {
if (std.ascii.isDigit(maybe_color_fmt[0])) {
if (parse256OrTrueColor(maybe_color_fmt)) |result| {
output = output ++ result;
var start = 0;
var end = 0;
var is_background = false;

style_loop: while (start < maybe_color_fmt.len) {
while (end < maybe_color_fmt.len and maybe_color_fmt[end] != ',') : (end += 1) {}

var modifier_end = start;
while (modifier_end < maybe_color_fmt.len and maybe_color_fmt[modifier_end] != ':') : (modifier_end += 1) {}

if (modifier_end != maybe_color_fmt.len) {
if (std.mem.eql(u8, maybe_color_fmt[start..modifier_end], "bg")) {
is_background = true;
end = modifier_end + 1;
start = end;
continue :style_loop;
} else if (std.mem.eql(u8, maybe_color_fmt[start..modifier_end], "fg")) {
is_background = false;
end = modifier_end + 1;
start = end;
continue :style_loop;
}
}

if (std.ascii.isDigit(maybe_color_fmt[start])) {
const color = parse256OrTrueColor(maybe_color_fmt[start..end], is_background);
output = output ++ color;
at_least_one_color = true;
} else {
@compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}");
}
} else {
var found = false;
for (@typeInfo(AnsiColor).Enum.fields) |field| {
if (std.mem.eql(u8, field.name, maybe_color_fmt)) {
const color: AnsiColor = @enumFromInt(field.value);
at_least_one_color = true;
output = output ++ "\x1b[" ++ color.code() ++ "m";
found = true;
break;
var found = false;
for (@typeInfo(AnsiCode).Enum.fields) |field| {
if (std.mem.eql(u8, field.name, maybe_color_fmt[start..end])) {
// HACK: this would not work if I put bgMagenta for example as a color
// TODO: fix this eheh
const color: AnsiCode = @enumFromInt(field.value + if (is_background) 10 else 0);
at_least_one_color = true;
output = output ++ "\x1b[" ++ color.code() ++ "m";
found = true;
break;
}
}
}

if (!found) {
output = output ++ "{" ++ maybe_color_fmt ++ "}";
if (!found) {
output = output ++ "{" ++ maybe_color_fmt ++ "}";
}
}

end = end + 1;
start = end;
is_background = false;
}
}

Expand All @@ -100,7 +132,7 @@ pub fn format(comptime fmt: []const u8) []const u8 {
}

// TODO: maybe keep the compile error and dedicate this function to be comptime only
fn parse256OrTrueColor(fmt: []const u8) ?[]const u8 {
fn parse256OrTrueColor(fmt: []const u8, background: bool) []const u8 {
var channels_value: [3]u8 = .{ 0, 0, 0 };
var channels_length: [3]u8 = .{ 0, 0, 0 };
var channel = 0;
Expand All @@ -111,15 +143,13 @@ fn parse256OrTrueColor(fmt: []const u8) ?[]const u8 {
'0'...'9' => {
var res = @mulWithOverflow(channels_value[channel], 10);
if (res[1] > 0) {
return null;
// @compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}");
@compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}");
}
channels_value[channel] = res[0];

res = @addWithOverflow(channels_value[channel], c - '0');
if (res[1] > 0) {
return null;
// @compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}");
@compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}");
}
channels_value[channel] = res[0];

Expand All @@ -129,21 +159,26 @@ fn parse256OrTrueColor(fmt: []const u8) ?[]const u8 {
channel += 1;

if (channel >= 3) {
return null;
// @compileError("Invalid number format, too many channels, expected: {0-255} or {0-255;0-255;0-255}");
@compileError("Invalid number format, too many channels, expected: {0-255} or {0-255;0-255;0-255}");
}
},
',' => {
break;
},
else => {
return null;
// @compileError("Invalid number format, expected: {0-255} or {0-255;0-255;0-255}");
@compileError("Invalid number format, expected: {0-255} or {0-255;0-255;0-255}");
},
}
}

// ANSI 256 extended
if (channel == 0) {
const color: []const u8 = fmt[0..channels_length[0]];
output = output ++ "\x1b[38;5;" ++ color ++ "m";
if (background) {
output = output ++ "\x1b[48;5;" ++ color ++ "m";
} else {
output = output ++ "\x1b[38;5;" ++ color ++ "m";
}
}
// TRUECOLOR
// TODO: check for compatibility, is it possible at comptime ??
Expand All @@ -157,10 +192,13 @@ fn parse256OrTrueColor(fmt: []const u8) ?[]const u8 {
// +1 to skip the ;
start += channels_length[c] + 1;
}
output = output ++ "\x1b[38;2;" ++ color ++ "m";
if (background) {
output = output ++ "\x1b[48;2;" ++ color ++ "m";
} else {
output = output ++ "\x1b[38;2;" ++ color ++ "m";
}
} else {
return null;
// @compileError("Invalid number format, check the number of channels, must be 1 or 3, expected: {0-255} or {0-255;0-255;0-255}");
@compileError("Invalid number format, check the number of channels, must be 1 or 3, expected: {0-255} or {0-255;0-255;0-255}");
}

return output;
Expand Down
55 changes: 33 additions & 22 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,42 @@ const std = @import("std");
const chroma = @import("lib.zig");

pub fn main() !void {
const examples = [_]struct { fmt: []const u8, arg: []const u8 }{
// ANSI foreground and background colors
.{ .fmt = "{yellow}ANSI {s}", .arg = "SUPPORTED" },
.{ .fmt = "{blue}JJK is the best new {s}", .arg = "generation" },
.{ .fmt = "{red}Disagree, and {cyan}Satoru Gojo will throw a {magenta}{s}{reset} at you", .arg = "purple ball" },
.{ .fmt = "{bgMagenta}{white}Yuji Itadori's resolve: {s}", .arg = "I'll eat the finger." },
.{ .fmt = "{bgYellow}{black}With this treasure, I summon: {s}", .arg = "Mahoraga or Makora idk" },
.{ .fmt = "{bgBlue}{white}LeBron {s}", .arg = "James" },
.{ .fmt = "{green}Please, Lonzo Ball, come back in {s}", .arg = "2024" },
.{ .fmt = "{blue}JJK is the best new {s}{red} once again", .arg = "generation" },

// ANSI 256 extended colors
.{ .fmt = "\n{221}256 Extended set, too! {s}", .arg = "eheh" },
.{ .fmt = "{121}Finding examples is hard, {s}", .arg = "shirororororo" },

// TrueColors
.{ .fmt = "\n{221;10;140}How about {13;45;200}{s}??", .arg = "true colors" },
.{ .fmt = "{255;202;255}Toge Inumaki says: {s}", .arg = "Salmon" },
.{ .fmt = "{255;105;180}Nobara Kugisaki's fierce {s}", .arg = "Nail Hammer" },
.{ .fmt = "{10;94;13}Juujika no {s}", .arg = "Rokunin" },
const examples = [_]struct { fmt: []const u8, arg: ?[]const u8 }{
// Basic color and style
.{ .fmt = "{bold,red}Bold and Red{reset}", .arg = null },
// Combining background and foreground with styles
.{ .fmt = "{fg:cyan,bg:magenta}{underline}Cyan on Magenta underline{reset}", .arg = null },
// Nested styles and colors
.{ .fmt = "{green}Green {bold}and Bold{reset,blue,italic} to blue italic{reset}", .arg = null },
// Extended ANSI color with arg example
.{ .fmt = "{bg:120}Extended ANSI {s}{reset}", .arg = "Background" },
// True color specification
.{ .fmt = "{fg:255;100;0}True Color Orange Text{reset}", .arg = null },
// Mixed color and style formats
.{ .fmt = "{bg:28,italic}{fg:231}Mixed Background and Italic{reset}", .arg = null },
// Unsupported/Invalid color code >= 256, Error thrown at compile time
// .{ .fmt = "{fg:999}This should not crash{reset}", .arg = null },
// Demonstrating blink, note: may not be supported in all terminals
.{ .fmt = "{blink}Blinking Text (if supported){reset}", .arg = null },
// Using dim and reverse video
.{ .fmt = "{dim,reverse}Dim and Reversed{reset}", .arg = null },
// Custom message with dynamic content
.{ .fmt = "{blue,bg:magenta}User {bold}{s}{reset,0;255;0} logged in successfully.", .arg = "Charlie" },
// Combining multiple styles and reset
.{ .fmt = "{underline,cyan}Underlined Cyan{reset} then normal", .arg = null },
// Multiple format specifiers for complex formatting
.{ .fmt = "{fg:144,bg:52,bold,italic}Fancy {underline}Styling{reset}", .arg = null },
// Jujutsu Kaisen !!
.{ .fmt = "{bg:72,bold,italic}Jujutsu Kaisen !!{reset}", .arg = null },
};

inline for (examples) |example| {
std.debug.print(chroma.format(example.fmt) ++ "\n", .{example.arg});
if (example.arg) |arg| {
std.debug.print(chroma.format(example.fmt) ++ "\n", .{arg});
} else {
std.debug.print(chroma.format(example.fmt) ++ "\n", .{});
}
}

std.debug.print(chroma.format("{blue}Eventually, the {red}formatting{reset} looks like {130;43;122}{s}!\n"), .{"this"});
std.debug.print(chroma.format("{blue}{underline}Eventually{reset}, the {red}formatting{reset} looks like {130;43;122}{s}!\n"), .{"this"});
}
2 changes: 1 addition & 1 deletion src/tests.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const std = @import("std");
const AnsiColor = @import("ansi.zig").AnsiColor;
const AnsiCode = @import("ansi.zig").AnsiCode;
const chroma = @import("lib.zig");

// TESTS
Expand Down
4 changes: 3 additions & 1 deletion src/utils.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/// Asserts the given condition is true; triggers a compile Error if not.
/// Asserts the provided condition is true; if not, it triggers a compile-time error
/// with the specified message. This utility function is designed to enforce
/// invariants and ensure correctness throughout the codebase.
pub fn compileAssert(ok: bool, msg: []const u8) void {
if (!ok) {
@compileError("Assertion failed: " ++ msg);
Expand Down

0 comments on commit aeea721

Please sign in to comment.