7#include <ascii-chat/options/config.h>
8#include <ascii-chat/common/error_codes.h>
9#include <ascii-chat/options/options.h>
10#include <ascii-chat/options/validation.h>
11#include <ascii-chat/options/schema.h>
12#include <ascii-chat/options/rcu.h>
13#include <ascii-chat/util/path.h>
14#include <ascii-chat/util/utf8.h>
15#include <ascii-chat/common.h>
16#include <ascii-chat/platform/terminal.h>
17#include <ascii-chat/platform/system.h>
18#include <ascii-chat/platform/question.h>
19#include <ascii-chat/platform/stat.h>
20#include <ascii-chat/platform/filesystem.h>
21#include <ascii-chat/crypto/crypto.h>
22#include <ascii-chat/log/logging.h>
23#include <ascii-chat/video/palette.h>
24#include <ascii-chat/video/color_filter.h>
25#include <ascii-chat/version.h>
26#include <ascii-chat/tooling/defer/defer.h>
28#include <ascii-chat-deps/tomlc17/src/tomlc17.h>
46#define CONFIG_WARN(fmt, ...) \
48 log_warn("Config file: " fmt, ##__VA_ARGS__); \
57#define CONFIG_DEBUG(fmt, ...) \
61 log_debug(fmt, ##__VA_ARGS__); \
87static const char *get_toml_string_validated(toml_datum_t datum) {
88 if (datum.type != TOML_STRING) {
89 SET_ERRNO(ERROR_INVALID_PARAM,
"not a toml string");
93 const char *str = datum.u.s;
95 SET_ERRNO(ERROR_INVALID_PARAM,
"no toml string");
101 log_warn(
"Config value contains invalid UTF-8 sequence");
122 char str_value[OPTIONS_BUFF_SIZE];
146 void (*extract)(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
148 asciichat_error_t (*parse_validate)(
const char *value_str,
const config_option_metadata_t *meta,
152 options_t *opts,
char *error_msg,
154 void (*format_output)(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
159static void extract_string(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
161static void extract_int(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
163static void extract_bool(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
165static void extract_double(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
167static asciichat_error_t parse_validate_string(
const char *value_str,
const config_option_metadata_t *meta,
169static asciichat_error_t parse_validate_int(
const char *value_str,
const config_option_metadata_t *meta,
171static asciichat_error_t parse_validate_bool(
const char *value_str,
const config_option_metadata_t *meta,
173static asciichat_error_t parse_validate_double(
const char *value_str,
const config_option_metadata_t *meta,
175static asciichat_error_t write_string(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
176 options_t *opts,
char *error_msg,
size_t error_size);
177static asciichat_error_t write_int(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
178 options_t *opts,
char *error_msg,
size_t error_size);
179static asciichat_error_t write_bool(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
180 options_t *opts,
char *error_msg,
size_t error_size);
181static asciichat_error_t write_double(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
182 options_t *opts,
char *error_msg,
size_t error_size);
183static void format_string(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
185static void format_int(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
187static void format_bool(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
189static void format_double(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
191static void format_callback(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
196 [OPTION_TYPE_STRING] = {extract_string, parse_validate_string, write_string, format_string},
197 [OPTION_TYPE_INT] = {extract_int, parse_validate_int, write_int, format_int},
198 [OPTION_TYPE_BOOL] = {extract_bool, parse_validate_bool, write_bool, format_bool},
199 [OPTION_TYPE_DOUBLE] = {extract_double, parse_validate_double, write_double, format_double},
200 [OPTION_TYPE_CALLBACK] = {NULL, NULL, NULL, format_callback},
201 [OPTION_TYPE_ACTION] = {NULL, NULL, NULL, NULL},
211static void extract_string(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
217 if (datum.type == TOML_STRING) {
218 const char *str = get_toml_string_validated(datum);
219 if (str && strlen(str) > 0) {
220 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
223 }
else if (datum.type == TOML_INT64) {
225 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM,
"%lld", (
long long)datum.u.int64);
233static void extract_int(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
238 if (datum.type == TOML_INT64) {
239 *int_val = (int)datum.u.int64;
240 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM,
"%d", *int_val);
242 }
else if (datum.type == TOML_STRING) {
243 const char *str = get_toml_string_validated(datum);
245 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
254static void extract_bool(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
259 if (datum.type == TOML_BOOLEAN) {
260 *bool_val = datum.u.boolean;
262 SAFE_STRNCPY(value_str, *bool_val ?
"true" :
"false", BUFFER_SIZE_MEDIUM);
270static void extract_double(toml_datum_t datum,
char *value_str,
int *int_val,
bool *bool_val,
double *double_val,
275 if (datum.type == TOML_FP64) {
276 *double_val = datum.u.fp64;
277 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM,
"%.10g", *double_val);
279 }
else if (datum.type == TOML_STRING) {
280 const char *str = get_toml_string_validated(datum);
282 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
291static asciichat_error_t parse_validate_string(
const char *value_str,
const config_option_metadata_t *meta,
304static asciichat_error_t parse_validate_int(
const char *value_str,
const config_option_metadata_t *meta,
308 bool is_enum =
false;
310 if (meta->field_offset == offsetof(options_t, color_mode)) {
313 }
else if (meta->field_offset == offsetof(options_t, color_filter)) {
315 if (filter != COLOR_FILTER_NONE || strcmp(value_str,
"none") == 0) {
316 enum_val = (int)filter;
318 SAFE_SNPRINTF(error_msg, error_size,
"Invalid color filter '%s'", value_str);
322 }
else if (meta->field_offset == offsetof(options_t, render_mode)) {
325 }
else if (meta->field_offset == offsetof(options_t, palette_type)) {
335 long raw_enum = strtol(value_str, &endptr, 10);
336 bool parsed_numeric = (value_str && *value_str !=
'\0' && endptr && *endptr ==
'\0');
337 bool numeric_valid =
false;
339 if (parsed_numeric && raw_enum >= INT_MIN && raw_enum <= INT_MAX) {
340 int enum_int = (int)raw_enum;
341 if (meta->field_offset == offsetof(options_t, color_mode)) {
343 (enum_int == COLOR_MODE_AUTO || enum_int == COLOR_MODE_NONE || enum_int == COLOR_MODE_16_COLOR ||
344 enum_int == COLOR_MODE_256_COLOR || enum_int == COLOR_MODE_TRUECOLOR);
345 }
else if (meta->field_offset == offsetof(options_t, color_filter)) {
346 numeric_valid = (enum_int >= COLOR_FILTER_NONE && enum_int < COLOR_FILTER_COUNT);
347 }
else if (meta->field_offset == offsetof(options_t, render_mode)) {
348 numeric_valid = (enum_int == RENDER_MODE_FOREGROUND || enum_int == RENDER_MODE_BACKGROUND ||
349 enum_int == RENDER_MODE_HALF_BLOCK);
350 }
else if (meta->field_offset == offsetof(options_t, palette_type)) {
351 numeric_valid = (enum_int >= PALETTE_STANDARD && enum_int < PALETTE_COUNT);
359 if (strlen(error_msg) == 0) {
360 SAFE_SNPRINTF(error_msg, error_size,
"Invalid enum value: %s", value_str);
369 long parsed_val = strtol(value_str, &endptr, 10);
370 if (*endptr !=
'\0') {
371 SAFE_SNPRINTF(error_msg, error_size,
"Invalid integer: %s", value_str);
374 if (parsed_val < INT_MIN || parsed_val > INT_MAX) {
375 SAFE_SNPRINTF(error_msg, error_size,
"Integer out of range: %s", value_str);
379 int int_val = (int)parsed_val;
383 if (meta && meta->constraints.int_range.max != 0) {
385 bool is_auto_detect_field =
386 (meta->field_offset == offsetof(options_t, width) || meta->field_offset == offsetof(options_t, height) ||
387 meta->field_offset == offsetof(options_t, fps) ||
388 meta->field_offset == offsetof(options_t, compression_level));
390 if (int_val == 0 && is_auto_detect_field) {
392 }
else if (int_val < meta->constraints.int_range.min || int_val > meta->constraints.int_range.max) {
393 SAFE_SNPRINTF(error_msg, error_size,
"Integer %d out of range [%d-%d]: %s", int_val,
394 meta->constraints.int_range.min, meta->constraints.int_range.max, value_str);
407static asciichat_error_t parse_validate_bool(
const char *value_str,
const config_option_metadata_t *meta,
414 if (value_str && (strcmp(value_str,
"true") == 0 || strcmp(value_str,
"1") == 0 || strcmp(value_str,
"yes") == 0)) {
425static asciichat_error_t parse_validate_double(
const char *value_str,
const config_option_metadata_t *meta,
428 double parsed_val = strtod(value_str, &endptr);
429 if (*endptr !=
'\0') {
430 SAFE_SNPRINTF(error_msg, error_size,
"Invalid float: %s", value_str);
435 if (meta && meta->constraints.int_range.max != 0) {
437 double min = (double)meta->constraints.int_range.min;
438 double max = (double)meta->constraints.int_range.max;
439 if (parsed_val < min || parsed_val > max) {
440 SAFE_SNPRINTF(error_msg, error_size,
"Float %.2f out of range [%.2f-%.2f]: %s", parsed_val, min, max, value_str);
452static asciichat_error_t write_string(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
453 options_t *opts,
char *error_msg,
size_t error_size) {
457 char *field_ptr = ((
char *)opts) + meta->field_offset;
458 const char *final_value = parsed->
str_value;
461 bool is_path_option = (strstr(meta->toml_key,
"key") != NULL || strstr(meta->toml_key,
"log_file") != NULL ||
462 strstr(meta->toml_key,
"keyfile") != NULL);
463 if (is_path_option) {
466 char *normalized = NULL;
467 path_role_t role = PATH_ROLE_CONFIG_FILE;
468 if (strstr(meta->toml_key,
"key") != NULL) {
469 role = (strstr(meta->toml_key,
"server_key") != NULL || strstr(meta->toml_key,
"client_keys") != NULL)
470 ? PATH_ROLE_KEY_PUBLIC
471 : PATH_ROLE_KEY_PRIVATE;
472 }
else if (strstr(meta->toml_key,
"log_file") != NULL) {
473 role = PATH_ROLE_LOG_FILE;
477 if (path_result != ASCIICHAT_OK) {
478 SAFE_FREE(normalized);
481 SAFE_STRNCPY(field_ptr, normalized, meta->field_size);
482 SAFE_FREE(normalized);
485 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
489 if (strstr(meta->toml_key,
"crypto.key") != NULL || strstr(meta->toml_key,
"crypto.password") != NULL ||
490 strstr(meta->toml_key,
"crypto.keyfile") != NULL) {
491 opts->encrypt_enabled = 1;
494 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
503static asciichat_error_t write_int(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
504 options_t *opts,
char *error_msg,
size_t error_size) {
508 char *field_ptr = ((
char *)opts) + meta->field_offset;
511 if (meta->field_size ==
sizeof(
unsigned short int)) {
512 *(
unsigned short int *)field_ptr = (
unsigned short int)parsed->
int_value;
523static asciichat_error_t write_bool(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
524 options_t *opts,
char *error_msg,
size_t error_size) {
528 char *field_ptr = ((
char *)opts) + meta->field_offset;
530 bool is_inverted_no_splash =
false;
531 if (meta && meta->field_offset == offsetof(options_t, splash) && meta->toml_key) {
532 const char *key = meta->toml_key;
533 size_t key_len = strlen(key);
534 static const char suffix[] =
".no_splash";
535 size_t suffix_len =
sizeof(suffix) - 1;
536 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
538 if (is_inverted_no_splash) {
539 bool_value = !bool_value;
543 if (meta->field_size ==
sizeof(
unsigned short int)) {
544 *(
unsigned short int *)field_ptr = bool_value ? 1 : 0;
546 *(
bool *)field_ptr = bool_value;
555static asciichat_error_t write_double(
const option_parsed_value_t *parsed,
const config_option_metadata_t *meta,
556 options_t *opts,
char *error_msg,
size_t error_size) {
560 char *field_ptr = ((
char *)opts) + meta->field_offset;
564 if (meta->field_size ==
sizeof(
float)) {
566 memcpy(field_ptr, &float_val,
sizeof(
float));
569 memcpy(field_ptr, &double_val,
sizeof(
double));
578static void format_string(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
583 const char *str_value = (
const char *)field_ptr;
584 if (str_value && strlen(str_value) > 0) {
585 char escaped_str[BUFFER_SIZE_MEDIUM * 2];
587 for (
size_t i = 0; i < strlen(str_value) && j <
sizeof(escaped_str) - 2; ++i) {
588 if (str_value[i] ==
'%') {
589 escaped_str[j++] =
'%';
590 escaped_str[j++] =
'%';
592 escaped_str[j++] = str_value[i];
595 escaped_str[j] =
'\0';
597 SAFE_SNPRINTF(buf, bufsize,
"\"%s\"", escaped_str);
599 SAFE_SNPRINTF(buf, bufsize,
"\"\"");
606static void format_int(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
609 if (field_size ==
sizeof(
unsigned short int)) {
610 int_value = *(
unsigned short int *)field_ptr;
612 int_value = *(
int *)field_ptr;
616 if (meta && meta->field_offset == offsetof(options_t, color_mode)) {
617 const char *name =
"auto";
618 if (int_value == COLOR_MODE_NONE) {
620 }
else if (int_value == COLOR_MODE_16_COLOR) {
622 }
else if (int_value == COLOR_MODE_256_COLOR) {
624 }
else if (int_value == COLOR_MODE_TRUECOLOR) {
627 SAFE_SNPRINTF(buf, bufsize,
"\"%s\"", name);
631 if (meta && meta->field_offset == offsetof(options_t, color_filter)) {
632 const char *name =
"none";
633 if (int_value > COLOR_FILTER_NONE && int_value < COLOR_FILTER_COUNT) {
635 if (def && def->cli_name) {
636 name = def->cli_name;
639 SAFE_SNPRINTF(buf, bufsize,
"\"%s\"", name);
643 if (meta && meta->field_offset == offsetof(options_t, render_mode)) {
644 const char *name =
"foreground";
645 if (int_value == RENDER_MODE_BACKGROUND) {
647 }
else if (int_value == RENDER_MODE_HALF_BLOCK) {
650 SAFE_SNPRINTF(buf, bufsize,
"\"%s\"", name);
654 SAFE_SNPRINTF(buf, bufsize,
"%d", int_value);
660static void format_bool(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
662 bool bool_value =
false;
663 if (field_size ==
sizeof(
unsigned short int)) {
664 bool_value = *(
unsigned short int *)field_ptr != 0;
666 bool_value = *(
bool *)field_ptr;
668 bool is_inverted_no_splash =
false;
669 if (meta && meta->field_offset == offsetof(options_t, splash) && meta->toml_key) {
670 const char *key = meta->toml_key;
671 size_t key_len = strlen(key);
672 static const char suffix[] =
".no_splash";
673 size_t suffix_len =
sizeof(suffix) - 1;
674 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
676 if (is_inverted_no_splash) {
677 bool_value = !bool_value;
679 SAFE_SNPRINTF(buf, bufsize,
"%s", bool_value ?
"true" :
"false");
685static void format_double(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
689 if (field_size ==
sizeof(
float)) {
690 float float_value = 0.0f;
691 memcpy(&float_value, field_ptr,
sizeof(
float));
692 SAFE_SNPRINTF(buf, bufsize,
"%.1f", (
double)float_value);
694 double double_value = 0.0;
695 memcpy(&double_value, field_ptr,
sizeof(
double));
696 SAFE_SNPRINTF(buf, bufsize,
"%.1f", double_value);
706static void format_callback(
const char *field_ptr,
size_t field_size,
const config_option_metadata_t *meta,
char *buf,
708 if (!field_ptr || !buf || !meta) {
712 if (meta->field_offset == offsetof(options_t, log_file) ||
713 meta->field_offset == offsetof(options_t, palette_custom) ||
714 meta->field_offset == offsetof(options_t, yt_dlp_options)) {
715 format_string(field_ptr, field_size, meta, buf, bufsize);
719 if (meta->field_offset == offsetof(options_t, media_seek_timestamp) ||
720 meta->field_offset == offsetof(options_t, microphone_sensitivity) ||
721 meta->field_offset == offsetof(options_t, speakers_volume)) {
722 format_double(field_ptr, field_size, meta, buf, bufsize);
726 if (field_size ==
sizeof(
bool)) {
727 format_bool(field_ptr, field_size, meta, buf, bufsize);
731 format_int(field_ptr, field_size, meta, buf, bufsize);
760static asciichat_error_t config_apply_schema(toml_datum_t toptab, asciichat_mode_t detected_mode, options_t *opts,
762 size_t metadata_count = 0;
764 asciichat_error_t first_error = ASCIICHAT_OK;
767 size_t flags_count = metadata_count > 0 ? metadata_count : 1;
768 bool *option_set_flags = SAFE_CALLOC(flags_count,
sizeof(
bool),
bool *);
769 if (!option_set_flags) {
770 asciichat_error_t err = SET_ERRNO(ERROR_MEMORY,
"Failed to allocate config option flags");
773 defer(SAFE_FREE(option_set_flags));
775 for (
size_t i = 0; i < metadata_count; i++) {
776 const config_option_metadata_t *meta = &metadata[i];
780 if (meta->mode_bitmask != 0 && !(meta->mode_bitmask & OPTION_MODE_BINARY)) {
782 bool applies_to_mode =
false;
783 if (detected_mode >= 0 && detected_mode <= MODE_DISCOVERY) {
784 option_mode_bitmask_t mode_bit = (1 << detected_mode);
785 applies_to_mode = (meta->mode_bitmask & mode_bit) != 0;
788 if (!applies_to_mode) {
789 log_debug(
"Config: Option '%s' is not supported for this mode (skipping)", meta->toml_key);
791 SAFE_FREE(option_set_flags);
792 return SET_ERRNO(ERROR_CONFIG,
"Option '%s' is not supported for this mode", meta->toml_key);
799 if (option_set_flags[i]) {
804 toml_datum_t datum = toml_seek(toptab, meta->toml_key);
805 if (datum.type == TOML_UNKNOWN) {
810 char value_str[BUFFER_SIZE_MEDIUM] = {0};
811 bool has_value =
false;
813 bool bool_val =
false;
814 double double_val = 0.0;
818 if (meta->type == OPTION_TYPE_CALLBACK) {
819 if (datum.type == TOML_STRING) {
820 const char *str = get_toml_string_validated(datum);
821 if (str && strlen(str) > 0) {
822 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
825 }
else if (datum.type == TOML_INT64) {
827 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM,
"%lld", (
long long)datum.u.int64);
830 }
else if (g_type_handlers[meta->type].extract) {
831 g_type_handlers[meta->type].
extract(datum, value_str, &int_val, &bool_val, &double_val, &has_value);
840 if (strcmp(meta->toml_key,
"display.palette_chars") == 0) {
841 const char *chars_str = get_toml_string_validated(datum);
842 if (chars_str && strlen(chars_str) > 0) {
843 if (strlen(chars_str) <
sizeof(opts->palette_custom)) {
844 SAFE_STRNCPY(opts->palette_custom, chars_str,
sizeof(opts->palette_custom));
845 opts->palette_custom[
sizeof(opts->palette_custom) - 1] =
'\0';
846 opts->palette_custom_set =
true;
847 opts->palette_type = PALETTE_CUSTOM;
848 option_set_flags[i] =
true;
850 CONFIG_WARN(
"Invalid palette.chars: too long (%zu chars, max %zu, skipping)", strlen(chars_str),
851 sizeof(opts->palette_custom) - 1);
853 SAFE_FREE(option_set_flags);
854 return SET_ERRNO(ERROR_CONFIG,
"palette.chars too long");
863 if (strcmp(meta->toml_key,
"security.no_encrypt") == 0) {
864 bool no_encrypt_val =
false;
865 if (strcmp(value_str,
"true") == 0 || strcmp(value_str,
"1") == 0) {
866 no_encrypt_val =
true;
867 }
else if (strcmp(value_str,
"false") == 0 || strcmp(value_str,
"0") == 0) {
868 no_encrypt_val =
false;
870 CONFIG_WARN(
"Invalid no_encrypt value '%s' (expected true/false), skipping", value_str);
872 SAFE_FREE(option_set_flags);
873 return SET_ERRNO(ERROR_CONFIG,
"Invalid no_encrypt value");
878 opts->no_encrypt = no_encrypt_val;
879 if (no_encrypt_val) {
881 opts->encrypt_enabled =
false;
883 option_set_flags[i] =
true;
888 char error_msg[BUFFER_SIZE_SMALL] = {0};
890 asciichat_error_t parse_result = ASCIICHAT_OK;
893 if (meta->type == OPTION_TYPE_CALLBACK) {
894 if (meta->parse_fn) {
896 void *field_ptr = (
char *)opts + meta->field_offset;
897 char *callback_error = NULL;
898 if (meta->parse_fn(value_str, field_ptr, &callback_error)) {
900 if (callback_error) {
901 SAFE_FREE(callback_error);
904 option_set_flags[i] =
true;
908 CONFIG_WARN(
"Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str,
909 callback_error ? callback_error :
"parsing failed");
910 if (callback_error) {
911 SAFE_FREE(callback_error);
914 if (first_error == ASCIICHAT_OK) {
915 first_error = SET_ERRNO(ERROR_CONFIG,
"Invalid %s value", meta->toml_key);
922 CONFIG_WARN(
"No parser for callback %s (parse_fn is NULL) (skipping)", meta->toml_key);
925 }
else if (meta->type == OPTION_TYPE_ACTION) {
928 }
else if (g_type_handlers[meta->type].parse_validate) {
930 parse_result = g_type_handlers[meta->type].
parse_validate(value_str, meta, &parsed, error_msg,
sizeof(error_msg));
931 if (parse_result != ASCIICHAT_OK) {
932 CONFIG_WARN(
"Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str, error_msg);
934 if (first_error == ASCIICHAT_OK) {
935 first_error = SET_ERRNO(ERROR_CONFIG,
"Invalid %s: %s", meta->toml_key, error_msg);
942 if (g_type_handlers[meta->type].write_to_struct) {
943 char error_msg_write[BUFFER_SIZE_SMALL] = {0};
944 asciichat_error_t write_result =
945 g_type_handlers[meta->type].
write_to_struct(&parsed, meta, opts, error_msg_write,
sizeof(error_msg_write));
946 if (write_result != ASCIICHAT_OK) {
947 CONFIG_WARN(
"Failed to write %s: %s (skipping)", meta->toml_key, error_msg_write);
949 if (first_error == ASCIICHAT_OK) {
950 first_error = write_result;
958 option_set_flags[i] =
true;
961 CONFIG_WARN(
"No handler for %s (skipping)", meta->toml_key);
966 if (meta->validate_fn) {
967 char *validate_error = NULL;
968 if (!meta->validate_fn(opts, &validate_error)) {
969 CONFIG_WARN(
"Validation failed for %s: %s (skipping)", meta->toml_key,
970 validate_error ? validate_error :
"validation failed");
971 if (validate_error) {
972 SAFE_FREE(validate_error);
975 if (first_error == ASCIICHAT_OK) {
976 first_error = SET_ERRNO(ERROR_CONFIG,
"Validation failed for %s", meta->toml_key);
985 option_set_flags[i] =
true;
989 toml_datum_t no_encrypt = toml_seek(toptab,
"crypto.no_encrypt");
990 if (no_encrypt.type == TOML_BOOLEAN && no_encrypt.u.boolean) {
991 opts->no_encrypt = 1;
997 toml_datum_t password = toml_seek(toptab,
"crypto.password");
998 if (password.type == TOML_UNKNOWN) {
999 password = toml_seek(toptab,
"security.password");
1001 if (password.type == TOML_STRING) {
1002 const char *password_str = get_toml_string_validated(password);
1003 if (password_str && strlen(password_str) > 0) {
1004 CONFIG_WARN(
"Password stored in config file is insecure! Use CLI --password instead.");
1008 SAFE_FREE(option_set_flags);
1047 char *config_path_expanded = NULL;
1048 defer(SAFE_FREE(config_path_expanded));
1053 if (!config_path_expanded) {
1060 defer(SAFE_FREE(config_dir));
1062 size_t len = strlen(config_dir) + strlen(
"config.toml") + 1;
1063 config_path_expanded = SAFE_MALLOC(len,
char *);
1064 if (config_path_expanded) {
1065 safe_snprintf(config_path_expanded, len,
"%sconfig.toml", config_dir);
1070 if (!config_path_expanded) {
1071 config_path_expanded =
expand_path(
"~/.ascii-chat/config.toml");
1075 if (!config_path_expanded) {
1077 return SET_ERRNO(ERROR_CONFIG,
"Failed to resolve config file path");
1079 return ASCIICHAT_OK;
1082 char *validated_config_path = NULL;
1083 asciichat_error_t validate_result =
1085 if (validate_result != ASCIICHAT_OK) {
1086 SAFE_FREE(validated_config_path);
1087 SAFE_FREE(config_path_expanded);
1088 return validate_result;
1091 if (config_path_expanded != validated_config_path) {
1092 SAFE_FREE(config_path_expanded);
1094 config_path_expanded = validated_config_path;
1097 const char *display_path = config_path ? config_path : config_path_expanded;
1102 log_debug(
"Loading configuration from: %s", display_path);
1107 if (stat(config_path_expanded, &st) != 0) {
1109 return SET_ERRNO(ERROR_CONFIG,
"Config file does not exist: '%s'", display_path);
1112 return ASCIICHAT_OK;
1116 if (!S_ISREG(st.st_mode)) {
1118 return SET_ERRNO(ERROR_CONFIG,
"Config file exists but is not a regular file: '%s'", display_path);
1120 CONFIG_WARN(
"Config file exists but is not a regular file: '%s' (skipping)", display_path);
1121 return ASCIICHAT_OK;
1125 toml_result_t result = toml_parse_file_ex(config_path_expanded);
1127 defer(toml_free(result));
1131 const char *errmsg = (strlen(result.errmsg) > 0) ? result.errmsg :
"Unknown parse error";
1137 char error_buffer[BUFFER_SIZE_MEDIUM];
1138 safe_snprintf(error_buffer,
sizeof(error_buffer),
"Failed to parse config file '%s': %s", display_path, errmsg);
1140 return SET_ERRNO(ERROR_CONFIG,
"%s", error_buffer);
1142 CONFIG_WARN(
"Failed to parse config file '%s': %s (skipping)", display_path, errmsg);
1144 return ASCIICHAT_OK;
1148 asciichat_error_t schema_result = config_apply_schema(result.toptab, detected_mode, opts, strict);
1150 if (schema_result != ASCIICHAT_OK && strict) {
1152 return schema_result;
1156 CONFIG_DEBUG(
"Loaded configuration from %s", display_path);
1161 log_debug(
"Loaded configuration from: %s", display_path);
1167 if (rcu_result != ASCIICHAT_OK) {
1170 CONFIG_WARN(
"Failed to update RCU options state: %d (values may not be persisted)", rcu_result);
1174 return ASCIICHAT_OK;
1193static bool config_builder_append(
config_builder_t *builder,
const char *fmt, ...) {
1204 va_start(
args, fmt);
1208 if (written < 0 || builder->size + written >= builder->
capacity) {
1213 builder->
size += written;
1217static bool config_key_should_be_commented(
const char *toml_key) {
1222 return strcmp(toml_key,
"logging.log_file") == 0 || strcmp(toml_key,
"security.password") == 0 ||
1223 strcmp(toml_key,
"security.key") == 0 || strcmp(toml_key,
"security.server_key") == 0 ||
1224 strcmp(toml_key,
"security.client_keys") == 0 || strcmp(toml_key,
"media.file") == 0 ||
1225 strcmp(toml_key,
"media.url") == 0 || strcmp(toml_key,
"media.yt_dlp_options") == 0 ||
1226 strcmp(toml_key,
"network.turn_secret") == 0;
1234static asciichat_mode_t extract_mode_from_bitmask(option_mode_bitmask_t mode_bitmask) {
1236 if (mode_bitmask == OPTION_MODE_SERVER)
1238 if (mode_bitmask == OPTION_MODE_CLIENT)
1240 if (mode_bitmask == OPTION_MODE_MIRROR)
1242 if (mode_bitmask == OPTION_MODE_DISCOVERY_SVC)
1243 return MODE_DISCOVERY_SERVICE;
1244 if (mode_bitmask == OPTION_MODE_DISCOVERY)
1245 return MODE_DISCOVERY;
1247 return MODE_INVALID;
1251 char *config_path_expanded = NULL;
1253 defer(SAFE_FREE(config_path_expanded));
1259 const size_t BUFFER_CAPACITY = 256 * 1024;
1261 builder.
buffer = SAFE_MALLOC(BUFFER_CAPACITY,
char *);
1263 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate config buffer");
1265 defer(SAFE_FREE(builder.
buffer));
1266 builder.
capacity = BUFFER_CAPACITY;
1269 if (!config_builder_append(&builder,
"# ascii-chat configuration file\n")) {
1270 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1272 if (!config_builder_append(&builder,
"# Generated by ascii-chat v%d.%d.%d-%s\n", ASCII_CHAT_VERSION_MAJOR,
1273 ASCII_CHAT_VERSION_MINOR, ASCII_CHAT_VERSION_PATCH, ASCII_CHAT_GIT_VERSION)) {
1274 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1276 if (!config_builder_append(&builder,
"#\n")) {
1277 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1279 if (!config_builder_append(&builder,
"# All options below are commented out because some configuration options\n")) {
1280 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1282 if (!config_builder_append(&builder,
"# conflict with each other (e.g., --file vs --url, --loop vs --url).\n")) {
1283 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1285 if (!config_builder_append(&builder,
"# Uncomment only the options you need and avoid conflicting combinations.\n")) {
1286 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1288 if (!config_builder_append(&builder,
"#\n")) {
1289 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1291 if (!config_builder_append(&builder,
1292 "# If you upgrade ascii-chat and this version comment changes, you may need to\n")) {
1293 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1295 if (!config_builder_append(&builder,
"# delete and regenerate this file with: ascii-chat --config-create\n")) {
1296 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1298 if (!config_builder_append(&builder,
"#\n\n")) {
1299 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1303 size_t metadata_count = 0;
1307 const char *categories[16] = {0};
1308 size_t category_count = 0;
1310 for (
size_t i = 0; i < metadata_count && category_count < 16; i++) {
1311 const char *category = metadata[i].category;
1318 for (
size_t j = 0; j < category_count; j++) {
1319 if (categories[j] && strcmp(categories[j], category) == 0) {
1326 categories[category_count++] = category;
1331 for (
size_t cat_idx = 0; cat_idx < category_count; cat_idx++) {
1332 const char *category = categories[cat_idx];
1338 size_t cat_option_count = 0;
1341 if (!cat_options || cat_option_count == 0) {
1346 if (!config_builder_append(&builder,
"[%s]\n", category)) {
1347 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1351 bool written_flags[64] = {0};
1354 for (
size_t opt_idx = 0; opt_idx < cat_option_count && opt_idx < 64; opt_idx++) {
1355 const config_option_metadata_t *meta = cat_options[opt_idx];
1356 if (!meta || !meta->toml_key) {
1361 if (written_flags[opt_idx]) {
1367 bool is_duplicate =
false;
1368 for (
size_t j = 0; j < opt_idx; j++) {
1369 if (cat_options[j] && cat_options[j]->toml_key && meta->toml_key &&
1370 strcmp(cat_options[j]->toml_key, meta->toml_key) == 0) {
1371 is_duplicate =
true;
1380 const char *field_ptr = ((
const char *)&defaults) + meta->field_offset;
1383 char mode_default_buffer[OPTIONS_BUFF_SIZE] = {0};
1384 int mode_default_int = 0;
1387 if (meta->mode_default_getter) {
1388 asciichat_mode_t mode = extract_mode_from_bitmask(meta->mode_bitmask);
1389 if (mode != MODE_INVALID) {
1390 const void *default_value = meta->mode_default_getter(mode);
1391 if (default_value) {
1393 if (meta->type == OPTION_TYPE_STRING || meta->type == OPTION_TYPE_CALLBACK) {
1394 const char *str_value = (
const char *)default_value;
1395 SAFE_STRNCPY(mode_default_buffer, str_value,
sizeof(mode_default_buffer));
1396 field_ptr = mode_default_buffer;
1397 }
else if (meta->type == OPTION_TYPE_INT) {
1398 mode_default_int = *(
const int *)default_value;
1399 field_ptr = (
const char *)&mode_default_int;
1406 if (meta->description && strlen(meta->description) > 0) {
1407 if (!config_builder_append(&builder,
"# %s\n", meta->description)) {
1408 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1414 char formatted_value[BUFFER_SIZE_MEDIUM] = {0};
1415 g_type_handlers[meta->type].
format_output(field_ptr, meta->field_size, meta, formatted_value,
1416 sizeof(formatted_value));
1417 const char *output_key = meta->toml_key;
1418 size_t category_len = strlen(category);
1419 if (strncmp(meta->toml_key, category, category_len) == 0 && meta->toml_key[category_len] ==
'.') {
1420 output_key = meta->toml_key + category_len + 1;
1423 if (config_key_should_be_commented(meta->toml_key)) {
1424 if (!config_builder_append(&builder,
"# %s = %s\n", output_key, formatted_value)) {
1425 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1428 if (!config_builder_append(&builder,
"%s = %s\n", output_key, formatted_value)) {
1429 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1435 if (!config_builder_append(&builder,
"\n")) {
1436 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1439 written_flags[opt_idx] =
true;
1443 if (cat_idx < category_count - 1) {
1444 if (!config_builder_append(&builder,
"\n")) {
1445 return SET_ERRNO(ERROR_CONFIG,
"Config too large to fit in buffer");
1451 if (config_path && strlen(config_path) > 0) {
1456 if (!config_path_expanded) {
1460 if (!config_path_expanded) {
1461 return SET_ERRNO(ERROR_CONFIG,
"Failed to resolve config file path");
1464 char *validated_config_path = NULL;
1465 asciichat_error_t validate_result =
1467 if (validate_result != ASCIICHAT_OK) {
1468 SAFE_FREE(validated_config_path);
1469 SAFE_FREE(config_path_expanded);
1470 return validate_result;
1473 if (config_path_expanded != validated_config_path) {
1474 SAFE_FREE(config_path_expanded);
1476 config_path_expanded = validated_config_path;
1480 if (stat(config_path_expanded, &st) == 0) {
1482 log_plain(
"Config file already exists: %s", config_path_expanded);
1486 log_plain(
"Config file creation cancelled.");
1487 return SET_ERRNO(ERROR_CONFIG,
"User cancelled overwrite");
1490 log_plain(
"Overwriting existing config file...");
1496 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate memory for directory path");
1498 defer(SAFE_FREE(dir_path));
1501 char *last_sep = strrchr(dir_path, PATH_DELIM);
1507 if (mkdir_result != ASCIICHAT_OK) {
1508 return mkdir_result;
1515 return SET_ERRNO_SYS(ERROR_CONFIG,
"Failed to open config file for writing: %s", config_path_expanded);
1517 defer(SAFE_FCLOSE(output_file));
1520 size_t written = fwrite(builder.
buffer, 1, builder.
size, output_file);
1521 if (written != builder.
size) {
1522 return SET_ERRNO_SYS(ERROR_CONFIG,
"Failed to write config to file: %s", config_path_expanded);
1528 (void)fflush(stdout);
1532 return ASCIICHAT_OK;
1537 config_file_list_t config_files = {0};
1541 if (search_result != ASCIICHAT_OK) {
1542 CONFIG_DEBUG(
"Failed to search for config files: %d", search_result);
1544 return search_result;
1551 asciichat_error_t result = ASCIICHAT_OK;
1552 for (
size_t i = config_files.count; i > 0; i--) {
1553 const config_file_result_t *file = &config_files.files[i - 1];
1558 bool is_user_config = !file->is_system_config;
1559 bool file_strict = is_user_config ? strict :
false;
1561 CONFIG_DEBUG(
"Loading config from %s (system=%s, strict=%s)", file->path, file->is_system_config ?
"yes" :
"no",
1562 file_strict ?
"true" :
"false");
1564 asciichat_error_t load_result =
config_load_and_apply(detected_mode, file->path, file_strict, opts);
1566 if (load_result != ASCIICHAT_OK) {
1569 CONFIG_DEBUG(
"Strict config loading failed for %s", file->path);
1570 result = load_result;
1573 CONFIG_DEBUG(
"Non-strict config loading warning for %s: %d (continuing)", file->path, load_result);
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all data to file descriptor with automatic retry on transient errors.
color_filter_t color_filter_from_cli_name(const char *cli_name)
const color_filter_def_t * color_filter_get_metadata(color_filter_t filter)
asciichat_error_t config_load_system_and_user(asciichat_mode_t detected_mode, bool strict, options_t *opts)
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)
#define CONFIG_DEBUG(fmt,...)
Print configuration debug message.
#define CONFIG_WARN(fmt,...)
Print configuration warning using the logging system.
options_t options_t_new(void)
bool log_get_terminal_output(void)
bool path_looks_like_path(const char *value)
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
char * expand_path(const char *path)
char * get_config_dir(void)
asciichat_error_t options_state_set(const options_t *opts)
const config_option_metadata_t ** config_schema_get_by_category(const char *category, size_t *count)
const config_option_metadata_t * config_schema_get_all(size_t *count)
Helper structure for building config content in a buffer.
Type handler - encapsulates all 4 operations for one 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.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Safe formatted string printing with va_list.
Union holding all possible parsed option values.
char str_value[OPTIONS_BUFF_SIZE]
bool utf8_is_valid(const char *str)
bool platform_prompt_yes_no(const char *question, bool default_yes)
int validate_opt_render_mode(const char *value_str, char *error_msg, size_t error_msg_size)
int validate_opt_color_mode(const char *value_str, char *error_msg, size_t error_msg_size)
int validate_opt_palette(const char *value_str, char *error_msg, size_t error_msg_size)
FILE * platform_fopen(const char *filename, const char *mode)
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
asciichat_error_t platform_mkdir_recursive(const char *path, int mode)
void config_file_list_destroy(config_file_list_t *list)