ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
options/config/config.c
Go to the documentation of this file.
1
6
7/* Storage capacities in elements; text buffers include the terminating NUL. */
8#define OPTIONS_CONFIG_QUESTION_BUFFER_SIZE 4096
9
10/* Existing buffer budgets in bytes, including NUL for text storage. */
11#define CONFIG_WRITTEN_FLAG_CAPACITY 64
12
13/* Existing buffer budgets in bytes, including NUL for text storage. */
14#define CONFIG_CATEGORY_CAPACITY 16
15
17
26#include <libplatform/util/string.h>
28#include <ascii-chat/common.h>
29#include <libplatform/terminal.h>
30#include <libplatform/system.h>
32#include <sys/stat.h>
33#include <libplatform/filesystem.h>
35#include <ascii-chat/log/log.h>
38#include <ascii-chat/version.h>
39#include <defer.h>
40
41#include <ascii-chat-deps/tomlc17/src/tomlc17.h>
42#include <string.h>
43#include <stdlib.h>
44#include <stdio.h>
45#include <errno.h>
46#include <sys/stat.h>
47
56
63#define CONFIG_WARN(fmt, ...) \
64 do { \
65 log_warn("Config file: " fmt, ##__VA_ARGS__); \
66 } while (0)
67
74#define CONFIG_DEBUG(fmt, ...) \
75 do { \
76 /* Debug messages are only shown in debug builds after logging is initialized */ \
77 /* Use log_debug which safely checks initialization itself */ \
78 log_debug(fmt, ##__VA_ARGS__); \
79 } while (0)
80
82
83// Configuration state tracking removed - not needed with schema-driven approach
84// CLI arguments always override config values, so no need to track what was set
85
86/* Validation functions are now provided by options/validation.h */
87
94
104static const char *get_toml_string_validated(toml_datum_t datum) {
105 if (datum.type != TOML_STRING) {
106 SET_ERRNO(ERROR_INVALID_PARAM, "not a toml string");
107 return NULL;
108 }
109
110 const char *str = datum.u.s;
111 if (!str) {
112 SET_ERRNO(ERROR_INVALID_PARAM, "no toml string");
113 return NULL;
114 }
115
116 // Validate UTF-8 encoding using utility function
117 if (!utf8_is_valid(str)) {
118 log_warn("Config value contains invalid UTF-8 sequence");
119 return NULL;
120 }
121
122 return str;
123}
124
126
127// ============================================================================
128// Type Handler Registry - Consolidates 4 duplicated switch statements
129// ============================================================================
130// TECHNICAL DEBT FIX: The code previously had 4 separate switch(meta->type)
131// blocks for extract, parse, write, and format operations. This consolidated
132// registry eliminates ~300 lines of code duplication while maintaining all
133// type-specific logic and special case handling.
134
143typedef union {
144 char str_value[OPTIONS_BUFF_SIZE]; // STRING type
145 int int_value; // INT type
146 bool bool_value; // BOOL type
147 float float_value; // DOUBLE type (float variant)
148 double double_value; // DOUBLE type (double variant)
150
160
167typedef struct {
168 void (*extract)(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
169 bool *has_value);
170 asciichat_error_t (*parse_validate)(const char *value_str, const config_option_metadata_t *meta,
171 option_parsed_value_t *parsed, char *error_msg,
172 size_t error_size);
174 options_t *opts, char *error_msg,
175 size_t error_size);
176 void (*format_output)(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
177 size_t bufsize);
179
180// Forward declarations
181static void extract_string(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
182 bool *has_value);
183static void extract_int(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
184 bool *has_value);
185static void extract_bool(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
186 bool *has_value);
187static void extract_double(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
188 bool *has_value);
189static asciichat_error_t parse_validate_string(const char *value_str, const config_option_metadata_t *meta,
190 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
191static asciichat_error_t parse_validate_int(const char *value_str, const config_option_metadata_t *meta,
192 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
193static asciichat_error_t parse_validate_bool(const char *value_str, const config_option_metadata_t *meta,
194 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
195static asciichat_error_t parse_validate_double(const char *value_str, const config_option_metadata_t *meta,
196 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
197static asciichat_error_t write_string(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
198 options_t *opts, char *error_msg, size_t error_size);
199static asciichat_error_t write_int(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
200 options_t *opts, char *error_msg, size_t error_size);
201static asciichat_error_t write_bool(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
202 options_t *opts, char *error_msg, size_t error_size);
203static asciichat_error_t write_double(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
204 options_t *opts, char *error_msg, size_t error_size);
205static void format_string(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
206 size_t bufsize);
207static void format_int(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
208 size_t bufsize);
209static void format_bool(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
210 size_t bufsize);
211static void format_double(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
212 size_t bufsize);
213static void format_callback(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
214 size_t bufsize);
215
216// Handler registry - indexed by option_type_t
217static const option_type_handler_t g_type_handlers[] = {
218 [OPTION_TYPE_STRING] = {extract_string, parse_validate_string, write_string, format_string},
219 [OPTION_TYPE_INT] = {extract_int, parse_validate_int, write_int, format_int},
220 [OPTION_TYPE_BOOL] = {extract_bool, parse_validate_bool, write_bool, format_bool},
221 [OPTION_TYPE_DOUBLE] = {extract_double, parse_validate_double, write_double, format_double},
222 [OPTION_TYPE_CALLBACK] = {NULL, NULL, NULL, format_callback},
223 [OPTION_TYPE_ACTION] = {NULL, NULL, NULL, NULL},
224};
225
226// ============================================================================
227// Type Handler Implementations
228// ============================================================================
229
233static void extract_string(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
234 bool *has_value) {
235 (void)int_val;
236 (void)bool_val;
237 (void)double_val;
238
239 if (datum.type == TOML_STRING) {
240 const char *str = get_toml_string_validated(datum);
241 if (str && strlen(str) > 0) {
242 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
243 *has_value = true;
244 }
245 } else if (datum.type == TOML_INT64) {
246 // Convert integer to string (e.g., port = 7777)
247 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%lld", (long long)datum.u.int64);
248 *has_value = true;
249 }
250}
251
255static void extract_int(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
256 bool *has_value) {
257 (void)int_val;
258 (void)bool_val;
259 (void)double_val;
260
261 if (datum.type == TOML_INT64) {
262 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%lld", (long long)datum.u.int64);
263 *has_value = true;
264 } else if (datum.type == TOML_STRING) {
265 const char *str = get_toml_string_validated(datum);
266 if (str) {
267 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
268 *has_value = true;
269 }
270 }
271}
272
276static void extract_bool(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
277 bool *has_value) {
278 (void)int_val;
279 (void)double_val;
280
281 if (datum.type == TOML_BOOLEAN) {
282 *bool_val = datum.u.boolean;
283 // Also set value_str for parse_validate phase
285 *has_value = true;
286 }
287}
288
292static void extract_double(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
293 bool *has_value) {
294 (void)int_val;
295 (void)bool_val;
296
297 if (datum.type == TOML_FP64) {
298 *double_val = datum.u.fp64;
299 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%.10g", *double_val);
300 *has_value = true;
301 } else if (datum.type == TOML_STRING) {
302 const char *str = get_toml_string_validated(datum);
303 if (str) {
304 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
305 *has_value = true;
306 }
307 }
308}
309
313static asciichat_error_t parse_validate_string(const char *value_str, const config_option_metadata_t *meta,
314 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
315 (void)meta;
316 (void)error_msg;
317 (void)error_size;
318
319 SAFE_STRNCPY(parsed->str_value, value_str, sizeof(parsed->str_value));
320 return ASCIICHAT_OK;
321}
322
326static asciichat_error_t parse_validate_int(const char *value_str, const config_option_metadata_t *meta,
327 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
328 // Detect enum fields by checking field_offset
329 int enum_val = -1;
330 bool is_enum = false;
331
332 if (meta->field_offset == offsetof(options_t, color_mode)) {
333 enum_val = validate_opt_color_mode(value_str, error_msg, error_size);
334 is_enum = true;
335 } else if (meta->field_offset == offsetof(options_t, color_filter)) {
336 color_filter_t filter = color_filter_from_cli_name(value_str);
337 if (filter != COLOR_FILTER_NONE || strcmp(value_str, OPT_COLOR_FILTER_NONE) == 0) {
338 enum_val = (int)filter;
339 } else {
340 SAFE_SNPRINTF(error_msg, error_size, "Invalid color filter '%s'", value_str);
341 enum_val = -1;
342 }
343 is_enum = true;
344 } else if (meta->field_offset == offsetof(options_t, render_mode)) {
345 enum_val = validate_opt_render_mode(value_str, error_msg, error_size);
346 is_enum = true;
347 } else if (meta->field_offset == offsetof(options_t, palette_type)) {
348 enum_val = validate_opt_palette(value_str, error_msg, error_size);
349 is_enum = true;
350 }
351
352 if (is_enum) {
353 // Enum parsing
354 bool named_auto = meta->field_offset == offsetof(options_t, color_mode) && strcmp(value_str, OPT_VALUE_AUTO) == 0;
355 if (enum_val < 0 && !named_auto) {
356 // Backward compatibility: accept numeric enum values too.
357 char *endptr = NULL;
358 long raw_enum = strtol(value_str, &endptr, 10);
359 bool parsed_numeric = (value_str && *value_str != '\0' && endptr && *endptr == '\0');
360 bool numeric_valid = false;
361
362 if (parsed_numeric && raw_enum >= INT_MIN && raw_enum <= INT_MAX) {
363 int enum_int = (int)raw_enum;
364 if (meta->field_offset == offsetof(options_t, color_mode)) {
365 numeric_valid =
366 (enum_int == COLOR_MODE_AUTO || enum_int == COLOR_MODE_NONE || enum_int == COLOR_MODE_16_COLOR ||
367 enum_int == COLOR_MODE_256_COLOR || enum_int == COLOR_MODE_TRUECOLOR);
368 } else if (meta->field_offset == offsetof(options_t, color_filter)) {
369 numeric_valid = (enum_int >= COLOR_FILTER_NONE && enum_int < COLOR_FILTER_COUNT);
370 } else if (meta->field_offset == offsetof(options_t, render_mode)) {
371 numeric_valid = (enum_int == RENDER_MODE_FOREGROUND || enum_int == RENDER_MODE_BACKGROUND ||
372 enum_int == RENDER_MODE_HALF_BLOCK);
373 } else if (meta->field_offset == offsetof(options_t, palette_type)) {
374 numeric_valid = (enum_int >= PALETTE_STANDARD && enum_int < PALETTE_COUNT);
375 }
376 if (numeric_valid) {
377 enum_val = enum_int;
378 }
379 }
380
381 if (!numeric_valid) {
382 if (strlen(error_msg) == 0) {
383 SAFE_SNPRINTF(error_msg, error_size, "Invalid enum value: %s", value_str);
384 }
385 return ERROR_CONFIG;
386 }
387 }
388 parsed->int_value = enum_val;
389 } else {
390 // Regular integer parsing
391 char *endptr = NULL;
392 long parsed_val = strtol(value_str, &endptr, 10);
393 if (*endptr != '\0') {
394 SAFE_SNPRINTF(error_msg, error_size, "Invalid integer: %s", value_str);
395 return ERROR_CONFIG;
396 }
397 if (parsed_val < INT_MIN || parsed_val > INT_MAX) {
398 SAFE_SNPRINTF(error_msg, error_size, "Integer out of range: %s", value_str);
399 return ERROR_CONFIG;
400 }
401
402 int int_val = (int)parsed_val;
403
404 // Check numeric range constraints if defined
405 // Constraints are present if max != 0 (max will always be non-zero for bounded integer ranges)
406 if (meta && meta->constraints.int_range.max != 0) {
407 // Allow 0 as special "use default" value for certain fields
408 bool is_auto_detect_field =
409 (meta->field_offset == offsetof(options_t, width) || meta->field_offset == offsetof(options_t, height) ||
410 meta->field_offset == offsetof(options_t, fps) ||
411 meta->field_offset == offsetof(options_t, compression_level));
412
413 if (int_val == 0 && is_auto_detect_field) {
414 // 0 is allowed for these fields (means auto-detect or use default)
415 } else if (int_val < meta->constraints.int_range.min || int_val > meta->constraints.int_range.max) {
416 SAFE_SNPRINTF(error_msg, error_size, "Integer %d out of range [%d-%d]: %s", int_val,
417 meta->constraints.int_range.min, meta->constraints.int_range.max, value_str);
418 return ERROR_CONFIG;
419 }
420 }
421
422 parsed->int_value = int_val;
423 }
424 return ASCIICHAT_OK;
425}
426
430static asciichat_error_t parse_validate_bool(const char *value_str, const config_option_metadata_t *meta,
431 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
432 (void)meta;
433 (void)error_msg;
434 (void)error_size;
435
436 // Parse boolean from string representation (OPT_VALUE_TRUE or OPT_VALUE_FALSE)
437 parsed->bool_value = value_str && (strcmp(value_str, OPT_VALUE_TRUE) == 0 || strcmp(value_str, "1") == 0 ||
438 strcmp(value_str, "yes") == 0);
439 return ASCIICHAT_OK;
440}
441
445static asciichat_error_t parse_validate_double(const char *value_str, const config_option_metadata_t *meta,
446 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
447 char *endptr = NULL;
448 double parsed_val = strtod(value_str, &endptr);
449 if (*endptr != '\0') {
450 SAFE_SNPRINTF(error_msg, error_size, "Invalid float: %s", value_str);
451 return ERROR_CONFIG;
452 }
453
454 // Check numeric range constraints if defined (for double types)
455 if (meta && meta->constraints.int_range.max != 0) {
456 // Use int_range for doubles too (the values are stored as doubles in the constraint)
457 double min = (double)meta->constraints.int_range.min;
458 double max = (double)meta->constraints.int_range.max;
459 if (parsed_val < min || parsed_val > max) {
460 SAFE_SNPRINTF(error_msg, error_size, "Float %.2f out of range [%.2f-%.2f]: %s", parsed_val, min, max, value_str);
461 return ERROR_CONFIG;
462 }
463 }
464
465 parsed->float_value = (float)parsed_val;
466 return ASCIICHAT_OK;
467}
468
472static asciichat_error_t write_string(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
473 options_t *opts, char *error_msg, size_t error_size) {
474 (void)error_msg;
475 (void)error_size;
476
477 char *field_ptr = ((char *)opts) + meta->field_offset;
478 const char *final_value = parsed->str_value;
479
480 // Special handling for path-based options (keys, log files)
481 bool is_path_option = (strstr(meta->toml_key, "key") != NULL || strstr(meta->toml_key, "log_file") != NULL ||
482 strstr(meta->toml_key, "keyfile") != NULL);
483 if (is_path_option) {
484 // Check if it's a path that needs normalization
485 if (path_looks_like_path(final_value)) {
486 char *normalized = NULL;
487 path_role_t role = PATH_ROLE_CONFIG_FILE; // Default
488 if (strstr(meta->toml_key, "key") != NULL) {
489 role = (strstr(meta->toml_key, "server_key") != NULL || strstr(meta->toml_key, "client_keys") != NULL)
492 } else if (strstr(meta->toml_key, "log_file") != NULL) {
493 role = PATH_ROLE_LOG_FILE;
494 }
495
496 asciichat_error_t path_result = path_validate_user_path(final_value, role, &normalized);
497 if (path_result != ASCIICHAT_OK) {
498 SAFE_FREE(normalized);
499 return path_result;
500 }
501 SAFE_STRNCPY(field_ptr, normalized, meta->field_size);
502 SAFE_FREE(normalized);
503 } else {
504 // Not a path, just an identifier (e.g., "gpg:keyid", "github:user")
505 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
506 }
507
508 } else {
509 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
510 }
511
512 return ASCIICHAT_OK;
513}
514
518static asciichat_error_t write_int(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
519 options_t *opts, char *error_msg, size_t error_size) {
520 (void)error_msg;
521 (void)error_size;
522
523 char *field_ptr = ((char *)opts) + meta->field_offset;
524
525 // Handle unsigned short int fields (webcam_index)
526 if (meta->field_size == sizeof(unsigned short int)) {
527 *(unsigned short int *)field_ptr = (unsigned short int)parsed->int_value;
528 } else {
529 *(int *)field_ptr = parsed->int_value;
530 }
531
532 return ASCIICHAT_OK;
533}
534
538static asciichat_error_t write_bool(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
539 options_t *opts, char *error_msg, size_t error_size) {
540 (void)error_msg;
541 (void)error_size;
542
543 char *field_ptr = ((char *)opts) + meta->field_offset;
544 bool bool_value = parsed->bool_value;
545 bool is_inverted_no_splash = false;
546 if (meta && meta->field_offset == offsetof(options_t, splash_screen) && meta->toml_key) {
547 const char *key = meta->toml_key;
548 size_t key_len = strlen(key);
549 static const char suffix[] = ".no_splash";
550 size_t suffix_len = sizeof(suffix) - 1;
551 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
552 }
553 if (is_inverted_no_splash) {
554 bool_value = !bool_value;
555 }
556
557 // Handle unsigned short int bool fields (common in options_t)
558 if (meta->field_size == sizeof(unsigned short int)) {
559 *(unsigned short int *)field_ptr = bool_value ? 1 : 0;
560 } else {
561 *(bool *)field_ptr = bool_value;
562 }
563
564 if (meta->field_offset == offsetof(options_t, flip_x))
565 opts->flip_x_explicit = true;
566
567 return ASCIICHAT_OK;
568}
569
573static asciichat_error_t write_double(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
574 options_t *opts, char *error_msg, size_t error_size) {
575 (void)error_msg;
576 (void)error_size;
577
578 char *field_ptr = ((char *)opts) + meta->field_offset;
579
580 // Check field_size to distinguish float from double
581 // Use memcpy for alignment-safe writes
582 if (meta->field_size == sizeof(float)) {
583 float float_val = (float)parsed->float_value;
584 memcpy(field_ptr, &float_val, sizeof(float));
585 } else {
586 double double_val = (double)parsed->float_value;
587 memcpy(field_ptr, &double_val, sizeof(double));
588 }
589
590 return ASCIICHAT_OK;
591}
592
596static void format_string(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
597 size_t bufsize) {
598 (void)field_size;
599 (void)meta;
600
601 const char *str_value = (const char *)field_ptr;
602 if (str_value && strlen(str_value) > 0) {
603 char escaped_str[BUFFER_SIZE_MEDIUM * 2]; // Max 2x size for escaping '%'
604 size_t j = 0;
605 for (size_t i = 0; i < strlen(str_value) && j < sizeof(escaped_str) - 2; ++i) {
606 if (str_value[i] == '%') {
607 escaped_str[j++] = '%';
608 escaped_str[j++] = '%';
609 } else {
610 escaped_str[j++] = str_value[i];
611 }
612 }
613 escaped_str[j] = '\0';
614
615 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", escaped_str);
616 } else {
617 SAFE_SNPRINTF(buf, bufsize, "\"\"");
618 }
619}
620
624static void format_int(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
625 size_t bufsize) {
626 int int_value = 0;
627 if (field_size == sizeof(unsigned short int)) {
628 int_value = *(unsigned short int *)field_ptr;
629 } else {
630 int_value = *(int *)field_ptr;
631 }
632
633 // Emit symbolic names for enum-backed values in generated config.
634 if (meta && meta->field_offset == offsetof(options_t, color_mode)) {
635 const char *name = OPT_VALUE_AUTO;
636 if (int_value == COLOR_MODE_NONE) {
637 name = OPT_COLOR_MODE_NONE;
638 } else if (int_value == COLOR_MODE_16_COLOR) {
639 name = OPT_COLOR_MODE_16;
640 } else if (int_value == COLOR_MODE_256_COLOR) {
641 name = OPT_COLOR_MODE_256;
642 } else if (int_value == COLOR_MODE_TRUECOLOR) {
644 }
645 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
646 return;
647 }
648
649 if (meta && meta->field_offset == offsetof(options_t, color_filter)) {
650 const char *name = OPT_COLOR_FILTER_NONE;
651 if (int_value > COLOR_FILTER_NONE && int_value < COLOR_FILTER_COUNT) {
652 const color_filter_def_t *def = color_filter_get_metadata((color_filter_t)int_value);
653 if (def && def->cli_name) {
654 name = def->cli_name;
655 }
656 }
657 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
658 return;
659 }
660
661 if (meta && meta->field_offset == offsetof(options_t, renderer)) {
662 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", int_value == ASCII_RENDERER_GLYPH ? "glyph" : "luminance");
663 return;
664 }
665
666 if (meta && meta->field_offset == offsetof(options_t, render_mode)) {
667 const char *name = OPT_RENDER_MODE_FOREGROUND;
668 if (int_value == RENDER_MODE_BACKGROUND) {
670 } else if (int_value == RENDER_MODE_HALF_BLOCK) {
672 }
673 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
674 return;
675 }
676
677 SAFE_SNPRINTF(buf, bufsize, "%d", int_value);
678}
679
683static void format_bool(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
684 size_t bufsize) {
685 bool bool_value = false;
686 if (field_size == sizeof(unsigned short int)) {
687 bool_value = *(unsigned short int *)field_ptr != 0;
688 } else {
689 bool_value = *(bool *)field_ptr;
690 }
691 bool is_inverted_no_splash = false;
692 if (meta && meta->field_offset == offsetof(options_t, splash_screen) && meta->toml_key) {
693 const char *key = meta->toml_key;
694 size_t key_len = strlen(key);
695 static const char suffix[] = ".no_splash";
696 size_t suffix_len = sizeof(suffix) - 1;
697 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
698 }
699 if (is_inverted_no_splash) {
700 bool_value = !bool_value;
701 }
702 SAFE_SNPRINTF(buf, bufsize, "%s", bool_value ? OPT_VALUE_TRUE : OPT_VALUE_FALSE);
703}
704
708static void format_double(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
709 size_t bufsize) {
710 (void)meta;
711
712 if (field_size == sizeof(float)) {
713 float float_value = 0.0f;
714 memcpy(&float_value, field_ptr, sizeof(float));
715 SAFE_SNPRINTF(buf, bufsize, "%.1f", (double)float_value);
716 } else {
717 double double_value = 0.0;
718 memcpy(&double_value, field_ptr, sizeof(double));
719 SAFE_SNPRINTF(buf, bufsize, "%.1f", double_value);
720 }
721}
722
729static void format_callback(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
730 size_t bufsize) {
731 if (!field_ptr || !buf || !meta) {
732 return;
733 }
734
735 if (meta->field_offset == offsetof(options_t, zalgo)) {
736 int level = *(const int *)field_ptr;
737 SAFE_SNPRINTF(buf, bufsize, "%d", level > 0 ? level : 7);
738 return;
739 }
740 if (meta->field_offset == offsetof(options_t, audio_source)) {
741 static const char *const names[] = {"all", "call", "mic", "media"};
742 int value = *(const int *)field_ptr;
743 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", value >= 0 && (size_t)value < ARRAY_SIZE(names) ? names[value] : "all");
744 return;
745 }
746 if (meta->field_offset == offsetof(options_t, audio_capture_source)) {
747 static const char *const names[] = {"auto", "mic", "media", "both", "remote"};
748 int value = *(const int *)field_ptr;
749 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", value >= 0 && (size_t)value < ARRAY_SIZE(names) ? names[value] : "auto");
750 return;
751 }
752 if (meta->field_offset == offsetof(options_t, test_pattern)) {
753 const options_t *opts = (const options_t *)(field_ptr - offsetof(options_t, test_pattern));
754 if (opts->test_pattern)
755 SAFE_SNPRINTF(buf, bufsize, "%d", opts->test_pattern_index);
756 else
757 SAFE_SNPRINTF(buf, bufsize, "false");
758 return;
759 }
760 if (meta->field_offset == offsetof(options_t, log_file) ||
761 meta->field_offset == offsetof(options_t, palette_custom) ||
762 meta->field_offset == offsetof(options_t, yt_dlp_options)) {
763 format_string(field_ptr, field_size, meta, buf, bufsize);
764 return;
765 }
766
767 if (meta->field_offset == offsetof(options_t, media_seek_timestamp) ||
768 meta->field_offset == offsetof(options_t, microphone_sensitivity) ||
769 meta->field_offset == offsetof(options_t, speakers_volume)) {
770 format_double(field_ptr, field_size, meta, buf, bufsize);
771 return;
772 }
773
774 if (field_size == sizeof(bool)) {
775 format_bool(field_ptr, field_size, meta, buf, bufsize);
776 return;
777 }
778
779 format_int(field_ptr, field_size, meta, buf, bufsize);
780}
781
786
808static asciichat_error_t config_apply_schema(toml_datum_t toptab, asciichat_mode_t detected_mode, options_t *opts,
809 bool strict) {
810 size_t metadata_count = 0;
811 const config_option_metadata_t *metadata = config_schema_get_all(&metadata_count);
812 asciichat_error_t first_error = ASCIICHAT_OK;
813
814 // Track which options were set to avoid duplicates (e.g., log_file vs logging.log_file)
815 size_t flags_count = metadata_count > 0 ? metadata_count : 1;
816 bool *option_set_flags = SAFE_CALLOC(flags_count, sizeof(bool), bool *);
817 if (!option_set_flags) {
818 asciichat_error_t err = SET_ERRNO(ERROR_MEMORY, "Failed to allocate config option flags");
819 return err;
820 }
821 defer(SAFE_FREE(option_set_flags));
822
823 for (size_t i = 0; i < metadata_count; i++) {
824 const config_option_metadata_t *meta = &metadata[i];
825
826 toml_datum_t datum = toml_seek(toptab, meta->toml_key);
827 if (datum.type == TOML_UNKNOWN) {
828 continue; // Absent options do not impose mode restrictions.
829 }
830
831 // Validate mode compatibility using mode_bitmask
832 // If mode_bitmask is 0 or BINARY, option applies to all modes
833 if (meta->mode_bitmask != 0 && !(meta->mode_bitmask & OPTION_MODE_BINARY)) {
834 // Option has specific mode restrictions - check if current mode matches
835 bool applies_to_mode = false;
836 if (detected_mode >= 0 && detected_mode < MODE_INVALID) {
837 option_mode_bitmask_t mode_bit = (option_mode_bitmask_t)(1 << detected_mode);
838 applies_to_mode = (meta->mode_bitmask & mode_bit) != 0;
839 }
840
841 if (!applies_to_mode) {
842 log_debug("Config: Option '%s' is not supported for this mode (skipping)", meta->toml_key);
843 continue;
844 }
845 }
846
847 // Skip if already set (avoid processing duplicates like log_file vs logging.log_file)
848 if (option_set_flags[i]) {
849 continue;
850 }
851
852 // Extract value based on type using handler
853 char value_str[BUFFER_SIZE_MEDIUM] = {0};
854 bool has_value = false;
855 int int_val = 0;
856 bool bool_val = false;
857 double double_val = 0.0;
858
859 // Use type handler for extraction (consolidated from 5 switch cases)
860 // For CALLBACK types, extract manually since they don't have extract handlers
861 if (meta->type == OPTION_TYPE_CALLBACK) {
862 if (datum.type == TOML_STRING) {
863 const char *str = get_toml_string_validated(datum);
864 if (str && strlen(str) > 0) {
865 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
866 has_value = true;
867 }
868 } else if (datum.type == TOML_INT64) {
869 // Convert integer to string (e.g., port = 8080)
870 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%lld", (long long)datum.u.int64);
871 has_value = true;
872 } else if (datum.type == TOML_BOOLEAN && meta->field_offset == offsetof(options_t, test_pattern)) {
873 SAFE_STRNCPY(value_str, datum.u.boolean ? "true" : "false", BUFFER_SIZE_MEDIUM);
874 has_value = true;
875 }
876 } else if (g_type_handlers[meta->type].extract) {
877 g_type_handlers[meta->type].extract(datum, value_str, &int_val, &bool_val, &double_val, &has_value);
878 }
879
880 if (!has_value) {
881 continue;
882 }
883
884 // Special handling for palette-chars (auto-sets palette_type to CUSTOM)
885 // The TOML key is "display.palette_chars" (from DISPLAY group, palette-chars option)
886 if (strcmp(meta->toml_key, "display.palette_chars") == 0) {
887 const char *chars_str = get_toml_string_validated(datum);
888 if (chars_str && strlen(chars_str) > 0) {
889 if (strlen(chars_str) < sizeof(opts->palette_custom)) {
890 SAFE_STRNCPY(opts->palette_custom, chars_str, sizeof(opts->palette_custom));
891 opts->palette_custom[sizeof(opts->palette_custom) - 1] = '\0';
892 opts->palette_custom_set = true;
893 opts->palette_explicit = true;
895 option_set_flags[i] = true;
896 } else {
897 CONFIG_WARN("Invalid palette.chars: too long (%zu chars, max %zu, skipping)", strlen(chars_str),
898 sizeof(opts->palette_custom) - 1);
899 if (strict) {
900 SAFE_FREE(option_set_flags);
901 return SET_ERRNO(ERROR_CONFIG, "palette.chars too long");
902 }
903 }
904 }
905 continue;
906 }
907
908 // Special handling for no-encrypt (auto-disables encrypt_enabled)
909 // The TOML key is "security.no_encrypt" (from SECURITY group, no-encrypt option)
910 if (strcmp(meta->toml_key, "security.no_encrypt") == 0) {
911 bool no_encrypt_val = false;
912 if (strcmp(value_str, OPT_VALUE_TRUE) == 0 || strcmp(value_str, "1") == 0) {
913 no_encrypt_val = true;
914 } else if (strcmp(value_str, OPT_VALUE_FALSE) == 0 || strcmp(value_str, "0") == 0) {
915 no_encrypt_val = false;
916 } else {
917 CONFIG_WARN("Invalid no_encrypt value '%s' (expected true/false), skipping", value_str);
918 if (strict) {
919 SAFE_FREE(option_set_flags);
920 return SET_ERRNO(ERROR_CONFIG, "Invalid no_encrypt value");
921 }
922 continue;
923 }
924
925 opts->no_encrypt = no_encrypt_val;
926
927 option_set_flags[i] = true;
928 continue;
929 }
930
931 // Preserve boolean test_pattern config files; integers select an animation.
932 if (meta->field_offset == offsetof(options_t, test_pattern) &&
933 (strcmp(value_str, "true") == 0 || strcmp(value_str, "false") == 0)) {
934 opts->test_pattern = strcmp(value_str, "true") == 0;
935 option_set_flags[i] = true;
936 continue;
937 }
938
939 // Parse and validate value using handler
940 char error_msg[BUFFER_SIZE_SMALL] = {0};
941 option_parsed_value_t parsed = {0};
942 asciichat_error_t parse_result = ASCIICHAT_OK;
943
944 // MCP bind strings have a CLI parser that also validates address families.
945 // Keep their string schema, but apply that parser to TOML values as well.
946 bool mcp_bind =
947 detected_mode == MODE_MCP_SERVER && meta->type == OPTION_TYPE_STRING && meta->parse_fn &&
948 (meta->field_offset == offsetof(options_t, address) || meta->field_offset == offsetof(options_t, address6));
949 if (meta->type == OPTION_TYPE_CALLBACK || mcp_bind) {
950 // Unified generated configs use "localhost"; the bind parser accepts
951 // numeric addresses, so preserve that loopback default for MCP.
952 if (mcp_bind && meta->field_offset == offsetof(options_t, address) && !strcmp(value_str, "localhost"))
953 SAFE_STRNCPY(value_str, "127.0.0.1", BUFFER_SIZE_MEDIUM);
954 if (meta->parse_fn) {
955 // Use custom parse function for callbacks (e.g., port parsing)
956 void *field_ptr = (char *)opts + meta->field_offset;
957 char *callback_error = NULL;
958 if (meta->parse_fn(value_str, field_ptr, &callback_error)) {
959 // Successfully parsed - callback has written to opts directly
960 if (callback_error) {
961 SAFE_FREE(callback_error);
962 }
963 // Mark as set to continue to validation
964 option_set_flags[i] = true;
965 // Don't continue - fall through to validation check below
966 } else {
967 // Parsing failed
968 CONFIG_WARN("Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str,
969 callback_error ? callback_error : "parsing failed");
970 if (callback_error) {
971 SAFE_FREE(callback_error);
972 }
973 if (strict) {
974 if (first_error == ASCIICHAT_OK) {
975 first_error = SET_ERRNO(ERROR_CONFIG, "Invalid %s value", meta->toml_key);
976 }
977 }
978 continue;
979 }
980 } else {
981 // No parse function for callback type
982 CONFIG_WARN("No parser for callback %s (parse_fn is NULL) (skipping)", meta->toml_key);
983 continue;
984 }
985 } else if (meta->type == OPTION_TYPE_ACTION) {
986 // Skip action types (not loaded from config)
987 continue;
988 } else if (g_type_handlers[meta->type].parse_validate) {
989 // Use standard type handler for parsing/validation
990 parse_result = g_type_handlers[meta->type].parse_validate(value_str, meta, &parsed, error_msg, sizeof(error_msg));
991 if (parse_result != ASCIICHAT_OK) {
992 CONFIG_WARN("Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str, error_msg);
993 if (strict) {
994 if (first_error == ASCIICHAT_OK) {
995 first_error = SET_ERRNO(ERROR_CONFIG, "Invalid %s: %s", meta->toml_key, error_msg);
996 }
997 }
998 continue;
999 }
1000
1001 // Write value to options_t using handler
1002 if (g_type_handlers[meta->type].write_to_struct) {
1003 char error_msg_write[BUFFER_SIZE_SMALL] = {0};
1004 asciichat_error_t write_result =
1005 g_type_handlers[meta->type].write_to_struct(&parsed, meta, opts, error_msg_write, sizeof(error_msg_write));
1006 if (write_result != ASCIICHAT_OK) {
1007 CONFIG_WARN("Failed to write %s: %s (skipping)", meta->toml_key, error_msg_write);
1008 if (strict) {
1009 if (first_error == ASCIICHAT_OK) {
1010 first_error = write_result;
1011 }
1012 }
1013 continue;
1014 }
1015 }
1016
1017 // Mark option as set
1018 option_set_flags[i] = true;
1019 } else {
1020 // No handler for this type
1021 CONFIG_WARN("No handler for %s (skipping)", meta->toml_key);
1022 continue;
1023 }
1024
1025 // Call builder's validate function if it exists (for cross-field validation)
1026 if (meta->validate_fn) {
1027 asciichat_errno_scope_t validation_scope = errno_checkpoint();
1028 if (!meta->validate_fn(opts)) {
1030 bool has_error = errno_peek_since(validation_scope, &error);
1031 CONFIG_WARN("Validation failed for %s: %s (skipping)", meta->toml_key,
1032 has_error ? error.context_message : "validation failed");
1033 if (strict && first_error == ASCIICHAT_OK) {
1034 first_error = SET_ERRNO(ERROR_CONFIG, "Validation failed for %s: %s", meta->toml_key,
1035 has_error ? error.context_message : "validation failed");
1036 } else {
1037 errno_scope_end(validation_scope, ASCIICHAT_ERRNO_DISMISSED);
1038 }
1039 continue;
1040 }
1041 }
1042
1043 // Mark this option as set
1044 option_set_flags[i] = true;
1045 }
1046
1047 // Handle special crypto.no_encrypt logic
1048 toml_datum_t no_encrypt = toml_seek(toptab, "crypto.no_encrypt");
1049 if (no_encrypt.type == TOML_BOOLEAN && no_encrypt.u.boolean) {
1050 opts->no_encrypt = 1;
1051 // Don't modify encrypt_enabled - they are independent flags
1052 // The conflict validation will catch if both are set
1053 }
1054
1055 // Handle password warning (check both crypto and security sections)
1056 toml_datum_t password = toml_seek(toptab, "crypto.password");
1057 if (password.type == TOML_UNKNOWN) {
1058 password = toml_seek(toptab, "security.password");
1059 }
1060 if (password.type == TOML_STRING) {
1061 const char *password_str = get_toml_string_validated(password);
1062 if (password_str && strlen(password_str) > 0) {
1063 CONFIG_WARN("Password stored in config file is insecure! Use CLI --password instead.");
1064 }
1065 }
1066
1067 SAFE_FREE(option_set_flags);
1068 return first_error;
1069}
1070
1072
1103asciichat_error_t config_load_and_apply(asciichat_mode_t detected_mode, const char *config_path, bool strict,
1104 options_t *opts) {
1105 // detected_mode is used in config_apply_schema for bitmask validation
1106 char *config_path_expanded = NULL;
1107 defer(SAFE_FREE(config_path_expanded));
1108
1109 if (config_path) {
1110 // Use custom path provided
1111 config_path_expanded = expand_path(config_path);
1112 if (!config_path_expanded) {
1113 // If expansion fails, try using as-is (might already be absolute)
1114 config_path_expanded = platform_strdup(config_path);
1115 }
1116 } else {
1117 // Use default location with XDG support
1118 char *config_dir = get_config_dir();
1119 defer(SAFE_FREE(config_dir));
1120 if (config_dir) {
1121 size_t len = strlen(config_dir) + strlen("config.toml") + 1;
1122 config_path_expanded = SAFE_MALLOC(len, char *);
1123 if (config_path_expanded) {
1124 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
1125 }
1126 }
1127
1128 // Fallback to ~/.ascii-chat/config.toml
1129 if (!config_path_expanded) {
1130 config_path_expanded = expand_path("~/.ascii-chat/config.toml");
1131 }
1132 }
1133
1134 if (!config_path_expanded) {
1135 if (strict) {
1136 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1137 }
1138 return ASCIICHAT_OK;
1139 }
1140
1141 char *validated_config_path = NULL;
1142 // An optional default config is allowed not to exist. Check this before the
1143 // whitelist validator, which quite correctly rejects paths whose parent
1144 // directory has not been created yet but should not turn that normal case
1145 // into a configuration error.
1146 if (!strict && platform_access(config_path_expanded, PLATFORM_ACCESS_EXISTS) != 0 && errno == ENOENT) {
1147 SAFE_FREE(config_path_expanded);
1148 return ASCIICHAT_OK;
1149 }
1150 asciichat_error_t validate_result =
1151 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1152 if (validate_result != ASCIICHAT_OK) {
1153 SAFE_FREE(validated_config_path);
1154 SAFE_FREE(config_path_expanded);
1155 return validate_result;
1156 }
1157 // Free the old path before reassigning (defer will free the new one)
1158 if (config_path_expanded != validated_config_path) {
1159 SAFE_FREE(config_path_expanded);
1160 }
1161 config_path_expanded = validated_config_path;
1162
1163 // Determine display path for error messages (before any early returns)
1164 const char *display_path = config_path ? config_path : config_path_expanded;
1165
1166 // Log that we're attempting to load config (before logging is initialized, use stderr)
1167 // Only print if terminal output is enabled (suppress with --quiet)
1168 if (config_path && log_get_terminal_output()) {
1169 log_debug("Loading configuration from: %s", display_path);
1170 }
1171
1172 // Check if config file exists
1173 struct stat st;
1174 if (stat(config_path_expanded, &st) != 0) {
1175 if (strict) {
1176 return SET_ERRNO(ERROR_CONFIG, "Config file does not exist: '%s'", display_path);
1177 }
1178 // File doesn't exist, that's OK - not required (non-strict mode)
1179 return ASCIICHAT_OK;
1180 }
1181
1182 // Verify it's a regular file
1183 if (!S_ISREG(st.st_mode)) {
1184 if (strict) {
1185 return SET_ERRNO(ERROR_CONFIG, "Config file exists but is not a regular file: '%s'", display_path);
1186 }
1187 CONFIG_WARN("Config file exists but is not a regular file: '%s' (skipping)", display_path);
1188 return ASCIICHAT_OK;
1189 }
1190
1191 // Parse TOML file
1192 toml_result_t result = toml_parse_file_ex(config_path_expanded);
1193 // Ensure TOML resources are freed at ALL function exit points (defer handles cleanup)
1194 defer(toml_free(result));
1195
1196 if (!result.ok) {
1197 // result.errmsg is an array, so check its first character
1198 const char *errmsg = (strlen(result.errmsg) > 0) ? result.errmsg : "Unknown parse error";
1199
1200 if (strict) {
1201 // For strict mode, return detailed error message directly
1202 // Note: SET_ERRNO stores the message in context, but asciichat_error_string() only returns generic codes
1203 // So we need to format the error message ourselves here
1204 char error_buffer[BUFFER_SIZE_MEDIUM];
1205 safe_snprintf(error_buffer, sizeof(error_buffer), "Failed to parse config file '%s': %s", display_path, errmsg);
1206 return SET_ERRNO(ERROR_CONFIG, "%s", error_buffer);
1207 }
1208 CONFIG_WARN("Failed to parse config file '%s': %s (skipping)", display_path, errmsg);
1209 return ASCIICHAT_OK; // Non-fatal error
1210 }
1211
1212 // Apply configuration using schema-driven parser with bitmask validation
1214 asciichat_error_t schema_result = config_apply_schema(result.toptab, detected_mode, opts, strict);
1215
1216 if (schema_result != ASCIICHAT_OK && strict) {
1217 return schema_result;
1218 }
1219 if (strict && schema_result == ASCIICHAT_OK)
1220 ASSERT_NO_ERRNO_SINCE(schema_scope);
1222 // In non-strict mode, continue even if some options failed validation
1223
1224 CONFIG_DEBUG("Loaded configuration from %s", display_path);
1225
1226 // Log successful config load (use stderr since logging may not be initialized yet)
1227 // Only print if terminal output is enabled (suppress with --quiet)
1229 log_debug("Loaded configuration from: %s", display_path);
1230 }
1231
1232 // Update RCU system with modified options (for test compatibility)
1233 // In real usage, options_state_set is called later after CLI parsing
1235 asciichat_error_t rcu_result = options_state_set(opts);
1236 if (rcu_result != ASCIICHAT_OK) {
1237 // Non-fatal - RCU might not be initialized yet in some test scenarios
1238 // But log as warning so tests can see if this is the issue
1239 CONFIG_WARN("Failed to update RCU options state: %d (values may not be persisted)", rcu_result);
1240 } else {
1241 ASSERT_NO_ERRNO_SINCE(rcu_scope);
1242 }
1244
1245 return ASCIICHAT_OK;
1246}
1247
1251typedef struct {
1252 char *buffer; // Dynamically allocated buffer
1253 size_t size; // Current bytes used
1254 size_t capacity; // Total capacity
1255 bool overflow; // Set if buffer overflows
1257
1264static bool config_builder_append(config_builder_t *builder, const char *fmt, ...) {
1265 if (builder->overflow) {
1266 return false;
1267 }
1268
1269 if (builder->size >= builder->capacity) {
1270 builder->overflow = true;
1271 return false;
1272 }
1273
1274 va_list args;
1275 va_start(args, fmt);
1276 int written = safe_vsnprintf(builder->buffer + builder->size, builder->capacity - builder->size, fmt, args);
1277 va_end(args);
1278
1279 if (written < 0 || builder->size + written >= builder->capacity) {
1280 builder->overflow = true;
1281 return false;
1282 }
1283
1284 builder->size += written;
1285 return true;
1286}
1287
1288static bool config_key_should_be_commented(const char *toml_key) {
1289 if (!toml_key) {
1290 return false;
1291 }
1292
1293 return strcmp(toml_key, "display.zalgo") == 0 || strcmp(toml_key, "logging.log_file") == 0 ||
1294 strcmp(toml_key, "security.password") == 0 || strcmp(toml_key, "security.key") == 0 ||
1295 strcmp(toml_key, "security.server_key") == 0 || strcmp(toml_key, "security.client_keys") == 0 ||
1296 strcmp(toml_key, "media.file") == 0 || strcmp(toml_key, "media.url") == 0 ||
1297 strcmp(toml_key, "media.yt_dlp_options") == 0 || strcmp(toml_key, "network.turn_secret") == 0;
1298}
1299
1305static asciichat_mode_t extract_mode_from_bitmask(option_mode_bitmask_t mode_bitmask) {
1306 // Check each mode bit
1307 if (mode_bitmask == OPTION_MODE_SERVER)
1308 return MODE_SERVER;
1309 if (mode_bitmask == OPTION_MODE_CLIENT)
1310 return MODE_CLIENT;
1311 if (mode_bitmask == OPTION_MODE_MIRROR)
1312 return MODE_MIRROR;
1313 if (mode_bitmask == OPTION_MODE_DISCOVERY_SVC)
1315 if (mode_bitmask == OPTION_MODE_DISCOVERY)
1316 return MODE_DISCOVERY;
1317 if (mode_bitmask == OPTION_MODE_MCP_SERVER)
1318 return MODE_MCP_SERVER;
1319 if (mode_bitmask == OPTION_MODE_API_SERVER)
1320 return MODE_API_SERVER;
1321 // Bitmask has zero or multiple bits set
1322 return MODE_INVALID;
1323}
1324
1326 char *config_path_expanded = NULL;
1327
1328 defer(SAFE_FREE(config_path_expanded));
1329
1330 // Create fresh options with all OPT_*_DEFAULT values
1331 options_t defaults = options_t_new();
1332
1333 // Allocate buffer for building config content (256KB should be plenty)
1334 const size_t BUFFER_CAPACITY = 256 * 1024;
1335 config_builder_t builder = {0};
1336 builder.buffer = SAFE_MALLOC(BUFFER_CAPACITY, char *);
1337 if (!builder.buffer) {
1338 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate config buffer");
1339 }
1340 defer(SAFE_FREE(builder.buffer));
1341 builder.capacity = BUFFER_CAPACITY;
1342
1343 // Build version comment in buffer
1344 if (!config_builder_append(&builder, "# ascii-chat configuration file\n")) {
1345 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1346 }
1347 if (!config_builder_append(&builder, "# Generated by ascii-chat v%d.%d.%d-%s\n", ASCII_CHAT_VERSION_MAJOR,
1348 ASCII_CHAT_VERSION_MINOR, ASCII_CHAT_VERSION_PATCH, ASCII_CHAT_GIT_VERSION)) {
1349 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1350 }
1351 if (!config_builder_append(&builder, "#\n")) {
1352 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1353 }
1354 if (!config_builder_append(&builder, "# All options below are commented out because some configuration options\n")) {
1355 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1356 }
1357 if (!config_builder_append(&builder, "# conflict with each other (e.g., --file vs --url, --loop vs --url).\n")) {
1358 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1359 }
1360 if (!config_builder_append(&builder, "# Uncomment only the options you need and avoid conflicting combinations.\n")) {
1361 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1362 }
1363 if (!config_builder_append(&builder, "#\n")) {
1364 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1365 }
1366 if (!config_builder_append(&builder,
1367 "# If you upgrade ascii-chat and this version comment changes, you may need to\n")) {
1368 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1369 }
1370 if (!config_builder_append(&builder, "# delete and regenerate this file with: ascii-chat --config-create\n")) {
1371 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1372 }
1373 if (!config_builder_append(&builder, "#\n\n")) {
1374 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1375 }
1376
1377 // Get all options from schema
1378 size_t metadata_count = 0;
1379 const config_option_metadata_t *metadata = config_schema_get_all(&metadata_count);
1380 {
1381 options_config_t *unified_config = options_preset_unified(NULL, NULL);
1382 if (unified_config) {
1383 const options_config_t *schema_config = unified_config;
1384 (void)config_schema_build_from_configs(&schema_config, 1);
1385 options_config_destroy(unified_config);
1386 metadata = config_schema_get_all(&metadata_count);
1387 }
1388 }
1389
1390 // Build list of unique categories in order of first appearance
1391 const char *categories[CONFIG_CATEGORY_CAPACITY] = {0}; // Max expected categories
1392 size_t category_count = 0;
1393
1394 for (size_t i = 0; i < metadata_count && category_count < CONFIG_CATEGORY_CAPACITY; i++) {
1395 const char *category = metadata[i].category;
1396 if (!category) {
1397 continue;
1398 }
1399
1400 // Check if category already in list
1401 bool found = false;
1402 for (size_t j = 0; j < category_count; j++) {
1403 if (categories[j] && strcmp(categories[j], category) == 0) {
1404 found = true;
1405 break;
1406 }
1407 }
1408
1409 if (!found) {
1410 categories[category_count++] = category;
1411 }
1412 }
1413
1414 // Build each section dynamically from schema
1415 for (size_t cat_idx = 0; cat_idx < category_count; cat_idx++) {
1416 const char *category = categories[cat_idx];
1417 if (!category) {
1418 continue;
1419 }
1420
1421 // Get all options for this category
1422 size_t cat_option_count = 0;
1423 const config_option_metadata_t **cat_options = config_schema_get_by_category(category, &cat_option_count);
1424
1425 if (!cat_options || cat_option_count == 0) {
1426 continue;
1427 }
1428
1429 // Add section header
1430 if (!config_builder_append(&builder, "[%s]\n", category)) {
1431 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1432 }
1433
1434 // Track which options we've written (to avoid duplicates)
1435 bool written_flags[CONFIG_WRITTEN_FLAG_CAPACITY] = {0}; // Max options per category
1436
1437 // Add each option in this category
1438 for (size_t opt_idx = 0; opt_idx < cat_option_count && opt_idx < 64; opt_idx++) {
1439 const config_option_metadata_t *meta = cat_options[opt_idx];
1440 if (!meta || !meta->toml_key) {
1441 continue;
1442 }
1443
1444 // Skip if already written (duplicate)
1445 if (written_flags[opt_idx]) {
1446 continue;
1447 }
1448
1449 // Skip if this is a duplicate of another option (check by TOML key, not field_offset)
1450 // Note: Multiple options can map to the same field (e.g., server_log_file, client_log_file)
1451 bool is_duplicate = false;
1452 for (size_t j = 0; j < opt_idx; j++) {
1453 if (cat_options[j] && cat_options[j]->toml_key && meta->toml_key &&
1454 strcmp(cat_options[j]->toml_key, meta->toml_key) == 0) {
1455 is_duplicate = true;
1456 break;
1457 }
1458 }
1459 if (is_duplicate) {
1460 continue;
1461 }
1462
1463 // Get field pointer from default options (or mode-specific default if available)
1464 const char *field_ptr = ((const char *)&defaults) + meta->field_offset;
1465
1466 // Buffer to hold mode-specific default value if needed
1467 char mode_default_buffer[OPTIONS_BUFF_SIZE] = {0};
1468 int mode_default_int = 0;
1469
1470 // If this option has a mode_default_getter, use it to get the correct default
1471 if (meta->mode_default_getter) {
1472 asciichat_mode_t mode = extract_mode_from_bitmask(meta->mode_bitmask);
1473 if (mode != MODE_INVALID) {
1474 const void *default_value = meta->mode_default_getter(mode);
1475 if (default_value) {
1476 // Copy the default value to our buffer based on type
1477 if (meta->type == OPTION_TYPE_STRING || meta->type == OPTION_TYPE_CALLBACK) {
1478 const char *str_value = (const char *)default_value;
1479 SAFE_STRNCPY(mode_default_buffer, str_value, sizeof(mode_default_buffer));
1480 field_ptr = mode_default_buffer;
1481 } else if (meta->type == OPTION_TYPE_INT) {
1482 mode_default_int = *(const int *)default_value;
1483 field_ptr = (const char *)&mode_default_int;
1484 }
1485 }
1486 }
1487 }
1488
1489 // Add description comment if available
1490 if (meta->description && strlen(meta->description) > 0) {
1491 if (!config_builder_append(&builder, "# %s\n", meta->description)) {
1492 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1493 }
1494 }
1495
1496 // Format and add the option value using handler (commented out to avoid conflicts)
1497 if (g_type_handlers[meta->type].format_output) {
1498 char formatted_value[BUFFER_SIZE_MEDIUM] = {0};
1499 g_type_handlers[meta->type].format_output(field_ptr, meta->field_size, meta, formatted_value,
1500 sizeof(formatted_value));
1501 const char *output_key = meta->toml_key;
1502 size_t category_len = strlen(category);
1503 if (strncmp(meta->toml_key, category, category_len) == 0 && meta->toml_key[category_len] == '.') {
1504 output_key = meta->toml_key + category_len + 1; // Strip "<category>."
1505 }
1506
1507 if (config_key_should_be_commented(meta->toml_key)) {
1508 if (!config_builder_append(&builder, "# %s = %s\n", output_key, formatted_value)) {
1509 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1510 }
1511 } else {
1512 if (!config_builder_append(&builder, "%s = %s\n", output_key, formatted_value)) {
1513 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1514 }
1515 }
1516 }
1517
1518 // Add blank line after each option
1519 if (!config_builder_append(&builder, "\n")) {
1520 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1521 }
1522
1523 written_flags[opt_idx] = true;
1524 }
1525
1526 // Add blank line between sections (but not after the last section)
1527 if (cat_idx < category_count - 1) {
1528 if (!config_builder_append(&builder, "\n")) {
1529 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1530 }
1531 }
1532 }
1533
1534 // Now write the buffer to either stdout or a file
1535 if (config_path && strlen(config_path) > 0) {
1536 // User provided a filepath - write to that file with overwrite prompt
1537
1538 // Expand and validate the path
1539 config_path_expanded = expand_path(config_path);
1540 if (!config_path_expanded) {
1541 config_path_expanded = platform_strdup(config_path);
1542 }
1543
1544 if (!config_path_expanded) {
1545 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1546 }
1547
1548 char *validated_config_path = NULL;
1549 asciichat_error_t validate_result =
1550 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1551 if (validate_result != ASCIICHAT_OK) {
1552 SAFE_FREE(validated_config_path);
1553 SAFE_FREE(config_path_expanded);
1554 return validate_result;
1555 }
1556 // Free the old path before reassigning (defer will free the new one)
1557 if (config_path_expanded != validated_config_path) {
1558 SAFE_FREE(config_path_expanded);
1559 }
1560 config_path_expanded = validated_config_path;
1561
1562 // Check if file already exists
1563 struct stat st;
1564 if (stat(config_path_expanded, &st) == 0) {
1565 // File exists - ask user if they want to overwrite
1566 log_plain("Config file already exists: %s", config_path_expanded);
1567
1569 safe_snprintf(question, sizeof(question), "File already exists: %s\n\nOverwrite", config_path_expanded);
1570 bool overwrite =
1571 ui_notice_confirm_kind(UI_PROMPT_CONFIG_OVERWRITE, UI_NOTICE_WARNING, question, 30); // Default to No
1572 if (!overwrite) {
1573 log_plain("Config file creation cancelled.");
1574 return SET_ERRNO(ERROR_CONFIG, "User cancelled overwrite");
1575 }
1576
1577 log_plain("Overwriting existing config file...");
1578 }
1579
1580 // Create directory if needed
1581 char *dir_path = platform_strdup(config_path_expanded);
1582 if (!dir_path) {
1583 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
1584 }
1585 defer(SAFE_FREE(dir_path));
1586
1587 // Find the last path separator
1588 char *last_sep = strrchr(dir_path, PATH_DELIM);
1589
1590 if (last_sep) {
1591 *last_sep = '\0';
1592 // Create directory recursively
1593 asciichat_error_t mkdir_result = platform_mkdir_recursive(dir_path, DIR_PERM_PRIVATE);
1594 if (mkdir_result != ASCIICHAT_OK) {
1595 return mkdir_result;
1596 }
1597 }
1598
1599 // Open file for writing
1600 FILE *output_file = platform_fopen("file_stream", config_path_expanded, "w");
1601 if (!output_file) {
1602 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open config file for writing: %s", config_path_expanded);
1603 }
1604 defer(SAFE_FCLOSE(output_file));
1605
1606 // Write buffer to file
1607 size_t written = fwrite(builder.buffer, 1, builder.size, output_file);
1608 if (written != builder.size) {
1609 asciichat_error_t error = SET_ERRNO_SYS(ERROR_CONFIG, "Failed to write config to file: %s", config_path_expanded);
1610 SAFE_FCLOSE(output_file);
1611 return error;
1612 }
1613 // Publish buffered bytes before returning, including builds without defer instrumentation.
1614 int close_result = fclose(output_file);
1615 output_file = NULL;
1616 if (close_result != 0) {
1617 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to close config file: %s", config_path_expanded);
1618 }
1619 } else {
1620 // No filepath provided - write buffer to stdout with automatic retry on transient errors
1621 (void)platform_write_all(STDOUT_FILENO, builder.buffer, builder.size);
1622 // Flush C stdio buffer and terminal to ensure piped output is written immediately
1623 (void)fflush(stdout);
1624 (void)terminal_flush(STDOUT_FILENO);
1625 }
1626
1627 return ASCIICHAT_OK;
1628}
1629
1631 // Use platform abstraction to find all config.toml files across standard locations
1632 config_file_list_t config_files = {0};
1633 asciichat_error_t search_result = platform_find_config_file("config.toml", &config_files);
1634 defer(config_file_list_destroy(&config_files));
1635
1636 if (search_result != ASCIICHAT_OK) {
1637 CONFIG_DEBUG("Failed to search for config files: %d", search_result);
1638 return search_result;
1639 }
1640
1641 // Cascade load: Load all found configs in reverse order (lowest priority first)
1642 // This allows higher-priority configs to override lower-priority values.
1643 // Example: System configs load first, then user configs override them.
1644
1646 for (size_t i = config_files.count; i > 0; i--) {
1647 const config_file_result_t *file = &config_files.files[i - 1];
1648
1649 // Determine strictness based on whether this is a system or user config
1650 // System configs are non-strict (values can be missing, errors are non-fatal)
1651 // User config is strict or non-strict based on parameter
1652 bool is_user_config = !file->is_system_config;
1653 bool file_strict = is_user_config ? strict : false;
1654
1655 CONFIG_DEBUG("Loading config from %s (system=%s, strict=%s)", file->path, file->is_system_config ? "yes" : "no",
1656 file_strict ? OPT_VALUE_TRUE : OPT_VALUE_FALSE);
1657
1659 asciichat_error_t load_result = config_load_and_apply(detected_mode, file->path, file_strict, opts);
1660
1661 if (load_result != ASCIICHAT_OK) {
1662 if (file_strict) {
1663 // Strict mode: errors are fatal
1664 CONFIG_DEBUG("Strict config loading failed for %s", file->path);
1665 result = load_result;
1666 } else {
1667 // Non-strict mode: errors are non-fatal, just log and continue
1668 CONFIG_DEBUG("Non-strict config loading warning for %s: %d (continuing)", file->path, load_result);
1670 ASSERT_NO_ERRNO_SINCE(load_scope);
1671 }
1672 } else {
1673 ASSERT_NO_ERRNO_SINCE(load_scope);
1675 }
1676 }
1677
1678 return result;
1679}
action_args_t args
Definition actions.c:78
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_checkpoint(void)
asciichat_errno_scope_t errno_scope_begin(void)
#define SET_ERRNO(code,...)
#define SET_ERRNO_SYS(code,...)
#define ASSERT_NO_ERRNO_SINCE(scope)
@ ASCIICHAT_ERRNO_HANDLED
@ ASCIICHAT_ERRNO_DISMISSED
@ OPTION_TYPE_INT
Integer value (–count 42).
Definition builder.h:168
@ OPTION_TYPE_DOUBLE
Floating point (–ratio 1.5).
Definition builder.h:170
@ OPTION_TYPE_STRING
String value (–name foo).
Definition builder.h:169
@ OPTION_TYPE_ACTION
Action that executes and may exit (–list-webcams, etc.).
Definition builder.h:172
@ OPTION_TYPE_CALLBACK
Custom parser function.
Definition builder.h:171
@ OPTION_TYPE_BOOL
Boolean flag (–flag, no value).
Definition builder.h:167
void options_config_destroy(options_config_t *config)
Free options config.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Option enum value definitions - single source of truth.
#define OPT_VALUE_FALSE
Definition enums.h:79
#define OPT_RENDER_MODE_FOREGROUND
Definition enums.h:128
#define OPT_RENDER_MODE_HALF_BLOCK
Definition enums.h:132
#define OPT_COLOR_MODE_256
Definition enums.h:101
#define OPT_VALUE_TRUE
Definition enums.h:78
#define OPT_COLOR_MODE_NONE
Definition enums.h:98
#define OPT_COLOR_FILTER_NONE
Definition enums.h:105
#define OPT_VALUE_AUTO
Definition enums.h:77
#define OPT_COLOR_MODE_TRUECOLOR
Definition enums.h:102
#define OPT_RENDER_MODE_BACKGROUND
Definition enums.h:130
#define OPT_COLOR_MODE_16
Definition enums.h:100
Error and exit codes - unified status values (0-255).
int color_mode
Definition fft.c:25
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes).
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes).
#define ARRAY_SIZE(arr)
Calculate the number of elements in an array.
Definition common.h:180
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define SAFE_FCLOSE(fp)
Definition common.h:395
asciichat_error_t config_load_system_and_user(asciichat_mode_t detected_mode, bool strict, options_t *opts)
Load system config first, then user config (user config overrides system).
asciichat_error_t config_load_and_apply(asciichat_mode_t detected_mode, const char *config_path, bool strict, options_t *opts)
Main function to load configuration from file and apply to global options.
asciichat_error_t config_create_default(const char *config_path)
Create default configuration file with all default values.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_CONFIG
Definition error_codes.h:52
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
bool log_get_terminal_output(void)
Get current terminal output setting.
Definition log.c:763
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define log_file(...)
File-only logging - writes to log file only, no stderr output.
Definition log/log.h:693
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:665
#define COLOR_MODE_16_COLOR
16-color mode (full name)
Definition options.h:225
#define COLOR_MODE_256_COLOR
256-color mode (full name)
Definition options.h:227
#define COLOR_MODE_TRUECOLOR
24-bit truecolor mode
Definition options.h:228
int validate_opt_render_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Validate render mode string.
Definition validation.c:185
asciichat_mode_t
Mode type for options parsing.
Definition options.h:919
#define COLOR_MODE_AUTO
Backward compatibility aliases for color mode enum values.
Definition options.h:222
int validate_opt_color_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Validate color mode string.
Definition validation.c:151
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
int validate_opt_palette(const char *value_str, char *error_msg, size_t error_msg_size)
Validate palette type string.
Definition validation.c:213
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
Definition options.h:310
option_mode_bitmask_t
Option mode bitmask.
Definition options.h:936
#define COLOR_MODE_NONE
Monochrome mode.
Definition options.h:223
options_t options_t_new(void)
Initialize options by parsing command-line arguments.
Definition options.c:547
@ MODE_CLIENT
Client mode - network client options.
Definition options.h:921
@ MODE_MCP_SERVER
Model Context Protocol rendering server.
Definition options.h:925
@ MODE_DISCOVERY_SERVICE
Discovery server mode - session management and WebRTC signaling.
Definition options.h:923
@ MODE_API_SERVER
Image-to-ASCII HTTP API server.
Definition options.h:926
@ MODE_INVALID
Invalid mode.
Definition options.h:927
@ MODE_SERVER
Server mode - network server options.
Definition options.h:920
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network).
Definition options.h:922
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
Definition options.h:924
@ ASCII_RENDERER_GLYPH
Definition options.h:966
@ OPTION_MODE_BINARY
Binary-level options (parsed before mode detection).
Definition options.h:945
@ OPTION_MODE_CLIENT
Client mode (bit 1).
Definition options.h:939
@ OPTION_MODE_API_SERVER
Definition options.h:944
@ OPTION_MODE_DISCOVERY
Discovery mode (bit 4).
Definition options.h:942
@ OPTION_MODE_SERVER
Server mode (bit 0).
Definition options.h:938
@ OPTION_MODE_MIRROR
Mirror mode (bit 2).
Definition options.h:940
@ OPTION_MODE_MCP_SERVER
Definition options.h:943
@ OPTION_MODE_DISCOVERY_SVC
Discovery server mode (bit 3).
Definition options.h:941
@ PALETTE_COUNT
Number of palette types (not a valid palette).
Definition palette.h:106
@ PALETTE_CUSTOM
User-defined via –palette-chars.
Definition palette.h:104
@ PALETTE_STANDARD
Standard ASCII palette: " ...',;:clodxkO0KXNWM".
Definition palette.h:94
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Platform-safe vsnprintf wrapper.
int errno
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:128
path_role_t
Classification for user-supplied filesystem paths.
Definition path.h:318
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Validate and canonicalize a user-supplied filesystem path.
Definition path.c:712
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:259
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:281
@ PATH_ROLE_CONFIG_FILE
Definition path.h:319
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:322
@ PATH_ROLE_LOG_FILE
Definition path.h:320
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:321
color_filter_t color_filter_from_cli_name(const char *cli_name)
Convert color filter CLI name to enum value.
const color_filter_def_t * color_filter_get_metadata(color_filter_t filter)
Get color filter metadata by enum value.
📝 Logging API with multiple log levels and terminal output control
bool ui_notice_confirm_kind(ui_prompt_kind_t kind, ui_notice_severity_t severity, const char *text, unsigned timeout_seconds)
Definition notice.c:498
@ UI_NOTICE_WARNING
Definition notice.h:11
#define CONFIG_CATEGORY_CAPACITY
#define CONFIG_WRITTEN_FLAG_CAPACITY
#define OPTIONS_CONFIG_QUESTION_BUFFER_SIZE
#define CONFIG_DEBUG(fmt,...)
Print configuration debug message.
#define CONFIG_WARN(fmt,...)
Print configuration warning using the logging system.
TOML configuration file support for ascii-chat.
Validation functions for options parsing.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
ASCII Palette Management for Video-to-ASCII Conversion.
📂 Path Manipulation Utilities
options_config_t * options_preset_unified(const char *program_name, const char *description)
Build unified options config with ALL options (binary + all modes).
Definition presets.c:58
asciichat_error_t options_state_set(const options_t *opts)
Set options from a parsed options struct.
Definition rcu.c:450
const config_option_metadata_t ** config_schema_get_by_category(const char *category, size_t *count)
Get all option metadata for a category.
Definition schema.c:416
const config_option_metadata_t * config_schema_get_all(size_t *count)
Get all option metadata.
Definition schema.c:449
asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs)
Build schema dynamically from options builder configs.
Definition schema.c:252
Schema metadata for config file options.
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
For color filter def, this is the live record that ties name, cli_name, r, g together....
const char * cli_name
CLI argument name (e.g., "matrix-green").
Helper structure for building config content in a buffer.
Option metadata for config file parsing.
Definition schema.h:48
option_type_t type
Value type (from builder).
Definition schema.h:51
size_t field_size
Size of field in options_t.
Definition schema.h:55
bool(* parse_fn)(const char *arg, void *dest, char **error_msg)
Custom parser for callbacks (or NULL).
Definition schema.h:61
struct config_option_metadata_t::@047245164140222033316365214353336112047172131121::@271224077242344302200333333005050320172070077212 int_range
option_mode_bitmask_t mode_bitmask
Which modes this option applies to.
Definition schema.h:64
const void *(* mode_default_getter)(asciichat_mode_t mode)
Get mode-specific default value (NULL if not mode-aware).
Definition schema.h:82
size_t field_offset
offsetof(options_t, field) - where to store value
Definition schema.h:54
const char * description
Description for docs/help generation.
Definition schema.h:67
const char * category
Category name (e.g., "network", "client", "audio").
Definition schema.h:53
builder_validate_fn_t validate_fn
Builder's validation function (can be NULL for simple types).
Definition schema.h:58
const char * toml_key
TOML key path (e.g., "network.port", "client.address").
Definition schema.h:49
union config_option_metadata_t::@047245164140222033316365214353336112047172131121 constraints
Function table for extracting, validating, storing, and formatting one TOML option type.
void(* format_output)(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf, size_t bufsize)
Format for TOML output.
void(* extract)(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val, bool *has_value)
Extract value from TOML datum.
asciichat_error_t(* parse_validate)(const char *value_str, const config_option_metadata_t *meta, option_parsed_value_t *parsed, char *error_msg, size_t error_size)
Parse and validate value.
asciichat_error_t(* write_to_struct)(const option_parsed_value_t *parsed, const config_option_metadata_t *meta, options_t *opts, char *error_msg, size_t error_size)
Write to options struct.
Immutable parsing tables produced from an options builder.
Definition builder.h:414
bool flip_x_explicit
User selected an orientation for this process lifetime.
Definition options.h:1104
int test_pattern_index
Synthetic pattern selector (0 or 1).
Definition options.h:1072
bool palette_explicit
Palette explicitly selected.
Definition options.h:1232
bool no_encrypt
Disable encryption (opt-out).
Definition options.h:1182
char palette_custom[256]
Custom palette characters.
Definition options.h:1230
bool palette_custom_set
True if custom palette was set.
Definition options.h:1231
bool test_pattern
Use test pattern instead of webcam.
Definition options.h:1071
palette_type_t palette_type
Selected palette type.
Definition options.h:1229
test pattern is intentionally a small, concrete record. Fields such as image, rgba give callers the i...
Scratch storage for the scalar value produced while parsing one option.
🔤 UTF-8 Encoding and Decoding Utilities