diff options
| author | david <[email protected]> | 2026-10-13 07:43:48 -0300 |
|---|---|---|
| committer | david <[email protected]> | 2026-10-13 07:43:48 -0300 |
| commit | f0f954d463f7ab0e0ce792527e6567611ed67bf9 (patch) | |
| tree | 862c75c87380f67c11c865797faa9cc0c25a5970 /src/colors.c | |
| parent | 3b16b2141ef5ce085ce50b737bc53ee3f7e556f3 (diff) | |
| download | mikusays-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.c | 565 |
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 ""; + } +} |
