Author:ptrace
Comitter:ptrace
Date:2026-01-16 01:15:34 UTC
diff --git a/README.md b/README.md
index 04fe40c..74c159c 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,28 @@
# Terminal Colors for Jai
This is a small declarative library which allows you to create colorful terminal messages.
Note, that Jai already provides a module for printing colors: `modules/Print_Color`.
The differences are, that the builtin module prints the characters directly, where
this library returns strings.
APIs of this library also returning the amount of characters, which were used
for representing the colors. Which makes it useful for CLI programs, that are aware
of the terminal window width.
The last thing: The builtin library uses a default color palette.
Where this library extends it a bit more and allows you to use RGB values.
### Windows
I don't develop for Windows, so I'm not sure if this library will work on it.
But I don't mind supporting Windows, you can provide me more information or a patch.
## Usage
Put the directory `termcolors` in your projects `modules` directory, or link it while building.
### Example
```plain
@@ -11,7 +30,10 @@ Put the directory `termcolors` in your projects `modules` directory, or link it
#import "termcolors";
main :: () {
context.allocator = temp;
termcolors_init_allocator_and_context();
// If you want, you can reset the library pool with this proc
defer termcolors_reset_pool();
font style background color
diff --git a/termcolors/lib.jai b/termcolors/lib.jai
index 02f921b..3437393 100644
--- a/termcolors/lib.jai
+++ b/termcolors/lib.jai
@@ -24,12 +24,26 @@
*/
/*
+Version: 1.0.1
+Version: 2.0.0
-------------------
--- [ paint() ] ---
---- [ 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:
@@ -54,6 +68,31 @@
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() ] ---
----------------------
@@ -118,149 +157,29 @@
- paint_raw()
Which basically only fprints this string: `"\u001b[%m%\u001b[0m"`.
--------------------------------------
--- [ Notes on Memory Allocation ] ---
--------------------------------------
All APIs returning a string, which you have to manage.
Depending on your use case, you might be fine with pushing it to
the temporary storage via comma-comma:
log(pain("Foo Baar", .BOLD, .BLACK, .WHITE,, temp));
or
context.allocator = temp;
log(pain("Foo Baar"));
log(pain("Foo Baar", .BOLD, .BLACK, .WHITE));
*/
#import "Basic";
#import "String";
#import "Math";
#import "Flat_Pool";
paint :: (
str: string,
style: Text_Style = .RESET,
fg: Color_Foreground = .NONE,
bg: Color_Background = .NONE
) -> string
{
return __paint(str, .[style], fg, bg, "", "");
}
paint :: (
str: string,
style: []Text_Style = .[],
fg: Color_Foreground = .NONE,
bg: Color_Background = .NONE
) -> string
{
return __paint(str, style, fg, bg, "", "");
}
paint_ex :: (
str: string,
style: Text_Style = .RESET,
fg_color: Color_Table = .NONE,
bg_color: Color_Table = .NONE
) -> string
{
return __paint(str, .[style], fg_color, bg_color,
FOREGROUND_COLOR_FROM_EXT_TABLE,
BACKGROUND_COLOR_FROM_EXT_TABLE
);
}
paint_ex :: (
str: string,
style: Text_Style = .RESET,
fg_color: int = -1,
bg_color: int = -1
) -> string
{
return __paint(str, .[style], fg_color, bg_color,
FOREGROUND_COLOR_FROM_EXT_TABLE,
BACKGROUND_COLOR_FROM_EXT_TABLE
);
}
paint_ex :: (
str: string,
style: []Text_Style = .[],
fg_color: int = -1,
bg_color: int = -1
) -> string
{
return __paint(str, style, fg_color, bg_color,
FOREGROUND_COLOR_FROM_EXT_TABLE,
BACKGROUND_COLOR_FROM_EXT_TABLE
);
}
paint_ex :: (
str: string,
style: []Text_Style = .[],
fg_color: Color_Table = .NONE,
bg_color: Color_Table = .NONE
) -> string
{
return __paint(str, style, fg_color, bg_color,
FOREGROUND_COLOR_FROM_EXT_TABLE,
BACKGROUND_COLOR_FROM_EXT_TABLE
);
}
termcolors_context: #Context;
termcolors_pool: Flat_Pool;
paint_ex_custom :: (
str: string,
style: Text_Style = .RESET,
fg_color: $A,
bg_color: $B
) -> string
{
return __paint(str, .[style], fg_color, bg_color,
FOREGROUND_COLOR_FROM_EXT_TABLE,
BACKGROUND_COLOR_FROM_EXT_TABLE
);
}
paint_rgb :: (
str: string,
style: Text_Style = .RESET,
fg_rgb: Term_Rgb,
bg_rgb: Term_Rgb
) -> string
{
return __paint_rgb(str, .[style], fg_rgb, bg_rgb);
}
paint_rgb :: (
str: string,
style: []Text_Style = .[],
fg_rgb: Term_Rgb,
bg_rgb: Term_Rgb
) -> string
{
return __paint_rgb(str, style, fg_rgb, bg_rgb);
}
paint_raw :: (codes: string, str: string) -> string {
return sprint(#insert TERM_ESCAPE_CODE, codes, str);
}
// 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;
@@ -388,35 +307,202 @@ Color_Table :: enum #specified {
}
// leaving those constants public, maybe someone wants to use them
TERM_ESCAPE_CODE :: #code "\u001b[%m%\u001b[0m";
FOREGROUND_COLOR_FROM_EXT_TABLE :: "38;5;";
BACKGROUND_COLOR_FROM_EXT_TABLE :: "48;5;";
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;
buf_style.allocator = temp;
for style array_add(*buf_style, tprint("%", cast(int)it));
s_style := join(.. buf_style, ";",, temp);
return trim_right(s_style, ";",, temp);
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 #expand {
s := join(.. `buf, ";",, temp);
s = trim_right(s, ";",, temp);
return paint_raw(s, `str);
__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] = tprint("%0%", color_type, term_color_code); `count += 1;
`buf[`count] = sprint("%0%", color_type, term_color_code); `count += 1;
}
__paint__buffer_add_style :: () #expand {
@@ -433,22 +519,27 @@ __paint :: (
fg: int,
bg: int,
fg_code: string,
bg_code: string
) -> string
bg_code: string,
no_termination := false
) -> string, int
{
buf: [3]string;
count: int;
push_context termcolors_context {
buf: [3]string;
count: int;
__paint__buffer_add_style();
__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 fg != -1 { __paint__buffer_add_term_codes(fg_code, fg); }
if bg != -1 { __paint__buffer_add_term_codes(bg_code, bg); }
if count == 0 {
return paint_raw("", str);
}
if count == 0 {
a, b := paint_raw("", str, no_termination);
return a, b;
}
return __paint__to_term_code_args();
a, b := __paint__to_term_code_args();
return a, b,;
}
}
__paint :: (
@@ -457,43 +548,52 @@ __paint :: (
fg: $A,
bg: $B,
fg_code: string,
bg_code: string
) -> string
bg_code: string,
no_termination := false
) -> string, int
{
buf: [3]string;
count: int;
push_context termcolors_context {
buf: [3]string;
count: int;
__paint__buffer_add_style();
__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 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 {
return paint_raw("", str);
}
if count == 0 {
a, b := paint_raw("", str, no_termination);
return a, b;
}
return __paint__to_term_code_args();
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
) -> string
bg_rgb: Term_Rgb,
no_termination := false
) -> string, int
{
buf: [3]string;
push_context termcolors_context {
buf: [3]string;
if style.count > 0 {
buf[0] = __paint__build_style_str(style);
}
if style.count > 0 {
buf[0] = __paint__build_style_str(style);
}
buf[1] = tprint("38;2;%;%;%", fg_rgb.r, fg_rgb.g, fg_rgb.b);
buf[2] = tprint("48;2;%;%;%", bg_rgb.r, bg_rgb.g, bg_rgb.b);
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, ";",, temp);
s = trim_right(s, ";",, temp);
s := join(.. buf, ";");
s = trim_right(s, ";");
return paint_raw(s, str);
a, b := paint_raw(s, str, no_termination);
return a, b;
}
}
diff --git a/test/test.jai b/test/test.jai
index e2c61de..aeb138b 100644
--- a/test/test.jai
+++ b/test/test.jai
@@ -10,7 +10,11 @@ MEMORY_DEBUGGER :: false;
main :: () {
#if MEMORY_DEBUGGER defer report_memory_leaks();
context.allocator = temp;
termcolors_init_allocator_and_context();
// Technically you don't need do reset the pool for this example.
// But I keep it here, so you're aware of this procedure.
defer termcolors_reset_pool(overwrite_memory = true);
log("--- paint() -----------------------------");
log(paint("1 Plain"));
@@ -61,8 +65,8 @@ main :: () {
log(paint_rgb("13 Bold, Underline, Italic, fg rgb(255,0,0), bg rgb(0,0,255)", .[.BOLD, .UNDERLINE, .ITALIC], .{ 255, 0, 0 }, .{ 0, 0, 255 }));
print("\n\n");
log("--- paint_raw_temp() -----------------------------");
log(paint_raw("1;3;32;45", "14 tbd tbd tbd"));
log("--- paint_raw() -----------------------------");
log(paint_raw("1;3;32;45", "14 tbd tbd tbd", no_termination = false));
print("\n\n");
}