ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
colorscheme.c
Go to the documentation of this file.
1
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>
18#include <string.h>
19#include <stdlib.h>
20#include <stdio.h>
21#include <limits.h>
22
23/* ============================================================================
24 * Global State
25 * ============================================================================ */
26
27static color_scheme_t g_active_scheme = {0};
28static bool g_colorscheme_initialized = false;
29
30/* Mutex for color scheme compilation - used by both colorscheme.c and logging.c */
31/* Must be statically initialized with PTHREAD_MUTEX_INITIALIZER to avoid deadlock
32 * EXCEPTION: Emscripten/WASM with pthreads doesn't support static mutex initialization properly,
33 * so we initialize it explicitly in colorscheme_init() like Windows. */
34#if defined(_WIN32) || defined(__EMSCRIPTEN__)
35mutex_t g_colorscheme_mutex = {
36 0}; /* Windows CRITICAL_SECTION or Emscripten pthread_mutex_t - initialized in colorscheme_init() */
37#else
38mutex_t g_colorscheme_mutex = PTHREAD_MUTEX_INITIALIZER; /* POSIX pthread_mutex_t */
39#endif
40
41/* ============================================================================
42 * Built-In Color Schemes
43 * ============================================================================ */
44
49static const color_scheme_t PASTEL_SCHEME = {.name = "pastel",
50 .description = "Soft pastel colors (ascii-chat default)",
51 .log_colors_dark =
52 {
53 {240, 150, 100}, /* DEV: Orange */
54 {101, 172, 225}, /* DEBUG: Cyan */
55 {144, 224, 112}, /* INFO: Green */
56 {240, 204, 145}, /* WARN: Yellow */
57 {232, 93, 111}, /* ERROR: Red */
58 {200, 160, 216}, /* FATAL: Magenta */
59 {128, 128, 128}, /* GREY */
60 {255, 255, 255} /* RESET */
61 },
62 .has_light_variant = true,
63 .log_colors_light =
64 {
65 {156, 97, 65}, /* DEV: Darker orange (matches dark) */
66 {65, 111, 146}, /* DEBUG: Darker cyan (matches dark) */
67 {93, 145, 72}, /* INFO: Darker green (matches dark) */
68 {156, 132, 94}, /* WARN: Darker yellow (matches dark) */
69 {150, 60, 72}, /* ERROR: Darker red (matches dark) */
70 {130, 104, 140}, /* FATAL: Darker magenta (matches dark) */
71 {80, 80, 80}, /* GREY: Darker gray */
72 {0, 0, 0} /* RESET */
73 },
74 .is_builtin = true};
75
80static const color_scheme_t NORD_SCHEME = {.name = "nord",
81 .description = "Arctic, muted Nord theme colors",
82 .log_colors_dark =
83 {
84 {191, 144, 97}, /* DEV: Nord orange */
85 {143, 188, 187}, /* DEBUG: Nord frost */
86 {163, 190, 140}, /* INFO: Nord green */
87 {235, 203, 139}, /* WARN: Nord sun */
88 {191, 97, 106}, /* ERROR: Nord red */
89 {180, 142, 173}, /* FATAL: Nord purple */
90 {216, 222, 233}, /* GREY: Nord snow */
91 {255, 255, 255} /* RESET */
92 },
93 .has_light_variant = true,
94 .log_colors_light =
95 {
96 {76, 86, 106}, /* DEV: Nord bg dark */
97 {67, 76, 94}, /* DEBUG: Nord bg darker */
98 {89, 131, 52}, /* INFO: Darker green */
99 {191, 144, 0}, /* WARN: Darker yellow */
100 {129, 30, 44}, /* ERROR: Darker red */
101 {110, 76, 101}, /* FATAL: Darker purple */
102 {76, 86, 106}, /* GREY */
103 {0, 0, 0} /* RESET */
104 },
105 .is_builtin = true};
106
111static const color_scheme_t SOLARIZED_SCHEME = {.name = "solarized",
112 .description = "Solarized theme - precision colors",
113 .log_colors_dark =
114 {
115 {203, 75, 22}, /* DEV: Orange */
116 {42, 161, 152}, /* DEBUG: Cyan */
117 {133, 153, 0}, /* INFO: Green */
118 {181, 137, 0}, /* WARN: Yellow */
119 {220, 50, 47}, /* ERROR: Red */
120 {108, 113, 196}, /* FATAL: Violet */
121 {101, 123, 142}, /* GREY: Base0 */
122 {255, 255, 255} /* RESET */
123 },
124 .has_light_variant = true,
125 .log_colors_light =
126 {
127 {161, 105, 70}, /* DEV: Brown/Orange */
128 {20, 110, 101}, /* DEBUG: Darker cyan */
129 {89, 100, 0}, /* INFO: Darker green */
130 {101, 76, 0}, /* WARN: Darker yellow */
131 {153, 0, 0}, /* ERROR: Darker red */
132 {68, 68, 153}, /* FATAL: Darker violet */
133 {42, 61, 76}, /* GREY */
134 {0, 0, 0} /* RESET */
135 },
136 .is_builtin = true};
137
142static const color_scheme_t DRACULA_SCHEME = {.name = "dracula",
143 .description = "Dracula dark theme - vampiric colors",
144 .log_colors_dark =
145 {
146 {255, 121, 84}, /* DEV: Orange */
147 {139, 233, 253}, /* DEBUG: Cyan */
148 {80, 250, 123}, /* INFO: Green */
149 {241, 250, 140}, /* WARN: Yellow */
150 {255, 121, 198}, /* ERROR: Pink */
151 {189, 147, 249}, /* FATAL: Purple */
152 {98, 114, 164}, /* GREY: Comment */
153 {255, 255, 255} /* RESET */
154 },
155 .has_light_variant = false,
156 .is_builtin = true};
157
162static const color_scheme_t GRUVBOX_SCHEME = {.name = "gruvbox",
163 .description = "Gruvbox theme - retro warm colors",
164 .log_colors_dark =
165 {
166 {254, 128, 25}, /* DEV: bright_orange */
167 {142, 192, 124}, /* DEBUG: bright_green */
168 {142, 192, 124}, /* INFO: bright_green */
169 {250, 189, 47}, /* WARN: bright_yellow */
170 {251, 73, 52}, /* ERROR: bright_red */
171 {211, 134, 155}, /* FATAL: bright_purple */
172 {168, 153, 132}, /* GREY: gray */
173 {255, 255, 255} /* RESET */
174 },
175 .has_light_variant = true,
176 .log_colors_light =
177 {
178 {175, 58, 3}, /* DEV: faded_orange */
179 {121, 116, 14}, /* DEBUG: faded_green */
180 {121, 116, 14}, /* INFO: faded_green */
181 {181, 118, 20}, /* WARN: faded_yellow */
182 {157, 0, 6}, /* ERROR: faded_red */
183 {108, 52, 107}, /* FATAL: faded_purple */
184 {105, 104, 98}, /* GREY */
185 {0, 0, 0} /* RESET */
186 },
187 .is_builtin = true};
188
193static const color_scheme_t MONOKAI_SCHEME = {.name = "monokai",
194 .description = "Monokai theme - vibrant coding colors",
195 .log_colors_dark =
196 {
197 {253, 151, 31}, /* DEV: Orange */
198 {166, 226, 46}, /* DEBUG: Green */
199 {174, 213, 129}, /* INFO: Light green */
200 {241, 250, 140}, /* WARN: Yellow */
201 {249, 38, 114}, /* ERROR: Magenta */
202 {174, 129, 255}, /* FATAL: Purple */
203 {117, 113, 94}, /* GREY: Comment */
204 {255, 255, 255} /* RESET */
205 },
206 .has_light_variant = false,
207 .is_builtin = true};
208
213static const color_scheme_t BASE16_DEFAULT_SCHEME = {.name = "base16-default",
214 .description = "Base16 Default theme (Chris Kempson)",
215 .log_colors_dark =
216 {
217 {220, 150, 86}, /* DEV: base09 (orange) */
218 {134, 193, 185}, /* DEBUG: base0C (cyan) */
219 {161, 181, 108}, /* INFO: base0B (green) */
220 {247, 202, 136}, /* WARN: base0A (yellow) */
221 {171, 70, 66}, /* ERROR: base08 (red) */
222 {186, 139, 175}, /* FATAL: base0E (magenta) */
223 {88, 88, 88}, /* GREY: base03 */
224 {248, 248, 248} /* RESET: base07 */
225 },
226 .has_light_variant = true,
227 .log_colors_light =
228 {
229 {161, 105, 70}, /* DEV: base0F (brown) */
230 {124, 175, 194}, /* DEBUG: base0D (blue) */
231 {161, 181, 108}, /* INFO: base0B (green) */
232 {247, 202, 136}, /* WARN: base0A (yellow) */
233 {171, 70, 66}, /* ERROR: base08 (red) */
234 {186, 139, 175}, /* FATAL: base0E (magenta) */
235 {184, 184, 184}, /* GREY: base04 */
236 {24, 24, 24} /* RESET: base00 */
237 },
238 .is_builtin = true};
239
243static const color_scheme_t *BUILTIN_SCHEMES[] = {&PASTEL_SCHEME, &NORD_SCHEME, &SOLARIZED_SCHEME,
244 &DRACULA_SCHEME, &GRUVBOX_SCHEME, &MONOKAI_SCHEME,
245 &BASE16_DEFAULT_SCHEME};
246
247#define NUM_BUILTIN_SCHEMES (sizeof(BUILTIN_SCHEMES) / sizeof(BUILTIN_SCHEMES[0]))
248
249/* ============================================================================
250 * Internal Functions
251 * ============================================================================ */
252
256static const color_scheme_t *find_builtin_scheme(const char *name) {
257 if (!name)
258 return NULL;
259
260 /* Handle "default" alias */
261 if (strcmp(name, "default") == 0) {
262 name = "pastel";
263 }
264
265 for (size_t i = 0; i < NUM_BUILTIN_SCHEMES; i++) {
266 if (strcmp(BUILTIN_SCHEMES[i]->name, name) == 0) {
267 return BUILTIN_SCHEMES[i];
268 }
269 }
270
271 return NULL;
272}
273
274/* ============================================================================
275 * Public API: Initialization
276 * ============================================================================ */
277
278asciichat_error_t colorscheme_init(void) {
279 if (g_colorscheme_initialized) {
280 return ASCIICHAT_OK;
281 }
282
283 /* NOTE: Mutex is already statically initialized on POSIX with PTHREAD_MUTEX_INITIALIZER.
284 * On Windows and Emscripten/WASM, we initialize it here. Do NOT call mutex_init() on native POSIX
285 * because double-initialization of pthread_mutex_t causes undefined behavior and deadlocks.
286 * Emscripten with pthreads requires explicit initialization because PTHREAD_MUTEX_INITIALIZER
287 * doesn't work correctly in threaded WASM builds. */
288#if defined(_WIN32) || defined(__EMSCRIPTEN__)
289 static bool mutex_initialized = false;
290 if (!mutex_initialized) {
291#ifdef __EMSCRIPTEN__
292#else
293#endif
295 mutex_initialized = true;
296 } else {
297 }
298#else
299#endif
300
301 /* Load default scheme */
302 const color_scheme_t *pastel = find_builtin_scheme("pastel");
303 if (!pastel) {
304 return SET_ERRNO(ERROR_CONFIG, "Failed to load default pastel scheme");
305 }
306
307 memcpy(&g_active_scheme, pastel, sizeof(color_scheme_t));
308 g_colorscheme_initialized = true;
309
310 return ASCIICHAT_OK;
311}
312
313void colorscheme_cleanup_compiled(compiled_color_scheme_t *compiled) {
314 if (!compiled) {
315 return;
316 }
317
318 /* Free allocated color code strings */
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];
323 /* Only free non-NULL pointers - protects against uninitialized data */
324 if (str_16 != NULL) {
325 SAFE_FREE(str_16);
326 }
327 if (str_256 != NULL) {
328 SAFE_FREE(str_256);
329 }
330 if (str_truecolor != NULL) {
331 SAFE_FREE(str_truecolor);
332 }
333 }
334 memset(compiled, 0, sizeof(compiled_color_scheme_t));
335}
336
338 if (!g_colorscheme_initialized) {
339 return;
340 }
341
342 mutex_lock(&g_colorscheme_mutex);
343 memset(&g_active_scheme, 0, sizeof(color_scheme_t));
344 g_colorscheme_initialized = false;
345 mutex_unlock(&g_colorscheme_mutex);
346
347 /* NOTE: Do NOT call mutex_destroy() on native POSIX because the mutex is statically
348 * initialized with PTHREAD_MUTEX_INITIALIZER. Destroying a statically-initialized
349 * mutex is undefined behavior. On Windows and Emscripten, we must destroy the mutex
350 * because it was explicitly initialized in colorscheme_init(). */
351#if defined(_WIN32) || defined(__EMSCRIPTEN__)
353#endif
354}
355
356/* ============================================================================
357 * Public API: Scheme Management
358 * ============================================================================ */
359
360const color_scheme_t *colorscheme_get_active_scheme(void) {
361 if (!g_colorscheme_initialized) {
362 /* Lazy initialization of color system */
364 }
365
366 return &g_active_scheme;
367}
368
369asciichat_error_t colorscheme_set_active_scheme(const char *name) {
370 if (!name) {
371 return SET_ERRNO(ERROR_INVALID_PARAM, "Scheme name is NULL");
372 }
373
374 /* Ensure color system is initialized */
375 if (!g_colorscheme_initialized) {
377 }
378
379 color_scheme_t scheme = {0};
380 asciichat_error_t result = ASCIICHAT_OK;
381
382 /* Try loading as built-in scheme first */
383 const color_scheme_t *builtin = find_builtin_scheme(name);
384 if (builtin) {
385 memcpy(&scheme, builtin, sizeof(color_scheme_t));
386 } else if (strchr(name, '/') || strchr(name, '.')) {
387 /* Try loading as file path if it contains / or . */
388 result = colorscheme_load_from_file(name, &scheme);
389 if (result != ASCIICHAT_OK) {
390 return result;
391 }
392 } else {
393 return SET_ERRNO(ERROR_CONFIG, "Unknown color scheme: %s (not a built-in scheme or valid file path)", name);
394 }
395
396 mutex_lock(&g_colorscheme_mutex);
397 memcpy(&g_active_scheme, &scheme, sizeof(color_scheme_t));
398 mutex_unlock(&g_colorscheme_mutex);
399
400 log_debug("Switched to color scheme: %s", name);
401 return ASCIICHAT_OK;
402}
403
404asciichat_error_t colorscheme_load_builtin(const char *name, color_scheme_t *scheme) {
405 if (!name || !scheme) {
406 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL name or scheme pointer");
407 }
408
409 const color_scheme_t *builtin = find_builtin_scheme(name);
410 if (!builtin) {
411 return SET_ERRNO(ERROR_CONFIG, "Unknown built-in color scheme: %s", name);
412 }
413
414 memcpy(scheme, builtin, sizeof(color_scheme_t));
415 return ASCIICHAT_OK;
416}
417
418asciichat_error_t colorscheme_load_from_file(const char *path, color_scheme_t *scheme) {
419 if (!path || !scheme) {
420 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL path or scheme pointer");
421 }
422
423 /* Check if file exists and is readable */
424 struct stat sb;
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);
427 }
428
429 /* Parse TOML file */
430 toml_result_t result = toml_parse_file_ex(path);
431 if (!result.ok) {
432 toml_free(result);
433 return SET_ERRNO(ERROR_CONFIG, "Failed to parse color scheme file '%s': %s", path, result.errmsg);
434 }
435
436 /* Extract scheme information */
437 memset(scheme, 0, sizeof(color_scheme_t));
438
439 /* Get scheme section */
440 toml_datum_t scheme_section = toml_get(result.toptab, "scheme");
441 if (scheme_section.type == TOML_TABLE) {
442 /* Get scheme name */
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));
446 }
447
448 /* Get scheme description */
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));
452 }
453 }
454
455 /* Get colors section */
456 toml_datum_t colors_section = toml_get(result.toptab, "colors");
457 if (colors_section.type == TOML_TABLE) {
458 /* Parse dark mode colors */
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);
467 }
468 }
469 }
470
471 /* Parse light mode colors (optional) */
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);
481 }
482 }
483 }
484 }
485
486 scheme->is_builtin = false;
487 SAFE_STRNCPY(scheme->source_file, path, sizeof(scheme->source_file));
488
489 toml_free(result);
490 return ASCIICHAT_OK;
491}
492
493/* ============================================================================
494 * Public API: Color Conversion
495 * ============================================================================ */
496
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");
500 }
501
502 /* Skip '#' prefix if present */
503 if (hex[0] == '#') {
504 hex++;
505 }
506
507 /* Validate hex string length */
508 if (strlen(hex) != 6) {
509 return SET_ERRNO(ERROR_CONFIG, "Invalid hex color (must be #RRGGBB): %s", hex);
510 }
511
512 /* Parse hex digits */
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);
516 }
517
518 *r = (rgb >> 16) & 0xFF;
519 *g = (rgb >> 8) & 0xFF;
520 *b = rgb & 0xFF;
521
522 return ASCIICHAT_OK;
523}
524
525/* Note: rgb_to_16color() and rgb_to_256color() are already defined in lib/video/ansi_fast.c
526 * We use those definitions instead of duplicating them here.
527 * External declarations from ansi_fast.h are used above. */
528
529void rgb_to_truecolor_ansi(uint8_t r, uint8_t g, uint8_t b, char *buf, size_t size) {
530 if (!buf || size < 20)
531 return;
532 safe_snprintf(buf, size, "\x1b[38;2;%d;%d;%dm", r, g, b);
533}
534
535/* ============================================================================
536 * Public API: Scheme Compilation
537 * ============================================================================ */
538
539asciichat_error_t colorscheme_compile_scheme(const color_scheme_t *scheme, terminal_color_mode_t mode,
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");
543 }
544
545 /* Note: mode parameter reserved for future use */
546 (void)mode;
547
548 /* Free any previously compiled strings before recompiling */
549 /* This prevents memory leaks when the color scheme is recompiled */
550 /* Only cleanup if we've actually compiled before (codes_16[0] != NULL means already compiled) */
551 if (compiled->codes_16[0] != NULL) {
553 } else {
554 }
555
556 /* Select color array based on background */
557 const rgb_pixel_t *colors = (background == TERM_BACKGROUND_LIGHT && scheme->has_light_variant)
558 ? scheme->log_colors_light
559 : scheme->log_colors_dark;
560 /* Helper: allocate and format a color code string */
561 char temp_buf[128];
562
563 /* Compile for 16-color mode */
564 for (int i = 0; i < 8; i++) {
565 if (i == 7) {
566 /* RESET */
567 SAFE_STRNCPY(temp_buf, "\x1b[0m", sizeof(temp_buf));
568 } else {
569 uint8_t color_idx = rgb_to_16color(colors[i].r, colors[i].g, colors[i].b);
570 /* ANSI color codes: 30-37 for normal, 90-97 for bright */
571 if (color_idx < 8) {
572 safe_snprintf(temp_buf, sizeof(temp_buf), "\x1b[%dm", 30 + color_idx);
573 } else {
574 safe_snprintf(temp_buf, sizeof(temp_buf), "\x1b[%dm", 90 + (color_idx - 8));
575 }
576 }
577 /* Allocate string with SAFE_MALLOC and copy */
578 size_t len = strlen(temp_buf) + 1;
579 char *allocated = SAFE_MALLOC(len, char *);
580 if (!allocated) {
581 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate color code string");
582 }
583 memcpy(allocated, temp_buf, len);
584 compiled->codes_16[i] = allocated;
585 }
586 /* Compile for 256-color mode */
587 for (int i = 0; i < 8; i++) {
588 if (i == 7) {
589 SAFE_STRNCPY(temp_buf, "\x1b[0m", sizeof(temp_buf));
590 } else {
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);
593 }
594 /* Allocate string with SAFE_MALLOC and copy */
595 size_t len = strlen(temp_buf) + 1;
596 char *allocated = SAFE_MALLOC(len, char *);
597 if (!allocated) {
598 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate color code string");
599 }
600 memcpy(allocated, temp_buf, len);
601 compiled->codes_256[i] = allocated;
602 }
603
604 /* Compile for truecolor mode */
605 for (int i = 0; i < 8; i++) {
606 if (i == 7) {
607 SAFE_STRNCPY(temp_buf, "\x1b[0m", sizeof(temp_buf));
608 } else {
609 rgb_to_truecolor_ansi(colors[i].r, colors[i].g, colors[i].b, temp_buf, sizeof(temp_buf));
610 }
611 /* Allocate string with SAFE_MALLOC and copy */
612 size_t len = strlen(temp_buf) + 1;
613 char *allocated = SAFE_MALLOC(len, char *);
614 if (!allocated) {
615 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate color code string");
616 }
617 memcpy(allocated, temp_buf, len);
618 compiled->codes_truecolor[i] = allocated;
619 }
620
621 return ASCIICHAT_OK;
622}
623
624/* ============================================================================
625 * Public API: Export
626 * ============================================================================ */
627
628asciichat_error_t colorscheme_export_scheme(const char *scheme_name, const char *file_path) {
629 if (!scheme_name) {
630 return SET_ERRNO(ERROR_INVALID_PARAM, "Scheme name is NULL");
631 }
632
633 color_scheme_t scheme = {0};
634 asciichat_error_t result = colorscheme_load_builtin(scheme_name, &scheme);
635 if (result != ASCIICHAT_OK) {
636 return result;
637 }
638
639 /* Generate TOML content */
640 char toml_content[8192] = {0};
641 size_t offset = 0;
642
643 /* Scheme header section */
644 offset += safe_snprintf(toml_content + offset, sizeof(toml_content) - offset,
645 "[scheme]\n"
646 "name = \"%s\"\n"
647 "description = \"%s\"\n\n",
648 scheme.name, scheme.description);
649
650 /* Dark mode colors */
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");
653
654 for (int i = 0; i < 8; i++) {
655 offset +=
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);
658 }
659
660 /* Light mode colors if available */
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);
667 }
668 }
669
670 /* Write to file or stdout */
671 if (file_path) {
672 FILE *fp = platform_fopen(file_path, "w");
673 if (!fp) {
674 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Cannot open %s for writing", file_path);
675 }
676 if (fputs(toml_content, fp) < 0) {
677 fclose(fp);
678 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to write to %s", file_path);
679 }
680 fclose(fp);
681 } else {
682 /* Write to stdout */
683 if (fputs(toml_content, stdout) < 0) {
684 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to write to stdout");
685 }
686 /* Flush to ensure piped output is written immediately */
687 (void)fflush(stdout);
688 }
689
690 return ASCIICHAT_OK;
691}
692
693/* ============================================================================
694 * Terminal Background Detection
695 * ============================================================================ */
696
697terminal_background_t detect_terminal_background(void) {
698 /* Method 1: Check environment variable override (highest priority) */
699 const char *term_bg = SAFE_GETENV("TERM_BACKGROUND");
700 if (term_bg) {
701 if (platform_strcasecmp(term_bg, "light") == 0) {
702 return TERM_BACKGROUND_LIGHT;
703 }
704 if (platform_strcasecmp(term_bg, "dark") == 0) {
705 return TERM_BACKGROUND_DARK;
706 }
707 }
708
709 /* Method 2: Use OSC 11 query with luminance calculation + environment fallbacks
710 * This automatically queries the terminal via OSC 11, calculates luminance,
711 * and falls back to $COLORFGBG, $TERM_PROGRAM, etc. if query fails */
712 bool is_dark = terminal_has_dark_background();
713 return is_dark ? TERM_BACKGROUND_DARK : TERM_BACKGROUND_LIGHT;
714}
715
716/* ============================================================================
717 * Early Color Scheme Loading (called from main() before log_init)
718 * ============================================================================ */
719
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) {
732 return argv[i + 1];
733 }
734 }
735 return NULL;
736}
737
753static asciichat_error_t load_config_color_scheme(color_scheme_t *scheme) {
754 if (!scheme) {
755 return SET_ERRNO(ERROR_INVALID_PARAM, "scheme pointer is NULL");
756 }
757
758 /* Use platform abstraction to find colors.toml across standard locations */
759 config_file_list_t config_files = {0};
760 asciichat_error_t search_result = platform_find_config_file("colors.toml", &config_files);
761
762 if (search_result != ASCIICHAT_OK) {
763 /* Platform search failed - non-fatal, will use built-in defaults */
764 return ERROR_NOT_FOUND;
765 }
766
767 /* Use first match (highest priority) - override semantics */
768 asciichat_error_t load_result = ERROR_NOT_FOUND;
769 if (config_files.count > 0) {
770 load_result = colorscheme_load_from_file(config_files.files[0].path, scheme);
771 }
772
773 /* Clean up search results */
774 config_file_list_destroy(&config_files);
775
776 return load_result;
777}
778
793asciichat_error_t options_colorscheme_init_early(int argc, const char *const argv[]) {
794 /* Initialize the color system with defaults */
795 asciichat_error_t result = colorscheme_init();
796 if (result != ASCIICHAT_OK) {
797 /* Non-fatal: use built-in defaults */
798 return ASCIICHAT_OK;
799 }
800
801 /* Step 1: Try to load from config file (~/.config/ascii-chat/colors.toml) */
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) {
805 /* Config file loaded successfully, apply it */
806 colorscheme_set_active_scheme(config_scheme.name);
807 }
808
809 /* Step 2: CLI --color-scheme overrides config file */
810 const char *cli_scheme = find_cli_color_scheme(argc, argv);
811 if (cli_scheme) {
812 asciichat_error_t cli_result = colorscheme_set_active_scheme(cli_scheme);
813 if (cli_result != ASCIICHAT_OK) {
814 /* Invalid scheme name from CLI, continue with current scheme */
815 return cli_result;
816 }
817 }
818
819 return ASCIICHAT_OK;
820}
uint8_t rgb_to_16color(uint8_t r, uint8_t g, uint8_t b)
Definition ansi_fast.c:314
uint8_t rgb_to_256color(uint8_t r, uint8_t g, uint8_t b)
Definition ansi_fast.c:230
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
Definition colorscheme.c:38
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
int platform_strcasecmp(const char *s1, const char *s2)
bool terminal_has_dark_background(void)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
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)