11#include <ascii-chat/options/colorscheme.h>
12#include <ascii-chat/common.h>
13#include <ascii-chat/platform/terminal.h>
14#include <ascii-chat/video/ansi_fast.h>
15#include <ascii-chat/platform/stat.h>
16#include <ascii-chat/platform/util.h>
17#include <ascii-chat-deps/tomlc17/src/tomlc17.h>
27static color_scheme_t g_active_scheme = {0};
28static bool g_colorscheme_initialized =
false;
34#if defined(_WIN32) || defined(__EMSCRIPTEN__)
49static const color_scheme_t PASTEL_SCHEME = {.name =
"pastel",
50 .description =
"Soft pastel colors (ascii-chat default)",
62 .has_light_variant =
true,
80static const color_scheme_t NORD_SCHEME = {.name =
"nord",
81 .description =
"Arctic, muted Nord theme colors",
93 .has_light_variant =
true,
111static const color_scheme_t SOLARIZED_SCHEME = {.name =
"solarized",
112 .description =
"Solarized theme - precision colors",
124 .has_light_variant =
true,
142static const color_scheme_t DRACULA_SCHEME = {.name =
"dracula",
143 .description =
"Dracula dark theme - vampiric colors",
155 .has_light_variant =
false,
162static const color_scheme_t GRUVBOX_SCHEME = {.name =
"gruvbox",
163 .description =
"Gruvbox theme - retro warm colors",
175 .has_light_variant =
true,
193static const color_scheme_t MONOKAI_SCHEME = {.name =
"monokai",
194 .description =
"Monokai theme - vibrant coding colors",
206 .has_light_variant =
false,
213static const color_scheme_t BASE16_DEFAULT_SCHEME = {.name =
"base16-default",
214 .description =
"Base16 Default theme (Chris Kempson)",
226 .has_light_variant =
true,
243static const color_scheme_t *BUILTIN_SCHEMES[] = {&PASTEL_SCHEME, &NORD_SCHEME, &SOLARIZED_SCHEME,
244 &DRACULA_SCHEME, &GRUVBOX_SCHEME, &MONOKAI_SCHEME,
245 &BASE16_DEFAULT_SCHEME};
247#define NUM_BUILTIN_SCHEMES (sizeof(BUILTIN_SCHEMES) / sizeof(BUILTIN_SCHEMES[0]))
256static const color_scheme_t *find_builtin_scheme(
const char *name) {
261 if (strcmp(name,
"default") == 0) {
266 if (strcmp(BUILTIN_SCHEMES[i]->name, name) == 0) {
267 return BUILTIN_SCHEMES[i];
279 if (g_colorscheme_initialized) {
288#if defined(_WIN32) || defined(__EMSCRIPTEN__)
289 static bool mutex_initialized =
false;
290 if (!mutex_initialized) {
295 mutex_initialized =
true;
302 const color_scheme_t *pastel = find_builtin_scheme(
"pastel");
304 return SET_ERRNO(ERROR_CONFIG,
"Failed to load default pastel scheme");
307 memcpy(&g_active_scheme, pastel,
sizeof(color_scheme_t));
308 g_colorscheme_initialized =
true;
319 for (
int i = 0; i < 8; i++) {
320 char *str_16 = (
char *)compiled->codes_16[i];
321 char *str_256 = (
char *)compiled->codes_256[i];
322 char *str_truecolor = (
char *)compiled->codes_truecolor[i];
324 if (str_16 != NULL) {
327 if (str_256 != NULL) {
330 if (str_truecolor != NULL) {
331 SAFE_FREE(str_truecolor);
334 memset(compiled, 0,
sizeof(compiled_color_scheme_t));
338 if (!g_colorscheme_initialized) {
343 memset(&g_active_scheme, 0,
sizeof(color_scheme_t));
344 g_colorscheme_initialized =
false;
351#if defined(_WIN32) || defined(__EMSCRIPTEN__)
361 if (!g_colorscheme_initialized) {
366 return &g_active_scheme;
371 return SET_ERRNO(ERROR_INVALID_PARAM,
"Scheme name is NULL");
375 if (!g_colorscheme_initialized) {
379 color_scheme_t scheme = {0};
380 asciichat_error_t result = ASCIICHAT_OK;
383 const color_scheme_t *builtin = find_builtin_scheme(name);
385 memcpy(&scheme, builtin,
sizeof(color_scheme_t));
386 }
else if (strchr(name,
'/') || strchr(name,
'.')) {
389 if (result != ASCIICHAT_OK) {
393 return SET_ERRNO(ERROR_CONFIG,
"Unknown color scheme: %s (not a built-in scheme or valid file path)", name);
397 memcpy(&g_active_scheme, &scheme,
sizeof(color_scheme_t));
400 log_debug(
"Switched to color scheme: %s", name);
405 if (!name || !scheme) {
406 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL name or scheme pointer");
409 const color_scheme_t *builtin = find_builtin_scheme(name);
411 return SET_ERRNO(ERROR_CONFIG,
"Unknown built-in color scheme: %s", name);
414 memcpy(scheme, builtin,
sizeof(color_scheme_t));
419 if (!path || !scheme) {
420 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL path or scheme pointer");
425 if (stat(path, &sb) != 0 || !S_ISREG(sb.st_mode)) {
426 return SET_ERRNO(ERROR_FILE_NOT_FOUND,
"Color scheme file not found or not readable: %s", path);
430 toml_result_t result = toml_parse_file_ex(path);
433 return SET_ERRNO(ERROR_CONFIG,
"Failed to parse color scheme file '%s': %s", path, result.errmsg);
437 memset(scheme, 0,
sizeof(color_scheme_t));
440 toml_datum_t scheme_section = toml_get(result.toptab,
"scheme");
441 if (scheme_section.type == TOML_TABLE) {
443 toml_datum_t name_datum = toml_get(scheme_section,
"name");
444 if (name_datum.type == TOML_STRING) {
445 SAFE_STRNCPY(scheme->name, name_datum.u.s,
sizeof(scheme->name));
449 toml_datum_t desc_datum = toml_get(scheme_section,
"description");
450 if (desc_datum.type == TOML_STRING) {
451 SAFE_STRNCPY(scheme->description, desc_datum.u.s,
sizeof(scheme->description));
456 toml_datum_t colors_section = toml_get(result.toptab,
"colors");
457 if (colors_section.type == TOML_TABLE) {
459 toml_datum_t dark_section = toml_get(colors_section,
"dark");
460 if (dark_section.type == TOML_TABLE) {
461 const char *color_names[] = {
"dev",
"debug",
"warn",
"info",
"error",
"fatal",
"grey",
"reset"};
462 for (
int i = 0; i < 8; i++) {
463 toml_datum_t color_value = toml_get(dark_section, color_names[i]);
464 if (color_value.type == TOML_STRING) {
465 parse_hex_color(color_value.u.s, &scheme->log_colors_dark[i].r, &scheme->log_colors_dark[i].g,
466 &scheme->log_colors_dark[i].b);
472 toml_datum_t light_section = toml_get(colors_section,
"light");
473 if (light_section.type == TOML_TABLE) {
474 scheme->has_light_variant =
true;
475 const char *color_names[] = {
"dev",
"debug",
"warn",
"info",
"error",
"fatal",
"grey",
"reset"};
476 for (
int i = 0; i < 8; i++) {
477 toml_datum_t color_value = toml_get(light_section, color_names[i]);
478 if (color_value.type == TOML_STRING) {
479 parse_hex_color(color_value.u.s, &scheme->log_colors_light[i].r, &scheme->log_colors_light[i].g,
480 &scheme->log_colors_light[i].b);
486 scheme->is_builtin =
false;
487 SAFE_STRNCPY(scheme->source_file, path,
sizeof(scheme->source_file));
497asciichat_error_t
parse_hex_color(
const char *hex, uint8_t *r, uint8_t *g, uint8_t *b) {
498 if (!hex || !r || !g || !b) {
499 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter");
508 if (strlen(hex) != 6) {
509 return SET_ERRNO(ERROR_CONFIG,
"Invalid hex color (must be #RRGGBB): %s", hex);
513 unsigned int rgb = 0;
514 if (SAFE_SSCANF(hex,
"%6x", &rgb) != 1) {
515 return SET_ERRNO(ERROR_CONFIG,
"Invalid hex color format: %s", hex);
518 *r = (rgb >> 16) & 0xFF;
519 *g = (rgb >> 8) & 0xFF;
530 if (!buf || size < 20)
540 terminal_background_t background, compiled_color_scheme_t *compiled) {
541 if (!scheme || !compiled) {
542 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL scheme or compiled pointer");
551 if (compiled->codes_16[0] != NULL) {
557 const rgb_pixel_t *colors = (background == TERM_BACKGROUND_LIGHT && scheme->has_light_variant)
558 ? scheme->log_colors_light
559 : scheme->log_colors_dark;
564 for (
int i = 0; i < 8; i++) {
567 SAFE_STRNCPY(temp_buf,
"\x1b[0m",
sizeof(temp_buf));
569 uint8_t color_idx =
rgb_to_16color(colors[i].r, colors[i].g, colors[i].b);
572 safe_snprintf(temp_buf,
sizeof(temp_buf),
"\x1b[%dm", 30 + color_idx);
574 safe_snprintf(temp_buf,
sizeof(temp_buf),
"\x1b[%dm", 90 + (color_idx - 8));
578 size_t len = strlen(temp_buf) + 1;
579 char *allocated = SAFE_MALLOC(len,
char *);
581 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate color code string");
583 memcpy(allocated, temp_buf, len);
584 compiled->codes_16[i] = allocated;
587 for (
int i = 0; i < 8; i++) {
589 SAFE_STRNCPY(temp_buf,
"\x1b[0m",
sizeof(temp_buf));
591 uint8_t color_idx =
rgb_to_256color(colors[i].r, colors[i].g, colors[i].b);
592 safe_snprintf(temp_buf,
sizeof(temp_buf),
"\x1b[38;5;%dm", color_idx);
595 size_t len = strlen(temp_buf) + 1;
596 char *allocated = SAFE_MALLOC(len,
char *);
598 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate color code string");
600 memcpy(allocated, temp_buf, len);
601 compiled->codes_256[i] = allocated;
605 for (
int i = 0; i < 8; i++) {
607 SAFE_STRNCPY(temp_buf,
"\x1b[0m",
sizeof(temp_buf));
612 size_t len = strlen(temp_buf) + 1;
613 char *allocated = SAFE_MALLOC(len,
char *);
615 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate color code string");
617 memcpy(allocated, temp_buf, len);
618 compiled->codes_truecolor[i] = allocated;
630 return SET_ERRNO(ERROR_INVALID_PARAM,
"Scheme name is NULL");
633 color_scheme_t scheme = {0};
635 if (result != ASCIICHAT_OK) {
640 char toml_content[8192] = {0};
644 offset +=
safe_snprintf(toml_content + offset,
sizeof(toml_content) - offset,
647 "description = \"%s\"\n\n",
648 scheme.name, scheme.description);
651 const char *color_names[] = {
"dev",
"debug",
"warn",
"info",
"error",
"fatal",
"grey",
"reset"};
652 offset +=
safe_snprintf(toml_content + offset,
sizeof(toml_content) - offset,
"[colors.dark]\n");
654 for (
int i = 0; i < 8; i++) {
656 safe_snprintf(toml_content + offset,
sizeof(toml_content) - offset,
"%s = \"#%02X%02X%02X\"\n", color_names[i],
657 scheme.log_colors_dark[i].r, scheme.log_colors_dark[i].g, scheme.log_colors_dark[i].b);
661 if (scheme.has_light_variant) {
662 offset +=
safe_snprintf(toml_content + offset,
sizeof(toml_content) - offset,
"\n[colors.light]\n");
663 for (
int i = 0; i < 8; i++) {
664 offset +=
safe_snprintf(toml_content + offset,
sizeof(toml_content) - offset,
"%s = \"#%02X%02X%02X\"\n",
665 color_names[i], scheme.log_colors_light[i].r, scheme.log_colors_light[i].g,
666 scheme.log_colors_light[i].b);
674 return SET_ERRNO_SYS(ERROR_FILE_OPERATION,
"Cannot open %s for writing",
file_path);
676 if (fputs(toml_content, fp) < 0) {
678 return SET_ERRNO_SYS(ERROR_FILE_OPERATION,
"Failed to write to %s",
file_path);
683 if (fputs(toml_content, stdout) < 0) {
684 return SET_ERRNO_SYS(ERROR_FILE_OPERATION,
"Failed to write to stdout");
687 (void)fflush(stdout);
699 const char *term_bg = SAFE_GETENV(
"TERM_BACKGROUND");
702 return TERM_BACKGROUND_LIGHT;
705 return TERM_BACKGROUND_DARK;
713 return is_dark ? TERM_BACKGROUND_DARK : TERM_BACKGROUND_LIGHT;
729static const char *find_cli_color_scheme(
int argc,
const char *
const argv[]) {
730 for (
int i = 1; i < argc - 1; i++) {
731 if (strcmp(argv[i],
"--color-scheme") == 0) {
753static asciichat_error_t load_config_color_scheme(color_scheme_t *scheme) {
755 return SET_ERRNO(ERROR_INVALID_PARAM,
"scheme pointer is NULL");
759 config_file_list_t config_files = {0};
762 if (search_result != ASCIICHAT_OK) {
764 return ERROR_NOT_FOUND;
768 asciichat_error_t load_result = ERROR_NOT_FOUND;
769 if (config_files.count > 0) {
796 if (result != ASCIICHAT_OK) {
802 color_scheme_t config_scheme = {0};
803 asciichat_error_t config_result = load_config_color_scheme(&config_scheme);
804 if (config_result == ASCIICHAT_OK) {
810 const char *cli_scheme = find_cli_color_scheme(argc, argv);
813 if (cli_result != ASCIICHAT_OK) {
uint8_t rgb_to_16color(uint8_t r, uint8_t g, uint8_t b)
uint8_t rgb_to_256color(uint8_t r, uint8_t g, uint8_t b)
asciichat_error_t options_colorscheme_init_early(int argc, const char *const argv[])
Initialize color scheme early (before logging)
asciichat_error_t colorscheme_export_scheme(const char *scheme_name, const char *file_path)
asciichat_error_t colorscheme_init(void)
asciichat_error_t parse_hex_color(const char *hex, uint8_t *r, uint8_t *g, uint8_t *b)
asciichat_error_t colorscheme_load_builtin(const char *name, color_scheme_t *scheme)
const color_scheme_t * colorscheme_get_active_scheme(void)
terminal_background_t detect_terminal_background(void)
asciichat_error_t colorscheme_load_from_file(const char *path, color_scheme_t *scheme)
void colorscheme_destroy(void)
asciichat_error_t colorscheme_set_active_scheme(const char *name)
asciichat_error_t colorscheme_compile_scheme(const color_scheme_t *scheme, terminal_color_mode_t mode, terminal_background_t background, compiled_color_scheme_t *compiled)
void rgb_to_truecolor_ansi(uint8_t r, uint8_t g, uint8_t b, char *buf, size_t size)
void colorscheme_cleanup_compiled(compiled_color_scheme_t *compiled)
#define NUM_BUILTIN_SCHEMES
mutex_t g_colorscheme_mutex
char file_path[PLATFORM_MAX_PATH_LENGTH]
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
int mutex_init(mutex_t *mutex)
int mutex_destroy(mutex_t *mutex)
FILE * platform_fopen(const char *filename, const char *mode)
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
void config_file_list_destroy(config_file_list_t *list)