Author:ptrace
Comitter:ptrace
Date:2025-11-08 22:21:09 UTC
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..94e910b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
# Jai
.build
bin
*.codex
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..103d568
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..04fe40c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
# Terminal Colors for Jai
This is a small declarative library which allows you to create colorful terminal messages.
## Usage
Put the directory `termcolors` in your projects `modules` directory, or link it while building.
### Example
```plain
#import "termcolors";
main :: () {
context.allocator = temp;
font style background color
v v
log(paint("FooBar", .BOLD, .BLACK, .WHITE));
^ ^
text foreground color
background color
v
log(paint_ex("FooBar", .[.BOLD, .UNDERLINE, .ITALIC], 84, 124));
^ ^ ^
text multiple font styles foreground 256bit color
}
```
### More Information
There are several APIs and overloads, which are ranging from static/more declarative, to
flexible/fully customizable.
Please consult `termcolors/lib.jai` for a more detailed documentation.
You can also view `test/test.jai` for more examples. If you want to run those examples,
execute them with `jai build.jai - run silent`.
## Credits
- https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
diff --git a/build.jai b/build.jai
new file mode 100644
index 0000000..2749bbb
--- /dev/null
+++ b/build.jai
@@ -0,0 +1,115 @@
#import "Basic";
#import "Compiler";
#import "File";
#import "Autorun";
build :: () {
args := get_build_options().compile_time_command_line;
// args
args_help := array_find(args, "help");
args_compiler_silent := array_find(args, "silent");
args_program_run := array_find(args, "run");
args_build_release := array_find(args, "release");
// -----------------------------------------
w := compiler_create_workspace();
if !w {
log("Workspace creation failed.");
return;
}
set_build_options_dc(.{ do_output = false });
if args_help {
args_help_print();
return;
}
print("The workspace w is %\n", w);
make_directory_if_it_does_not_exist("bin");
target_options := get_build_options(w);
target_options.output_executable_name = "program";
target_options.output_path = "bin";
import_path: [..] string;
array_add(*import_path, ..target_options.import_path);
array_add(*import_path, ".");
target_options.import_path = import_path;
if args_compiler_silent target_options.text_output_flags = 0;
if args_build_release {
build_release(w, *target_options);
} else {
build_debug(w, *target_options);
}
compiler_begin_intercept(w);
add_build_file(tprint("%test/test.jai", #filepath), w);
jinit_compiler_response := jinit_compiler_intercept();
compiler_end_intercept(w);
if !jinit_compiler_response {
log("Compiler response failed.");
return;
}
if args_program_run run_build_result(w);
}
// adapted from: https://github.com/Ivo-Balbaert/The_Way_to_Jai/blob/main/book/30B_Manipulating_the_build_process.md
build_debug :: (w: Workspace, target_options: *Build_Options) {
log("Choosing debug options...");
target_options.backend =.X64;
set_optimization(target_options, Optimization_Type.DEBUG, true);
set_build_options(target_options.*, w);
}
build_release :: (w: Workspace, target_options: *Build_Options) {
log("Choosing release options...");
target_options.backend = .LLVM;
set_optimization(target_options, Optimization_Type.VERY_OPTIMIZED);
set_build_options(target_options.*, w);
}
args_help_print :: () {
help_message := #string _END_
Usage: jai build.jai - [ARGS]
Options:
help Prints this help menu
silent Disables compiler/linker statistics
run Runs your program afterwards
release Builds with release options. If omitted, it builds a debug build.
_END_;
log(help_message);
}
jinit_compiler_intercept :: () -> success: bool {
while true {
message := compiler_wait_for_message();
if !message break;
if message.kind == {
case .COMPLETE;
message_complete := cast(*Message_Complete) message;
return message_complete.error_code == 0;
}
}
return false;
}
main :: () {}
#run build();
diff --git a/docs/termcodes.md b/docs/termcodes.md
new file mode 100644
index 0000000..ebb1d3f
--- /dev/null
+++ b/docs/termcodes.md
@@ -0,0 +1,47 @@
# Terminal Codes
Credits: https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
Code Effect Note
0 Reset / Normal all attributes off
1 Bold or increased intensity
2 Faint (decreased intensity) Not widely supported.
3 Italic Not widely supported. Sometimes treated as inverse.
4 Underline
5 Slow Blink less than 150 per minute
6 Rapid Blink MS-DOS ANSI.SYS; 150+ per minute; not widely supported
7 [[reverse video]] swap foreground and background colors
8 Conceal Not widely supported.
9 Crossed-out Characters legible, but marked for deletion. Not widely supported.
10 Primary(default) font
11–19 Alternate font Select alternate font n-10
20 Fraktur hardly ever supported
21 Bold off or Double Underline Bold off not widely supported; double underline hardly ever supported.
22 Normal color or intensity Neither bold nor faint
23 Not italic, not Fraktur
24 Underline off Not singly or doubly underlined
25 Blink off
27 Inverse off
28 Reveal conceal off
29 Not crossed out
30–37 Set foreground color See color table below
38 Set foreground color Next arguments are 5;<n> or 2;<r>;<g>;<b>, see below
39 Default foreground color implementation defined (according to standard)
40–47 Set background color See color table below
48 Set background color Next arguments are 5;<n> or 2;<r>;<g>;<b>, see below
49 Default background color implementation defined (according to standard)
51 Framed
52 Encircled
53 Overlined
54 Not framed or encircled
55 Not overlined
60 ideogram underline hardly ever supported
61 ideogram double underline hardly ever supported
62 ideogram overline hardly ever supported
63 ideogram double overline hardly ever supported
64 ideogram stress marking hardly ever supported
65 ideogram attributes off reset the effects of all of 60-64
90–97 Set bright foreground color aixterm (not in standard)
100–107 Set bright background color aixterm (not in standard)
diff --git a/termcolors/lib.jai b/termcolors/lib.jai
new file mode 100644
index 0000000..92a61c4
--- /dev/null
+++ b/termcolors/lib.jai
@@ -0,0 +1,498 @@
/*
* 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: 1.0.0
-------------------
--- [ paint() ] ---
-------------------
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 multiple text decorations
log(paint("Foo Bar", .[.BOLD, .UNDERLINE, .ITALIC], .BLACK, .WHITE));
----------------------
--- [ 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"`.
--------------------------------------
--- [ 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";
paint :: (
str: string,
style: Text_Style = .NONE,
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 = .NONE,
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 = .NONE,
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
);
}
paint_ex_custom :: (
str: string,
style: Text_Style = .NONE,
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 = .NONE,
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);
}
Term_Rgb :: struct {
r, g, b: u8 = 255, 255, 255;
}
Text_Style :: enum #specified {
NONE :: -1;
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;<Color_Table> OR 2;<r>;<g>;<b>
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;<Color_Table> OR 2;<r>;<g>;<b>
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;
}
// 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;";
#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);
}
__paint__to_term_code_args :: () -> string #expand {
s := join(.. `buf, ";",, temp);
s = trim_right(s, ";",, temp);
return paint_raw(s, `str);
}
__paint__buffer_add_term_codes :: (color_type: string, term_color_code: int) #expand {
`buf[`count] = tprint("%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
) -> string
{
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 {
return paint_raw("", str);
}
return __paint__to_term_code_args();
}
__paint :: (
str: string,
style: []Text_Style,
fg: $A,
bg: $B,
fg_code: string,
bg_code: string
) -> string
{
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 {
return paint_raw("", str);
}
return __paint__to_term_code_args();
}
__paint_rgb :: (
str: string,
style: []Text_Style,
fg_rgb: Term_Rgb,
bg_rgb: Term_Rgb
) -> string
{
buf: [3]string;
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);
s := join(.. buf, ";",, temp);
s = trim_right(s, ";",, temp);
return paint_raw(s, str);
}
diff --git a/termcolors/module.jai b/termcolors/module.jai
new file mode 100644
index 0000000..dce250c
--- /dev/null
+++ b/termcolors/module.jai
@@ -0,0 +1,5 @@
/*
License and docs are in `lib.jai`
*/
#load "lib.jai";
diff --git a/test/test.jai b/test/test.jai
new file mode 100644
index 0000000..10f36af
--- /dev/null
+++ b/test/test.jai
@@ -0,0 +1,65 @@
#import "Basic"()(
MEMORY_DEBUGGER = MEMORY_DEBUGGER
);
#import "termcolors";
MEMORY_DEBUGGER :: true;
main :: () {
#if MEMORY_DEBUGGER defer report_memory_leaks();
context.allocator = temp;
log("--- paint() -----------------------------");
log(paint("1 Plain"));
print("\n");
log(paint("2 Bold, fg Black", .BOLD, .BLACK));
print("\n");
log(paint("3 Bold, fg Black, bg White", .BOLD, .BLACK, .WHITE));
print("\n");
log(paint("4 Bold, Underline, Italic, fg Black, bg White", .[.BOLD, .UNDERLINE, .ITALIC], .BLACK, .WHITE));
print("\n\n");
log("--- paint_ex() -----------------------------");
log(paint_ex("5 Underline, fg GreenDark, bg OrangeLight", .UNDERLINE, .GREEN_DARK, .ORANGE_LIGHT));
print("\n");
log(paint_ex("6 Underline, fg GreenDark", .UNDERLINE, .GREEN_DARK));
print("\n");
log(paint_ex("7 Bold, Underline, Italic, fg GreenDark, bg OrangeLight", .[.BOLD, .UNDERLINE, .ITALIC], .GREEN_DARK, .ORANGE_LIGHT));
print("\n");
log(paint_ex("8 Underline, fg 84, bg 124", .UNDERLINE, 84, 124));
print("\n");
log(paint_ex("9 Bold, Underline, Italic, 84, 124", .[.BOLD, .UNDERLINE, .ITALIC], 84, 124));
print("\n\n");
log("--- paint_ex_custom() -----------------------------");
My_Colors :: enum #specified {
NONE :: -1;
COL1 :: 95;
COL2 :: 201;
}
log(paint_ex_custom("10 Underline, fg Custom, bg Custom", .UNDERLINE, My_Colors.COL1, My_Colors.COL2));
print("\n\n");
log("--- paint_rgb() -----------------------------");
log(paint_rgb("11 Underline, fg rgb(255,0,0), bg rgb(0,0,255)", .UNDERLINE, .{ 255, 0, 0 }, .{ 0, 0, 255 }));
print("\n");
log(paint_rgb("12 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", "13 tbd tbd tbd"));
print("\n\n");
}