/* * MIT License * * Copyright (c) 2025 dev@ptrace.dev * * 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. * */ /* +Version: 2.0.0 ------------------- ---- [ Setup ] ---- ------------------- Initialize this lib via `termcolors_init_allocator_and_context()`. It creates a arena for this lib which you can reset with `termcolors_reset_pool()`. Everything is thrown into the arena, so you don't have to free individual strings. The lib context is also exposed as: `termcolors_context`. The arena is exposed as: `termcolors_pool`. ---------------------------------- --- [ 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. ---------------------- --- [ 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"`. */ #import "Basic"; #import "String"; #import "Math"; #import "Flat_Pool"; termcolors_context: #Context; termcolors_pool: Flat_Pool; // leaving those constants public, maybe someone wants to use them TERM_ESCAPE_START :: "\e[%m%"; TERM_ESCPAE_RESET :: "\e[0m"; FOREGROUND_COLOR_FROM_EXT_TABLE :: "38;5;"; BACKGROUND_COLOR_FROM_EXT_TABLE :: "48;5;"; Term_Rgb :: struct { r, g, b: u8 = 255, 255, 255; } 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 } 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; } 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; } 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; } termcolors_init_allocator_and_context :: () { allocator: Allocator; allocator.proc = flat_pool_allocator_proc; allocator.data = *termcolors_pool; termcolors_context.allocator = allocator; } termcolors_reset_pool :: (overwrite_memory: bool) { reset(*termcolors_pool, overwrite_memory); } paint :: ( str: string, style: Text_Style = .RESET, fg: Color_Foreground = .NONE, bg: Color_Background = .NONE, no_termination := false ) -> string, int { a, b := __paint(str, .[style], fg, bg, "", "", no_termination); return a, b; } paint :: ( str: string, style: []Text_Style = .[], fg: Color_Foreground = .NONE, bg: Color_Background = .NONE, no_termination := false ) -> string, int { a, b := __paint(str, style, fg, bg, "", "", no_termination); return a, b; } paint_ex :: ( str: string, style: Text_Style = .RESET, fg_color: Color_Table = .NONE, bg_color: Color_Table = .NONE, no_termination := false ) -> string, int { a, b := __paint(str, .[style], fg_color, bg_color, FOREGROUND_COLOR_FROM_EXT_TABLE, BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex :: ( str: string, style: Text_Style = .RESET, fg_color: int = -1, bg_color: int = -1, no_termination := false ) -> string, int { a, b := __paint(str, .[style], fg_color, bg_color, FOREGROUND_COLOR_FROM_EXT_TABLE, BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex :: ( str: string, style: []Text_Style = .[], fg_color: int = -1, bg_color: int = -1, no_termination := false ) -> string, int { a, b := __paint(str, style, fg_color, bg_color, FOREGROUND_COLOR_FROM_EXT_TABLE, BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex :: ( str: string, style: []Text_Style = .[], fg_color: Color_Table = .NONE, bg_color: Color_Table = .NONE, no_termination := false ) -> string, int { a, b := __paint(str, style, fg_color, bg_color, FOREGROUND_COLOR_FROM_EXT_TABLE, BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_ex_custom :: ( str: string, style: Text_Style = .RESET, fg_color: $A, bg_color: $B, no_termination := false ) -> string, int { a, b := __paint(str, .[style], fg_color, bg_color, FOREGROUND_COLOR_FROM_EXT_TABLE, BACKGROUND_COLOR_FROM_EXT_TABLE, no_termination ); return a, b; } paint_rgb :: ( str: string, style: Text_Style = .RESET, fg_rgb: Term_Rgb, bg_rgb: Term_Rgb, no_termination := false ) -> string, int { a, b := __paint_rgb(str, .[style], fg_rgb, bg_rgb, no_termination); return a, b; } paint_rgb :: ( str: string, style: []Text_Style = .[], fg_rgb: Term_Rgb, bg_rgb: Term_Rgb, no_termination := false ) -> string, int { a, b := __paint_rgb(str, style, fg_rgb, bg_rgb, no_termination); return a, b; } paint_raw :: (codes: string, str: string, no_termination: bool) -> string, int { push_context termcolors_context { 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; // ---------------------------------------- // Internal Paint API // ------------------ __paint__build_style_str :: (style: []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, ";"); } __paint__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; } __paint__buffer_add_term_codes :: (color_type: string, term_color_code: int) #expand { `buf[`count] = sprint("%0%", color_type, term_color_code); `count += 1; } __paint__buffer_add_style :: () #expand { if `style.count > 0 { s_style := __paint__build_style_str(`style); `buf[`count] = s_style; `count += 1; } } __paint :: ( str: string, style: []Text_Style, fg: int, bg: int, fg_code: string, bg_code: string, no_termination := false ) -> string, int { push_context termcolors_context { buf: [3]string; count: int; __paint__buffer_add_style(); if fg != -1 { __paint__buffer_add_term_codes(fg_code, fg); } if bg != -1 { __paint__buffer_add_term_codes(bg_code, bg); } if count == 0 { a, b := paint_raw("", str, no_termination); return a, b; } a, b := __paint__to_term_code_args(); return a, b,; } } __paint :: ( str: string, style: []Text_Style, fg: $A, bg: $B, fg_code: string, bg_code: string, no_termination := false ) -> string, int { push_context termcolors_context { buf: [3]string; count: int; __paint__buffer_add_style(); if fg != .NONE { __paint__buffer_add_term_codes(fg_code, cast(int)fg); } if bg != .NONE { __paint__buffer_add_term_codes(bg_code, cast(int)bg); } if count == 0 { a, b := paint_raw("", str, no_termination); return a, b; } a, b := __paint__to_term_code_args(); return a, b,; } } __paint_rgb :: ( str: string, style: []Text_Style, fg_rgb: Term_Rgb, bg_rgb: Term_Rgb, no_termination := false ) -> string, int { push_context termcolors_context { buf: [3]string; if style.count > 0 { buf[0] = __paint__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 a, b; } }