525 memset(&opts, 0,
sizeof(opts));
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';
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));
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;
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;
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;
577 opts.snapshot_mode = OPT_SNAPSHOT_MODE_DEFAULT;
578 opts.snapshot_delay = SNAPSHOT_DELAY_DEFAULT;
583 opts.compression_level = OPT_COMPRESSION_LEVEL_DEFAULT;
584 opts.no_compress = OPT_NO_COMPRESS_DEFAULT;
585 opts.encode_audio = OPT_ENCODE_AUDIO_DEFAULT;
590 opts.encrypt_enabled = OPT_ENCRYPT_ENABLED_DEFAULT;
594 opts.no_encrypt = OPT_NO_ENCRYPT_DEFAULT;
597 opts.discovery_insecure = OPT_ACDS_INSECURE_DEFAULT;
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;
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));
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;
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;
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;
662 opts.help = OPT_HELP_DEFAULT;
663 opts.version = OPT_VERSION_DEFAULT;
665 SAFE_STRNCPY(opts.address, OPT_ADDRESS_DEFAULT,
sizeof(opts.address));
666 SAFE_STRNCPY(opts.address6, OPT_ADDRESS6_DEFAULT,
sizeof(opts.address6));
832 if (argc < 0 || argc > 128) {
833 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid argc: %d", argc);
836 return SET_ERRNO(ERROR_INVALID_PARAM,
"argv is NULL");
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);
847 if (rcu_init_result != ASCIICHAT_OK) {
848 return rcu_init_result;
856 bool show_version =
false;
857 bool create_config =
false;
858 bool create_manpage =
false;
860 const char *config_create_path = NULL;
861 const char *manpage_create_path = NULL;
870 bool user_quiet =
false;
871 int parsed_color_setting = COLOR_SETTING_AUTO;
872 bool color_setting_found =
false;
873 bool check_update_flag_seen =
false;
874 bool no_check_update_flag_seen =
false;
877 for (
int i = 1; i < argc; i++) {
878 if (strcmp(argv[i],
"--help") == 0 || strcmp(argv[i],
"-h") == 0) {
880 asciichat_mode_t help_mode = MODE_DISCOVERY;
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;
899 usage(stdout, help_mode);
906 for (
int i = 1; i < argc; i++) {
908 if (argv[i][0] !=
'-') {
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);
916 if (argv[i][0] ==
'-') {
918 bool temp_quiet =
false;
919 if (parse_binary_bool_arg(argv[i], &temp_quiet,
"quiet",
'q')) {
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)");
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]);
941 if (strcmp(argv[i],
"--color") == 0) {
942 if (i + 1 < argc && argv[i + 1][0] !=
'-') {
944 const char *next_arg = argv[i + 1];
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);
951 parsed_color_setting = COLOR_SETTING_TRUE;
952 color_setting_found =
true;
955 char *error_msg = NULL;
957 color_setting_found =
true;
961 log_error(
"Error parsing --color: %s", error_msg);
968 parsed_color_setting = COLOR_SETTING_TRUE;
969 color_setting_found =
true;
973 if (strncmp(argv[i],
"--color=", 8) == 0) {
974 const char *value = argv[i] + 8;
975 char *error_msg = NULL;
977 color_setting_found =
true;
980 log_error(
"Error parsing --color: %s", error_msg);
985 if (strcmp(argv[i],
"--version") == 0 || strcmp(argv[i],
"-v") == 0) {
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");
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");
1009 if (strcmp(argv[i],
"--config-create") == 0) {
1010 create_config =
true;
1013 if (i + 1 < argc && argv[i + 1][0] !=
'-') {
1014 config_create_path = argv[i + 1];
1019 if (strcmp(argv[i],
"--man-page-create") == 0) {
1020 create_manpage =
true;
1023 if (i + 1 < argc && argv[i + 1][0] !=
'-') {
1024 manpage_create_path = argv[i + 1];
1029 if (strcmp(argv[i],
"--completions") == 0) {
1032 if (i + 1 < argc && argv[i + 1][0] !=
'-') {
1033 const char *shell_name = argv[i + 1];
1037 const char *output_file = NULL;
1038 if (i + 1 < argc && argv[i + 1][0] !=
'-') {
1039 output_file = argv[i + 1];
1046 log_plain_stderr(
"Error: --completions requires shell name (bash, fish, zsh, powershell)");
1051 if (strcmp(argv[i],
"--list-webcams") == 0) {
1057 if (strcmp(argv[i],
"--list-microphones") == 0) {
1063 if (strcmp(argv[i],
"--list-speakers") == 0) {
1070 if (strcmp(argv[i],
"--show-capabilities") == 0) {
1083 asciichat_mode_t detected_mode = MODE_DISCOVERY;
1084 char detected_session_string[SESSION_STRING_BUFFER_SIZE] = {0};
1085 int mode_index = -1;
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;
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;
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]);
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);
1118 log_init(opts.log_file, GET_OPTION(log_level), force_stderr,
false);
1131 if (color_setting_found) {
1132 opts.color = parsed_color_setting;
1134 opts.color = OPT_COLOR_DEFAULT;
1138 if (show_version || create_config || create_manpage) {
1140 opts.version =
true;
1142 return ASCIICHAT_OK;
1144 if (create_config) {
1147 if (unified_config) {
1149 if (schema_build_result != ASCIICHAT_OK) {
1151 (void)schema_build_result;
1160 if (create_manpage) {
1173 for (
int i = 1; i < argc; i++) {
1174 if (argv[i][0] ==
'-') {
1176 if (strcmp(argv[i],
"-V") == 0 || strcmp(argv[i],
"--verbose") == 0) {
1178 if (i + 1 < argc && argv[i + 1][0] !=
'-') {
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;
1189 opts.verbose_level++;
1193 if (parse_binary_bool_arg(argv[i], &opts.quiet,
"quiet",
'q')) {
1196 if (parse_binary_bool_arg(argv[i], &opts.json,
"json",
'\0')) {
1199 if (parse_binary_bool_arg(argv[i], &opts.log_format_console_only,
"log-format-console",
'\0')) {
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)");
1208 if (argv[i + 1][0] ==
'-') {
1209 log_plain_stderr(
"Error: --log-level requires a value (dev, debug, info, warn, error, fatal)");
1212 char *error_msg = NULL;
1217 log_plain_stderr(
"Error: %s", error_msg);
1220 log_plain_stderr(
"Error: invalid log level value: %s", argv[i + 1]);
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]);
1231 if (argv[i + 1][0] ==
'-') {
1232 log_plain_stderr(
"Error: %s requires a file path", argv[i]);
1235 SAFE_STRNCPY(opts.log_file, argv[i + 1],
sizeof(opts.log_file));
1245 opts.version =
true;
1247 return ASCIICHAT_OK;
1250 if (create_config) {
1254 if (config_create_path) {
1255 SAFE_STRNCPY(config_path, config_create_path,
sizeof(config_path));
1260 log_error(
"Error: Failed to determine default config directory");
1261 return ERROR_CONFIG;
1263 safe_snprintf(config_path,
sizeof(config_path),
"%sconfig.toml", config_dir);
1264 SAFE_FREE(config_dir);
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);
1274 log_error(
"Error: Failed to create config file at %s", config_path);
1279 log_plain(
"Created default config file at: %s", config_path);
1280 return ASCIICHAT_OK;
1283 if (create_manpage) {
1286 const char *existing_template_path =
"share/man/man1/ascii-chat.1.in";
1289 log_error(
"Error: Failed to get binary options config");
1290 return ERROR_MEMORY;
1296 "Video chat in your terminal");
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);
1305 log_error(
"Error: Failed to generate man page");
1310 log_plain(
"Generated man page: %s", existing_template_path);
1311 return ASCIICHAT_OK;
1319 int mode_argc = argc;
1320 char **mode_argv = (
char **)argv;
1321 char **allocated_mode_argv = NULL;
1323 if (mode_index == -1) {
1325 int max_mode_argc = argc;
1327 if (max_mode_argc > 256) {
1328 return SET_ERRNO(ERROR_INVALID_PARAM,
"Too many arguments: %d", max_mode_argc);
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");
1335 allocated_mode_argv = new_mode_argv;
1337 new_mode_argv[0] = argv[0];
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;
1346 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1348 if (takes_arg && i + 1 < argc) {
1350 }
else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] !=
'-') {
1356 new_mode_argv[new_argv_idx++] = argv[i];
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) {
1367 int args_after_mode = argc - mode_index - 1;
1369 int max_mode_argc = 1 + (mode_index - 1) + args_after_mode;
1371 if (max_mode_argc > 256) {
1372 return SET_ERRNO(ERROR_INVALID_PARAM,
"Too many arguments: %d", max_mode_argc);
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");
1380 allocated_mode_argv = new_mode_argv;
1383 if (mode_index == 0) {
1385 new_mode_argv[0] =
"ascii-chat";
1387 new_mode_argv[0] = argv[0];
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;
1397 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1399 if (takes_arg && i + 1 < mode_index) {
1401 }
else if (takes_optional_arg && i + 1 < mode_index && argv[i + 1][0] !=
'-' &&
1402 isdigit((
unsigned char)argv[i + 1][0])) {
1408 new_mode_argv[new_argv_idx++] = argv[i];
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;
1417 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1419 if (takes_arg && i + 1 < argc) {
1421 }
else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] !=
'-') {
1427 new_mode_argv[new_argv_idx + args_after_mode_idx++] = argv[i];
1430 mode_argc = new_argv_idx + args_after_mode_idx;
1431 new_mode_argv[mode_argc] = NULL;
1433 mode_argv = new_mode_argv;
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);
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';
1459 if (detected_mode == MODE_CLIENT || detected_mode == MODE_MIRROR || detected_mode == MODE_DISCOVERY) {
1461 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE,
"localhost");
1462 opts.address6[0] =
'\0';
1463 }
else if (detected_mode == MODE_SERVER) {
1465 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE,
"127.0.0.1");
1467 opts.address6[0] =
'\0';
1468 }
else if (detected_mode == MODE_DISCOVERY_SERVICE) {
1470 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE,
"0.0.0.0");
1471 opts.address6[0] =
'\0';
1480 if (unified_config) {
1482 if (schema_build_result != ASCIICHAT_OK) {
1484 (void)schema_build_result;
1499 if (config_publish_result != ASCIICHAT_OK) {
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;
1509 (void)config_result;
1511 restore_binary_level(&opts, &binary_before_config);
1514 opts.flip_x = saved_flip_x_from_config;
1515 opts.flip_y = saved_flip_y_from_config;
1520 opts.encrypt_enabled = saved_encrypt_enabled;
1528 asciichat_mode_t mode_saved_for_parsing = detected_mode;
1532 SAFE_FREE(allocated_mode_argv);
1533 return SET_ERRNO(ERROR_CONFIG,
"Failed to create options configuration");
1536 char **remaining_argv;
1539 bool saved_flip_x = opts.flip_x;
1540 bool saved_flip_y = opts.flip_y;
1543 if (defaults_result != ASCIICHAT_OK) {
1545 SAFE_FREE(allocated_mode_argv);
1546 return defaults_result;
1549 restore_binary_level(&opts, &binary_before_defaults);
1553 opts.flip_x = saved_flip_x;
1554 opts.flip_y = saved_flip_y;
1557 opts.detected_mode = mode_saved_for_parsing;
1560 bool saved_flip_x_for_parse = opts.flip_x;
1561 bool saved_flip_y_for_parse = opts.flip_y;
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);
1568 opts.flip_x = saved_flip_x_for_parse;
1569 opts.flip_y = saved_flip_y_for_parse;
1571 if (result != ASCIICHAT_OK) {
1573 SAFE_FREE(allocated_mode_argv);
1575 if (result == ERROR_CONFIG) {
1586 log_debug(
"Publishing parsed options to RCU before validation");
1588 if (early_publish != ASCIICHAT_OK) {
1589 log_error(
"Failed to publish parsed options to RCU state early");
1591 SAFE_FREE(allocated_mode_argv);
1592 return early_publish;
1594 log_debug(
"Successfully published options to RCU");
1597 if (opts.palette_custom[0] !=
'\0') {
1599 opts.palette_type = PALETTE_CUSTOM;
1600 opts.palette_custom_set =
true;
1601 log_debug(
"Set PALETTE_CUSTOM because --palette-chars was provided");
1605 log_error(
"Error: --palette-chars contains invalid UTF-8 sequences");
1607 SAFE_FREE(allocated_mode_argv);
1608 return option_error_invalid();
1613 if (has_non_ascii) {
1616 bool utf8_disabled = (opts.force_utf8 == COLOR_SETTING_FALSE);
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");
1623 SAFE_FREE(allocated_mode_argv);
1624 return option_error_invalid();
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");
1631 SAFE_FREE(allocated_mode_argv);
1632 return option_error_invalid();
1638 if (opts.encrypt_key[0] !=
'\0') {
1642 if (stat(opts.encrypt_key, &st) != 0) {
1643 log_error(
"Key file not found: %s", opts.encrypt_key);
1645 SAFE_FREE(allocated_mode_argv);
1646 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Key file not found: %s", opts.encrypt_key);
1648 if ((st.st_mode & S_IFMT) != S_IFREG) {
1649 log_error(
"Key path is not a regular file: %s", opts.encrypt_key);
1651 SAFE_FREE(allocated_mode_argv);
1652 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Key path is not a regular file: %s", opts.encrypt_key);
1655 opts.encrypt_enabled = 1;
1656 log_debug(
"Auto-enabled encryption because --key was provided");
1660 if (opts.color_filter != COLOR_FILTER_NONE) {
1662 if (opts.color == COLOR_SETTING_FALSE) {
1663 log_error(
"Error: --color-filter cannot be used with --color=false");
1665 SAFE_FREE(allocated_mode_argv);
1666 return option_error_invalid();
1669 opts.color = COLOR_SETTING_TRUE;
1670 log_debug(
"Auto-enabled color because --color-filter was provided");
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;
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;
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;
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");
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},
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];
1729 if (!field_value || field_value[0] ==
'\0') {
1735 log_error(
"Error: Option --%s contains invalid UTF-8 sequences", field_name);
1736 log_error(
" Value: %s", field_value);
1738 SAFE_FREE(allocated_mode_argv);
1739 return option_error_invalid();
1744 if (result != ASCIICHAT_OK) {
1746 SAFE_FREE(allocated_mode_argv);
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]);
1756 SAFE_FREE(allocated_mode_argv);
1757 return option_error_invalid();
1763 log_dev(
"Applied mode-specific defaults: port=%d, websocket_port=%d", opts.port, opts.websocket_port);
1765 if (detected_mode == MODE_DISCOVERY_SERVICE) {
1767 if (opts.discovery_database_path[0] ==
'\0') {
1773 SAFE_FREE(allocated_mode_argv);
1774 return SET_ERRNO(ERROR_CONFIG,
"Failed to get database directory (tried system and user locations)");
1776 safe_snprintf(opts.discovery_database_path,
sizeof(opts.discovery_database_path),
"%sdiscovery.db", db_dir);
1790 if (detected_mode == MODE_SERVER || detected_mode == MODE_DISCOVERY_SERVICE) {
1793 SAFE_FREE(allocated_mode_argv);
1794 return SET_ERRNO(ERROR_INVALID_PARAM,
"Failed to collect identity keys");
1806 if (opts.width != OPT_WIDTH_DEFAULT && opts.width != 0) {
1807 opts.auto_width =
false;
1809 if (opts.height != OPT_HEIGHT_DEFAULT && opts.height != 0) {
1810 opts.auto_height =
false;
1817 if (opts.verbose_level > 0) {
1819 int new_level = (int)current_level - (
int)opts.verbose_level;
1820 if (new_level < LOG_DEV) {
1821 new_level = LOG_DEV;
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 ||
1832 opts.test_pattern =
true;
1836 if (opts.no_compress) {
1837 opts.encode_audio =
false;
1838 log_debug(
"--no-compress set: disabling audio encoding");
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");
1848 if (opts.media_seek_timestamp > 0.0) {
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;
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;
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;
1875 if (opts.media_url[0] !=
'\0') {
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;
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));
1890 log_error(
"Failed to normalize URL (too long): %s", opts.media_url);
1891 SAFE_FREE(allocated_mode_argv);
1892 return ERROR_INVALID_PARAM;
1902#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
1903 bool quiet_for_memory_report = opts.quiet;
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;
1916#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
1917 debug_memory_set_quiet_mode(quiet_for_memory_report);
1924 if (opts.color_scheme_name[0] !=
'\0') {
1926 if (scheme_result == ASCIICHAT_OK) {
1930 log_debug(
"Color scheme applied: %s", opts.color_scheme_name);
1933 log_warn(
"Failed to apply color scheme: %s", opts.color_scheme_name);
1950 SAFE_FREE(allocated_mode_argv);
1951 return ASCIICHAT_OK;