aboutsummaryrefslogtreecommitdiff
path: root/src/colors.c
diff options
context:
space:
mode:
authordavid <[email protected]>2026-10-13 07:43:48 -0300
committerdavid <[email protected]>2026-10-13 07:43:48 -0300
commitf0f954d463f7ab0e0ce792527e6567611ed67bf9 (patch)
tree862c75c87380f67c11c865797faa9cc0c25a5970 /src/colors.c
parent3b16b2141ef5ce085ce50b737bc53ee3f7e556f3 (diff)
downloadmikusays-c-f0f954d463f7ab0e0ce792527e6567611ed67bf9.tar.gz
mikusays-c-f0f954d463f7ab0e0ce792527e6567611ed67bf9.zip
50% colors i'll do rainbow and whatever else i'm missing from the rust version later too lseepy rn
Diffstat (limited to 'src/colors.c')
-rw-r--r--src/colors.c565
1 files changed, 549 insertions, 16 deletions
diff --git a/src/colors.c b/src/colors.c
index 8d0a955..0978c60 100644
--- a/src/colors.c
+++ b/src/colors.c
@@ -1,19 +1,76 @@
#include "../defs/all.h"
#include <stdlib.h>
#include <string.h>
-#include <strings.h>
+#include <ctype.h>
-void colorconfig_init(colorconfig config){
- config.saturation = 100;
- config.brightness = 50;
+#ifdef _WIN32
+ #include <windows.h>
+#endif
+
+//try to enable ansi (virtual terminal processing) on windows consoles.
+//returns true if enabeling succeded or isn't necesary, false otherwise.
+bool enable_windows_ansi(void){
+#ifdef _WIN32
+ #ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
+ #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
+ #endif
+ HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
+ DWORD mode = 0;
+
+ if(out == INVALID_HANDLE_VALUE)
+ return false;
+ if(GetConsoleMode(out, &mode) == 0)
+ return false;
+ if(SetConsoleMode(out, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
+ return false;
+#endif
+ return true;
+}
+
+#ifdef _WIN32
+//returns true if str ends with suffix, ignorning case
+static bool ends_with_ignore_case(const char *str, const char *suffix){
+ size_t str_len = strlen(str);
+ size_t suffix_len = strlen(suffix);
+ size_t i;
+
+ if(suffix_len > str_len)
+ return false;
+ str += str_len - suffix_len;
+ for(i = 0; i < suffix_len; i++){
+ if(tolower((unsigned char)str[i]) != tolower((unsigned char)suffix[i]))
+ return false;
+ }
+ return true;
+}
+
+//detect the parent shell name on windows, using enviroment hints.
+//returns null if nothing matched
+static const char *detect_windows_shell(void){
+ const char *comspec;
+
+ //heuristic checks using common env vars
+ if(getenv("PSModulePath"))
+ return "powershell";
+ if(getenv("PWshPROFILE") || getenv("PWSH_VERSION"))
+ return "pwsh";
+ comspec = getenv("COMSPEC");
+ if(comspec && ends_with_ignore_case(comspec, "cmd.exe"))
+ return "cmd.exe";
+ if(getenv("WT_SESSION"))
+ return "windows_terminal";
+ return NULL;
}
+#endif
enum color_mode detect_color_mode(void){
const char *colorterm, *term;
+ enum color_mode mode = MODE_ANSI16;
if(getenv("NO_COLOR"))
return MODE_NONE;
+ //early checks
colorterm = getenv("COLORTERM");
if(colorterm && (strstr(colorterm, "truecolor") || strstr(colorterm, "24bit")))
return MODE_TRUECOLOR;
@@ -21,23 +78,499 @@ enum color_mode detect_color_mode(void){
term = getenv("TERM");
if(term && strstr(term, "256color"))
return MODE_ANSI256;
+ if(term && strstr(term, "color"))
+ return MODE_ANSI16;
- return MODE_ANSI16;
-}
+ //default fallback is mode_ansi16 (set above)
#ifdef _WIN32
-//i'll do this later
+ //if running on windows, try to enable ansi (virtual terminal processing)
+ if(enable_windows_ansi()){
+ //re-check for a better mode
+ if(colorterm && (strstr(colorterm, "truecolor") || strstr(colorterm, "24bit")))
+ mode = MODE_TRUECOLOR;
+ if(term && strstr(term, "256color"))
+ mode = MODE_ANSI256;
+ }else{
+ const char *shell = detect_windows_shell();
+ if(shell){
+ if(strstr(shell, "cmd.exe")){
+ //cmd historically doesn't support ansi; use 16 colors fallback
+ mode = MODE_ANSI16;
+ }else if(strstr(shell, "powershell") || strstr(shell, "pwsh")){
+ if(term && strstr(term, "256color"))
+ mode = MODE_ANSI256;
+ }else{
+ mode = MODE_ANSI16;
+ }
+ }
+ }
#endif
-char parse_namedcolors(rgbcolor name, char *outrgb){
- /*
- table = array of { name_string, rgb } for the 16 colors
- normalized = lowercase copy of name, with "_" removed // so that it handles bright_red and brightred
- for each entry in the table:
- if normalized equals entry.name -> out_rgb = entry.rgb, return success
- return fail
- */
+ return mode;
+}
+
+void colorconfig_init(colorconfig *config){
+ memset(config, 0, sizeof(*config));
+ config->saturation = 100;
+ config->brightness = 50;
+ config->mode = detect_color_mode();
+}
+
+unsigned char rgb_to_ansi16(rgbcolor rgb){
+ unsigned char r = rgb.red;
+ unsigned char g = rgb.green;
+ unsigned char b = rgb.blue;
+
+ unsigned char brightness = (unsigned char)((r * 299 + g * 587 + b * 114 + 500) / 1000); //avoid using math.h because i don't want to use it
+ int is_bright = brightness > 128;
+
+ //determine base color using simple thresholding on each channel, producing an index 0..7
+ int r_on = r > 128;
+ int g_on = g > 128;
+ int b_on = b > 128;
+ unsigned char base_idx = (unsigned char)r_on + (unsigned char)g_on * 2 + (unsigned char)b_on * 4; // 0..7
+
+ //sgr color codes: 30..37 for normal, 90..97 for bright
+ if(is_bright)
+ return (unsigned char)(90 + base_idx);
+ return (unsigned char)(30 + base_idx);
+}
+
+//round half away from zero for positive values only
+static int round_pos(float x){
+ return (int)(x + 0.5f);
+}
+
+unsigned char rgb_to_ansi256(rgbcolor rgb){
+ unsigned char r = rgb.red;
+ unsigned char g = rgb.green;
+ unsigned char b = rgb.blue;
+
+ if(r == g && g == b){
+ //calulate the grayscale value
+ unsigned char gray = (unsigned char)round_pos(r * 0.299f + g * 0.587f + b * 0.114f);
+ //ramp (232-255)
+ if(gray <= 8)
+ return 16;
+ if(gray >= 248)
+ return 231;
+ return (unsigned char)(round_pos(((float)gray - 8.0f) / 247.0f * 24.0f) + 232);
+ }
+
+ //color cube (16-231)
+ {
+ int ri = round_pos(r / 255.0f * 5.0f);
+ int gi = round_pos(g / 255.0f * 5.0f);
+ int bi = round_pos(b / 255.0f * 5.0f);
+
+ return (unsigned char)(16 + ri * 36 + gi * 6 + bi);
+ }
+}
+
+//convert ansi color code to rgb
+static rgbcolor ansi_to_rgb(unsigned char code){
+ //standard 16 colors
+ static const rgbcolor standard[16] = {
+ {0, 0, 0}, //black
+ {128, 0, 0}, //red
+ {0, 128, 0}, //green
+ {128, 128, 0}, //yellow
+ {0, 0, 128}, //blue
+ {128, 0, 128}, // magenta
+ {0, 128, 128}, //cyan
+ {192, 192, 192}, //white
+ {128, 128, 128}, //bright blakc whatever that is
+ {255, 0, 0}, //bright red
+ {0, 255, 0}, //brightgreen
+ {255, 255, 0},//bright yellow
+ {0, 0, 255}, //bright blue
+ {255, 0, 255}, //bright magenta
+ {0, 255, 255}, //bright cyan
+ {255, 255, 255}, //Brightwhite
+ };
+ rgbcolor out;
+
+ if(code < 16)
+ return standard[code];
+
+ //256-color mode
+ if(code <= 231){
+ //color cube
+ int cube = code - 16;
+ out.red = (unsigned char)(((cube / 36) % 6) * 51);
+ out.green = (unsigned char)(((cube / 6) % 6) * 51);
+ out.blue = (unsigned char)((cube % 6) * 51);
+ return out;
+ }
+
+ //grayscale ramp
+ out.red = out.green = out.blue = (unsigned char)((code - 232) * 10 + 8);
+ return out;
+}
+
+//helper for hsl_to_rgb
+static float hue_to_rgb(float p, float q, float t){
+ if(t < 0.0f)
+ t += 1.0f;
+ if(t > 1.0f)
+ t -= 1.0f;
+ if(t < 1.0f / 6.0f)
+ return p + (q - p) * 6.0f * t;
+ if(t < 1.0f / 2.0f)
+ return q;
+ if(t < 2.0f / 3.0f)
+ return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
+ return p;
+}
+
+//convert hsl to rgb
+//h: 0-360, s: 0-100, l: 0-100
+rgbcolor hsl_to_rgb(unsigned short h, unsigned char s, unsigned char l){
+ float hue = (float)h / 360.0f;
+ float sat = (float)s / 100.0f;
+ float lig = (float)l / 100.0f;
+ float r, g, b;
+ rgbcolor out;
+
+ if(sat == 0.0f){
+ r = g = b = lig;
+ }else{
+ float q = lig < 0.5f ? lig * (1.0f + sat) : lig + sat - lig * sat;
+ float p = 2.0f * lig - q;
+
+ r = hue_to_rgb(p, q, hue + 1.0f / 3.0f);
+ g = hue_to_rgb(p, q, hue);
+ b = hue_to_rgb(p, q, hue - 1.0f / 3.0f);
+ }
+
+ out.red = (unsigned char)round_pos(r * 255.0f);
+ out.green = (unsigned char)round_pos(g * 255.0f);
+ out.blue = (unsigned char)round_pos(b * 255.0f);
+ return out;
+}
+
+//convert color to ansi escape code for foreground
+//out gets an empty string for mode_none
+void rgb_to_fg_escape(rgbcolor rgb, color_mode mode, char *out, size_t size){
+ switch(mode){
+ case MODE_TRUECOLOR:
+ snprintf(out, size, "\x1b[38;2;%d;%d;%dm", rgb.red, rgb.green, rgb.blue);
+ break;
+ case MODE_ANSI256:
+ snprintf(out, size, "\x1b[38;5;%dm", rgb_to_ansi256(rgb));
+ break;
+ case MODE_ANSI16:
+ snprintf(out, size, "\x1b[%dm", rgb_to_ansi16(rgb));
+ break;
+ case MODE_NONE:
+ default:
+ if(size > 0)
+ out[0] = '\0';
+ break;
+ }
+}
+
+static float float_mod(float a, float b){
+ return a - b * (float)(long)(a / b);
+}
+
+static float float_abs(float x){
+ return x < 0.0f ? -x : x;
+}
+
+//converts rgb to hsv
+static void rgb_to_hsv(rgbcolor rgb, float *h_out, float *s_out, float *v_out){
+ float r = rgb.red / 255.0f;
+ float g = rgb.green / 255.0f;
+ float b = rgb.blue / 255.0f;
+ float max = r > g ? r : g;
+ float min = r < g ? r : g;
+ float delta, h;
+
+ if(b > max) max = b;
+ if(b < min) min = b;
+ delta = max - min;
+
+ if(delta == 0.0f)
+ h = 0.0f;
+ else if(max == r)
+ h = 60.0f * float_mod((g - b) / delta, 6.0f);
+ else if(max == g)
+ h = 60.0f * ((b - r) / delta + 2.0f);
+ else
+ h = 60.0f * ((r - g) / delta + 4.0f);
+
+ if(h < 0.0f)
+ h += 360.0f;
-
+ *h_out = h;
+ *s_out = max == 0.0f ? 0.0f : delta / max;
+ *v_out = max;
}
+//convert hsv to rgb
+static rgbcolor hsv_to_rgb(float h, float s, float v){
+ float c = v * s;
+ float x = c * (1.0f - float_abs(float_mod(h / 60.0f, 2.0f) - 1.0f));
+ float m = v - c;
+ float r, g, b;
+ rgbcolor out;
+
+ if(h >= 0.0f && h < 60.0f)
+ {r = c; g = x; b = 0.0f;}
+ else if(h >= 60.0f && h < 120.0f)
+ {r = x; g = c; b = 0.0f;}
+ else if(h >= 120.0f && h < 180.0f)
+ {r = 0.0f; g = c;b = x;}
+ else if(h >= 180.0f && h < 240.0f)
+ {r = 0.0f; g = x; b = c;}
+ else if(h >= 240.0f && h < 300.0f)
+ {r = x; g = 0.0f; b = c; }
+ else {r = c; g = 0.0f; b = x;}
+
+ out.red = (unsigned char)round_pos((r + m) * 255.0f);
+ out.green = (unsigned char)round_pos((g + m) * 255.0f);
+ out.blue = (unsigned char)round_pos((b + m) * 255.0f);
+ return out;
+}
+
+//linear interpolation between two colors in hsv space
+rgbcolor lerp_color_hsv(rgbcolor start, rgbcolor end, float t){
+ float h1, s1, v1, h2, s2, v2;
+ float h_diff, h, s, v;
+
+ rgb_to_hsv(start, &h1, &s1, &v1);
+ rgb_to_hsv(end, &h2, &s2, &v2);
+
+ //handle hue interpolation correctly (shortest path around the color wheel)
+ h_diff = float_mod(h2 - h1 + 180.0f, 360.0f) - 180.0f;
+ h = float_mod(h1 + h_diff * t + 360.0f, 360.0f);
+
+ s = s1 + (s2 - s1) * t;
+ v = v1 + (v2 - v1) * t;
+
+ return hsv_to_rgb(h, s, v);
+}
+
+//parsing helpers
+
+//returns the numeric value of a hex digit,, or -1
+static int hex_value(char c){
+ if(c >= '0' && c <= '9') return c - '0';
+ if(c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if(c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+}
+
+//skip spaces at both ends of [*start, *end)
+static void trim_range(const char **start, const char **end){
+ while(*start < *end && isspace((unsigned char)**start))
+ (*start)++;
+ while(*end > *start && isspace((unsigned char)(*end)[-1]))
+ (*end)--;
+}
+
+//parse [start, end) as a decimal number from 0 to max. 1 good 0 bad
+static int parse_uint(const char *start, const char *end, unsigned long max, unsigned long *out){
+ unsigned long value = 0;
+
+ if(start >= end)
+ return 0;
+ for(; start < end; start++){
+ if(!isdigit((unsigned char)*start))
+ return 0;
+ value = value * 10 + (unsigned long)(*start - '0');
+ if(value > max)
+ return 0;
+ }
+ *out = value;
+ return 1;
+}
+
+//returns 1 if str starts with prefix ignoring case
+static int starts_with_ignore_case(const char *str, const char *prefix){
+ for(; *prefix != '\0'; str++, prefix++){
+ if(*str == '\0' || tolower((unsigned char)*str) != tolower((unsigned char)*prefix))
+ return 0;
+ }
+ return 1;
+}
+
+static ColorParseError parse_hex_color(const char *s, rgbcolor *out){
+ const char *hex = s;
+ int value[6];
+ size_t length, i;
+
+ while(*hex == '#')
+ hex++;
+ length = strlen(hex);
+
+ if(length != 3 && length != 6)
+ return COLOR_ERR_HEX;
+ for(i = 0; i < length; i++){
+ value[i] = hex_value(hex[i]);
+ if(value[i] < 0)
+ return COLOR_ERR_HEX;
+ }
+
+ if(length == 3){
+ //#rgb format
+ out->red = (unsigned char)(value[0] * 17);
+ out->green = (unsigned char)(value[1] * 17);
+ out->blue = (unsigned char)(value[2] * 17);
+ }else{
+ //#rrggbb format
+ out->red = (unsigned char)(value[0] * 16 + value[1]);
+ out->green = (unsigned char)(value[2] * 16 + value[3]);
+ out->blue = (unsigned char)(value[4] * 16 + value[5]);
+ }
+ return COLOR_OK;
+}
+
+//splits [start, end) on exactly two commas into three trimmed ranges. 1 good 0 bad
+static int split_three(const char *start, const char *end, const char *range[3][2]){
+ const char *comma1 = NULL, *comma2 = NULL, *p;
+ int commas = 0;
+
+ for(p = start; p < end; p++){
+ if(*p != ',')
+ continue;
+ commas++;
+ if(commas == 1) comma1 = p;
+ else if(commas == 2) comma2 = p;
+ }
+ if(commas != 2)
+ return 0;
+
+ range[0][0] = start; range[0][1] = comma1;
+ range[1][0] = comma1 + 1; range[1][1] = comma2;
+ range[2][0] = comma2 + 1; range[2][1] = end;
+ for(commas = 0; commas < 3; commas++){
+ trim_range(&range[commas][0], &range[commas][1]);
+ }
+ return 1;
+}
+
+static ColorParseError parse_rgb_color(const char *s, rgbcolor *out){
+ const char *range[3][2];
+ unsigned long r, g, b;
+
+ if(!split_three(s, s + strlen(s), range))
+ return COLOR_ERR_RGB;
+ if(!parse_uint(range[0][0], range[0][1], 255, &r)|| !parse_uint(range[1][0], range[1][1], 255, &g)|| !parse_uint(range[2][0], range[2][1], 255, &b))
+ return COLOR_ERR_RGB;
+
+ out->red = (unsigned char)r;
+ out->green = (unsigned char)g;
+ out->blue = (unsigned char)b;
+ return COLOR_OK;
+}
+
+static ColorParseError parse_hsl_color(const char *s, rgbcolor *out){
+ const char *start = s;
+ const char *end = s + strlen(s);
+ const char *range[3][2];
+ unsigned long h, sat, l;
+
+ //strip the "hsl(" prefix and the ")" suffix
+ while(strncmp(start, "hsl(", 4) == 0)
+ start += 4;
+ while(end > start && end[-1] == ')')
+ end--;
+
+ if(!split_three(start, end, range))
+ return COLOR_ERR_HSL;
+
+ //hue can have a trailing unit like "180deg", so drop trailing non-digits
+ while(range[0][1] > range[0][0] && !isdigit((unsigned char)range[0][1][-1]))
+ range[0][1]--;
+ //saturation and lightness can have trailing '%'
+ while(range[1][1] > range[1][0] && range[1][1][-1] == '%')
+ range[1][1]--;
+ while(range[2][1] > range[2][0] && range[2][1][-1] == '%')
+ range[2][1]--;
+
+ if(!parse_uint(range[0][0], range[0][1], 65535, &h)
+ || !parse_uint(range[1][0], range[1][1], 255, &sat)
+ || !parse_uint(range[2][0], range[2][1], 255, &l))
+ return COLOR_ERR_HSL;
+
+ if(h > 360 || sat > 100 || l > 100)
+ return COLOR_ERR_HSL;
+
+ *out = hsl_to_rgb((unsigned short)h, (unsigned char)sat, (unsigned char)l);
+ return COLOR_OK;
+}
+
+static int parse_named_color(const char *s, rgbcolor *out){
+ static const struct { const char *name; rgbcolor rgb; } table[] = {
+ {"black", {0,0,0}}, {"red", {197,15,31}}, {"green", {19,161,14}},
+ {"yellow", {193,156,0}}, {"blue", {0,55,218}}, {"magenta", {136,23,152}},
+ {"cyan", {58,150,221}}, {"white", {204,204,204}},
+ {"brightblack", {118,118,118}}, {"brightred", {231,72,86}},
+ {"brightgreen", {22,198,12}}, {"brightyellow", {249,241,165}},
+ {"brightblue", {59,120,255}}, {"brightmagenta", {180,0,158}},
+ {"brightcyan", {97,214,214}}, {"brightwhite", {242,242,242}},
+ };
+ char norm[32];
+ size_t n = 0, i;
+
+ for(; *s && n < sizeof(norm) - 1; s++) //lowercase, drop '_'
+ if(*s != '_') norm[n++] = (char)tolower((unsigned char)*s);
+ norm[n] = '\0';
+ for(i = 0; i < sizeof(table) / sizeof(table[0]); i++){
+ if(strcmp(norm, table[i].name) == 0){
+ *out = table[i].rgb;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+//parse any supported color format into rgb:
+//#rrggbb, #rgb, hsl(h,s%,l%), r,g,b or an ansi code 0-255
+ColorParseError parse_color(const char *s, rgbcolor *out){
+ if(parse_named_color(s, out)) return COLOR_OK;
+ size_t length;
+ unsigned long code;
+
+ //try hex color (#rrggbb or #rgb)
+ if(s[0] == '#')
+ return parse_hex_color(s, out);
+
+ //try hsl (hsl(180,100%,50%))
+ length = strlen(s);
+ if(starts_with_ignore_case(s, "hsl(") && length > 0 && s[length - 1] == ')')
+ return parse_hsl_color(s, out);
+
+ //try rgb tuple (255,0,128)
+ if(strchr(s, ','))
+ return parse_rgb_color(s, out);
+
+ //try ansi code (number between 0-255)
+ if(parse_uint(s, s + length, 255, &code)){
+ *out = ansi_to_rgb((unsigned char)code);
+ return COLOR_OK;
+ }
+
+ return COLOR_ERR_FORMAT;
+}
+
+const char *color_parse_error_message(ColorParseError error){
+ switch(error){
+ case COLOR_ERR_FORMAT:
+ return "Invaliid color format";
+ case COLOR_ERR_HEX:
+ return "Invaliid hex color code";
+ case COLOR_ERR_RGB:
+ return "Invaliid RGB format. Expected format: R,G,B";
+ case COLOR_ERR_HSL:
+ return "Invaliid HSL format. Expected format: hsl(H,S%,L%)";
+ case COLOR_ERR_UNKNOWN_NAME:
+ return "Unkown named color";
+ case COLOR_OK:
+ default:
+ return "";
+ }
+}