ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
colorscheme.c
Go to the documentation of this file.
1
12#include <ascii-chat/common.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 lifecycle_t g_colorscheme_lifecycle = LIFECYCLE_INIT;
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__)
36 .impl = {0},
37 .name = NULL}; /* Windows CRITICAL_SECTION or Emscripten pthread_mutex_t - initialized in colorscheme_init() */
38#else
39mutex_t g_colorscheme_mutex = {.impl = PTHREAD_MUTEX_INITIALIZER, .name = NULL}; /* POSIX pthread_mutex_t */
40#endif
41
42/* ============================================================================
43 * Built-In Color Schemes
44 * ============================================================================ */
45
50static const color_scheme_t PASTEL_SCHEME = {.name = "pastel",
51 .description = "Soft pastel colors (ascii-chat default)",
52 .log_colors_dark =
53 {
54 {240, 150, 100}, /* DEV: Orange */
55 {101, 172, 225}, /* DEBUG: Cyan */
56 {144, 224, 112}, /* INFO: Green */
57 {240, 204, 145}, /* WARN: Yellow */
58 {232, 93, 111}, /* ERROR: Red */
59 {200, 160, 216}, /* FATAL: Magenta */
60 {128, 128, 128}, /* GREY */
61 {255, 255, 255} /* RESET */
62 },
63 .has_light_variant = true,
64 .log_colors_light =
65 {
66 {156, 97, 65}, /* DEV: Darker orange (matches dark) */
67 {65, 111, 146}, /* DEBUG: Darker cyan (matches dark) */
68 {93, 145, 72}, /* INFO: Darker green (matches dark) */
69 {156, 132, 94}, /* WARN: Darker yellow (matches dark) */
70 {150, 60, 72}, /* ERROR: Darker red (matches dark) */
71 {130, 104, 140}, /* FATAL: Darker magenta (matches dark) */
72 {80, 80, 80}, /* GREY: Darker gray */
73 {0, 0, 0} /* RESET */
74 },
75 .is_builtin = true};
76
81static const color_scheme_t NORD_SCHEME = {.name = "nord",
82 .description = "Arctic, muted Nord theme colors",
83 .log_colors_dark =
84 {
85 {191, 144, 97}, /* DEV: Nord orange */
86 {143, 188, 187}, /* DEBUG: Nord frost */
87 {163, 190, 140}, /* INFO: Nord green */
88 {235, 203, 139}, /* WARN: Nord sun */
89 {191, 97, 106}, /* ERROR: Nord red */
90 {180, 142, 173}, /* FATAL: Nord purple */
91 {216, 222, 233}, /* GREY: Nord snow */
92 {255, 255, 255} /* RESET */
93 },
94 .has_light_variant = true,
95 .log_colors_light =
96 {
97 {76, 86, 106}, /* DEV: Nord bg dark */
98 {67, 76, 94}, /* DEBUG: Nord bg darker */
99 {89, 131, 52}, /* INFO: Darker green */
100 {191, 144, 0}, /* WARN: Darker yellow */
101 {129, 30, 44}, /* ERROR: Darker red */
102 {110, 76, 101}, /* FATAL: Darker purple */
103 {76, 86, 106}, /* GREY */
104 {0, 0, 0} /* RESET */
105 },
106 .is_builtin = true};
107
112static const color_scheme_t SOLARIZED_SCHEME = {.name = "solarized",
113 .description = "Solarized theme - precision colors",
114 .log_colors_dark =
115 {
116 {203, 75, 22}, /* DEV: Orange */
117 {42, 161, 152}, /* DEBUG: Cyan */
118 {133, 153, 0}, /* INFO: Green */
119 {181, 137, 0}, /* WARN: Yellow */
120 {220, 50, 47}, /* ERROR: Red */
121 {108, 113, 196}, /* FATAL: Violet */
122 {101, 123, 142}, /* GREY: Base0 */
123 {255, 255, 255} /* RESET */
124 },
125 .has_light_variant = true,
126 .log_colors_light =
127 {
128 {161, 105, 70}, /* DEV: Brown/Orange */
129 {20, 110, 101}, /* DEBUG: Darker cyan */
130 {89, 100, 0}, /* INFO: Darker green */
131 {101, 76, 0}, /* WARN: Darker yellow */
132 {153, 0, 0}, /* ERROR: Darker red */
133 {68, 68, 153}, /* FATAL: Darker violet */
134 {42, 61, 76}, /* GREY */
135 {0, 0, 0} /* RESET */
136 },
137 .is_builtin = true};
138
143static const color_scheme_t DRACULA_SCHEME = {.name = "dracula",
144 .description = "Dracula dark theme - vampiric colors",
145 .log_colors_dark =
146 {
147 {255, 121, 84}, /* DEV: Orange */
148 {139, 233, 253}, /* DEBUG: Cyan */
149 {80, 250, 123}, /* INFO: Green */
150 {241, 250, 140}, /* WARN: Yellow */
151 {255, 121, 198}, /* ERROR: Pink */
152 {189, 147, 249}, /* FATAL: Purple */
153 {98, 114, 164}, /* GREY: Comment */
154 {255, 255, 255} /* RESET */
155 },
156 .has_light_variant = false,
157 .is_builtin = true};
158
163static const color_scheme_t GRUVBOX_SCHEME = {.name = "gruvbox",
164 .description = "Gruvbox theme - retro warm colors",
165 .log_colors_dark =
166 {
167 {254, 128, 25}, /* DEV: bright_orange */
168 {142, 192, 124}, /* DEBUG: bright_green */
169 {142, 192, 124}, /* INFO: bright_green */
170 {250, 189, 47}, /* WARN: bright_yellow */
171 {251, 73, 52}, /* ERROR: bright_red */
172 {211, 134, 155}, /* FATAL: bright_purple */
173 {168, 153, 132}, /* GREY: gray */
174 {255, 255, 255} /* RESET */
175 },
176 .has_light_variant = true,
177 .log_colors_light =
178 {
179 {175, 58, 3}, /* DEV: faded_orange */
180 {121, 116, 14}, /* DEBUG: faded_green */
181 {121, 116, 14}, /* INFO: faded_green */
182 {181, 118, 20}, /* WARN: faded_yellow */
183 {157, 0, 6}, /* ERROR: faded_red */
184 {108, 52, 107}, /* FATAL: faded_purple */
185 {105, 104, 98}, /* GREY */
186 {0, 0, 0} /* RESET */
187 },
188 .is_builtin = true};
189
194static const color_scheme_t MONOKAI_SCHEME = {.name = "monokai",
195 .description = "Monokai theme - vibrant coding colors",
196 .log_colors_dark =
197 {
198 {253, 151, 31}, /* DEV: Orange */
199 {166, 226, 46}, /* DEBUG: Green */
200 {174, 213, 129}, /* INFO: Light green */
201 {241, 250, 140}, /* WARN: Yellow */
202 {249, 38, 114}, /* ERROR: Magenta */
203 {174, 129, 255}, /* FATAL: Purple */
204 {117, 113, 94}, /* GREY: Comment */
205 {255, 255, 255} /* RESET */
206 },
207 .has_light_variant = false,
208 .is_builtin = true};
209
214static const color_scheme_t BASE16_DEFAULT_SCHEME = {.name = "base16-default",
215 .description = "Base16 Default theme (Chris Kempson)",
216 .log_colors_dark =
217 {
218 {220, 150, 86}, /* DEV: base09 (orange) */
219 {134, 193, 185}, /* DEBUG: base0C (cyan) */
220 {161, 181, 108}, /* INFO: base0B (green) */
221 {247, 202, 136}, /* WARN: base0A (yellow) */
222 {171, 70, 66}, /* ERROR: base08 (red) */
223 {186, 139, 175}, /* FATAL: base0E (magenta) */
224 {88, 88, 88}, /* GREY: base03 */
225 {248, 248, 248} /* RESET: base07 */
226 },
227 .has_light_variant = true,
228 .log_colors_light =
229 {
230 {161, 105, 70}, /* DEV: base0F (brown) */
231 {124, 175, 194}, /* DEBUG: base0D (blue) */
232 {161, 181, 108}, /* INFO: base0B (green) */
233 {247, 202, 136}, /* WARN: base0A (yellow) */
234 {171, 70, 66}, /* ERROR: base08 (red) */
235 {186, 139, 175}, /* FATAL: base0E (magenta) */
236 {184, 184, 184}, /* GREY: base04 */
237 {24, 24, 24} /* RESET: base00 */
238 },
239 .is_builtin = true};
240
244static const color_scheme_t *BUILTIN_SCHEMES[] = {&PASTEL_SCHEME, &NORD_SCHEME, &SOLARIZED_SCHEME,
245 &DRACULA_SCHEME, &GRUVBOX_SCHEME, &MONOKAI_SCHEME,
246 &BASE16_DEFAULT_SCHEME};
247
248#define NUM_BUILTIN_SCHEMES (sizeof(BUILTIN_SCHEMES) / sizeof(BUILTIN_SCHEMES[0]))
249
250/* ============================================================================
251 * Internal Functions
252 * ============================================================================ */
253
257static const color_scheme_t *find_builtin_scheme(const char *name) {
258 if (!name)
259 return NULL;
260
261 /* Handle "default" alias */
262 if (strcmp(name, "default") == 0) {
263 name = "pastel";
264 }
265
266 for (size_t i = 0; i < NUM_BUILTIN_SCHEMES; i++) {
267 if (strcmp(BUILTIN_SCHEMES[i]->name, name) == 0) {
268 return BUILTIN_SCHEMES[i];
269 }
270 }
271
272 return NULL;
273}
274
275/* ============================================================================
276 * Public API: Initialization
277 * ============================================================================ */
278
280 if (lifecycle_is_initialized(&g_colorscheme_lifecycle)) {
281 return ASCIICHAT_OK;
282 }
283
284 /* NOTE: Mutex is already statically initialized on POSIX with PTHREAD_MUTEX_INITIALIZER.
285 * On Windows and Emscripten/WASM, we initialize it here. Do NOT call mutex_init() on native POSIX
286 * because double-initialization of pthread_mutex_t causes undefined behavior and deadlocks.
287 * Emscripten with pthreads requires explicit initialization because PTHREAD_MUTEX_INITIALIZER
288 * doesn't work correctly in threaded WASM builds. */
289#if defined(_WIN32) || defined(__EMSCRIPTEN__)
290 static bool mutex_initialized = false;
291 if (!mutex_initialized) {
292#ifdef __EMSCRIPTEN__
293#else
294#endif
295 mutex_init(&g_colorscheme_mutex, "colorscheme");
296 mutex_initialized = true;
297 } else {
298 }
299#else
300#endif
301
302 /* Load default scheme */
303 const color_scheme_t *pastel = find_builtin_scheme("pastel");
304 if (!pastel) {
305 return SET_ERRNO(ERROR_CONFIG, "Failed to load default pastel scheme");
306 }
307
308 memcpy(&g_active_scheme, pastel, sizeof(color_scheme_t));
309
310 /* Mark as initialized */
311 lifecycle_init(&g_colorscheme_lifecycle, "colorscheme");
312
313 return ASCIICHAT_OK;
314}
315
317 if (!compiled) {
318 return;
319 }
320
321 /* Free allocated color code strings */
322 for (int i = 0; i < 8; i++) {
323 char *str_16 = (char *)compiled->codes_16[i];
324 char *str_256 = (char *)compiled->codes_256[i];
325 char *str_truecolor = (char *)compiled->codes_truecolor[i];
326 /* Only free non-NULL pointers - protects against uninitialized data */
327 if (str_16 != NULL) {
328 SAFE_FREE(str_16);
329 }
330 if (str_256 != NULL) {
331 SAFE_FREE(str_256);
332 }
333 if (str_truecolor != NULL) {
334 SAFE_FREE(str_truecolor);
335 }
336 }
337 memset(compiled, 0, sizeof(compiled_color_scheme_t));
338}
339
341 if (!lifecycle_is_initialized(&g_colorscheme_lifecycle)) {
342 return;
343 }
344
346 memset(&g_active_scheme, 0, sizeof(color_scheme_t));
347
348 /* Mark as uninitialized */
349 lifecycle_shutdown(&g_colorscheme_lifecycle);
350
352
353 /* NOTE: Do NOT call mutex_destroy() on native POSIX because the mutex is statically
354 * initialized with PTHREAD_MUTEX_INITIALIZER. Destroying a statically-initialized
355 * mutex is undefined behavior. On Windows and Emscripten, we must destroy the mutex
356 * because it was explicitly initialized in colorscheme_init(). */
357#if defined(_WIN32) || defined(__EMSCRIPTEN__)
359#endif
360}
361
362/* ============================================================================
363 * Public API: Scheme Management
364 * ============================================================================ */
365
367 if (!lifecycle_is_initialized(&g_colorscheme_lifecycle)) {
368 /* Lazy initialization of color system */
370 }
371
372 return &g_active_scheme;
373}
374
376 if (!name) {
377 return SET_ERRNO(ERROR_INVALID_PARAM, "Scheme name is NULL");
378 }
379
380 /* Ensure color system is initialized */
381 if (!lifecycle_is_initialized(&g_colorscheme_lifecycle)) {
383 }
384
385 color_scheme_t scheme = {0};
387
388 /* Try loading as built-in scheme first */
389 const color_scheme_t *builtin = find_builtin_scheme(name);
390 if (builtin) {
391 memcpy(&scheme, builtin, sizeof(color_scheme_t));
392 } else if (strchr(name, '/') || strchr(name, '.')) {
393 /* Try loading as file path if it contains / or . */
394 result = colorscheme_load_from_file(name, &scheme);
395 if (result != ASCIICHAT_OK) {
396 return result;
397 }
398 } else {
399 return SET_ERRNO(ERROR_CONFIG, "Unknown color scheme: %s (not a built-in scheme or valid file path)", name);
400 }
401
403 memcpy(&g_active_scheme, &scheme, sizeof(color_scheme_t));
405
406 log_debug("Switched to color scheme: %s", name);
407 return ASCIICHAT_OK;
408}
409
411 if (!name || !scheme) {
412 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL name or scheme pointer");
413 }
414
415 const color_scheme_t *builtin = find_builtin_scheme(name);
416 if (!builtin) {
417 return SET_ERRNO(ERROR_CONFIG, "Unknown built-in color scheme: %s", name);
418 }
419
420 memcpy(scheme, builtin, sizeof(color_scheme_t));
421 return ASCIICHAT_OK;
422}
423
425 if (!path || !scheme) {
426 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL path or scheme pointer");
427 }
428
429 /* Check if file exists and is readable */
430 struct stat sb;
431 if (stat(path, &sb) != 0 || !S_ISREG(sb.st_mode)) {
432 return SET_ERRNO(ERROR_FILE_NOT_FOUND, "Color scheme file not found or not readable: %s", path);
433 }
434
435 /* Parse TOML file */
436 toml_result_t result = toml_parse_file_ex(path);
437 if (!result.ok) {
438 toml_free(result);
439 return SET_ERRNO(ERROR_CONFIG, "Failed to parse color scheme file '%s': %s", path, result.errmsg);
440 }
441
442 /* Extract scheme information */
443 memset(scheme, 0, sizeof(color_scheme_t));
444
445 /* Get scheme section */
446 toml_datum_t scheme_section = toml_get(result.toptab, "scheme");
447 if (scheme_section.type == TOML_TABLE) {
448 /* Get scheme name */
449 toml_datum_t name_datum = toml_get(scheme_section, "name");
450 if (name_datum.type == TOML_STRING) {
451 SAFE_STRNCPY(scheme->name, name_datum.u.s, sizeof(scheme->name));
452 }
453
454 /* Get scheme description */
455 toml_datum_t desc_datum = toml_get(scheme_section, "description");
456 if (desc_datum.type == TOML_STRING) {
457 SAFE_STRNCPY(scheme->description, desc_datum.u.s, sizeof(scheme->description));
458 }
459 }
460
461 /* Get colors section */
462 toml_datum_t colors_section = toml_get(result.toptab, "colors");
463 if (colors_section.type == TOML_TABLE) {
464 /* Parse dark mode colors */
465 toml_datum_t dark_section = toml_get(colors_section, "dark");
466 if (dark_section.type == TOML_TABLE) {
467 const char *color_names[] = {"dev", "debug", "warn", "info", "error", "fatal", "grey", "reset"};
468 for (int i = 0; i < 8; i++) {
469 toml_datum_t color_value = toml_get(dark_section, color_names[i]);
470 if (color_value.type == TOML_STRING) {
471 parse_hex_color(color_value.u.s, &scheme->log_colors_dark[i].r, &scheme->log_colors_dark[i].g,
472 &scheme->log_colors_dark[i].b);
473 }
474 }
475 }
476
477 /* Parse light mode colors (optional) */
478 toml_datum_t light_section = toml_get(colors_section, "light");
479 if (light_section.type == TOML_TABLE) {
480 scheme->has_light_variant = true;
481 const char *color_names[] = {"dev", "debug", "warn", "info", "error", "fatal", "grey", "reset"};
482 for (int i = 0; i < 8; i++) {
483 toml_datum_t color_value = toml_get(light_section, color_names[i]);
484 if (color_value.type == TOML_STRING) {
485 parse_hex_color(color_value.u.s, &scheme->log_colors_light[i].r, &scheme->log_colors_light[i].g,
486 &scheme->log_colors_light[i].b);
487 }
488 }
489 }
490 }
491
492 scheme->is_builtin = false;
493 SAFE_STRNCPY(scheme->source_file, path, sizeof(scheme->source_file));
494
495 toml_free(result);
496 return ASCIICHAT_OK;
497}
498
499/* ============================================================================
500 * Public API: Color Conversion
501 * ============================================================================ */
502
504 if (!hex || !r || !g || !b) {
505 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter");
506 }
507
508 /* Skip '#' prefix if present */
509 if (hex[0] == '#') {
510 hex++;
511 }
512
513 /* Validate hex string length */
514 if (strlen(hex) != 6) {
515 return SET_ERRNO(ERROR_CONFIG, "Invalid hex color (must be #RRGGBB): %s", hex);
516 }
517
518 /* Parse hex digits */
519 unsigned int rgb = 0;
520 if (SAFE_SSCANF(hex, "%6x", &rgb) != 1) {
521 return SET_ERRNO(ERROR_CONFIG, "Invalid hex color format: %s", hex);
522 }
523
524 *r = (rgb >> 16) & 0xFF;
525 *g = (rgb >> 8) & 0xFF;
526 *b = rgb & 0xFF;
527
528 return ASCIICHAT_OK;
529}
530
531/* Note: rgb_to_16color() and rgb_to_256color() are already defined in lib/video/ansi_fast.c
532 * We use those definitions instead of duplicating them here.
533 * External declarations from ansi_fast.h are used above. */
534
535void rgb_to_truecolor_ansi(uint8_t r, uint8_t g, uint8_t b, char *buf, size_t size) {
536 if (!buf || size < 20)
537 return;
538 safe_snprintf(buf, size, "\x1b[38;2;%d;%d;%dm", r, g, b);
539}
540
541/* ============================================================================
542 * Public API: Scheme Compilation
543 * ============================================================================ */
544
546 terminal_background_t background, compiled_color_scheme_t *compiled) {
547 if (!scheme || !compiled) {
548 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL scheme or compiled pointer");
549 }
550
551 /* Note: mode parameter reserved for future use */
552 (void)mode;
553
554 /* Free any previously compiled strings before recompiling */
555 /* This prevents memory leaks when the color scheme is recompiled */
556 /* Only cleanup if we've actually compiled before (codes_16[0] != NULL means already compiled) */
557 if (compiled->codes_16[0] != NULL) {
559 } else {
560 }
561
562 /* Select color array based on background */
563 const rgb_pixel_t *colors = (background == TERM_BACKGROUND_LIGHT && scheme->has_light_variant)
564 ? scheme->log_colors_light
565 : scheme->log_colors_dark;
566 /* Helper: allocate and format a color code string */
567 char temp_buf[128];
568
569 /* Compile for 16-color mode */
570 for (int i = 0; i < 8; i++) {
571 if (i == 7) {
572 /* RESET */
573 SAFE_STRNCPY(temp_buf, "\x1b[0m", sizeof(temp_buf));
574 } else {
575 uint8_t color_idx = rgb_to_16color(colors[i].r, colors[i].g, colors[i].b);
576 /* ANSI color codes: 30-37 for normal, 90-97 for bright */
577 if (color_idx < 8) {
578 safe_snprintf(temp_buf, sizeof(temp_buf), "\x1b[%dm", 30 + color_idx);
579 } else {
580 safe_snprintf(temp_buf, sizeof(temp_buf), "\x1b[%dm", 90 + (color_idx - 8));
581 }
582 }
583 /* Allocate string with SAFE_MALLOC and copy */
584 size_t len = strlen(temp_buf) + 1;
585 char *allocated = SAFE_MALLOC(len, char *);
586 if (!allocated) {
587 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate color code string");
588 }
589 memcpy(allocated, temp_buf, len);
590 compiled->codes_16[i] = allocated;
591 }
592 /* Compile for 256-color mode */
593 for (int i = 0; i < 8; i++) {
594 if (i == 7) {
595 SAFE_STRNCPY(temp_buf, "\x1b[0m", sizeof(temp_buf));
596 } else {
597 uint8_t color_idx = rgb_to_256color(colors[i].r, colors[i].g, colors[i].b);
598 safe_snprintf(temp_buf, sizeof(temp_buf), "\x1b[38;5;%dm", color_idx);
599 }
600 /* Allocate string with SAFE_MALLOC and copy */
601 size_t len = strlen(temp_buf) + 1;
602 char *allocated = SAFE_MALLOC(len, char *);
603 if (!allocated) {
604 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate color code string");
605 }
606 memcpy(allocated, temp_buf, len);
607 compiled->codes_256[i] = allocated;
608 }
609
610 /* Compile for truecolor mode */
611 for (int i = 0; i < 8; i++) {
612 if (i == 7) {
613 SAFE_STRNCPY(temp_buf, "\x1b[0m", sizeof(temp_buf));
614 } else {
615 rgb_to_truecolor_ansi(colors[i].r, colors[i].g, colors[i].b, temp_buf, sizeof(temp_buf));
616 }
617 /* Allocate string with SAFE_MALLOC and copy */
618 size_t len = strlen(temp_buf) + 1;
619 char *allocated = SAFE_MALLOC(len, char *);
620 if (!allocated) {
621 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate color code string");
622 }
623 memcpy(allocated, temp_buf, len);
624 compiled->codes_truecolor[i] = allocated;
625 }
626
627 return ASCIICHAT_OK;
628}
629
630/* ============================================================================
631 * Public API: Export
632 * ============================================================================ */
633
634asciichat_error_t colorscheme_export_scheme(const char *scheme_name, const char *file_path) {
635 if (!scheme_name) {
636 return SET_ERRNO(ERROR_INVALID_PARAM, "Scheme name is NULL");
637 }
638
639 color_scheme_t scheme = {0};
640 asciichat_error_t result = colorscheme_load_builtin(scheme_name, &scheme);
641 if (result != ASCIICHAT_OK) {
642 return result;
643 }
644
645 /* Generate TOML content */
646 char toml_content[8192] = {0};
647 size_t offset = 0;
648
649 /* Scheme header section */
650 offset += safe_snprintf(toml_content + offset, sizeof(toml_content) - offset,
651 "[scheme]\n"
652 "name = \"%s\"\n"
653 "description = \"%s\"\n\n",
654 scheme.name, scheme.description);
655
656 /* Dark mode colors */
657 const char *color_names[] = {"dev", "debug", "warn", "info", "error", "fatal", "grey", "reset"};
658 offset += safe_snprintf(toml_content + offset, sizeof(toml_content) - offset, "[colors.dark]\n");
659
660 for (int i = 0; i < 8; i++) {
661 offset +=
662 safe_snprintf(toml_content + offset, sizeof(toml_content) - offset, "%s = \"#%02X%02X%02X\"\n", color_names[i],
663 scheme.log_colors_dark[i].r, scheme.log_colors_dark[i].g, scheme.log_colors_dark[i].b);
664 }
665
666 /* Light mode colors if available */
667 if (scheme.has_light_variant) {
668 offset += safe_snprintf(toml_content + offset, sizeof(toml_content) - offset, "\n[colors.light]\n");
669 for (int i = 0; i < 8; i++) {
670 offset += safe_snprintf(toml_content + offset, sizeof(toml_content) - offset, "%s = \"#%02X%02X%02X\"\n",
671 color_names[i], scheme.log_colors_light[i].r, scheme.log_colors_light[i].g,
672 scheme.log_colors_light[i].b);
673 }
674 }
675
676 /* Write to file or stdout */
677 if (file_path) {
678 FILE *fp = platform_fopen("file_stream", file_path, "w");
679 if (!fp) {
680 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Cannot open %s for writing", file_path);
681 }
682 if (fputs(toml_content, fp) < 0) {
683 fclose(fp);
684 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to write to %s", file_path);
685 }
686 fclose(fp);
687 } else {
688 /* Write to stdout */
689 if (fputs(toml_content, stdout) < 0) {
690 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to write to stdout");
691 }
692 /* Flush to ensure piped output is written immediately */
693 (void)fflush(stdout);
694 }
695
696 return ASCIICHAT_OK;
697}
698
699/* ============================================================================
700 * Terminal Theme Detection
701 * ============================================================================ */
702
716 /* Method 1: Check environment variable override (highest priority) */
717 const char *term_bg = SAFE_GETENV("TERM_BACKGROUND");
718 if (term_bg) {
719 if (platform_strcasecmp(term_bg, "light") == 0) {
721 }
722 if (platform_strcasecmp(term_bg, "dark") == 0) {
724 }
725 }
726
727 /* Method 2: Use terminal_has_dark_background() for automatic theme detection
728 * This checks OSC 11 background color, environment variables, and terminal type */
729 bool is_dark = terminal_has_dark_background();
731}
732
733/* ============================================================================
734 * Early Color Scheme Loading (called from main() before log_init)
735 * ============================================================================ */
736
746static const char *find_cli_color_scheme(int argc, const char *const argv[]) {
747 for (int i = 1; i < argc - 1; i++) {
748 if (strcmp(argv[i], "--color-scheme") == 0) {
749 return argv[i + 1];
750 }
751 }
752 return NULL;
753}
754
770static asciichat_error_t load_config_color_scheme(color_scheme_t *scheme) {
771 if (!scheme) {
772 return SET_ERRNO(ERROR_INVALID_PARAM, "scheme pointer is NULL");
773 }
774
775 /* Use platform abstraction to find colors.toml across standard locations */
776 config_file_list_t config_files = {0};
777 asciichat_error_t search_result = platform_find_config_file("colors.toml", &config_files);
778
779 if (search_result != ASCIICHAT_OK) {
780 /* Platform search failed - non-fatal, will use built-in defaults */
781 return ERROR_NOT_FOUND;
782 }
783
784 /* Use first match (highest priority) - override semantics */
786 if (config_files.count > 0) {
787 load_result = colorscheme_load_from_file(config_files.files[0].path, scheme);
788 }
789
790 /* Clean up search results */
791 config_file_list_destroy(&config_files);
792
793 return load_result;
794}
795
810asciichat_error_t options_colorscheme_init_early(int argc, const char *const argv[]) {
811 /* Initialize the color system with defaults */
813 if (result != ASCIICHAT_OK) {
814 /* Non-fatal: use built-in defaults */
815 return ASCIICHAT_OK;
816 }
817
818 /* Step 1: Try to load from config file (~/.config/ascii-chat/colors.toml) */
819 color_scheme_t config_scheme = {0};
820 asciichat_error_t config_result = load_config_color_scheme(&config_scheme);
821 if (config_result == ASCIICHAT_OK) {
822 /* Config file loaded successfully, apply it */
823 colorscheme_set_active_scheme(config_scheme.name);
824 }
825
826 /* Step 2: CLI --color-scheme overrides config file */
827 const char *cli_scheme = find_cli_color_scheme(argc, argv);
828 if (cli_scheme) {
829 asciichat_error_t cli_result = colorscheme_set_active_scheme(cli_scheme);
830 if (cli_result != ASCIICHAT_OK) {
831 /* Invalid scheme name from CLI, continue with current scheme */
832 return cli_result;
833 }
834 }
835
836 return ASCIICHAT_OK;
837}
ANSI escape sequence utilities and fast color code generation.
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)
Export a color scheme to TOML format.
asciichat_error_t colorscheme_init(void)
Initialize the color system.
asciichat_error_t parse_hex_color(const char *hex, uint8_t *r, uint8_t *g, uint8_t *b)
Parse a hex color string.
asciichat_error_t colorscheme_load_builtin(const char *name, color_scheme_t *scheme)
Load a built-in color scheme.
const color_scheme_t * colorscheme_get_active_scheme(void)
Get currently active color scheme.
terminal_background_t detect_terminal_background(void)
Detect the user's terminal theme (dark or light background)
asciichat_error_t colorscheme_load_from_file(const char *path, color_scheme_t *scheme)
Load a color scheme from a TOML file.
void colorscheme_destroy(void)
Shutdown the color system.
asciichat_error_t colorscheme_set_active_scheme(const char *name)
Set the active color scheme.
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)
Compile a color scheme to ANSI codes.
void rgb_to_truecolor_ansi(uint8_t r, uint8_t g, uint8_t b, char *buf, size_t size)
Generate truecolor ANSI escape code.
void colorscheme_cleanup_compiled(compiled_color_scheme_t *compiled)
Clean up allocated strings in a compiled color scheme.
#define NUM_BUILTIN_SCHEMES
mutex_t g_colorscheme_mutex
Shared mutex for color scheme compilation.
Definition colorscheme.c:39
terminal_background_t
Terminal theme detection result.
Definition colorscheme.h:50
@ TERM_BACKGROUND_DARK
Definition colorscheme.h:53
@ TERM_BACKGROUND_LIGHT
Definition colorscheme.h:52
⚙️ Common definitions, error codes, macros, and types shared throughout the application
uint8_t rgb_to_16color(uint8_t r, uint8_t g, uint8_t b)
Definition ansi.c:437
uint8_t rgb_to_256color(uint8_t r, uint8_t g, uint8_t b)
Definition ansi.c:360
#define SAFE_SSCANF(str, format,...)
Definition common.h:440
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_GETENV(name)
Definition common.h:434
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context, returning the error code.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_FILE_NOT_FOUND
@ ERROR_NOT_FOUND
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_FILE_OPERATION
@ ERROR_INVALID_PARAM
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
Find config file across multiple standard locations.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int platform_strcasecmp(const char *s1, const char *s2)
Case-insensitive string comparison.
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
void config_file_list_destroy(config_file_list_t *list)
Free config file list resources.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
Application limits and constraints.
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
bool terminal_has_dark_background(void)
Detect if terminal theme is dark.
Cross-platform stat and file type checking macros.
Color scheme definition.
Definition colorscheme.h:62
rgb_pixel_t log_colors_light[8]
Definition colorscheme.h:71
char source_file[4096]
Definition colorscheme.h:75
char description[256]
Definition colorscheme.h:64
rgb_pixel_t log_colors_dark[8]
Definition colorscheme.h:67
char name[64]
Definition colorscheme.h:63
bool has_light_variant
Definition colorscheme.h:70
Compiled ANSI escape codes for a color scheme.
Definition colorscheme.h:85
const char * codes_16[8]
Definition colorscheme.h:86
const char * codes_256[8]
Definition colorscheme.h:87
const char * codes_truecolor[8]
Definition colorscheme.h:88
List of config file search results.
Definition filesystem.h:595
size_t count
Number of results found.
Definition filesystem.h:597
config_file_result_t * files
Array of results (allocated, must be freed)
Definition filesystem.h:596
char * path
Absolute path to config file (allocated, must be freed)
Definition filesystem.h:583
Mutex type (POSIX: pthread_mutex_t with debug tracking)
pthread_mutex_t impl
Underlying POSIX mutex.
RGB pixel structure.
uint8_t b
Blue color component (0-255)
uint8_t g
Green color component (0-255)
uint8_t r
Red color component (0-255)
🖥️ Cross-platform terminal interface for ascii-chat
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578