Skip to content

Commit aeea721

Browse files
author
πŸ•΅οΈ Detective build.zig.zon
committed
🎨✨ Added advanced formatting features to chroma-zig library
- 🌈 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
1 parent 27e80a5 commit aeea721

File tree

5 files changed

+145
-73
lines changed

5 files changed

+145
-73
lines changed

β€Žsrc/ansi.zigβ€Ž

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
/// The `AnsiColor` enum provides a simple and type-safe way to use ANSI color codes
2-
/// in terminal output. It includes both foreground and background colors, as well as
3-
/// a method to reset the color. It offers two public methods to interact with the
4-
/// color values: `to_string`, which returns the string representation of the color,
5-
/// and `code`, which returns the ANSI escape code associated with the color.
6-
pub const AnsiColor = enum(u8) {
1+
/// The `AnsiCode` enum offers a comprehensive set of ANSI escape codes for both
2+
/// styling and coloring text in the terminal. This includes basic styles like bold
3+
/// and italic, foreground and background colors, and special modes like blinking or
4+
/// hidden text. It provides methods for obtaining the string name and the corresponding
5+
/// ANSI escape code of each color or style, enabling easy and readable text formatting.
6+
pub const AnsiCode = enum(u8) {
7+
// Standard style codes
8+
reset = 0,
9+
bold,
10+
dim,
11+
italic,
12+
underline,
13+
///Not widely supported
14+
blink,
15+
reverse = 7,
16+
hidden,
17+
718
// Standard text colors
819
black = 30,
920
red,
@@ -23,14 +34,13 @@ pub const AnsiColor = enum(u8) {
2334
bgMagenta,
2435
bgCyan,
2536
bgWhite,
26-
reset = 0,
2737

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

@@ -40,8 +50,19 @@ pub const AnsiColor = enum(u8) {
4050
///
4151
/// Returns:
4252
/// A slice of constant u8 bytes representing the ANSI escape code for the color.
43-
pub fn code(self: AnsiColor) []const u8 {
53+
pub fn code(self: AnsiCode) []const u8 {
4454
return switch (self) {
55+
// Standard style codes
56+
.reset => "0",
57+
.bold => "1",
58+
.dim => "2",
59+
.italic => "3",
60+
.underline => "4",
61+
// Not widely supported
62+
.blink => "5",
63+
.reverse => "7",
64+
.hidden => "8",
65+
// foregroond colors
4566
.black => "30",
4667
.red => "31",
4768
.green => "32",
@@ -50,6 +71,7 @@ pub const AnsiColor = enum(u8) {
5071
.magenta => "35",
5172
.cyan => "36",
5273
.white => "37",
74+
// background colors
5375
.bgBlack => "40",
5476
.bgRed => "41",
5577
.bgGreen => "42",
@@ -58,7 +80,6 @@ pub const AnsiColor = enum(u8) {
5880
.bgMagenta => "45",
5981
.bgCyan => "46",
6082
.bgWhite => "47",
61-
.reset => "0",
6283
};
6384
}
6485
};

β€Žsrc/lib.zigβ€Ž

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1+
//BUG: apparently {{}} is not reflected to {}
2+
13
/// This module provides a flexible way to format strings with ANSI color codes
24
/// dynamically using {colorName} placeholders within the text. It supports standard
35
/// ANSI colors, ANSI 256 extended colors, and true color (24-bit) formats.
46
/// It intelligently handles color formatting by parsing placeholders and replacing
57
/// them with the appropriate ANSI escape codes for terminal output.
68
const std = @import("std");
7-
const AnsiColor = @import("ansi.zig").AnsiColor;
9+
const AnsiCode = @import("ansi.zig").AnsiCode;
810
const compileAssert = @import("utils.zig").compileAssert;
911

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

6670
comptime {
67-
if (std.ascii.isDigit(maybe_color_fmt[0])) {
68-
if (parse256OrTrueColor(maybe_color_fmt)) |result| {
69-
output = output ++ result;
71+
var start = 0;
72+
var end = 0;
73+
var is_background = false;
74+
75+
style_loop: while (start < maybe_color_fmt.len) {
76+
while (end < maybe_color_fmt.len and maybe_color_fmt[end] != ',') : (end += 1) {}
77+
78+
var modifier_end = start;
79+
while (modifier_end < maybe_color_fmt.len and maybe_color_fmt[modifier_end] != ':') : (modifier_end += 1) {}
80+
81+
if (modifier_end != maybe_color_fmt.len) {
82+
if (std.mem.eql(u8, maybe_color_fmt[start..modifier_end], "bg")) {
83+
is_background = true;
84+
end = modifier_end + 1;
85+
start = end;
86+
continue :style_loop;
87+
} else if (std.mem.eql(u8, maybe_color_fmt[start..modifier_end], "fg")) {
88+
is_background = false;
89+
end = modifier_end + 1;
90+
start = end;
91+
continue :style_loop;
92+
}
93+
}
94+
95+
if (std.ascii.isDigit(maybe_color_fmt[start])) {
96+
const color = parse256OrTrueColor(maybe_color_fmt[start..end], is_background);
97+
output = output ++ color;
7098
at_least_one_color = true;
7199
} else {
72-
@compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}");
73-
}
74-
} else {
75-
var found = false;
76-
for (@typeInfo(AnsiColor).Enum.fields) |field| {
77-
if (std.mem.eql(u8, field.name, maybe_color_fmt)) {
78-
const color: AnsiColor = @enumFromInt(field.value);
79-
at_least_one_color = true;
80-
output = output ++ "\x1b[" ++ color.code() ++ "m";
81-
found = true;
82-
break;
100+
var found = false;
101+
for (@typeInfo(AnsiCode).Enum.fields) |field| {
102+
if (std.mem.eql(u8, field.name, maybe_color_fmt[start..end])) {
103+
// HACK: this would not work if I put bgMagenta for example as a color
104+
// TODO: fix this eheh
105+
const color: AnsiCode = @enumFromInt(field.value + if (is_background) 10 else 0);
106+
at_least_one_color = true;
107+
output = output ++ "\x1b[" ++ color.code() ++ "m";
108+
found = true;
109+
break;
110+
}
83111
}
84-
}
85112

86-
if (!found) {
87-
output = output ++ "{" ++ maybe_color_fmt ++ "}";
113+
if (!found) {
114+
output = output ++ "{" ++ maybe_color_fmt ++ "}";
115+
}
88116
}
117+
118+
end = end + 1;
119+
start = end;
120+
is_background = false;
89121
}
90122
}
91123

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

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

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

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

131161
if (channel >= 3) {
132-
return null;
133-
// @compileError("Invalid number format, too many channels, expected: {0-255} or {0-255;0-255;0-255}");
162+
@compileError("Invalid number format, too many channels, expected: {0-255} or {0-255;0-255;0-255}");
134163
}
135164
},
165+
',' => {
166+
break;
167+
},
136168
else => {
137-
return null;
138-
// @compileError("Invalid number format, expected: {0-255} or {0-255;0-255;0-255}");
169+
@compileError("Invalid number format, expected: {0-255} or {0-255;0-255;0-255}");
139170
},
140171
}
141172
}
142173

143174
// ANSI 256 extended
144175
if (channel == 0) {
145176
const color: []const u8 = fmt[0..channels_length[0]];
146-
output = output ++ "\x1b[38;5;" ++ color ++ "m";
177+
if (background) {
178+
output = output ++ "\x1b[48;5;" ++ color ++ "m";
179+
} else {
180+
output = output ++ "\x1b[38;5;" ++ color ++ "m";
181+
}
147182
}
148183
// TRUECOLOR
149184
// TODO: check for compatibility, is it possible at comptime ??
@@ -157,10 +192,13 @@ fn parse256OrTrueColor(fmt: []const u8) ?[]const u8 {
157192
// +1 to skip the ;
158193
start += channels_length[c] + 1;
159194
}
160-
output = output ++ "\x1b[38;2;" ++ color ++ "m";
195+
if (background) {
196+
output = output ++ "\x1b[48;2;" ++ color ++ "m";
197+
} else {
198+
output = output ++ "\x1b[38;2;" ++ color ++ "m";
199+
}
161200
} else {
162-
return null;
163-
// @compileError("Invalid number format, check the number of channels, must be 1 or 3, expected: {0-255} or {0-255;0-255;0-255}");
201+
@compileError("Invalid number format, check the number of channels, must be 1 or 3, expected: {0-255} or {0-255;0-255;0-255}");
164202
}
165203

166204
return output;

β€Žsrc/main.zigβ€Ž

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,42 @@ const std = @import("std");
22
const chroma = @import("lib.zig");
33

44
pub fn main() !void {
5-
const examples = [_]struct { fmt: []const u8, arg: []const u8 }{
6-
// ANSI foreground and background colors
7-
.{ .fmt = "{yellow}ANSI {s}", .arg = "SUPPORTED" },
8-
.{ .fmt = "{blue}JJK is the best new {s}", .arg = "generation" },
9-
.{ .fmt = "{red}Disagree, and {cyan}Satoru Gojo will throw a {magenta}{s}{reset} at you", .arg = "purple ball" },
10-
.{ .fmt = "{bgMagenta}{white}Yuji Itadori's resolve: {s}", .arg = "I'll eat the finger." },
11-
.{ .fmt = "{bgYellow}{black}With this treasure, I summon: {s}", .arg = "Mahoraga or Makora idk" },
12-
.{ .fmt = "{bgBlue}{white}LeBron {s}", .arg = "James" },
13-
.{ .fmt = "{green}Please, Lonzo Ball, come back in {s}", .arg = "2024" },
14-
.{ .fmt = "{blue}JJK is the best new {s}{red} once again", .arg = "generation" },
15-
16-
// ANSI 256 extended colors
17-
.{ .fmt = "\n{221}256 Extended set, too! {s}", .arg = "eheh" },
18-
.{ .fmt = "{121}Finding examples is hard, {s}", .arg = "shirororororo" },
19-
20-
// TrueColors
21-
.{ .fmt = "\n{221;10;140}How about {13;45;200}{s}??", .arg = "true colors" },
22-
.{ .fmt = "{255;202;255}Toge Inumaki says: {s}", .arg = "Salmon" },
23-
.{ .fmt = "{255;105;180}Nobara Kugisaki's fierce {s}", .arg = "Nail Hammer" },
24-
.{ .fmt = "{10;94;13}Juujika no {s}", .arg = "Rokunin" },
5+
const examples = [_]struct { fmt: []const u8, arg: ?[]const u8 }{
6+
// Basic color and style
7+
.{ .fmt = "{bold,red}Bold and Red{reset}", .arg = null },
8+
// Combining background and foreground with styles
9+
.{ .fmt = "{fg:cyan,bg:magenta}{underline}Cyan on Magenta underline{reset}", .arg = null },
10+
// Nested styles and colors
11+
.{ .fmt = "{green}Green {bold}and Bold{reset,blue,italic} to blue italic{reset}", .arg = null },
12+
// Extended ANSI color with arg example
13+
.{ .fmt = "{bg:120}Extended ANSI {s}{reset}", .arg = "Background" },
14+
// True color specification
15+
.{ .fmt = "{fg:255;100;0}True Color Orange Text{reset}", .arg = null },
16+
// Mixed color and style formats
17+
.{ .fmt = "{bg:28,italic}{fg:231}Mixed Background and Italic{reset}", .arg = null },
18+
// Unsupported/Invalid color code >= 256, Error thrown at compile time
19+
// .{ .fmt = "{fg:999}This should not crash{reset}", .arg = null },
20+
// Demonstrating blink, note: may not be supported in all terminals
21+
.{ .fmt = "{blink}Blinking Text (if supported){reset}", .arg = null },
22+
// Using dim and reverse video
23+
.{ .fmt = "{dim,reverse}Dim and Reversed{reset}", .arg = null },
24+
// Custom message with dynamic content
25+
.{ .fmt = "{blue,bg:magenta}User {bold}{s}{reset,0;255;0} logged in successfully.", .arg = "Charlie" },
26+
// Combining multiple styles and reset
27+
.{ .fmt = "{underline,cyan}Underlined Cyan{reset} then normal", .arg = null },
28+
// Multiple format specifiers for complex formatting
29+
.{ .fmt = "{fg:144,bg:52,bold,italic}Fancy {underline}Styling{reset}", .arg = null },
30+
// Jujutsu Kaisen !!
31+
.{ .fmt = "{bg:72,bold,italic}Jujutsu Kaisen !!{reset}", .arg = null },
2532
};
2633

2734
inline for (examples) |example| {
28-
std.debug.print(chroma.format(example.fmt) ++ "\n", .{example.arg});
35+
if (example.arg) |arg| {
36+
std.debug.print(chroma.format(example.fmt) ++ "\n", .{arg});
37+
} else {
38+
std.debug.print(chroma.format(example.fmt) ++ "\n", .{});
39+
}
2940
}
3041

31-
std.debug.print(chroma.format("{blue}Eventually, the {red}formatting{reset} looks like {130;43;122}{s}!\n"), .{"this"});
42+
std.debug.print(chroma.format("{blue}{underline}Eventually{reset}, the {red}formatting{reset} looks like {130;43;122}{s}!\n"), .{"this"});
3243
}

β€Žsrc/tests.zigβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const std = @import("std");
2-
const AnsiColor = @import("ansi.zig").AnsiColor;
2+
const AnsiCode = @import("ansi.zig").AnsiCode;
33
const chroma = @import("lib.zig");
44

55
// TESTS

β€Žsrc/utils.zigβ€Ž

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

0 commit comments

Comments
Β (0)