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.
68const std = @import ("std" );
7- const AnsiColor = @import ("ansi.zig" ).AnsiColor ;
9+ const AnsiCode = @import ("ansi.zig" ).AnsiCode ;
810const 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
2024pub 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 ;
0 commit comments