ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
options.c File Reference

⚙️ Main entry point for unified options parsing with mode detection More...

Go to the source code of this file.

Data Structures

struct  binary_level_opts_t
 Create a new options_t struct with all defaults set. More...
 

Functions

bool has_action_flag (void)
 Check if an action flag was detected.
 
options_t options_t_new (void)
 
options_t options_t_new_preserve_binary (const options_t *source)
 Create new options struct, preserving binary-level fields from source.
 
asciichat_error_t options_init (int argc, char **argv)
 

Detailed Description

⚙️ Main entry point for unified options parsing with mode detection

Purpose: Implements options_init() - the unified entry point that:

  1. Detects mode from command-line arguments (server, client, mirror, acds, etc.)
  2. Parses binary-level options (–help, –version, –log-file, etc.)
  3. Routes to mode-specific parsing using preset configurations
  4. Loads configuration files if –config specified
  5. Validates all options with cross-field checks
  6. Publishes options via RCU for lock-free thread-safe access

Architecture:

The parsing flow is:

options_init(argc, argv)
├─ Mode Detection
│ └─ Checks for --help, --version, mode keywords, session strings
├─ Binary-Level Option Parsing
│ └─ Handles: --help, --version, --log-file, --verbose, --quiet, etc.
├─ Get Mode-Specific Preset (unified)
│ └─ Uses options_preset_unified() from presets.h
├─ Mode-Specific Option Parsing
│ └─ Builder parses remaining args for the detected mode
├─ Configuration File Loading (if --config specified)
│ └─ Loads and merges TOML config file
├─ Validation
│ └─ Applies defaults, validates ranges, cross-field checks
└─ RCU Publishing
└─ Makes options available via options_get() and GET_OPTION()
asciichat_error_t options_init(int argc, char **argv)
action_args_t args
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:51
const options_t * options_get(void)
Definition rcu.c:347

Key Components:

  • detect_mode_and_parse_binary_options(): Mode detection and binary-level parsing
  • options_preset_unified(): Preset config for all modes (from presets.h)
  • options_config_parse_args(): Builder-based parsing of mode-specific options
  • Configuration file loading (if –config specified)
  • options_state_init() and options_state_set(): RCU publishing

Option Lifecycle in options_init():

  1. Create default options via options_t_new()
  2. Detect mode from argv
  3. Parse binary-level options (may exit for –help, –version)
  4. Get mode-specific preset from registry
  5. Parse mode-specific options into options struct
  6. Load config file if specified
  7. Validate all options with defaults
  8. Initialize RCU state
  9. Publish options for lock-free access

Error Handling:

  • Invalid options → ERROR_USAGE (usage already printed to stderr)
  • File errors → ERROR_IO or ERROR_CONFIG
  • Validation failures → ERROR_VALIDATION with context message
  • Success → ASCIICHAT_OK

Thread Safety:

  • options_init() must be called exactly once before worker threads
  • After options_init() completes, options are read-only
  • Access via lock-free GET_OPTION() macro from worker threads
  • Optional updates via options_set_*() functions
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Definition in file lib/options/options.c.

Function Documentation

◆ has_action_flag()

bool has_action_flag ( void  )

Check if an action flag was detected.

Returns
true if an action flag was passed

Definition at line 133 of file lib/options/options.c.

133 {
134 return g_action_flag;
135}

◆ options_init()

asciichat_error_t options_init ( int  argc,
char **  argv 
)

Definition at line 828 of file lib/options/options.c.

828 {
829 // NOTE: --grep filter is initialized in main.c BEFORE any logging starts
830 // This allows ALL logs (including from shared_init) to be filtered
831 // Validate arguments (safety check for tests)
832 if (argc < 0 || argc > 128) {
833 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid argc: %d", argc);
834 }
835 if (argv == NULL) {
836 return SET_ERRNO(ERROR_INVALID_PARAM, "argv is NULL");
837 }
838 // Validate all argv elements are non-NULL up to argc
839 for (int i = 0; i < argc; i++) {
840 if (argv[i] == NULL) {
841 return SET_ERRNO(ERROR_INVALID_PARAM, "argv[%d] is NULL (argc=%d)", i, argc);
842 }
843 }
844 // Initialize RCU options system (must be done before any threads start)
845 // This must happen FIRST, before any other initialization
846 asciichat_error_t rcu_init_result = options_state_init();
847 if (rcu_init_result != ASCIICHAT_OK) {
848 return rcu_init_result;
849 }
850 // ========================================================================
851 // STAGE 1: Mode Detection and Binary-Level Option Handling
852 // ========================================================================
853
854 // Check for binary-level actions FIRST (before mode detection)
855 // These actions may take arguments, so we need to check them before mode detection
856 bool show_version = false;
857 bool create_config = false;
858 bool create_manpage = false;
859 bool has_action = false; // Track if any action flag is present
860 const char *config_create_path = NULL;
861 const char *manpage_create_path = NULL;
862
863 // ========================================================================
864 // STAGE 1A: Quick scan for action flags FIRST (they bypass mode detection)
865 // ========================================================================
866 // Quick scan for action flags (they may have arguments)
867 // This must happen BEFORE logging initialization so we can suppress logs before shared_init()
868 // Also scan for --quiet / -q so we can suppress logging from the start
869 // Also scan for --color early so it affects help output colors
870 bool user_quiet = false;
871 int parsed_color_setting = COLOR_SETTING_AUTO; // Store parsed color value until opts is created
872 bool color_setting_found = false;
873 bool check_update_flag_seen = false;
874 bool no_check_update_flag_seen = false;
875
876 // FIRST: Scan entire argv for --help (special case - works before OR after mode)
877 for (int i = 1; i < argc; i++) {
878 if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
879 // Scan backwards to find if a mode was specified before --help
880 asciichat_mode_t help_mode = MODE_DISCOVERY; // Default to discovery mode (binary-level help)
881 for (int j = i - 1; j >= 1; j--) {
882 if (argv[j][0] != '-') {
883 if (strcmp(argv[j], "server") == 0) {
884 help_mode = MODE_SERVER;
885 } else if (strcmp(argv[j], "client") == 0) {
886 help_mode = MODE_CLIENT;
887 } else if (strcmp(argv[j], "mirror") == 0) {
888 help_mode = MODE_MIRROR;
889 } else if (strcmp(argv[j], "discovery-service") == 0) {
890 help_mode = MODE_DISCOVERY_SERVICE;
891 } else if (strcmp(argv[j], "discovery") == 0) {
892 help_mode = MODE_DISCOVERY;
893 }
894 break; // Found first non-flag argument
895 }
896 }
897
898 // Show help for the detected mode (or binary-level if no mode)
899 usage(stdout, help_mode);
900 fflush(NULL);
901 _Exit(0);
902 }
903 }
904
905 // THEN: Scan for other binary-level actions (stops at mode name)
906 for (int i = 1; i < argc; i++) {
907 // Stop scanning at mode name - binary-level options must come before the mode
908 if (argv[i][0] != '-') {
909 bool is_mode =
910 (strcmp(argv[i], "server") == 0 || strcmp(argv[i], "client") == 0 || strcmp(argv[i], "mirror") == 0 ||
911 strcmp(argv[i], "discovery") == 0 || strcmp(argv[i], "discovery-service") == 0);
912 if (is_mode) {
913 break; // Stop processing at mode name
914 }
915 }
916 if (argv[i][0] == '-') {
917 // Handle --quiet, -q, --quiet=value formats
918 bool temp_quiet = false;
919 if (parse_binary_bool_arg(argv[i], &temp_quiet, "quiet", 'q')) {
920 if (temp_quiet) {
921 user_quiet = true;
922 }
923 }
924 // Validate --log-level and --log-file require arguments
925 if (strcmp(argv[i], "--log-level") == 0) {
926 if (i + 1 >= argc || argv[i + 1][0] == '-') {
927 log_plain_stderr("Error: --log-level requires a value (dev, debug, info, warn, error, fatal)");
928 return ERROR_USAGE;
929 }
930 i++; // Skip the argument in this loop
931 }
932 if (strcmp(argv[i], "-L") == 0 || strcmp(argv[i], "--log-file") == 0) {
933 if (i + 1 >= argc || argv[i + 1][0] == '-') {
934 log_plain_stderr("Error: %s requires a file path", argv[i]);
935 return ERROR_USAGE;
936 }
937 i++; // Skip the argument in this loop
938 }
939 // --help is handled in the first scan loop above (works before OR after mode)
940 // Parse --color early so it affects help output colors
941 if (strcmp(argv[i], "--color") == 0) {
942 if (i + 1 < argc && argv[i + 1][0] != '-') {
943 // Check if next arg is a mode name (not a color value)
944 const char *next_arg = argv[i + 1];
945 bool is_mode =
946 (strcmp(next_arg, "server") == 0 || strcmp(next_arg, "client") == 0 || strcmp(next_arg, "mirror") == 0 ||
947 strcmp(next_arg, "discovery") == 0 || strcmp(next_arg, "discovery-service") == 0);
948
949 if (is_mode) {
950 // Next arg is a mode, not a color value - default to true
951 parsed_color_setting = COLOR_SETTING_TRUE;
952 color_setting_found = true;
953 } else {
954 // Try to parse as color value
955 char *error_msg = NULL;
956 if (parse_color_setting(next_arg, &parsed_color_setting, &error_msg)) {
957 color_setting_found = true;
958 i++; // Skip the setting argument
959 } else {
960 if (error_msg) {
961 log_error("Error parsing --color: %s", error_msg);
962 free(error_msg);
963 }
964 }
965 }
966 } else {
967 // --color without argument defaults to true (enable colors)
968 parsed_color_setting = COLOR_SETTING_TRUE;
969 color_setting_found = true;
970 }
971 }
972 // Check for --color=value format
973 if (strncmp(argv[i], "--color=", 8) == 0) {
974 const char *value = argv[i] + 8;
975 char *error_msg = NULL;
976 if (parse_color_setting(value, &parsed_color_setting, &error_msg)) {
977 color_setting_found = true;
978 } else {
979 if (error_msg) {
980 log_error("Error parsing --color: %s", error_msg);
981 free(error_msg);
982 }
983 }
984 }
985 if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) {
986 show_version = true;
987 has_action = true;
988 break;
989 }
990 if (strcmp(argv[i], "--check-update") == 0) {
991 check_update_flag_seen = true;
992 if (no_check_update_flag_seen) {
993 log_plain_stderr("Error: Cannot specify both --check-update and --no-check-update");
994 return ERROR_USAGE;
995 }
996 has_action = true;
998 // action_check_update_immediate() calls _Exit(), so we don't reach here
999 break;
1000 }
1001 if (strcmp(argv[i], "--no-check-update") == 0) {
1002 no_check_update_flag_seen = true;
1003 if (check_update_flag_seen) {
1004 log_plain_stderr("Error: Cannot specify both --check-update and --no-check-update");
1005 return ERROR_USAGE;
1006 }
1007 // Flag will be parsed normally later
1008 }
1009 if (strcmp(argv[i], "--config-create") == 0) {
1010 create_config = true;
1011 has_action = true;
1012 // Check for optional [FILE] argument
1013 if (i + 1 < argc && argv[i + 1][0] != '-') {
1014 config_create_path = argv[i + 1];
1015 i++; // Consume the file path argument
1016 }
1017 break;
1018 }
1019 if (strcmp(argv[i], "--man-page-create") == 0) {
1020 create_manpage = true;
1021 has_action = true;
1022 // Check for optional [FILE] argument
1023 if (i + 1 < argc && argv[i + 1][0] != '-') {
1024 manpage_create_path = argv[i + 1];
1025 i++; // Consume the file path argument
1026 }
1027 break;
1028 }
1029 if (strcmp(argv[i], "--completions") == 0) {
1030 has_action = true;
1031 // Handle --completions: generate shell completion scripts
1032 if (i + 1 < argc && argv[i + 1][0] != '-') {
1033 const char *shell_name = argv[i + 1];
1034 i++; // Consume shell name
1035
1036 // Check for optional output file
1037 const char *output_file = NULL;
1038 if (i + 1 < argc && argv[i + 1][0] != '-') {
1039 output_file = argv[i + 1];
1040 i++; // Consume output file
1041 }
1042
1043 action_completions(shell_name, output_file);
1044 // action_completions() calls _Exit(), so we don't reach here
1045 } else {
1046 log_plain_stderr("Error: --completions requires shell name (bash, fish, zsh, powershell)");
1047 return ERROR_USAGE;
1048 }
1049 break; // Unreachable, but for clarity
1050 }
1051 if (strcmp(argv[i], "--list-webcams") == 0) {
1052 has_action = true;
1054 // action_list_webcams() calls _Exit(), so we don't reach here
1055 break;
1056 }
1057 if (strcmp(argv[i], "--list-microphones") == 0) {
1058 has_action = true;
1060 // action_list_microphones() calls _Exit(), so we don't reach here
1061 break;
1062 }
1063 if (strcmp(argv[i], "--list-speakers") == 0) {
1064 has_action = true;
1066 // action_list_speakers() calls _Exit(), so we don't reach here
1067 break;
1068 }
1069 // Check for --show-capabilities (binary-level action)
1070 if (strcmp(argv[i], "--show-capabilities") == 0) {
1071 has_action = true;
1073 // action_show_capabilities_immediate() calls _Exit(), so we don't reach here
1074 break;
1075 }
1076 }
1077 }
1078 // Store action flag globally for use during cleanup
1079 set_action_flag(has_action);
1080 // ========================================================================
1081 // STAGE 1B: DO MODE DETECTION EARLY (needed for log_init)
1082 // ========================================================================
1083 asciichat_mode_t detected_mode = MODE_DISCOVERY; // Default mode
1084 char detected_session_string[SESSION_STRING_BUFFER_SIZE] = {0};
1085 int mode_index = -1;
1086
1087 asciichat_error_t mode_detect_result =
1088 options_detect_mode(argc, argv, &detected_mode, detected_session_string, &mode_index);
1089 if (mode_detect_result != ASCIICHAT_OK) {
1090 return mode_detect_result;
1091 }
1092
1093 // VALIDATE: Binary-level options must appear BEFORE the mode
1094 // Check if any binary-level options appear after the mode position
1095 if (mode_index > 0) {
1096 for (int i = mode_index + 1; i < argc; i++) {
1097 if (argv[i][0] == '-') {
1098 bool takes_arg = false;
1099 bool takes_optional_arg = false;
1100
1101 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1102 return SET_ERRNO(ERROR_USAGE, "Binary-level option '%s' must appear before the mode '%s', not after it",
1103 argv[i], argv[mode_index]);
1104 }
1105 }
1106 }
1107 }
1108 // ========================================================================
1109 // STAGE 1C: Initialize logging EARLY (before any log_dev calls)
1110 // ========================================================================
1111 // Create local options struct and initialize with defaults
1112 options_t opts = options_t_new(); // Initialize with all defaults
1113 opts.detected_mode = detected_mode;
1114 char *log_filename = options_get_log_filepath(detected_mode, opts);
1115 SAFE_SNPRINTF(opts.log_file, OPTIONS_BUFF_SIZE, "%s", log_filename);
1116 // Force stderr when stdout is not a TTY (piping or redirecting output)
1117 bool force_stderr = terminal_is_piped_output();
1118 log_init(opts.log_file, GET_OPTION(log_level), force_stderr, false);
1119 // NOTE: --color detection now happens in src/main.c BEFORE asciichat_shared_init()
1120 // This ensures g_color_flag_passed and g_color_flag_value are set before any logging.
1121 //
1122 // NOTE: Timer system and shared subsystems are initialized by src/main.c
1123 // via asciichat_shared_init() BEFORE options_init() is called.
1124 // This allows options_init() to use properly configured logging.
1125
1126 // If an action flag is detected OR user passed --quiet, silence logs for clean output
1127 if (user_quiet || has_action) {
1128 log_set_terminal_output(false); // Suppress console logging for clean action output
1129 }
1130 // Apply parsed color setting from STAGE 1A
1131 if (color_setting_found) {
1132 opts.color = parsed_color_setting;
1133 } else {
1134 opts.color = OPT_COLOR_DEFAULT; // Apply default
1135 }
1136
1137 // If we found version/config-create/create-manpage, handle them immediately (before mode detection)
1138 if (show_version || create_config || create_manpage) {
1139 if (show_version) {
1140 opts.version = true;
1141 options_state_set(&opts);
1142 return ASCIICHAT_OK;
1143 }
1144 if (create_config) {
1145 // Build the schema first so config_create_default can generate options from it
1146 const options_config_t *unified_config = options_preset_unified(NULL, NULL);
1147 if (unified_config) {
1148 asciichat_error_t schema_build_result = config_schema_build_from_configs(&unified_config, 1);
1149 if (schema_build_result != ASCIICHAT_OK) {
1150 // Schema build failed, but continue anyway
1151 (void)schema_build_result;
1152 }
1153 options_config_destroy(unified_config);
1154 }
1155
1156 // Call action handler which handles all output and prompts properly
1157 action_create_config(config_create_path);
1158 // action_create_config() calls _Exit(), so we don't reach here
1159 }
1160 if (create_manpage) {
1161 // Call action handler which handles all output and prompts properly
1162 action_create_manpage(manpage_create_path);
1163 // action_create_manpage() calls _Exit(), so we don't reach here
1164 }
1165 }
1166
1167 // Mode detection and logging already initialized early in STAGE 1B/1C above
1168 // (moved earlier to ensure logging is available before any log_dev() calls)
1169
1170 // Check for binary-level options that can appear before or after mode
1171 // Search entire argv to find --quiet, --log-file, --log-level, -V, etc.
1172 // These are documented as binary-level options that can appear anywhere
1173 for (int i = 1; i < argc; i++) {
1174 if (argv[i][0] == '-') {
1175 // Handle -V and --verbose (stackable verbosity)
1176 if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--verbose") == 0) {
1177 // Check if next argument is a number (optional argument)
1178 if (i + 1 < argc && argv[i + 1][0] != '-') {
1179 // Try to parse as integer count
1180 char *endptr;
1181 long value = strtol(argv[i + 1], &endptr, 10);
1182 if (*endptr == '\0' && value >= 0 && value <= 100) {
1183 opts.verbose_level = (unsigned short int)value;
1184 i++; // Skip next argument
1185 continue;
1186 }
1187 }
1188 // No valid number, just increment
1189 opts.verbose_level++;
1190 }
1191 // Handle binary-level boolean options using the abstraction function
1192 // -q/--quiet, --json, --log-format-console all use the same parser
1193 if (parse_binary_bool_arg(argv[i], &opts.quiet, "quiet", 'q')) {
1194 continue;
1195 }
1196 if (parse_binary_bool_arg(argv[i], &opts.json, "json", '\0')) {
1197 continue;
1198 }
1199 if (parse_binary_bool_arg(argv[i], &opts.log_format_console_only, "log-format-console", '\0')) {
1200 continue;
1201 }
1202 // Handle --log-level LEVEL (set log threshold)
1203 if (strcmp(argv[i], "--log-level") == 0) {
1204 if (i + 1 >= argc) {
1205 log_plain_stderr("Error: --log-level requires a value (dev, debug, info, warn, error, fatal)");
1206 return ERROR_USAGE;
1207 }
1208 if (argv[i + 1][0] == '-') {
1209 log_plain_stderr("Error: --log-level requires a value (dev, debug, info, warn, error, fatal)");
1210 return ERROR_USAGE;
1211 }
1212 char *error_msg = NULL;
1213 if (parse_log_level(argv[i + 1], &opts.log_level, &error_msg)) {
1214 i++; // Skip the level argument
1215 } else {
1216 if (error_msg) {
1217 log_plain_stderr("Error: %s", error_msg);
1218 free(error_msg);
1219 } else {
1220 log_plain_stderr("Error: invalid log level value: %s", argv[i + 1]);
1221 }
1222 return ERROR_USAGE;
1223 }
1224 }
1225 // Handle -L and --log-file FILE (set log file path)
1226 if ((strcmp(argv[i], "-L") == 0 || strcmp(argv[i], "--log-file") == 0)) {
1227 if (i + 1 >= argc) {
1228 log_plain_stderr("Error: %s requires a file path", argv[i]);
1229 return ERROR_USAGE;
1230 }
1231 if (argv[i + 1][0] == '-') {
1232 log_plain_stderr("Error: %s requires a file path", argv[i]);
1233 return ERROR_USAGE;
1234 }
1235 SAFE_STRNCPY(opts.log_file, argv[i + 1], sizeof(opts.log_file));
1236 i++; // Skip the file argument
1237 }
1238 // NOTE: --color is now parsed in STAGE 1A (early, before help processing)
1239 // to ensure help output colors are applied correctly
1240 }
1241 }
1242
1243 if (show_version) {
1244 // Show binary-level version from src/main.c
1245 opts.version = true;
1246 options_state_set(&opts);
1247 return ASCIICHAT_OK;
1248 }
1249
1250 if (create_config) {
1251 // Handle --config-create: create default config file and exit
1252 // Use provided path or default to user config location
1253 char config_path[PLATFORM_MAX_PATH_LENGTH];
1254 if (config_create_path) {
1255 SAFE_STRNCPY(config_path, config_create_path, sizeof(config_path));
1256 } else {
1257 // Use default config path: ~/.ascii-chat/config.toml
1258 char *config_dir = get_config_dir();
1259 if (!config_dir) {
1260 log_error("Error: Failed to determine default config directory");
1261 return ERROR_CONFIG;
1262 }
1263 safe_snprintf(config_path, sizeof(config_path), "%sconfig.toml", config_dir);
1264 SAFE_FREE(config_dir);
1265 }
1266
1267 // Create config with default options
1268 asciichat_error_t result = config_create_default(config_path);
1269 if (result != ASCIICHAT_OK) {
1270 asciichat_error_context_t err_ctx;
1271 if (HAS_ERRNO(&err_ctx)) {
1272 log_error("Error creating config: %s", err_ctx.context_message);
1273 } else {
1274 log_error("Error: Failed to create config file at %s", config_path);
1275 }
1276 return result;
1277 }
1278
1279 log_plain("Created default config file at: %s", config_path);
1280 return ASCIICHAT_OK; // Return successfully after creating config
1281 }
1282
1283 if (create_manpage) {
1284 // Handle --create-man-page-template: generate merged man page template
1285 // The .1.in file is the existing template to read from (not the output)
1286 const char *existing_template_path = "share/man/man1/ascii-chat.1.in";
1287 options_config_t *config = options_preset_unified(NULL, NULL);
1288 if (!config) {
1289 log_error("Error: Failed to get binary options config");
1290 return ERROR_MEMORY;
1291 }
1292
1293 // Generate merged man page from embedded or filesystem resources
1294 // (existing_template_path and manpage_content_file parameters are no longer supported)
1295 asciichat_error_t err = options_config_generate_manpage_merged(config, "ascii-chat", NULL, existing_template_path,
1296 "Video chat in your terminal");
1297
1298 options_config_destroy(config);
1299
1300 if (err != ASCIICHAT_OK) {
1301 asciichat_error_context_t err_ctx;
1302 if (HAS_ERRNO(&err_ctx)) {
1303 log_error("%s", err_ctx.context_message);
1304 } else {
1305 log_error("Error: Failed to generate man page");
1306 }
1307 return err;
1308 }
1309
1310 log_plain("Generated man page: %s", existing_template_path);
1311 return ASCIICHAT_OK; // Return successfully after generating man page
1312 }
1313
1314 // ========================================================================
1315 // STAGE 2: Build argv for mode-specific parsing
1316 // ========================================================================
1317 // If mode was found, build argv with only arguments after the mode
1318 // If mode_index == -1, filter out binary-level options from all arguments
1319 int mode_argc = argc;
1320 char **mode_argv = (char **)argv;
1321 char **allocated_mode_argv = NULL; // Track if we need to free mode_argv
1322
1323 if (mode_index == -1) {
1324 // No explicit mode - filter binary-level options from entire argv
1325 int max_mode_argc = argc; // Worst case: no binary opts skipped
1326
1327 if (max_mode_argc > 256) {
1328 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many arguments: %d", max_mode_argc);
1329 }
1330
1331 char **new_mode_argv = SAFE_MALLOC((size_t)(max_mode_argc + 1) * sizeof(char *), char **);
1332 if (!new_mode_argv) {
1333 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate mode_argv");
1334 }
1335 allocated_mode_argv = new_mode_argv;
1336
1337 new_mode_argv[0] = argv[0]; // Copy program name
1338
1339 // Copy all args except binary-level options
1340 int new_argv_idx = 1;
1341 for (int i = 1; i < argc; i++) {
1342 bool takes_arg = false;
1343 bool takes_optional_arg = false;
1344
1345 // Check if this is a binary-level option
1346 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1347 // Skip argument if needed
1348 if (takes_arg && i + 1 < argc) {
1349 i++; // Skip required argument
1350 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
1351 i++; // Skip optional argument
1352 }
1353 continue;
1354 }
1355 // Not a binary option, copy to mode_argv
1356 new_mode_argv[new_argv_idx++] = argv[i];
1357 }
1358
1359 mode_argc = new_argv_idx;
1360 new_mode_argv[mode_argc] = NULL;
1361 mode_argv = new_mode_argv;
1362 } else if (mode_index != -1) {
1363 // Mode found at position mode_index
1364 // Build new argv: [program_name, args_before_mode (no binary opts)..., args_after_mode...]
1365 // Binary-level options are not passed to mode-specific parsers
1366
1367 int args_after_mode = argc - mode_index - 1;
1368 // We'll calculate final argc after skipping binary options
1369 int max_mode_argc = 1 + (mode_index - 1) + args_after_mode; // Worst case: no binary opts skipped
1370
1371 if (max_mode_argc > 256) {
1372 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many arguments: %d", max_mode_argc);
1373 }
1374
1375 // Allocate max_mode_argc+1 to accommodate NULL terminator
1376 char **new_mode_argv = SAFE_MALLOC((size_t)(max_mode_argc + 1) * sizeof(char *), char **);
1377 if (!new_mode_argv) {
1378 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate mode_argv");
1379 }
1380 allocated_mode_argv = new_mode_argv; // Track allocation for cleanup
1381
1382 // Copy: [program_name, args_before_mode (excluding binary opts)..., args_after_mode...]
1383 if (mode_index == 0) {
1384 // Mode is at argv[0], use "ascii-chat" as program name
1385 new_mode_argv[0] = "ascii-chat";
1386 } else {
1387 new_mode_argv[0] = argv[0];
1388 }
1389
1390 // Copy args before mode, skipping binary-level options
1391 int new_argv_idx = 1;
1392 for (int i = 1; i < mode_index; i++) {
1393 bool takes_arg = false;
1394 bool takes_optional_arg = false;
1395
1396 // Check if this is a binary-level option
1397 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1398 // Skip argument if needed
1399 if (takes_arg && i + 1 < mode_index) {
1400 i++; // Skip required argument
1401 } else if (takes_optional_arg && i + 1 < mode_index && argv[i + 1][0] != '-' &&
1402 isdigit((unsigned char)argv[i + 1][0])) {
1403 i++; // Skip optional numeric argument
1404 }
1405 continue;
1406 }
1407 // Not a binary option, copy to mode_argv
1408 new_mode_argv[new_argv_idx++] = argv[i];
1409 }
1410 // Copy args after mode, filtering out any binary-level options (they shouldn't appear here)
1411 int args_after_mode_idx = 0;
1412 for (int i = mode_index + 1; i < argc; i++) {
1413 bool takes_arg = false;
1414 bool takes_optional_arg = false;
1415
1416 // Check if this is a binary-level option (shouldn't appear after mode)
1417 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1418 // Skip binary-level option and its argument if needed
1419 if (takes_arg && i + 1 < argc) {
1420 i++; // Skip required argument
1421 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
1422 i++; // Skip optional argument
1423 }
1424 continue; // Skip this option
1425 }
1426 // Not a binary option, copy to mode_argv
1427 new_mode_argv[new_argv_idx + args_after_mode_idx++] = argv[i];
1428 }
1429 // Calculate actual argc (program + filtered args before + filtered args after)
1430 mode_argc = new_argv_idx + args_after_mode_idx;
1431 new_mode_argv[mode_argc] = NULL;
1432
1433 mode_argv = new_mode_argv;
1434 }
1435
1436 // ========================================================================
1437 // STAGE 3: Set Mode-Specific Defaults
1438 // ========================================================================
1439 // Initialize all defaults using options_t_new_preserve_binary() to keep binary-level
1440 // options (--quiet, --verbose, --log-level, etc.) from being reset
1441 opts = options_t_new_preserve_binary(&opts);
1442 // If a session string was detected during mode detection, restore it after options reset
1443 // This ensures discovery mode knows which session to join
1444 if (detected_session_string[0] != '\0') {
1445 SAFE_STRNCPY(opts.session_string, detected_session_string, sizeof(opts.session_string));
1446 log_info("options_init: Detected session string from argv: '%s'", detected_session_string);
1447 }
1448
1449 // Encryption options default to disabled/empty
1450 opts.no_encrypt = 0;
1451 opts.encrypt_key[0] = '\0';
1452 opts.password[0] = '\0';
1453 opts.encrypt_keyfile[0] = '\0';
1454 opts.server_key[0] = '\0';
1455 opts.client_keys[0] = '\0';
1456 opts.palette_custom[0] = '\0';
1457
1458 // Set different default addresses for different modes
1459 if (detected_mode == MODE_CLIENT || detected_mode == MODE_MIRROR || detected_mode == MODE_DISCOVERY) {
1460 // Client/Mirror/Discovery: connects to localhost by default (discovery uses ACDS to find server)
1461 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "localhost");
1462 opts.address6[0] = '\0'; // Client doesn't use address6
1463 } else if (detected_mode == MODE_SERVER) {
1464 // Server: binds to 127.0.0.1 (IPv4) and ::1 (IPv6) by default
1465 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "127.0.0.1");
1466 // address6 is now a positional argument, not an option
1467 opts.address6[0] = '\0';
1468 } else if (detected_mode == MODE_DISCOVERY_SERVICE) {
1469 // ACDS: binds to all interfaces by default
1470 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "0.0.0.0");
1471 opts.address6[0] = '\0';
1472 }
1473
1474 // ========================================================================
1475 // STAGE 4: Build Dynamic Schema from Unified Options Config
1476 // ========================================================================
1477 // Build the config schema dynamically from the unified config
1478 // This generates TOML keys, CLI flags, categories, and types from builder data
1479 const options_config_t *unified_config = options_preset_unified(NULL, NULL);
1480 if (unified_config) {
1481 asciichat_error_t schema_build_result = config_schema_build_from_configs(&unified_config, 1);
1482 if (schema_build_result != ASCIICHAT_OK) {
1483 // Schema build failed, but continue with static schema as fallback
1484 (void)schema_build_result;
1485 } else {
1486 }
1487 options_config_destroy(unified_config);
1488 } else {
1489 }
1490
1491 // ========================================================================
1492 // STAGE 5: Load Configuration Files
1493 // ========================================================================
1494 // Extract binary-level options BEFORE config loading (config may reset them)
1495 binary_level_opts_t binary_before_config = extract_binary_level(&opts);
1496 // Publish options to RCU before config loading so that config logs get proper colors
1497 // from the parsed --color setting (e.g., --color=true)
1498 asciichat_error_t config_publish_result = options_state_set(&opts);
1499 if (config_publish_result != ASCIICHAT_OK) {
1500 } else {
1501 }
1502 // Load config files - now uses detected_mode directly for bitmask validation
1503 // Save flip_x and flip_y as they should not be reset by config files
1504 // Also save encryption settings - they should only be controlled via CLI, not config file
1505 bool saved_flip_x_from_config = opts.flip_x;
1506 bool saved_flip_y_from_config = opts.flip_y;
1507 bool saved_encrypt_enabled = opts.encrypt_enabled;
1508 asciichat_error_t config_result = config_load_system_and_user(detected_mode, false, &opts);
1509 (void)config_result; // Continue with defaults and CLI parsing regardless of result
1510 // Restore binary-level options (don't let config override command-line options)
1511 restore_binary_level(&opts, &binary_before_config);
1512
1513 // Restore flip_x and flip_y - config shouldn't override the defaults
1514 opts.flip_x = saved_flip_x_from_config;
1515 opts.flip_y = saved_flip_y_from_config;
1516
1517 // Restore encrypt_enabled - it should only be set via CLI, not config file
1518 // The config can set key/password which auto-enables encryption, but the encrypt_enabled
1519 // flag itself should stay at its default unless the user explicitly passes --encrypt
1520 opts.encrypt_enabled = saved_encrypt_enabled;
1521
1522 // ========================================================================
1523 // STAGE 6: Parse Command-Line Arguments (Unified)
1524 // ========================================================================
1525 // Extract binary-level options BEFORE applying unified defaults
1526 // (which might override them from config files)
1527 binary_level_opts_t binary_before_defaults = extract_binary_level(&opts);
1528 asciichat_mode_t mode_saved_for_parsing = detected_mode; // CRITICAL: Save before defaults reset
1529 // Get unified config
1530 options_config_t *config = options_preset_unified(NULL, NULL);
1531 if (!config) {
1532 SAFE_FREE(allocated_mode_argv);
1533 return SET_ERRNO(ERROR_CONFIG, "Failed to create options configuration");
1534 }
1535 int remaining_argc;
1536 char **remaining_argv;
1537
1538 // Save flip_x and flip_y before applying defaults (should not be reset by defaults)
1539 bool saved_flip_x = opts.flip_x;
1540 bool saved_flip_y = opts.flip_y;
1541 // Apply defaults from unified config
1542 asciichat_error_t defaults_result = options_config_set_defaults(config, &opts);
1543 if (defaults_result != ASCIICHAT_OK) {
1544 options_config_destroy(config);
1545 SAFE_FREE(allocated_mode_argv);
1546 return defaults_result;
1547 }
1548 // Restore binary-level options (they should never be overridden by defaults)
1549 restore_binary_level(&opts, &binary_before_defaults);
1550
1551 // Restore flip_x and flip_y - they should keep the values from options_t_new()
1552 // unless explicitly set by the user (but defaults shouldn't override them)
1553 opts.flip_x = saved_flip_x;
1554 opts.flip_y = saved_flip_y;
1555
1556 // Restore detected_mode before parsing so mode validation works.
1557 opts.detected_mode = mode_saved_for_parsing;
1558
1559 // Save flip_x and flip_y before parsing - they should not be reset by the parser
1560 bool saved_flip_x_for_parse = opts.flip_x;
1561 bool saved_flip_y_for_parse = opts.flip_y;
1562 // Note: json is already saved via extract_binary_level/restore_binary_level mechanism
1563 // Parse mode-specific arguments
1564 option_mode_bitmask_t mode_bitmask = (1 << mode_saved_for_parsing);
1565 asciichat_error_t result =
1566 options_config_parse(config, mode_argc, mode_argv, &opts, mode_bitmask, &remaining_argc, &remaining_argv);
1567 // Restore flip_x and flip_y - they should keep their values unless explicitly overridden
1568 opts.flip_x = saved_flip_x_for_parse;
1569 opts.flip_y = saved_flip_y_for_parse;
1570 // json is already restored via the call to options_state_set which calls restore_binary_level
1571 if (result != ASCIICHAT_OK) {
1572 options_config_destroy(config);
1573 SAFE_FREE(allocated_mode_argv);
1574 // Convert ERROR_CONFIG to ERROR_USAGE for command-line parsing errors
1575 if (result == ERROR_CONFIG) {
1576 return ERROR_USAGE;
1577 }
1578 return result;
1579 }
1580
1581 // ========================================================================
1582 // STAGE 6.5: Publish Parsed Options Early
1583 // ========================================================================
1584 // Publish options to RCU as soon as they're parsed
1585 // This ensures GET_OPTION() works during cleanup even if validation fails
1586 log_debug("Publishing parsed options to RCU before validation");
1587 asciichat_error_t early_publish = options_state_set(&opts);
1588 if (early_publish != ASCIICHAT_OK) {
1589 log_error("Failed to publish parsed options to RCU state early");
1590 options_config_destroy(config);
1591 SAFE_FREE(allocated_mode_argv);
1592 return early_publish;
1593 }
1594 log_debug("Successfully published options to RCU");
1595
1596 // Auto-enable custom palette if palette-chars was set
1597 if (opts.palette_custom[0] != '\0') {
1598 // palette-chars was set - always use PALETTE_CUSTOM (overrides any explicit --palette setting)
1599 opts.palette_type = PALETTE_CUSTOM;
1600 opts.palette_custom_set = true;
1601 log_debug("Set PALETTE_CUSTOM because --palette-chars was provided");
1602
1603 // Validate palette characters for UTF-8 correctness
1604 if (!utf8_is_valid(opts.palette_custom)) {
1605 log_error("Error: --palette-chars contains invalid UTF-8 sequences");
1606 options_config_destroy(config);
1607 SAFE_FREE(allocated_mode_argv);
1608 return option_error_invalid();
1609 }
1610
1611 // Check if palette contains non-ASCII characters
1612 bool has_non_ascii = !utf8_is_ascii_only(opts.palette_custom);
1613 if (has_non_ascii) {
1614 // Non-ASCII characters require UTF-8 support
1615 // Check if UTF-8 is explicitly disabled or unavailable
1616 bool utf8_disabled = (opts.force_utf8 == COLOR_SETTING_FALSE);
1617 bool utf8_auto_unavailable = (opts.force_utf8 == COLOR_SETTING_AUTO && !terminal_supports_utf8());
1618
1619 if (utf8_disabled) {
1620 log_error("Error: --palette-chars contains non-ASCII characters but --utf8=false was specified");
1621 log_error(" Remove --utf8=false or use ASCII-only palette characters");
1622 options_config_destroy(config);
1623 SAFE_FREE(allocated_mode_argv);
1624 return option_error_invalid();
1625 }
1626
1627 if (utf8_auto_unavailable) {
1628 log_error("Error: --palette-chars contains non-ASCII characters but terminal does not support UTF-8");
1629 log_error(" Use --utf8=true to force UTF-8 mode or use ASCII-only palette characters");
1630 options_config_destroy(config);
1631 SAFE_FREE(allocated_mode_argv);
1632 return option_error_invalid();
1633 }
1634 }
1635 }
1636
1637 // Auto-enable encryption if key was provided
1638 if (opts.encrypt_key[0] != '\0') {
1639 // Validate key file exists (skip for remote/virtual keys)
1640 if (!is_remote_key_path(opts.encrypt_key)) {
1641 struct stat st;
1642 if (stat(opts.encrypt_key, &st) != 0) {
1643 log_error("Key file not found: %s", opts.encrypt_key);
1644 options_config_destroy(config);
1645 SAFE_FREE(allocated_mode_argv);
1646 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key file not found: %s", opts.encrypt_key);
1647 }
1648 if ((st.st_mode & S_IFMT) != S_IFREG) {
1649 log_error("Key path is not a regular file: %s", opts.encrypt_key);
1650 options_config_destroy(config);
1651 SAFE_FREE(allocated_mode_argv);
1652 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key path is not a regular file: %s", opts.encrypt_key);
1653 }
1654 }
1655 opts.encrypt_enabled = 1;
1656 log_debug("Auto-enabled encryption because --key was provided");
1657 }
1658
1659 // Color filter validation and auto-enable
1660 if (opts.color_filter != COLOR_FILTER_NONE) {
1661 // Color filter requires color to be enabled
1662 if (opts.color == COLOR_SETTING_FALSE) {
1663 log_error("Error: --color-filter cannot be used with --color=false");
1664 options_config_destroy(config);
1665 SAFE_FREE(allocated_mode_argv);
1666 return option_error_invalid();
1667 }
1668 // Auto-enable color when color filter is specified
1669 opts.color = COLOR_SETTING_TRUE;
1670 log_debug("Auto-enabled color because --color-filter was provided");
1671 }
1672
1673 // Detect if splash or status_screen were explicitly set on command line
1674 for (int i = 0; i < mode_argc; i++) {
1675 if (mode_argv[i] && (strcmp(mode_argv[i], "--no-splash") == 0 || strncmp(mode_argv[i], "--no-splash=", 12) == 0)) {
1676 opts.splash_explicitly_set = true;
1677 }
1678 if (mode_argv[i] &&
1679 (strcmp(mode_argv[i], "--no-status-screen") == 0 || strncmp(mode_argv[i], "--no-status-screen=", 19) == 0)) {
1680 opts.status_screen_explicitly_set = true;
1681 }
1682 }
1683
1684 // Auto-disable splash when grep is used (since it's one-time startup screen)
1685 // UNLESS it was explicitly set by the user
1686 // Status screen is now compatible with --grep since we support auto-loading patterns
1687 bool grep_was_provided = false;
1688 for (int i = 0; i < mode_argc; i++) {
1689 if (mode_argv[i] && (strcmp(mode_argv[i], "--grep") == 0 || strncmp(mode_argv[i], "--grep=", 7) == 0)) {
1690 grep_was_provided = true;
1691 break;
1692 }
1693 }
1694
1695 if (grep_was_provided) {
1696 if (!opts.splash_explicitly_set) {
1697 opts.splash = false;
1698 log_debug("Auto-disabled splash because --grep was provided");
1699 }
1700 }
1701
1702 // Validate all string options contain valid UTF-8
1703 // This prevents crashes and corruption from invalid UTF-8 in any option
1704 const char *string_fields[][2] = {{"address", opts.address},
1705 {"address6", opts.address6},
1706 {"encrypt_key", opts.encrypt_key},
1707 {"encrypt_keyfile", opts.encrypt_keyfile},
1708 {"server_key", opts.server_key},
1709 {"client_keys", opts.client_keys},
1710 {"discovery_server", opts.discovery_server},
1711 {"discovery_service_key", opts.discovery_service_key},
1712 {"discovery_database_path", opts.discovery_database_path},
1713 {"log_file", opts.log_file},
1714 {"media_file", opts.media_file},
1715 {"palette_custom", opts.palette_custom},
1716 {"stun_servers", opts.stun_servers},
1717 {"turn_servers", opts.turn_servers},
1718 {"turn_username", opts.turn_username},
1719 {"turn_credential", opts.turn_credential},
1720 {"turn_secret", opts.turn_secret},
1721 {"session_string", opts.session_string},
1722 {NULL, NULL}};
1723
1724 for (int i = 0; string_fields[i][0] != NULL; i++) {
1725 const char *field_name = string_fields[i][0];
1726 const char *field_value = string_fields[i][1];
1727
1728 // Skip empty strings
1729 if (!field_value || field_value[0] == '\0') {
1730 continue;
1731 }
1732
1733 // Validate UTF-8
1734 if (!utf8_is_valid(field_value)) {
1735 log_error("Error: Option --%s contains invalid UTF-8 sequences", field_name);
1736 log_error(" Value: %s", field_value);
1737 options_config_destroy(config);
1738 SAFE_FREE(allocated_mode_argv);
1739 return option_error_invalid();
1740 }
1741 }
1742 // Validate options
1743 result = validate_options_and_report(config, &opts);
1744 if (result != ASCIICHAT_OK) {
1745 options_config_destroy(config);
1746 SAFE_FREE(allocated_mode_argv);
1747 return result;
1748 }
1749 // Check for unexpected remaining arguments
1750 if (remaining_argc > 0) {
1751 log_error("Error: Unexpected arguments after options:");
1752 for (int i = 0; i < remaining_argc; i++) {
1753 log_error(" %s", remaining_argv[i]);
1754 }
1755 options_config_destroy(config);
1756 SAFE_FREE(allocated_mode_argv);
1757 return option_error_invalid();
1758 }
1759
1760 // Mode-specific post-processing
1761 // Apply mode-specific defaults (port, websocket-port)
1763 log_dev("Applied mode-specific defaults: port=%d, websocket_port=%d", opts.port, opts.websocket_port);
1764
1765 if (detected_mode == MODE_DISCOVERY_SERVICE) {
1766 // Set default paths if not specified
1767 if (opts.discovery_database_path[0] == '\0') {
1768 // Database: Try system-wide location first, fall back to user directories
1769 // Preference: /usr/local/var/ascii-chat/ > ~/.local/share/ascii-chat/ > ~/.config/ascii-chat/
1770 char *db_dir = get_discovery_database_dir();
1771 if (!db_dir) {
1772 options_config_destroy(config);
1773 SAFE_FREE(allocated_mode_argv);
1774 return SET_ERRNO(ERROR_CONFIG, "Failed to get database directory (tried system and user locations)");
1775 }
1776 safe_snprintf(opts.discovery_database_path, sizeof(opts.discovery_database_path), "%sdiscovery.db", db_dir);
1777 SAFE_FREE(db_dir);
1778 }
1779 }
1780
1781 options_config_destroy(config);
1782
1783 // ========================================================================
1784 // STAGE 7: Post-Processing & Validation
1785 // ========================================================================
1786
1787 // Collect multiple --key flags for multi-key support (server/ACDS only)
1788 // This enables servers to load both SSH and GPG keys and select the right one
1789 // during handshake based on what the client expects
1790 if (detected_mode == MODE_SERVER || detected_mode == MODE_DISCOVERY_SERVICE) {
1791 int num_keys = options_collect_identity_keys(&opts, argc, argv);
1792 if (num_keys < 0) {
1793 SAFE_FREE(allocated_mode_argv);
1794 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to collect identity keys");
1795 }
1796 // num_keys == 0 is OK (no --key flags provided)
1797 }
1798
1799 // After parsing command line options, update dimensions
1800 // First set any auto dimensions to terminal size, then apply full height logic
1801
1802 // Clear auto flags if dimensions were explicitly parsed from options
1803 // This must happen before update_dimensions_to_terminal_size to prevent auto-detected
1804 // values from overwriting user-specified dimensions
1805 // Note: width=0 and height=0 are special values meaning "auto-detect", so don't clear auto flags for those
1806 if (opts.width != OPT_WIDTH_DEFAULT && opts.width != 0) {
1807 opts.auto_width = false;
1808 }
1809 if (opts.height != OPT_HEIGHT_DEFAULT && opts.height != 0) {
1810 opts.auto_height = false;
1811 }
1814 // Apply verbose level to log threshold
1815 // Each -V decreases the log level by 1 (showing more verbose output)
1816 // Minimum level is LOG_DEV (0)
1817 if (opts.verbose_level > 0) {
1818 log_level_t current_level = log_get_level();
1819 int new_level = (int)current_level - (int)opts.verbose_level;
1820 if (new_level < LOG_DEV) {
1821 new_level = LOG_DEV;
1822 }
1823 log_set_level((log_level_t)new_level);
1824 }
1825
1826 // Check WEBCAM_DISABLED environment variable to enable test pattern mode
1827 // Useful for CI/CD and testing environments without a physical webcam
1828 const char *webcam_disabled = SAFE_GETENV("WEBCAM_DISABLED");
1829 if (webcam_disabled &&
1830 (strcmp(webcam_disabled, "1") == 0 || platform_strcasecmp(webcam_disabled, "true") == 0 ||
1831 platform_strcasecmp(webcam_disabled, "yes") == 0 || platform_strcasecmp(webcam_disabled, "on") == 0)) {
1832 opts.test_pattern = true;
1833 }
1834
1835 // Apply --no-compress interaction with audio encoding
1836 if (opts.no_compress) {
1837 opts.encode_audio = false;
1838 log_debug("--no-compress set: disabling audio encoding");
1839 }
1840
1841 // Set media_from_stdin flag if media_file is "-"
1842 if (opts.media_file[0] != '\0' && strcmp(opts.media_file, "-") == 0) {
1843 opts.media_from_stdin = true;
1844 log_debug("Media file set to stdin");
1845 }
1846
1847 // Validate --seek option
1848 if (opts.media_seek_timestamp > 0.0) {
1849 // Can't seek stdin
1850 if (opts.media_from_stdin) {
1851 log_error("--seek cannot be used with stdin (--file -)");
1852 SAFE_FREE(allocated_mode_argv);
1853 return ERROR_INVALID_PARAM;
1854 }
1855
1856 // Require --file or --url
1857 if (opts.media_file[0] == '\0' && opts.media_url[0] == '\0') {
1858 log_error("--seek requires --file or --url");
1859 SAFE_FREE(allocated_mode_argv);
1860 return ERROR_INVALID_PARAM;
1861 }
1862 }
1863
1864 // Validate --pause option
1865 if (opts.pause) {
1866 // Require --file or --url (not webcam, not test pattern)
1867 if (opts.media_file[0] == '\0' && opts.media_url[0] == '\0') {
1868 log_error("--pause requires --file or --url");
1869 SAFE_FREE(allocated_mode_argv);
1870 return ERROR_INVALID_PARAM;
1871 }
1872 }
1873
1874 // Validate --url option
1875 if (opts.media_url[0] != '\0') {
1876 // URL must be a valid HTTP(S) URL (YouTube URLs are HTTPS URLs)
1877 if (!url_is_valid(opts.media_url)) {
1878 log_error("--url must be a valid HTTP(S) URL: %s", opts.media_url);
1879 SAFE_FREE(allocated_mode_argv);
1880 return ERROR_INVALID_PARAM;
1881 }
1882
1883 // Normalize bare URLs by prepending http:// if not present
1884 if (!strstr(opts.media_url, "://")) {
1885 char normalized_url[2048];
1886 int result = snprintf(normalized_url, sizeof(normalized_url), "http://%s", opts.media_url);
1887 if (result > 0 && result < (int)sizeof(normalized_url)) {
1888 SAFE_STRNCPY(opts.media_url, normalized_url, sizeof(opts.media_url));
1889 } else {
1890 log_error("Failed to normalize URL (too long): %s", opts.media_url);
1891 SAFE_FREE(allocated_mode_argv);
1892 return ERROR_INVALID_PARAM;
1893 }
1894 }
1895 }
1896
1897 // ========================================================================
1898 // STAGE 7: Publish to RCU
1899 // ========================================================================
1900
1901 // Save the quiet flag before publishing (RCU will be cleaned up before memory report runs)
1902#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
1903 bool quiet_for_memory_report = opts.quiet;
1904#endif
1905
1906 // Publish parsed options to RCU state (replaces options_state_populate_from_globals)
1907 // This makes the options visible to all threads via lock-free reads
1908 asciichat_error_t publish_result = options_state_set(&opts);
1909 if (publish_result != ASCIICHAT_OK) {
1910 log_error("Failed to publish parsed options to RCU state: %d", publish_result);
1911 SAFE_FREE(allocated_mode_argv);
1912 return publish_result;
1913 }
1914
1915 // Now update debug memory quiet mode with the saved quiet value
1916#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
1917 debug_memory_set_quiet_mode(quiet_for_memory_report);
1918#endif
1919
1920 // ========================================================================
1921 // Apply color scheme to logging
1922 // ========================================================================
1923 // Now that options are parsed, set and apply the selected color scheme to logging
1924 if (opts.color_scheme_name[0] != '\0') {
1925 asciichat_error_t scheme_result = colorscheme_set_active_scheme(opts.color_scheme_name);
1926 if (scheme_result == ASCIICHAT_OK) {
1927 const color_scheme_t *scheme = colorscheme_get_active_scheme();
1928 if (scheme) {
1929 log_set_color_scheme(scheme);
1930 log_debug("Color scheme applied: %s", opts.color_scheme_name);
1931 }
1932 } else {
1933 log_warn("Failed to apply color scheme: %s", opts.color_scheme_name);
1934 }
1935 }
1936
1937 // ========================================================================
1938 // STAGE 8: Execute deferred actions (after all options parsed and published)
1939 // ========================================================================
1940 // Deferred actions (--list-webcams, --list-microphones, --list-speakers, --show-capabilities)
1941 // are executed here after all options are fully parsed and published via RCU.
1942 // This ensures action output reflects the final parsed state (e.g., final dimensions
1943 // for --show-capabilities).
1944 // Re-enable terminal output so deferred actions can print their results.
1945 // It was disabled earlier (STAGE 1C) to suppress log noise during option parsing.
1946 if (has_action) {
1948 }
1950 SAFE_FREE(allocated_mode_argv);
1951 return ASCIICHAT_OK;
1952}
asciichat_error_t options_config_parse(const options_config_t *config, int argc, char **argv, void *options_struct, option_mode_bitmask_t detected_mode, int *remaining_argc, char ***remaining_argv)
Definition builder.c:1750
asciichat_error_t options_config_set_defaults(const options_config_t *config, void *options_struct)
Definition builder.c:1376
void options_config_destroy(options_config_t *config)
Definition builder.c:601
const color_scheme_t * colorscheme_get_active_scheme(void)
asciichat_error_t colorscheme_set_active_scheme(const char *name)
asciichat_error_t config_load_system_and_user(asciichat_mode_t detected_mode, bool strict, options_t *opts)
Definition config.c:1535
asciichat_error_t config_create_default(const char *config_path)
Definition config.c:1250
options_t options_t_new_preserve_binary(const options_t *source)
Create new options struct, preserving binary-level fields from source.
options_t options_t_new(void)
log_level_t log_get_level(void)
void log_set_level(log_level_t level)
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
void log_set_terminal_output(bool enabled)
void log_set_color_scheme(const color_scheme_t *scheme)
void update_dimensions_to_terminal_size(options_t *opts)
void usage(FILE *desc, asciichat_mode_t mode)
asciichat_error_t validate_options_and_report(const void *config, const void *opts)
void update_dimensions_for_full_height(options_t *opts)
asciichat_error_t options_config_generate_manpage_merged(const options_config_t *config, const char *program_name, const char *mode_name, const char *output_path, const char *brief_description)
void action_check_update_immediate(void)
Execute update check immediately (for early binary-level execution)
bool has_action
void action_create_manpage(const char *output_path)
void action_completions(const char *shell_name, const char *output_path)
void action_list_microphones(void)
void action_create_config(const char *output_path)
void action_list_webcams(void)
void action_list_speakers(void)
void actions_execute_deferred(void)
void action_show_capabilities_immediate(void)
Execute show capabilities immediately (for early binary-level execution)
void apply_mode_specific_defaults(options_t *opts)
Apply mode-specific defaults to an options struct after mode detection.
bool parse_color_setting(const char *arg, void *dest, char **error_msg)
Definition parsers.c:161
bool parse_log_level(const char *arg, void *dest, char **error_msg)
Definition parsers.c:408
char * get_discovery_database_dir(void)
Definition path.c:581
char * get_config_dir(void)
Definition path.c:493
bool terminal_is_piped_output(void)
int platform_strcasecmp(const char *s1, const char *s2)
bool terminal_supports_utf8(void)
asciichat_error_t options_state_init(void)
Definition rcu.c:218
asciichat_error_t options_state_set(const options_t *opts)
Definition rcu.c:284
asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs)
Definition schema.c:242
Create a new options_t struct with all defaults set.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
bool url_is_valid(const char *url)
Definition url.c:81
bool utf8_is_ascii_only(const char *str)
Definition utf8.c:167
bool utf8_is_valid(const char *str)
Definition utf8.c:158
bool is_remote_key_path(const char *key_path)
Definition validation.c:596
int options_collect_identity_keys(options_t *opts, int argc, char *argv[])
Definition validation.c:528

References action_check_update_immediate(), action_completions(), action_create_config(), action_create_manpage(), action_list_microphones(), action_list_speakers(), action_list_webcams(), action_show_capabilities_immediate(), actions_execute_deferred(), apply_mode_specific_defaults(), colorscheme_get_active_scheme(), colorscheme_set_active_scheme(), config_create_default(), config_load_system_and_user(), config_schema_build_from_configs(), get_config_dir(), get_discovery_database_dir(), has_action, is_remote_key_path(), log_get_level(), log_init(), log_set_color_scheme(), log_set_level(), log_set_terminal_output(), options_collect_identity_keys(), options_config_destroy(), options_config_generate_manpage_merged(), options_config_parse(), options_config_set_defaults(), options_preset_unified(), options_state_init(), options_state_set(), options_t_new(), options_t_new_preserve_binary(), parse_color_setting(), parse_log_level(), PLATFORM_MAX_PATH_LENGTH, platform_strcasecmp(), safe_snprintf(), terminal_is_piped_output(), terminal_supports_utf8(), update_dimensions_for_full_height(), update_dimensions_to_terminal_size(), url_is_valid(), usage(), utf8_is_ascii_only(), utf8_is_valid(), and validate_options_and_report().

Referenced by client_init_with_args(), main(), and mirror_init_with_args().

◆ options_t_new()

options_t options_t_new ( void  )

Definition at line 521 of file lib/options/options.c.

521 {
522 options_t opts;
523
524 // Zero-initialize all fields first
525 memset(&opts, 0, sizeof(opts));
526
527 // ============================================================================
528 // LOGGING
529 // ============================================================================
530 // log_file is mode-dependent at startup and intentionally left empty here
531 opts.log_level = OPT_LOG_LEVEL_DEFAULT;
532 opts.verbose_level = OPT_VERBOSE_LEVEL_DEFAULT;
533 opts.quiet = OPT_QUIET_DEFAULT;
534 opts.grep_pattern[0] = '\0'; // Explicitly ensure grep_pattern is empty
535
536 // ============================================================================
537 // TERMINAL
538 // ============================================================================
539 opts.width = OPT_WIDTH_DEFAULT;
540 opts.height = OPT_HEIGHT_DEFAULT;
541 opts.auto_width = OPT_AUTO_WIDTH_DEFAULT;
542 opts.auto_height = OPT_AUTO_HEIGHT_DEFAULT;
543 opts.color = OPT_COLOR_DEFAULT;
544 SAFE_STRNCPY(opts.color_scheme_name, OPT_COLOR_SCHEME_NAME_DEFAULT, sizeof(opts.color_scheme_name));
545
546 // ============================================================================
547 // WEBCAM
548 // ============================================================================
549 opts.webcam_index = OPT_WEBCAM_INDEX_DEFAULT;
550 opts.test_pattern = OPT_TEST_PATTERN_DEFAULT;
551 opts.no_audio_mixer = OPT_NO_AUDIO_MIXER_DEFAULT;
552
553 // ============================================================================
554 // DISPLAY
555 // ============================================================================
556 opts.color_mode = OPT_COLOR_MODE_DEFAULT;
557 opts.color_filter = OPT_COLOR_FILTER_DEFAULT;
558 opts.render_mode = OPT_RENDER_MODE_DEFAULT;
559 opts.palette_type = OPT_PALETTE_TYPE_DEFAULT;
560 // palette_custom is already zeroed by memset
561 opts.palette_custom_set = OPT_PALETTE_CUSTOM_SET_DEFAULT;
562 opts.show_capabilities = OPT_SHOW_CAPABILITIES_DEFAULT;
563 opts.force_utf8 = OPT_FORCE_UTF8_DEFAULT;
564 opts.stretch = OPT_STRETCH_DEFAULT;
565 opts.strip_ansi = OPT_STRIP_ANSI_DEFAULT;
566 opts.fps = OPT_FPS_DEFAULT;
567 opts.flip_x = OPT_FLIP_X_DEFAULT;
568 opts.flip_y = OPT_FLIP_Y_DEFAULT;
569 opts.splash = OPT_SPLASH_DEFAULT;
570 opts.splash_explicitly_set = false;
571 opts.status_screen = OPT_STATUS_SCREEN_DEFAULT;
572 opts.status_screen_explicitly_set = false;
573
574 // ============================================================================
575 // SNAPSHOT
576 // ============================================================================
577 opts.snapshot_mode = OPT_SNAPSHOT_MODE_DEFAULT;
578 opts.snapshot_delay = SNAPSHOT_DELAY_DEFAULT;
579
580 // ============================================================================
581 // PERFORMANCE
582 // ============================================================================
583 opts.compression_level = OPT_COMPRESSION_LEVEL_DEFAULT;
584 opts.no_compress = OPT_NO_COMPRESS_DEFAULT;
585 opts.encode_audio = OPT_ENCODE_AUDIO_DEFAULT;
586
587 // ============================================================================
588 // SECURITY
589 // ============================================================================
590 opts.encrypt_enabled = OPT_ENCRYPT_ENABLED_DEFAULT;
591 // encrypt_key is already zeroed by memset
592 // password is already zeroed by memset
593 // encrypt_keyfile is already zeroed by memset
594 opts.no_encrypt = OPT_NO_ENCRYPT_DEFAULT;
595 // server_key is already zeroed by memset
596 // client_keys is already zeroed by memset
597 opts.discovery_insecure = OPT_ACDS_INSECURE_DEFAULT;
598 // discovery_service_key is already zeroed by memset
599 // identity_keys array is already zeroed by memset
600 opts.num_identity_keys = 0;
601 opts.require_server_identity = OPT_REQUIRE_SERVER_IDENTITY_DEFAULT;
602 opts.require_client_identity = OPT_REQUIRE_CLIENT_IDENTITY_DEFAULT;
603 opts.require_server_verify = OPT_REQUIRE_SERVER_VERIFY_DEFAULT;
604 opts.require_client_verify = OPT_REQUIRE_CLIENT_VERIFY_DEFAULT;
605
606 // ============================================================================
607 // NETWORK
608 // ============================================================================
609 opts.port = OPT_PORT_INT_DEFAULT;
610 opts.websocket_port = OPT_WEBSOCKET_PORT_SERVER_DEFAULT;
611 opts.max_clients = OPT_MAX_CLIENTS_DEFAULT;
612 opts.reconnect_attempts = OPT_RECONNECT_ATTEMPTS_DEFAULT;
613 opts.lan_discovery = OPT_LAN_DISCOVERY_DEFAULT;
614 opts.no_mdns_advertise = OPT_NO_MDNS_ADVERTISE_DEFAULT;
615 opts.webrtc = OPT_WEBRTC_DEFAULT;
616 opts.no_webrtc = OPT_NO_WEBRTC_DEFAULT;
617 opts.prefer_webrtc = OPT_PREFER_WEBRTC_DEFAULT;
618 opts.webrtc_skip_stun = OPT_WEBRTC_SKIP_STUN_DEFAULT;
619 opts.webrtc_disable_turn = OPT_WEBRTC_DISABLE_TURN_DEFAULT;
620 SAFE_STRNCPY(opts.stun_servers, OPT_STUN_SERVERS_DEFAULT, sizeof(opts.stun_servers));
621 SAFE_STRNCPY(opts.turn_servers, OPT_TURN_SERVERS_DEFAULT, sizeof(opts.turn_servers));
622 SAFE_STRNCPY(opts.turn_username, OPT_TURN_USERNAME_DEFAULT, sizeof(opts.turn_username));
623 SAFE_STRNCPY(opts.turn_credential, OPT_TURN_CREDENTIAL_DEFAULT, sizeof(opts.turn_credential));
624 // turn_secret is already zeroed by memset
625 SAFE_STRNCPY(opts.discovery_server, OPT_ENDPOINT_DISCOVERY_SERVICE, sizeof(opts.discovery_server));
626 opts.discovery_port = OPT_ACDS_PORT_INT_DEFAULT;
627 opts.discovery_expose_ip = OPT_ACDS_EXPOSE_IP_DEFAULT;
628 opts.enable_upnp = OPT_ENABLE_UPNP_DEFAULT;
629 opts.discovery = OPT_ACDS_DEFAULT;
630
631 // ============================================================================
632 // MEDIA
633 // ============================================================================
634 // media_file is already zeroed by memset
635 // media_url is already zeroed by memset
636 opts.media_loop = OPT_MEDIA_LOOP_DEFAULT;
637 opts.pause = OPT_PAUSE_DEFAULT;
638 opts.media_from_stdin = OPT_MEDIA_FROM_STDIN_DEFAULT;
639 opts.media_seek_timestamp = OPT_MEDIA_SEEK_TIMESTAMP_DEFAULT;
640 // yt_dlp_options is already zeroed by memset
641
642 // ============================================================================
643 // AUDIO
644 // ============================================================================
645 opts.audio_enabled = OPT_AUDIO_ENABLED_DEFAULT;
646 opts.audio_source = OPT_AUDIO_SOURCE_DEFAULT;
647 opts.microphone_index = OPT_MICROPHONE_INDEX_DEFAULT;
648 opts.speakers_index = OPT_SPEAKERS_INDEX_DEFAULT;
649 opts.microphone_sensitivity = OPT_MICROPHONE_SENSITIVITY_DEFAULT;
650 opts.speakers_volume = OPT_SPEAKERS_VOLUME_DEFAULT;
651 opts.audio_no_playback = OPT_AUDIO_NO_PLAYBACK_DEFAULT;
652 opts.audio_analysis_enabled = OPT_AUDIO_ANALYSIS_ENABLED_DEFAULT;
653
654 // ============================================================================
655 // DATABASE (discovery-service only)
656 // ============================================================================
657 // discovery_database_path is already zeroed by memset
658
659 // ============================================================================
660 // Internal/Non-Displayed Fields
661 // ============================================================================
662 opts.help = OPT_HELP_DEFAULT;
663 opts.version = OPT_VERSION_DEFAULT;
664 // config_file is already zeroed by memset
665 SAFE_STRNCPY(opts.address, OPT_ADDRESS_DEFAULT, sizeof(opts.address));
666 SAFE_STRNCPY(opts.address6, OPT_ADDRESS6_DEFAULT, sizeof(opts.address6));
667 // session_string is already zeroed by memset
668 // detected_mode is already zeroed by memset
669
670 return opts;
671}

Referenced by config_create_default(), options_init(), options_state_init(), and options_t_new_preserve_binary().

◆ options_t_new_preserve_binary()

options_t options_t_new_preserve_binary ( const options_t *  source)

Create new options struct, preserving binary-level fields from source.

This version of options_t_new() extracts binary-level options from a source struct before performing a full reset, then restores them after. This allows binary-level options (–quiet, –verbose, –log-level, etc.) to persist through multiple reset cycles without manual save/restore code.

Parameters
sourceThe options struct to extract binary-level fields from (NULL-safe)
Returns
New options_t with defaults and binary-level fields preserved

Definition at line 684 of file lib/options/options.c.

684 {
685 // Extract binary-level options before reset
686 binary_level_opts_t binary = extract_binary_level(source);
687
688 // Create fresh options with all defaults
689 options_t opts = options_t_new();
690
691 // Restore binary-level options (overwrite defaults)
692 restore_binary_level(&opts, &binary);
693
694 return opts;
695}

References options_t_new().

Referenced by options_init().