/* -------------------- --- [ Reminder ] --- -------------------- Jai already provides a module for console color codes: `modules/Print_Color.jai`. There are small differences between the builtin lib and this lib. The builtin lib: - prints the characters directly to stdout/stderr, instead of returning them - uses a fixed default color palette, instead of supporting arbitrary values If you don't need the features provided by this lib, consider using the builtin variant. ---------------------------------- --- [ paint() / General Info ] --- ---------------------------------- paint() uses the 4bit terminal color palette. The structure of the proc signature through every API is the same: `text, font style, foreground color, background color` Only `text` is mandatory. Other params can be omitted since they default to `.NONE`. [Note]: You can overload each proc for more flexibility. This prints text without any formatting. log(paint("Foo Bar")); Applies a font weight, foreground and background colors log(paint("Foo Bar", .BOLD, .BLACK, .WHITE)); Applies only foreground and background colors log(paint("Foo Bar", .RESET, .BLACK, .WHITE)); Applies multiple text decorations log(paint("Foo Bar", .[.BOLD, .UNDERLINE, .ITALIC], .BLACK, .WHITE)); --- [ Buffering Text ] If you want to buffer a lot of text, you can provide `no_termination = true` to the APIs. Then they won't append the "reset" terminal code. When you're done with your string buffer, you can call the proc `paint_reset()`, which returns the reset code. --- [ Visual Width / Real Width ] If you do `paint("foo", fg = .RED).count`, it will return way more then three characters. This can be quite annoying if you want to build an CLI program, that is aware of its width. Because of that, every API returns additionally an integer, which describes the count of used characters by the terminal codes. ``` my_str, vcount := paint("foo", fg = .RED); ``` Using `vcount` you can now account for the shift in your application. ------------------ --- [ Memory ] --- ------------------ All strings returned must be freed by the caller. Internal procs are using the TS as scratch buffer. ---------------------- --- [ paint_ex() ] --- ---------------------- paint_ex() uses the 256bit color palette. There are some predefined colors you can use. log(paint_ex("Foo Bar", .UNDERLINE, .GREEN_DARK, .ORANGE_LIGHT)); It also allows multiple font styles. log(paint_ex("Foo Bar", .[.BOLD, .UNDERLINE, .ITALIC], .GREEN_DARK, .ORANGE_LIGHT)); If you want to have more control over the colors, you can use integers. log(paint_ex("Foo Bar", .UNDERLINE, 84, 124)); log(paint_ex("Foo Bar", .[.BOLD, .UNDERLINE, .ITALIC], 84, 124)); You could even create a own color palette. Just create a enum with this signature: My_Colors :: enum #specified { NONE :: -1; COL1 :: 84; COL2 :: 124; } And use `paint_ex_custom()` like this: log(paint_ex_custom("Foo Bar", .UNDERLINE, My_Colors.COL1, My_Colors.COL2)); The downside is, you cannot omit the fore- and background color. If you want that feature, you have to wrap this proc in a custom proc. ----------------------- --- [ paint_rgb() ] --- ----------------------- If you want to use RGB values you can to it like that: log(paint_rgb("Foo Bar", .UNDERLINE, .{ 255, 0, 0 }, .{ 0, 0, 255 })); log(paint_rgb("Foo Bar", .[.BOLD, .UNDERLINE, .ITALIC], .{ 255, 0, 0 }, .{ 0, 0, 255 })); Consult the enums below for more colors. --------------------------------------------------- --- [ Using String Literals as Terminal Codes ] --- --------------------------------------------------- If you need "raw" access because you want to build more complex stuff: paint_raw(); Example: log(paint_raw("1;3;32;45", "Foo Bar")); -------------------------------- --- [ Notes on Performance ] --- -------------------------------- Since those APIs providing flexibility, they have to branch a few times. Which won't be a negative hit on most programs. But if you're developing something hyper-fast, those APIs could be a perf hit. To bypass this, you can just use this proc: - paint_raw() Which basically only fprints this string: `"\u001b[%m%\u001b[0m"`. */ // leaving those constants public, maybe someone wants to use them TERM_ESCAPE_START :: "\e[%m%"; TERM_ESCPAE_RESET :: "\e[0m"; TERM_FOREGROUND_COLOR_FROM_EXT_TABLE :: "38;5;"; TERM_BACKGROUND_COLOR_FROM_EXT_TABLE :: "48;5;"; Term_Rgb :: struct { r, g, b: u8 = 255, 255, 255; } Term_Text_Style :: enum #specified { RESET :: 0; BOLD :: 1; FAINT :: 2; // not widely supported ITALIC :: 3; // not widely supported UNDERLINE :: 4; SLOW_BLINK :: 5; // less than 150 bpm RAPID_BLINK :: 6; // not widely supported SWAP_FG_BG :: 7; CONCEAL :: 8; // not widely supported CROSSED_OUT :: 9; // not widely supported PRIMARY_FONT :: 10; // omitting alternate fonts (11-19) since they aren't really supported anymore FRAKTUR :: 20; // not widely supported BOLD_OFF_OR_DOUBLE_UNDERLINE :: 21; // not widely supported NORMAL_COLOR_OR_INTENSITY :: 22; ITALIC_OFF_FRAKTUR_OFF :: 23; UNDERLINE_OFF :: 24; BLINK_OFF :: 25; // Code 26 does nothing (https://vt100.net/docs/vt510-rm/SGR.html) INVERSE_OFF :: 27; CONCEAL_OFF :: 28; CROSSED_OUT_OFF :: 29; FRAMED :: 51; ENCIRCLED :: 52; OVERLINED :: 53; ENCIRCLED_OFF_FRAMED_OFF :: 54; OVERLINED_OFF :: 55; // 60 - 65 Ideograms hardly ever supported // 90 - 107 Bright fg and bg color is non standard } Term_Color_Foreground :: enum #specified { NONE :: -1; BLACK :: 30; RED :: 31; GREEN :: 32; YELLOW :: 33; BLUE :: 34; MAGENTA :: 35; CYAN :: 36; WHITE :: 37; EXTEND :: 38; // 5; OR 2;;; DEFAULT :: 39; } Term_Color_Background :: enum #specified { NONE :: -1; BLACK :: 40; RED :: 41; GREEN :: 42; YELLOW :: 43; BLUE :: 44; MAGENTA :: 45; CYAN :: 46; WHITE :: 47; EXTEND :: 48; // 5; OR 2;;; DEFAULT :: 49; } Term_Color_Table :: enum #specified { NONE :: -1; // Standard ST_BLACK :: 0; ST_RED :: 1; ST_GREEN :: 2; ST_YELLOW :: 3; ST_BLUE :: 4; ST_PURPLE :: 5; ST_TEAL :: 6; ST_GRAY :: 7; // High Intensity HI_GRAY :: 8; HI_RED :: 9; HI_GREEN :: 10; HI_YELLOW :: 11; HI_BLUE :: 12; HI_PURPLE :: 13; HI_TEAL :: 14; HI_WHITE :: 15; // Selected Subset BLACK :: 16; WHITE :: 231; GRAY_DARK :: 234; GRAY_MID :: 243; GRAY_LIGHT :: 250; BLUE_DARK :: 17; BLUE_MID :: 21; BLUE_LIGHT :: 45; GREEN_DARK :: 22; GREEN_MID :: 34; GREEN_LIGHT :: 46; RED_DARK :: 52; RED_MID :: 124; RED_LIGHT :: 196; ROSE_DARK :: 163; ROSE_MID :: 201; ROSE_LIGHT :: 213; MINT_DARK :: 35; MINT_MID :: 78; MINT_LIGHT :: 84; VIOLET_DARK :: 53; VIOLET_MID :: 93; VIOLET_LIGHT :: 141; ORANGE_DARK :: 166; ORANGE_MID :: 202; ORANGE_LIGHT :: 214; YELLOW_DARK :: 220; YELLOW_MID :: 226; YELLOW_LIGHT :: 228; } paint :: ( str: string, style: Term_Text_Style = .RESET, fg: Term_Color_Foreground = .NONE, bg: Term_Color_Background = .NONE, no_termination := false ) -> string, int { a, b := base_paint(str, .[style], fg, bg, "", "", no_termination); return a, b; } paint :: ( str: string, style: []Term_Text_Style = .[], fg: Term_Color_Foreground = .NONE, bg: Term_Color_Background = .NONE, no_termination := false ) -> string, int { a, b := base_paint(str, style, fg, bg, "", "", no_termination); return a, b; } paint_ex :: ( str: string, style: Term_Text_Style = .RESET, fg_color: Term_Color_Table = .NONE, bg_color: Term_Color_Table = .NONE, no_termination := false ) -> string, int { a, b := base_paint(str, .[style], fg_color, bg_color, TERM_FOREGROUND_COLOR_FROM_EXT_TABLE, TERM_BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex :: ( str: string, style: Term_Text_Style = .RESET, fg_color: int = -1, bg_color: int = -1, no_termination := false ) -> string, int { a, b := base_paint(str, .[style], fg_color, bg_color, TERM_FOREGROUND_COLOR_FROM_EXT_TABLE, TERM_BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex :: ( str: string, style: []Term_Text_Style = .[], fg_color: int = -1, bg_color: int = -1, no_termination := false ) -> string, int { a, b := base_paint(str, style, fg_color, bg_color, TERM_FOREGROUND_COLOR_FROM_EXT_TABLE, TERM_BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex :: ( str: string, style: []Term_Text_Style = .[], fg_color: Term_Color_Table = .NONE, bg_color: Term_Color_Table = .NONE, no_termination := false ) -> string, int { a, b := base_paint(str, style, fg_color, bg_color, TERM_FOREGROUND_COLOR_FROM_EXT_TABLE, TERM_BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex_custom :: ( str: string, style: Term_Text_Style = .RESET, fg_color: $A, // TODO: type?? bg_color: $B, // TODO: type?? no_termination := false ) -> string, int { a, b := base_paint(str, .[style], fg_color, bg_color, TERM_FOREGROUND_COLOR_FROM_EXT_TABLE, TERM_BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_rgb :: ( str: string, style: Term_Text_Style = .RESET, fg_rgb: Term_Rgb, bg_rgb: Term_Rgb, no_termination := false ) -> string, int { a, b := base_paint_rgb(str, .[style], fg_rgb, bg_rgb, no_termination); return a, b; } paint_rgb :: ( str: string, style: []Term_Text_Style = .[], fg_rgb: Term_Rgb, bg_rgb: Term_Rgb, no_termination := false ) -> string, int { a, b := base_paint_rgb(str, style, fg_rgb, bg_rgb, no_termination); return a, b; } paint_raw :: (codes: string, str: string, no_termination: bool) -> string, int { out: string; if no_termination { out = sprint( TERM_ESCAPE_START, codes, str ); } else { out = sprint( #run -> string { return tprint("%%", TERM_ESCAPE_START, TERM_ESCPAE_RESET); }, codes, str ); } return out, abs(out.count - str.count); } paint_reset :: () -> string, int { out := TERM_ESCPAE_RESET; return out, out.count; } #scope_file; build_style_str :: (style: []Term_Text_Style) -> string { buf_style: [..]string; for style array_add(*buf_style, sprint("%", cast(int)it)); s_style := join(.. buf_style, ";"); return trim_right(s_style, ";"); } to_term_code_args :: () -> string, int #expand { s := join(.. `buf, ";"); s = trim_right(s, ";"); a, b := paint_raw(s, `str, `no_termination); return a, b; } buffer_add_term_codes :: (color_type: string, term_color_code: int) #expand { `buf[`count] = sprint("%0%", color_type, term_color_code); `count += 1; } buffer_add_style :: () #expand { if `style.count > 0 { s_style := build_style_str(`style); `buf[`count] = s_style; `count += 1; } } base_paint :: ( str: string, style: []Term_Text_Style, fg: int, bg: int, fg_code: string, bg_code: string, no_termination := false ) -> string, int { push_allocator(temp); buf: [3]string; count: int; buffer_add_style(); if fg != -1 { buffer_add_term_codes(fg_code, fg); } if bg != -1 { buffer_add_term_codes(bg_code, bg); } if count == 0 { a, b := paint_raw("", str, no_termination); return copy_string(a,, context.default_allocator), b; } a, b := to_term_code_args(); return copy_string(a,, context.default_allocator), b; } base_paint :: ( str: string, style: []Term_Text_Style, fg: $A, // TODO(adam, 5): This is garbage! bg: $B, // TODO(adam, 5): This is garbage! fg_code: string, bg_code: string, no_termination := false ) -> string, int { push_allocator(temp); buf: [3]string; count: int; buffer_add_style(); if fg != .NONE { buffer_add_term_codes(fg_code, cast(int)fg); } if bg != .NONE { buffer_add_term_codes(bg_code, cast(int)bg); } if count == 0 { a, b := paint_raw("", str, no_termination); return copy_string(a,, context.default_allocator), b; } a, b := to_term_code_args(); return copy_string(a,, context.default_allocator), b; } base_paint_rgb :: ( str: string, style: []Term_Text_Style, fg_rgb: Term_Rgb, bg_rgb: Term_Rgb, no_termination := false ) -> string, int { push_allocator(temp); buf: [3]string; if style.count > 0 { buf[0] = build_style_str(style); } buf[1] = sprint("38;2;%;%;%", fg_rgb.r, fg_rgb.g, fg_rgb.b); buf[2] = sprint("48;2;%;%;%", bg_rgb.r, bg_rgb.g, bg_rgb.b); s := join(.. buf, ";"); s = trim_right(s, ";"); a, b := paint_raw(s, str, no_termination); return copy_string(a,, context.default_allocator), b; } using,only(array_add, copy_string, sprint, tprint, temp, push_allocator) Basic :: #import "Basic"; using,only(join, trim_right) String :: #import "String"; using,only(abs) Math :: #import "Math"; /* ------------------------------------------------------------------------------ This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------ ALTERNATIVE A - MIT License Copyright (c) 2026 Adam Blazeowsky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ ALTERNATIVE B - Public Domain (www.unlicense.org) This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ */