828 {
829
830
831
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
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
845
847 if (rcu_init_result != ASCIICHAT_OK) {
848 return rcu_init_result;
849 }
850
851
852
853
854
855
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;
862
863
864
865
866
867
868
869
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;
875
876
877 for (int i = 1; i < argc; i++) {
878 if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
879
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;
893 }
894 break;
895 }
896 }
897
898
899 usage(stdout, help_mode);
900 fflush(NULL);
901 _Exit(0);
902 }
903 }
904
905
906 for (int i = 1; i < argc; i++) {
907
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;
914 }
915 }
916 if (argv[i][0] == '-') {
917
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
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++;
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++;
938 }
939
940
941 if (strcmp(argv[i], "--color") == 0) {
942 if (i + 1 < argc && argv[i + 1][0] != '-') {
943
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
951 parsed_color_setting = COLOR_SETTING_TRUE;
952 color_setting_found = true;
953 } else {
954
955 char *error_msg = NULL;
957 color_setting_found = true;
958 i++;
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
968 parsed_color_setting = COLOR_SETTING_TRUE;
969 color_setting_found = true;
970 }
971 }
972
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;
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;
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 }
998
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
1008 }
1009 if (strcmp(argv[i], "--config-create") == 0) {
1010 create_config = true;
1012
1013 if (i + 1 < argc && argv[i + 1][0] != '-') {
1014 config_create_path = argv[i + 1];
1015 i++;
1016 }
1017 break;
1018 }
1019 if (strcmp(argv[i], "--man-page-create") == 0) {
1020 create_manpage = true;
1022
1023 if (i + 1 < argc && argv[i + 1][0] != '-') {
1024 manpage_create_path = argv[i + 1];
1025 i++;
1026 }
1027 break;
1028 }
1029 if (strcmp(argv[i], "--completions") == 0) {
1031
1032 if (i + 1 < argc && argv[i + 1][0] != '-') {
1033 const char *shell_name = argv[i + 1];
1034 i++;
1035
1036
1037 const char *output_file = NULL;
1038 if (i + 1 < argc && argv[i + 1][0] != '-') {
1039 output_file = argv[i + 1];
1040 i++;
1041 }
1042
1044
1045 } else {
1046 log_plain_stderr("Error: --completions requires shell name (bash, fish, zsh, powershell)");
1047 return ERROR_USAGE;
1048 }
1049 break;
1050 }
1051 if (strcmp(argv[i], "--list-webcams") == 0) {
1054
1055 break;
1056 }
1057 if (strcmp(argv[i], "--list-microphones") == 0) {
1060
1061 break;
1062 }
1063 if (strcmp(argv[i], "--list-speakers") == 0) {
1066
1067 break;
1068 }
1069
1070 if (strcmp(argv[i], "--show-capabilities") == 0) {
1073
1074 break;
1075 }
1076 }
1077 }
1078
1080
1081
1082
1083 asciichat_mode_t detected_mode = MODE_DISCOVERY;
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
1094
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
1110
1111
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
1118 log_init(opts.log_file, GET_OPTION(log_level), force_stderr,
false);
1119
1120
1121
1122
1123
1124
1125
1126
1129 }
1130
1131 if (color_setting_found) {
1132 opts.color = parsed_color_setting;
1133 } else {
1134 opts.color = OPT_COLOR_DEFAULT;
1135 }
1136
1137
1138 if (show_version || create_config || create_manpage) {
1139 if (show_version) {
1140 opts.version = true;
1142 return ASCIICHAT_OK;
1143 }
1144 if (create_config) {
1145
1147 if (unified_config) {
1149 if (schema_build_result != ASCIICHAT_OK) {
1150
1151 (void)schema_build_result;
1152 }
1154 }
1155
1156
1158
1159 }
1160 if (create_manpage) {
1161
1163
1164 }
1165 }
1166
1167
1168
1169
1170
1171
1172
1173 for (int i = 1; i < argc; i++) {
1174 if (argv[i][0] == '-') {
1175
1176 if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--verbose") == 0) {
1177
1178 if (i + 1 < argc && argv[i + 1][0] != '-') {
1179
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++;
1185 continue;
1186 }
1187 }
1188
1189 opts.verbose_level++;
1190 }
1191
1192
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
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;
1214 i++;
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
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++;
1237 }
1238
1239
1240 }
1241 }
1242
1243 if (show_version) {
1244
1245 opts.version = true;
1247 return ASCIICHAT_OK;
1248 }
1249
1250 if (create_config) {
1251
1252
1254 if (config_create_path) {
1255 SAFE_STRNCPY(config_path, config_create_path, sizeof(config_path));
1256 } else {
1257
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
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;
1281 }
1282
1283 if (create_manpage) {
1284
1285
1286 const char *existing_template_path = "share/man/man1/ascii-chat.1.in";
1288 if (!config) {
1289 log_error("Error: Failed to get binary options config");
1290 return ERROR_MEMORY;
1291 }
1292
1293
1294
1296 "Video chat in your terminal");
1297
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;
1312 }
1313
1314
1315
1316
1317
1318
1319 int mode_argc = argc;
1320 char **mode_argv = (char **)argv;
1321 char **allocated_mode_argv = NULL;
1322
1323 if (mode_index == -1) {
1324
1325 int max_mode_argc = argc;
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];
1338
1339
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
1346 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1347
1348 if (takes_arg && i + 1 < argc) {
1349 i++;
1350 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
1351 i++;
1352 }
1353 continue;
1354 }
1355
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
1364
1365
1366
1367 int args_after_mode = argc - mode_index - 1;
1368
1369 int max_mode_argc = 1 + (mode_index - 1) + args_after_mode;
1370
1371 if (max_mode_argc > 256) {
1372 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many arguments: %d", max_mode_argc);
1373 }
1374
1375
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;
1381
1382
1383 if (mode_index == 0) {
1384
1385 new_mode_argv[0] = "ascii-chat";
1386 } else {
1387 new_mode_argv[0] = argv[0];
1388 }
1389
1390
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
1397 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1398
1399 if (takes_arg && i + 1 < mode_index) {
1400 i++;
1401 } else if (takes_optional_arg && i + 1 < mode_index && argv[i + 1][0] != '-' &&
1402 isdigit((unsigned char)argv[i + 1][0])) {
1403 i++;
1404 }
1405 continue;
1406 }
1407
1408 new_mode_argv[new_argv_idx++] = argv[i];
1409 }
1410
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
1417 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1418
1419 if (takes_arg && i + 1 < argc) {
1420 i++;
1421 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
1422 i++;
1423 }
1424 continue;
1425 }
1426
1427 new_mode_argv[new_argv_idx + args_after_mode_idx++] = argv[i];
1428 }
1429
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
1438
1439
1440
1442
1443
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
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
1459 if (detected_mode == MODE_CLIENT || detected_mode == MODE_MIRROR || detected_mode == MODE_DISCOVERY) {
1460
1461 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "localhost");
1462 opts.address6[0] = '\0';
1463 } else if (detected_mode == MODE_SERVER) {
1464
1465 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "127.0.0.1");
1466
1467 opts.address6[0] = '\0';
1468 } else if (detected_mode == MODE_DISCOVERY_SERVICE) {
1469
1470 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "0.0.0.0");
1471 opts.address6[0] = '\0';
1472 }
1473
1474
1475
1476
1477
1478
1480 if (unified_config) {
1482 if (schema_build_result != ASCIICHAT_OK) {
1483
1484 (void)schema_build_result;
1485 } else {
1486 }
1488 } else {
1489 }
1490
1491
1492
1493
1494
1496
1497
1499 if (config_publish_result != ASCIICHAT_OK) {
1500 } else {
1501 }
1502
1503
1504
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;
1510
1511 restore_binary_level(&opts, &binary_before_config);
1512
1513
1514 opts.flip_x = saved_flip_x_from_config;
1515 opts.flip_y = saved_flip_y_from_config;
1516
1517
1518
1519
1520 opts.encrypt_enabled = saved_encrypt_enabled;
1521
1522
1523
1524
1525
1526
1528 asciichat_mode_t mode_saved_for_parsing = detected_mode;
1529
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
1539 bool saved_flip_x = opts.flip_x;
1540 bool saved_flip_y = opts.flip_y;
1541
1543 if (defaults_result != ASCIICHAT_OK) {
1545 SAFE_FREE(allocated_mode_argv);
1546 return defaults_result;
1547 }
1548
1549 restore_binary_level(&opts, &binary_before_defaults);
1550
1551
1552
1553 opts.flip_x = saved_flip_x;
1554 opts.flip_y = saved_flip_y;
1555
1556
1557 opts.detected_mode = mode_saved_for_parsing;
1558
1559
1560 bool saved_flip_x_for_parse = opts.flip_x;
1561 bool saved_flip_y_for_parse = opts.flip_y;
1562
1563
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
1568 opts.flip_x = saved_flip_x_for_parse;
1569 opts.flip_y = saved_flip_y_for_parse;
1570
1571 if (result != ASCIICHAT_OK) {
1573 SAFE_FREE(allocated_mode_argv);
1574
1575 if (result == ERROR_CONFIG) {
1576 return ERROR_USAGE;
1577 }
1578 return result;
1579 }
1580
1581
1582
1583
1584
1585
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;
1593 }
1594 log_debug("Successfully published options to RCU");
1595
1596
1597 if (opts.palette_custom[0] != '\0') {
1598
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
1605 log_error("Error: --palette-chars contains invalid UTF-8 sequences");
1607 SAFE_FREE(allocated_mode_argv);
1608 return option_error_invalid();
1609 }
1610
1611
1613 if (has_non_ascii) {
1614
1615
1616 bool utf8_disabled = (opts.force_utf8 == COLOR_SETTING_FALSE);
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");
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");
1631 SAFE_FREE(allocated_mode_argv);
1632 return option_error_invalid();
1633 }
1634 }
1635 }
1636
1637
1638 if (opts.encrypt_key[0] != '\0') {
1639
1641 struct stat st;
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);
1647 }
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);
1653 }
1654 }
1655 opts.encrypt_enabled = 1;
1656 log_debug("Auto-enabled encryption because --key was provided");
1657 }
1658
1659
1660 if (opts.color_filter != COLOR_FILTER_NONE) {
1661
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();
1667 }
1668
1669 opts.color = COLOR_SETTING_TRUE;
1670 log_debug("Auto-enabled color because --color-filter was provided");
1671 }
1672
1673
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
1685
1686
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
1703
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
1729 if (!field_value || field_value[0] == '\0') {
1730 continue;
1731 }
1732
1733
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();
1740 }
1741 }
1742
1744 if (result != ASCIICHAT_OK) {
1746 SAFE_FREE(allocated_mode_argv);
1747 return result;
1748 }
1749
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 }
1756 SAFE_FREE(allocated_mode_argv);
1757 return option_error_invalid();
1758 }
1759
1760
1761
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
1767 if (opts.discovery_database_path[0] == '\0') {
1768
1769
1771 if (!db_dir) {
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
1782
1783
1784
1785
1786
1787
1788
1789
1790 if (detected_mode == MODE_SERVER || detected_mode == MODE_DISCOVERY_SERVICE) {
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
1797 }
1798
1799
1800
1801
1802
1803
1804
1805
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
1815
1816
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;
1822 }
1824 }
1825
1826
1827
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;
1833 }
1834
1835
1836 if (opts.no_compress) {
1837 opts.encode_audio = false;
1838 log_debug("--no-compress set: disabling audio encoding");
1839 }
1840
1841
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
1848 if (opts.media_seek_timestamp > 0.0) {
1849
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
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
1865 if (opts.pause) {
1866
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
1875 if (opts.media_url[0] != '\0') {
1876
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
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
1899
1900
1901
1902#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
1903 bool quiet_for_memory_report = opts.quiet;
1904#endif
1905
1906
1907
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
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
1922
1923
1924 if (opts.color_scheme_name[0] != '\0') {
1926 if (scheme_result == ASCIICHAT_OK) {
1928 if (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
1939
1940
1941
1942
1943
1944
1945
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)
asciichat_error_t options_config_set_defaults(const options_config_t *config, void *options_struct)
void options_config_destroy(options_config_t *config)
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)
asciichat_error_t config_create_default(const char *config_path)
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)
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)
bool parse_log_level(const char *arg, void *dest, char **error_msg)
char * get_discovery_database_dir(void)
char * get_config_dir(void)
asciichat_error_t options_state_init(void)
asciichat_error_t options_state_set(const options_t *opts)
asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs)
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.
#define PLATFORM_MAX_PATH_LENGTH
bool url_is_valid(const char *url)
bool utf8_is_ascii_only(const char *str)
bool utf8_is_valid(const char *str)
bool is_remote_key_path(const char *key_path)
int options_collect_identity_keys(options_t *opts, int argc, char *argv[])