#include "../defs/all.h" #include #include #include #ifdef _WIN32 #include #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; term = getenv("TERM"); if(term && strstr(term, "256color")) return MODE_ANSI256; if(term && strstr(term, "color")) return MODE_ANSI16; //default fallback is mode_ansi16 (set above) #ifdef _WIN32 //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 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 ""; } }