├── .github └── workflows │ └── zig_test.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── ansi.zig ├── lib.zig ├── main.zig ├── tests.zig └── utils.zig /.github/workflows/zig_test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "main" branch 8 | push: 9 | branches: ["main"] 10 | pull_request: 11 | branches: ["main"] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | jobs: 17 | test: 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | runs-on: ${{matrix.os}} 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: mlugg/setup-zig@v1 25 | with: 26 | version: 0.13.0 27 | - run: zig build test 28 | lint: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: mlugg/setup-zig@v1 33 | with: 34 | version: 0.13.0 35 | - run: zig fmt --check . 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/zig 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=zig 3 | 4 | ### zig ### 5 | # Zig programming language 6 | 7 | .zig-cache/ 8 | zig-cache/ 9 | zig-out/ 10 | build/ 11 | build-*/ 12 | docgen_tmp/ 13 | 14 | # End of https://www.toptal.com/developers/gitignore/api/zig 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 @adia-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chroma 2 | 3 | **Version:** 0.13.0 4 | **License:** MIT 5 | **Language:** [Zig](https://ziglang.org) 6 | 7 | Chroma is a Zig library for advanced ANSI color and text styling in terminal output. It allows developers to dynamically format strings with embedded placeholders (e.g. `{red}`, `{bold}`, `{fg:255;100;0}` for true color) and converts them into ANSI escape sequences. This makes it easy to apply complex styles, switch between foreground/background colors, and reset formatting on the fly—all at compile time. 8 | 9 | chroma 10 | 11 | ## ✨ Features 12 | 13 | - **Simple, Readable Syntax:** 14 | Use `{red}`, `{bold}`, or `{green,bgBlue}` inline within strings for clear and maintainable code. 15 | 16 | - **Comprehensive ANSI Codes:** 17 | Support for standard colors, background colors, bold, italic, underline, dim, and even less commonly supported effects like `blink` and `reverse`. 18 | 19 | - **Extended and True Color Support:** 20 | Take advantage of ANSI 256 extended color codes and true color (24-bit) formats using syntax like `{fg:120}`, `{bg:28}`, or `{fg:255;100;0}` for fine-grained color control. 21 | 22 | - **Compile-Time Safety:** 23 | Chroma verifies format strings at compile time, reducing runtime errors and ensuring your formatting instructions are valid. 24 | 25 | - **Reset-Friendly:** 26 | Automatically appends `"\x1b[0m"` when necessary, ensuring that styles don’t “bleed” into subsequent output. 27 | 28 | ## 🚀 Getting Started 29 | 30 | ### Prerequisite 31 | 32 | 1. Fetch the project using `zig fetch` 33 | 34 | ```bash 35 | zig fetch --save https://github.com/adia-dev/chroma-zig/archive/refs/heads/main.zip 36 | ``` 37 | 38 | Or manually paste this in your `build.zig.zon` 39 | 40 | ```zig 41 | .dependencies = .{ 42 | // other deps... 43 | .chroma = .{ 44 | .url = "https://github.com/adia-dev/chroma-zig/archive/refs/heads/main.zip", 45 | .hash = "12209a8a991121bba3b21f31d275588690dc7c0d7fa9c361fd892e782dd88e0fb2ba", 46 | }, 47 | // ... 48 | }, 49 | ``` 50 | 51 | 1. **Add Chroma to Your Zig Project:** 52 | Include Chroma as a dependency in your `build.zig` or your `build.zig.zon`. For example: 53 | 54 | ```zig 55 | const std = @import("std"); 56 | 57 | pub fn build(b: *std.Build) void { 58 | const target = b.standardTargetOptions(.{}); 59 | const optimize = b.standardOptimizeOption(.{}); 60 | 61 | const lib = b.addStaticLibrary(.{ 62 | .name = "chroma", 63 | .root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/lib.zig" } }, 64 | .target = target, 65 | .optimize = optimize, 66 | }); 67 | 68 | b.installArtifact(lib); 69 | } 70 | ``` 71 | 72 | 2. **Import and Use:** 73 | After building and installing, you can import `chroma` into your Zig code: 74 | 75 | ```zig 76 | const std = @import("std"); 77 | const chroma = @import("lib.zig"); 78 | 79 | pub fn main() !void { 80 | const examples = [_]struct { fmt: []const u8, arg: ?[]const u8 }{ 81 | // Basic color and style 82 | .{ .fmt = "{bold,red}Bold and Red{reset}", .arg = null }, 83 | // Combining background and foreground with styles 84 | .{ .fmt = "{fg:cyan,bg:magenta}{underline}Cyan on Magenta underline{reset}", .arg = null }, 85 | // Nested styles and colors 86 | .{ .fmt = "{green}Green {bold}and Bold{reset,blue,italic} to blue italic{reset}", .arg = null }, 87 | // Extended ANSI color with arg example 88 | .{ .fmt = "{bg:120}Extended ANSI {s}{reset}", .arg = "Background" }, 89 | // True color specification 90 | .{ .fmt = "{fg:255;100;0}True Color Orange Text{reset}", .arg = null }, 91 | // Mixed color and style formats 92 | .{ .fmt = "{bg:28,italic}{fg:231}Mixed Background and Italic{reset}", .arg = null }, 93 | // Unsupported/Invalid color code >= 256, Error thrown at compile time 94 | // .{ .fmt = "{fg:999}This should not crash{reset}", .arg = null }, 95 | // Demonstrating blink, note: may not be supported in all terminals 96 | .{ .fmt = "{blink}Blinking Text (if supported){reset}", .arg = null }, 97 | // Using dim and reverse video 98 | .{ .fmt = "{dim,reverse}Dim and Reversed{reset}", .arg = null }, 99 | // Custom message with dynamic content 100 | .{ .fmt = "{blue,bg:magenta}User {bold}{s}{reset,0;255;0} logged in successfully.", .arg = "Charlie" }, 101 | // Combining multiple styles and reset 102 | .{ .fmt = "{underline,cyan}Underlined Cyan{reset} then normal", .arg = null }, 103 | // Multiple format specifiers for complex formatting 104 | .{ .fmt = "{fg:144,bg:52,bold,italic}Fancy {underline}Styling{reset}", .arg = null }, 105 | // Jujutsu Kaisen !! 106 | .{ .fmt = "{bg:72,bold,italic}Jujutsu Kaisen !!{reset}", .arg = null }, 107 | }; 108 | 109 | inline for (examples) |example| { 110 | if (example.arg) |arg| { 111 | std.debug.print(chroma.format(example.fmt) ++ "\n", .{arg}); 112 | } else { 113 | std.debug.print(chroma.format(example.fmt) ++ "\n", .{}); 114 | } 115 | } 116 | 117 | std.debug.print(chroma.format("{blue}{underline}Eventually{reset}, the {red}formatting{reset} looks like {130;43;122}{s}!\n"), .{"this"}); 118 | } 119 | 120 | ``` 121 | 122 | 3. **Run and Test:** 123 | - Build your project with `zig build`. 124 | - Run your binary and see the styled output in your terminal! 125 | 126 | ## 🧪 Testing 127 | 128 | Chroma includes a suite of unit tests to ensure reliability: 129 | 130 | ```bash 131 | zig build test 132 | ``` 133 | 134 | If all tests pass, you’re good to go! 135 | 136 | ## 🔧 Configuration 137 | 138 | Chroma works out-of-the-box. For more complex scenarios (e.g., custom labels, multiple color formats), refer to `src/lib.zig` and `src/ansi.zig` for detailed code comments that explain available options and their intended usage. 139 | 140 | ## 📦 New in Version 0.13.0 141 | 142 | - **Updated Compatibility:** Now aligned with Zig `0.13.0`. 143 | - **Improved Parser Logic:** More robust handling of multiple formats within the same placeholder. 144 | - **Better Testing:** Additional tests ensure extended color and true color formats behave as expected. 145 | - **Performance Tweaks:** Minor compile-time optimizations for faster builds. 146 | 147 | ## 🤝 Contributing 148 | 149 | Contributions are welcome! To get involved: 150 | 151 | 1. **Fork & Clone:** 152 | Fork the repository and clone it locally. 153 | 154 | 2. **Branch & Develop:** 155 | Create a new branch and implement your changes or new features. 156 | 157 | 3. **Test & Document:** 158 | Run `zig build test` to ensure your changes haven’t broken anything. Update or add documentation as needed. 159 | 160 | 4. **Pull Request:** 161 | Submit a Pull Request describing what you changed and why. We’ll review and merge it if everything looks good. 162 | 163 | ## 📝 License 164 | 165 | [MIT License](./LICENSE) 166 | 167 | _Chroma aims to simplify ANSI coloring in Zig, making your command-line tools, logs, and output more expressive and visually appealing._ 168 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | _ = b.addModule("chroma", .{ .root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/lib.zig" } } }); 8 | 9 | const lib = b.addStaticLibrary(.{ 10 | .name = "chroma", 11 | .root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/lib.zig" } }, 12 | .target = target, 13 | .optimize = optimize, 14 | }); 15 | 16 | b.installArtifact(lib); 17 | 18 | const exe = b.addExecutable(.{ 19 | .name = "chroma", 20 | .root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/main.zig" } }, 21 | .target = target, 22 | .optimize = optimize, 23 | }); 24 | 25 | b.installArtifact(exe); 26 | 27 | const run_cmd = b.addRunArtifact(exe); 28 | 29 | run_cmd.step.dependOn(b.getInstallStep()); 30 | 31 | if (b.args) |args| { 32 | run_cmd.addArgs(args); 33 | } 34 | 35 | const run_step = b.step("run", "Run the app"); 36 | run_step.dependOn(&run_cmd.step); 37 | 38 | const lib_unit_tests = b.addTest(.{ 39 | .root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/lib.zig" } }, 40 | .target = target, 41 | .optimize = optimize, 42 | }); 43 | 44 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 45 | 46 | const exe_unit_tests = b.addTest(.{ 47 | .root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/main.zig" } }, 48 | .target = target, 49 | .optimize = optimize, 50 | }); 51 | 52 | const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); 53 | 54 | const test_step = b.step("test", "Run unit tests"); 55 | test_step.dependOn(&run_lib_unit_tests.step); 56 | test_step.dependOn(&run_exe_unit_tests.step); 57 | } 58 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "chroma", 3 | // This is a [Semantic Version](https://semver.org/). 4 | // In a future version of Zig it will be used for package deduplication. 5 | .version = "0.1.2", 6 | 7 | // This field is optional. 8 | // This is currently advisory only; Zig does not yet do anything 9 | // with this value. 10 | .minimum_zig_version = "0.13.0", 11 | 12 | // This field is optional. 13 | // Each dependency must either provide a `url` and `hash`, or a `path`. 14 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 15 | // Once all dependencies are fetched, `zig build` no longer requires 16 | // internet connectivity. 17 | .dependencies = .{}, 18 | .paths = .{ 19 | // This makes *all* files, recursively, included in this package. It is generally 20 | // better to explicitly list the files and directories instead, to insure that 21 | // fetching from tarballs, file system paths, and version control all result 22 | // in the same contents hash. 23 | // "", 24 | // For example... 25 | "build.zig", 26 | //"build.zig.zon", 27 | "src", 28 | //"LICENSE", 29 | //"README.md", 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/ansi.zig: -------------------------------------------------------------------------------- 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 | 18 | // Standard text colors 19 | black = 30, 20 | red, 21 | green, 22 | yellow, 23 | blue, 24 | magenta, 25 | cyan, 26 | white, 27 | 28 | // Standard background colors 29 | bgBlack = 40, 30 | bgRed, 31 | bgGreen, 32 | bgYellow, 33 | bgBlue, 34 | bgMagenta, 35 | bgCyan, 36 | bgWhite, 37 | 38 | /// Returns the string representation of the color. 39 | /// This method makes it easy to identify a color by its name in the source code. 40 | /// 41 | /// Returns: 42 | /// A slice of constant u8 bytes representing the color's name. 43 | pub fn to_string(self: AnsiCode) []const u8 { 44 | return @tagName(self); 45 | } 46 | 47 | /// Returns the ANSI escape code for the color as a string. 48 | /// This method is used to apply the color to terminal output by embedding 49 | /// the returned string into an output sequence. 50 | /// 51 | /// Returns: 52 | /// A slice of constant u8 bytes representing the ANSI escape code for the color. 53 | pub fn code(self: AnsiCode) []const u8 { 54 | 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 66 | .black => "30", 67 | .red => "31", 68 | .green => "32", 69 | .yellow => "33", 70 | .blue => "34", 71 | .magenta => "35", 72 | .cyan => "36", 73 | .white => "37", 74 | // background colors 75 | .bgBlack => "40", 76 | .bgRed => "41", 77 | .bgGreen => "42", 78 | .bgYellow => "43", 79 | .bgBlue => "44", 80 | .bgMagenta => "45", 81 | .bgCyan => "46", 82 | .bgWhite => "47", 83 | }; 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | //BUG: apparently {{}} is not reflected to {} 2 | 3 | /// This module provides a flexible way to format strings with ANSI color codes 4 | /// dynamically using {colorName} placeholders within the text. It supports standard 5 | /// ANSI colors, ANSI 256 extended colors, and true color (24-bit) formats. 6 | /// It intelligently handles color formatting by parsing placeholders and replacing 7 | /// them with the appropriate ANSI escape codes for terminal output. 8 | const std = @import("std"); 9 | const AnsiCode = @import("ansi.zig").AnsiCode; 10 | const compileAssert = @import("utils.zig").compileAssert; 11 | 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. 19 | /// 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 `}}`). 23 | // TODO: Refactor this lol 24 | pub fn format(comptime fmt: []const u8) []const u8 { 25 | @setEvalBranchQuota(2000000); 26 | comptime var i: usize = 0; 27 | comptime var output: []const u8 = ""; 28 | comptime var at_least_one_color = false; 29 | 30 | inline while (i < fmt.len) { 31 | const start_index = i; 32 | 33 | // Find next '{' or '}' or end of string 34 | inline while (i < fmt.len and fmt[i] != '{' and fmt[i] != '}') : (i += 1) {} 35 | 36 | // Handle escaped braces '{{' or '}}' 37 | if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) { 38 | i += 2; // Skip both braces 39 | } 40 | 41 | // Append text up to the next control character 42 | if (start_index != i) { 43 | output = output ++ fmt[start_index..i]; 44 | continue; 45 | } 46 | 47 | if (i >= fmt.len) break; // End of string 48 | 49 | // Process color formatting 50 | comptime compileAssert(fmt[i] == '{', "Expected '{' to start color format"); 51 | i += 1; // Skip '{' 52 | 53 | const fmt_begin = i; 54 | inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {} // Find closing '}' 55 | const fmt_end = i; 56 | 57 | comptime compileAssert(i < fmt.len, "Missing closing '}' in color format"); 58 | 59 | const maybe_color_fmt = fmt[fmt_begin..fmt_end]; 60 | 61 | if (maybe_color_fmt.len == 0) { 62 | // since empty, write the braces, skip the closing one 63 | // and continue 64 | output = output ++ "{" ++ maybe_color_fmt ++ "}"; 65 | i += 1; 66 | continue; 67 | } 68 | 69 | comptime { 70 | var start = 0; 71 | var end = 0; 72 | var is_background = false; 73 | 74 | style_loop: while (start < maybe_color_fmt.len) { 75 | while (end < maybe_color_fmt.len and maybe_color_fmt[end] != ',') : (end += 1) {} 76 | 77 | var modifier_end = start; 78 | while (modifier_end < maybe_color_fmt.len and maybe_color_fmt[modifier_end] != ':') : (modifier_end += 1) {} 79 | 80 | if (modifier_end != maybe_color_fmt.len) { 81 | if (std.mem.eql(u8, maybe_color_fmt[start..modifier_end], "bg")) { 82 | is_background = true; 83 | end = modifier_end + 1; 84 | start = end; 85 | continue :style_loop; 86 | } else if (std.mem.eql(u8, maybe_color_fmt[start..modifier_end], "fg")) { 87 | is_background = false; 88 | end = modifier_end + 1; 89 | start = end; 90 | continue :style_loop; 91 | } 92 | } 93 | 94 | if (std.ascii.isDigit(maybe_color_fmt[start])) { 95 | const color = parse256OrTrueColor(maybe_color_fmt[start..end], is_background); 96 | output = output ++ color; 97 | at_least_one_color = true; 98 | } else { 99 | var found = false; 100 | for (@typeInfo(AnsiCode).Enum.fields) |field| { 101 | if (std.mem.eql(u8, field.name, maybe_color_fmt[start..end])) { 102 | // HACK: this would not work if I put bgMagenta for example as a color 103 | // TODO: fix this eheh 104 | const color: AnsiCode = @enumFromInt(field.value + if (is_background) 10 else 0); 105 | at_least_one_color = true; 106 | output = output ++ "\x1b[" ++ color.code() ++ "m"; 107 | found = true; 108 | break; 109 | } 110 | } 111 | 112 | if (!found) { 113 | output = output ++ "{" ++ maybe_color_fmt ++ "}"; 114 | } 115 | } 116 | 117 | end = end + 1; 118 | start = end; 119 | is_background = false; 120 | } 121 | } 122 | 123 | i += 1; // Skip '}' 124 | } 125 | 126 | if (at_least_one_color) { 127 | return output ++ "\x1b[0m"; 128 | } 129 | 130 | return output; 131 | } 132 | 133 | // TODO: maybe keep the compile error and dedicate this function to be comptime only 134 | fn parse256OrTrueColor(fmt: []const u8, background: bool) []const u8 { 135 | var channels_value: [3]u8 = .{ 0, 0, 0 }; 136 | var channels_length: [3]u8 = .{ 0, 0, 0 }; 137 | var channel = 0; 138 | var output: []const u8 = ""; 139 | 140 | for (fmt) |c| { 141 | switch (c) { 142 | '0'...'9' => { 143 | var res = @mulWithOverflow(channels_value[channel], 10); 144 | if (res[1] > 0) { 145 | @compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}"); 146 | } 147 | channels_value[channel] = res[0]; 148 | 149 | res = @addWithOverflow(channels_value[channel], c - '0'); 150 | if (res[1] > 0) { 151 | @compileError("Invalid number format, channel value too high >= 256, expected: {0-255} or {0-255;0-255;0-255}"); 152 | } 153 | channels_value[channel] = res[0]; 154 | 155 | channels_length[channel] += 1; 156 | }, 157 | ';' => { 158 | channel += 1; 159 | 160 | if (channel >= 3) { 161 | @compileError("Invalid number format, too many channels, expected: {0-255} or {0-255;0-255;0-255}"); 162 | } 163 | }, 164 | ',' => { 165 | break; 166 | }, 167 | else => { 168 | @compileError("Invalid number format, expected: {0-255} or {0-255;0-255;0-255}"); 169 | }, 170 | } 171 | } 172 | 173 | // ANSI 256 extended 174 | if (channel == 0) { 175 | const color: []const u8 = fmt[0..channels_length[0]]; 176 | if (background) { 177 | output = output ++ "\x1b[48;5;" ++ color ++ "m"; 178 | } else { 179 | output = output ++ "\x1b[38;5;" ++ color ++ "m"; 180 | } 181 | } 182 | // TRUECOLOR 183 | // TODO: check for compatibility, is it possible at comptime ?? 184 | else if (channel == 2) { 185 | var color: []const u8 = ""; 186 | var start = 0; 187 | for (0..channel + 1) |c| { 188 | const end = start + channels_length[c]; 189 | color = color ++ fmt[start..end] ++ if (c == channel) "" else ";"; 190 | 191 | // +1 to skip the ; 192 | start += channels_length[c] + 1; 193 | } 194 | if (background) { 195 | output = output ++ "\x1b[48;2;" ++ color ++ "m"; 196 | } else { 197 | output = output ++ "\x1b[38;2;" ++ color ++ "m"; 198 | } 199 | } else { 200 | @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 | } 202 | 203 | return output; 204 | } 205 | 206 | comptime { 207 | _ = @import("tests.zig"); 208 | } 209 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const chroma = @import("lib.zig"); 3 | 4 | pub fn main() !void { 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 }, 32 | }; 33 | 34 | inline for (examples) |example| { 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 | } 40 | } 41 | 42 | std.debug.print(chroma.format("{blue}{underline}Eventually{reset}, the {red}formatting{reset} looks like {130;43;122}{s}!\n"), .{"this"}); 43 | } 44 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const AnsiCode = @import("ansi.zig").AnsiCode; 3 | const chroma = @import("lib.zig"); 4 | 5 | // TESTS 6 | const COLOR_OPEN = "\x1b["; 7 | const RESET = "\x1b[0m"; 8 | 9 | test "format - Red text" { 10 | const red_hello = chroma.format("{red}Hello"); 11 | 12 | const expected = COLOR_OPEN ++ "31m" ++ "Hello" ++ RESET; 13 | try std.testing.expectEqualStrings(expected, red_hello); 14 | } 15 | 16 | test "format - Multiple colors" { 17 | const colorful_text = chroma.format("{red}Hello{green}my name is{blue}Abdoulaye."); 18 | 19 | const expected = COLOR_OPEN ++ "31m" ++ "Hello" ++ COLOR_OPEN ++ "32m" ++ "my name is" ++ COLOR_OPEN ++ "34m" ++ "Abdoulaye." ++ RESET; 20 | try std.testing.expectEqualStrings(expected, colorful_text); 21 | } 22 | 23 | test "format - Background color and reset" { 24 | const bg_and_reset = chroma.format("{bgRed}Warning!{reset} Normal text."); 25 | 26 | const expected = COLOR_OPEN ++ "41m" ++ "Warning!" ++ RESET ++ " Normal text." ++ RESET; 27 | try std.testing.expectEqualStrings(expected, bg_and_reset); 28 | } 29 | 30 | test "format - Escaped braces" { 31 | const escaped_braces = chroma.format("{{This}} is {green}green."); 32 | 33 | const expected = "{{" ++ "This" ++ "}} is " ++ COLOR_OPEN ++ "32m" ++ "green." ++ RESET; 34 | try std.testing.expectEqualStrings(expected, escaped_braces); 35 | } 36 | 37 | // Test "format - Unmatched braces" would cause a compile-time error: 38 | // const unmatched_braces =chroma.format("{red}Unmatched"); 39 | // This test is documented to ensure awareness of the behavior. 40 | 41 | test "format - No color codes" { 42 | const no_color = chroma.format("Just plain text."); 43 | 44 | const expected = "Just plain text."; 45 | try std.testing.expectEqualStrings(expected, no_color); 46 | } 47 | 48 | test "format - Empty text" { 49 | const red_hello = chroma.format(""); 50 | 51 | const expected = ""; 52 | try std.testing.expectEqualStrings(expected, red_hello); 53 | } 54 | 55 | test "format - Empty format" { 56 | const red_hello = chroma.format("{}"); 57 | 58 | const expected = "{}"; 59 | try std.testing.expectEqualStrings(expected, red_hello); 60 | } 61 | 62 | test "format - Inline reset" { 63 | const inline_reset = chroma.format("{red}Colored{reset} Not colored."); 64 | 65 | const expected = COLOR_OPEN ++ "31m" ++ "Colored" ++ RESET ++ " Not colored." ++ RESET; 66 | try std.testing.expectEqualStrings(expected, inline_reset); 67 | } 68 | 69 | test "format - Text following color codes without braces" { 70 | const text_after_color = chroma.format("{red}Red {green}Green{reset} Reset."); 71 | 72 | const expected = COLOR_OPEN ++ "31m" ++ "Red " ++ COLOR_OPEN ++ "32m" ++ "Green" ++ RESET ++ " Reset." ++ RESET; 73 | try std.testing.expectEqualStrings(expected, text_after_color); 74 | } 75 | 76 | // test "format - Multiple color codes" { 77 | // const multiple_color_codes =chroma.format("{red}{bgGreen}Red on green"); 78 | 79 | // const expected = COLOR_OPEN ++ "31;42m" ++ "Red on green" ++ RESET; 80 | // try std.testing.expectEqualStrings(expected, multiple_color_codes); 81 | // } 82 | 83 | // test "format - Multiple color codes with reset" { 84 | // const multiple_color_codes_with_reset =chroma.format("{red}{bgGreen}Red on green{reset} Reset."); 85 | 86 | // const expected = COLOR_OPEN ++ "31;42m" ++ "Red on green" ++ RESET ++ " Reset." ++ RESET; 87 | // try std.testing.expectEqualStrings(expected, multiple_color_codes_with_reset); 88 | // } 89 | 90 | // test "format - Multiple color codes with inline reset" { 91 | // const multiple_color_codes_with_inline_reset =chroma.format("{red}{bgGreen}Red on green{reset} Reset."); 92 | 93 | // const expected = COLOR_OPEN ++ "31;42m" ++ "Red on green" ++ RESET ++ " Reset." ++ RESET; 94 | // try std.testing.expectEqualStrings(expected, multiple_color_codes_with_inline_reset); 95 | // } 96 | 97 | // test "format - Multiple color codes with inline reset and text after" { 98 | // const multiple_color_codes_with_inline_reset_and_text_after =chroma.format("{red}{bgGreen}Red on green{reset} Reset."); 99 | 100 | // const expected = COLOR_OPEN ++ "31;42m" ++ "Red on green" ++ RESET ++ " Reset." ++ RESET; 101 | // try std.testing.expectEqualStrings(expected, multiple_color_codes_with_inline_reset_and_text_after); 102 | // } 103 | -------------------------------------------------------------------------------- /src/utils.zig: -------------------------------------------------------------------------------- 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. 4 | pub fn compileAssert(ok: bool, msg: []const u8) void { 5 | if (!ok) { 6 | @compileError("Assertion failed: " ++ msg); 7 | } 8 | } 9 | --------------------------------------------------------------------------------