ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
lib/options/options.c
Go to the documentation of this file.
1
76#include <ctype.h>
77#include <sys/stat.h>
79#include <ascii-chat/options/rcu.h> // RCU-based thread-safe options
86#include <ascii-chat/options/registry/mode_defaults.h> // Mode-aware defaults
87
89#include <ascii-chat/options/strings.h> // Enum/mode string conversions
92#include <ascii-chat/common.h>
93#include <ascii-chat/log/log.h>
94#include <ascii-chat/log/grep.h>
100#include <ascii-chat/util/url.h>
101#include <ascii-chat/util/utf8.h>
102
103#ifdef _WIN32
104#include <io.h>
105#else
106#include <unistd.h>
107#endif
108
109#include <stdio.h>
110#include <stdlib.h>
111#include <string.h>
112
113static void js_log_options(const char *msg) {
114 (void)msg;
115}
116
117// ============================================================================
118// Global Action Flag Tracking
119// ============================================================================
120
122static bool g_action_flag = false;
123
128static void set_action_flag(bool action_present) {
129 g_action_flag = action_present;
130}
131
136bool has_action_flag(void) {
137 return g_action_flag;
138}
139
140// ============================================================================
141// Mode Detection Helper
142// ============================================================================
143
167static bool is_binary_level_option_with_args(const char *arg, bool *out_takes_arg, bool *out_takes_optional_arg) {
168 if (!arg) {
169 SET_ERRNO(ERROR_INVALID_PARAM, "arg is null");
170 return false;
171 }
172
173 if (out_takes_arg)
174 *out_takes_arg = false;
175 if (out_takes_optional_arg)
176 *out_takes_optional_arg = false;
177
178 // Extract option name (without leading dashes and without value after '=')
179 const char *opt_name = arg;
180 if (arg[0] == '-') {
181 opt_name = arg + (arg[1] == '-' ? 2 : 1); // Skip - or --
182 }
183
184 // For --option=value format, stop at the '=' sign
185 const char *equals = strchr(opt_name, '=');
186 size_t opt_len = equals ? (size_t)(equals - opt_name) : strlen(opt_name);
187 char opt_buffer[64];
188 if (opt_len >= sizeof(opt_buffer)) {
189 opt_len = sizeof(opt_buffer) - 1;
190 }
191 SAFE_STRNCPY(opt_buffer, opt_name, opt_len + 1);
192 opt_name = opt_buffer;
193
194 // Check option against binary-level options
195 if (strcmp(opt_name, "help") == 0 || strcmp(arg, "-h") == 0) {
196 return true;
197 }
198 if (strcmp(opt_name, "version") == 0 || strcmp(arg, "-v") == 0) {
199 return true;
200 }
201 if (strcmp(opt_name, "verbose") == 0 || strcmp(arg, "-V") == 0) {
202 if (out_takes_optional_arg)
203 *out_takes_optional_arg = true;
204 return true;
205 }
206 if (strcmp(opt_name, "quiet") == 0 || strcmp(arg, "-q") == 0) {
207 return true;
208 }
209 if (strcmp(opt_name, "log-file") == 0 || strcmp(arg, "-L") == 0) {
210 if (out_takes_arg)
211 *out_takes_arg = true;
212 return true;
213 }
214 if (strcmp(opt_name, "log-level") == 0) {
215 if (out_takes_arg)
216 *out_takes_arg = true;
217 return true;
218 }
219 if (strcmp(opt_name, "config") == 0) {
220 if (out_takes_arg)
221 *out_takes_arg = true;
222 return true;
223 }
224 if (strcmp(opt_name, "config-create") == 0) {
225 if (out_takes_optional_arg)
226 *out_takes_optional_arg = true;
227 return true;
228 }
229 if (strcmp(opt_name, "color") == 0) {
230 // --color takes an optional argument (auto/true/false)
231 // Can be used as --color (defaults to true) or --color=auto or --color auto
232 if (out_takes_optional_arg)
233 *out_takes_optional_arg = true;
234 return true;
235 }
236 if (strcmp(opt_name, "check-update") == 0) {
237 return true;
238 }
239 if (strcmp(opt_name, "no-check-update") == 0) {
240 return true;
241 }
242 if (strcmp(opt_name, "json") == 0) {
243 return true;
244 }
245 if (strcmp(opt_name, "log-format-console") == 0) {
246 return true;
247 }
248
249 return false;
250}
251
264static bool is_mirror_mode_option(const char *opt_name, char short_name) {
265 // Get unified config
266 options_config_t *config = options_preset_unified(NULL, NULL);
267 if (!config) {
268 return false;
269 }
270
271 // Check if this option exists and applies to mirror mode
272 bool found = false;
273 for (size_t i = 0; i < config->num_descriptors; i++) {
274 const option_descriptor_t *desc = &config->descriptors[i];
275
276 // Check by long name
277 if (opt_name && desc->long_name && strcmp(opt_name, desc->long_name) == 0) {
278 // Check if this option is EXCLUSIVE to mirror mode (not shared with other modes)
279 if ((desc->mode_bitmask & (1 << MODE_MIRROR)) && (desc->mode_bitmask == (1 << MODE_MIRROR))) {
280 found = true;
281 }
282 break;
283 }
284
285 // Check by short name
286 if (short_name != '\0' && desc->short_name == short_name) {
287 // Check if this option is EXCLUSIVE to mirror mode (not shared with other modes)
288 if ((desc->mode_bitmask & (1 << MODE_MIRROR)) && (desc->mode_bitmask == (1 << MODE_MIRROR))) {
289 found = true;
290 }
291 break;
292 }
293 }
294
296 return found;
297}
298
299static asciichat_error_t options_detect_mode(int argc, char **argv, asciichat_mode_t *out_mode,
300 char *out_session_string, int *out_mode_index) {
301 if (out_mode == NULL || out_mode_index == NULL) {
302 return SET_ERRNO(ERROR_INVALID_PARAM, "Output parameters must not be NULL");
303 }
304
305 *out_mode_index = -1;
306 if (out_session_string) {
307 out_session_string[0] = '\0';
308 }
309
310 // Check if argv[0] itself is a mode name (for test compatibility)
311 // This handles the case where tests pass ["client", "-p", "80"] without a binary name
312 const char *const mode_names_check[] = {"server", "client", "mirror", "discovery-service", NULL};
314 for (int i = 0; mode_names_check[i] != NULL; i++) {
315 if (strcmp(argv[0], mode_names_check[i]) == 0) {
316 log_dev("Mode detected from argv[0]: %s", argv[0]);
317 *out_mode = mode_values_check[i];
318 *out_mode_index = 0;
319 return ASCIICHAT_OK;
320 }
321 }
322
323 log_dev("argv[0] '%s' is not a mode name, looking for first non-option argument", argv[0]);
324
325 // Find the first non-option argument (potential mode or session string)
326 // Also detect mirror-specific options to infer mode if no positional arg found
327 int first_positional_idx = -1;
328 bool has_mirror_specific_option = false;
329 for (int i = 1; i < argc; i++) {
330 // Skip options and their arguments
331 if (argv[i][0] == '-') {
332 bool takes_arg = false;
333 bool takes_optional_arg = false;
334
335 // Check if this is a binary-level option
336 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
337 // For --option=value format, don't skip the next argument
338 // (the value is part of the current argument)
339 const char *equals = strchr(argv[i], '=');
340 if (!equals) {
341 // No equals sign, so value might be in next argument
342 if (takes_arg && i + 1 < argc) {
343 i++; // Skip required argument
344 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
345 // For optional arguments, skip if it doesn't look like a mode/session string
346 const char *next_arg = argv[i + 1];
347 bool looks_like_mode_or_session = false;
348
349 // Check if it looks like a mode name
350 const char *const mode_names[] = {"server", "client", "mirror", "discovery-service", NULL};
351 for (int j = 0; mode_names[j] != NULL; j++) {
352 if (strcmp(next_arg, mode_names[j]) == 0) {
353 looks_like_mode_or_session = true;
354 break;
355 }
356 }
357
358 // Check if it looks like a session string (word-word-word)
359 if (!looks_like_mode_or_session && is_session_string(next_arg)) {
360 looks_like_mode_or_session = true;
361 }
362
363 // If it doesn't look like a mode/session, it's the optional argument for this option
364 if (!looks_like_mode_or_session) {
365 i++; // Skip optional argument
366 }
367 }
368 }
369 } else {
370 // Extract option name (without leading dashes)
371 const char *opt_name = argv[i];
372 char short_name = '\0';
373
374 if (opt_name[0] == '-') {
375 if (opt_name[1] == '-') {
376 // Long option: skip "--"
377 opt_name = opt_name + 2;
378 } else {
379 // Short option: extract char and store
380 short_name = opt_name[1];
381 opt_name = opt_name + 1;
382 }
383 }
384
385 // Check if this option is mirror-specific by querying mirror config
386 if (is_mirror_mode_option(opt_name, short_name)) {
387 has_mirror_specific_option = true;
388 // Skip the argument if this option takes one
389 if (i + 1 < argc && argv[i + 1][0] != '-') {
390 i++; // Skip required argument
391 }
392 } else {
393 // Other mode-specific option - skip argument if it takes one
394 if (i + 1 < argc && argv[i + 1][0] != '-') {
395 i++; // Skip potential argument
396 }
397 }
398 }
399 continue;
400 }
401
402 // Found a positional argument
403 first_positional_idx = i;
404 break;
405 }
406
407 // If no positional argument found, check for mirror-specific options
408 if (first_positional_idx == -1) {
409 if (has_mirror_specific_option) {
410 // User specified mirror-specific option (-f or -u) without explicit mode keyword
411 *out_mode = MODE_MIRROR;
412 *out_mode_index = -1;
413 return ASCIICHAT_OK;
414 }
415 // Otherwise default to discovery mode (start new session)
416 *out_mode = MODE_DISCOVERY;
417 *out_mode_index = -1;
418 return ASCIICHAT_OK;
419 }
420
421 const char *positional = argv[first_positional_idx];
422
423 // Try to match against known modes
424 const char *const mode_names[] = {"server", "client", "mirror", "discovery-service", "default", NULL};
426
427 for (int i = 0; mode_names[i] != NULL; i++) {
428 if (strcmp(positional, mode_names[i]) == 0) {
429 if (mode_values[i] == MODE_INVALID) {
430 return SET_ERRNO(ERROR_USAGE,
431 "The default mode cannot be specified explicitly. Just run 'ascii-chat' without a mode.");
432 }
433 *out_mode = mode_values[i];
434 *out_mode_index = first_positional_idx;
435 return ASCIICHAT_OK;
436 }
437 }
438
439 // Not a known mode - check if it's a session string (word-word-word pattern)
440 if (is_session_string(positional)) {
441 *out_mode = MODE_DISCOVERY;
442 *out_mode_index = first_positional_idx;
443 if (out_session_string) {
444 SAFE_STRNCPY(out_session_string, positional, SESSION_STRING_BUFFER_SIZE);
445 }
446 return ASCIICHAT_OK;
447 }
448
449 // Unknown positional argument - suggest closest match
450 const char *suggestion = asciichat_suggest_mode(positional);
451 if (suggestion) {
452 return SET_ERRNO(ERROR_USAGE, "Unknown mode '%s'. Did you mean '%s'?", positional, suggestion);
453 }
454 return SET_ERRNO(ERROR_USAGE, "Unknown mode or invalid argument: %s", positional);
455}
456
457// ============================================================================
458// Default Initialization
459// ============================================================================
460
475typedef struct {
477 unsigned short int verbose_level;
478 bool quiet;
480 bool help;
482 char config_file[OPTIONS_BUFF_SIZE];
484 int color; // Color setting (COLOR_SETTING_AUTO/TRUE/FALSE) - binary-level option parsed early
485 bool json; // JSON logging format - binary-level option
486 bool log_format_console_only; // Apply log template only to console - binary-level option
487 bool no_check_update; // Disable the startup update check
489
490static inline binary_level_opts_t extract_binary_level(const options_t *opts) {
491 binary_level_opts_t binary = {0};
492 if (!opts)
493 return binary;
494 binary.log_level = opts->log_level;
495 binary.verbose_level = opts->verbose_level;
496 binary.quiet = opts->quiet;
497 SAFE_STRNCPY(binary.log_file, opts->log_file, sizeof(binary.log_file));
498 binary.help = opts->help;
499 binary.version = opts->version;
500 SAFE_STRNCPY(binary.config_file, opts->config_file, sizeof(binary.config_file));
501 binary.detected_mode = opts->detected_mode;
502 binary.color = opts->color; // Save color setting (parsed in STAGE 1A)
503 binary.json = opts->json; // Save JSON logging flag (binary-level option)
504 binary.log_format_console_only = opts->log_format_console_only; // Save log format console setting
505 binary.no_check_update = opts->no_check_update;
506 return binary;
507}
508
509static inline void restore_binary_level(options_t *opts, const binary_level_opts_t *binary) {
510 if (!opts || !binary)
511 return;
512 opts->log_level = binary->log_level;
513 opts->verbose_level = binary->verbose_level;
514 opts->quiet = binary->quiet;
515 SAFE_STRNCPY(opts->log_file, binary->log_file, sizeof(opts->log_file));
516 opts->help = binary->help;
517 opts->version = binary->version;
518 SAFE_STRNCPY(opts->config_file, binary->config_file, sizeof(opts->config_file));
519 opts->detected_mode = binary->detected_mode;
520 opts->color = binary->color; // Restore color setting (parsed in STAGE 1A)
521 opts->json = binary->json; // Restore JSON logging flag (binary-level option)
522 opts->log_format_console_only = binary->log_format_console_only; // Restore log format console setting
523 opts->no_check_update = binary->no_check_update;
524}
525
527 options_t opts;
528
529 // Zero-initialize all fields first
530 memset(&opts, 0, sizeof(opts));
531
532 // ============================================================================
533 // GENERAL CATEGORY - General-purpose options
534 // ============================================================================
535 opts.help = OPT_HELP_DEFAULT;
541 opts.fps_explicitly_set = false;
545
546 // ============================================================================
547 // LOGGING CATEGORY - Binary-level logging and output control options
548 // ============================================================================
549 // log_file is mode-dependent at startup and intentionally left empty here
553 opts.grep_pattern[0] = '\0'; // Explicitly ensure grep_pattern is empty
554 opts.json = OPT_JSON_DEFAULT;
556 // log_template and log_format are set by get_default_log_template()
557
558 // ============================================================================
559 // TERMINAL CATEGORY - Terminal display options
560 // ============================================================================
570
571 // ============================================================================
572 // DISPLAY CATEGORY - Display and rendering options
573 // ============================================================================
577 // palette_custom is already zeroed by memset
580 opts.fps = OPT_FPS_DEFAULT;
584 opts.waveform = false;
585 opts.fft = false;
588
589 // ============================================================================
590 // WEBCAM CATEGORY - Webcam and capture device options
591 // ============================================================================
595
596 // ============================================================================
597 // MEDIA CATEGORY - Media file streaming and playback options
598 // ============================================================================
599 // media_file is already zeroed by memset
600 // media_url is already zeroed by memset
605 // yt_dlp_options is already zeroed by memset
606
607 // Render-to-file options
608 // render_file is already zeroed by memset
610 // render_font is already zeroed by memset
612
613 // ============================================================================
614 // NETWORK CATEGORY - Network connectivity and protocol options
615 // ============================================================================
616 SAFE_STRNCPY(opts.address, OPT_ADDRESS_DEFAULT, sizeof(opts.address));
624 // Discovery Service (ACDS) Options
632 // discovery_service_key is already zeroed by memset
633 opts.discovery_database_path[0] = '\0'; // Explicitly ensure discovery_database_path is empty
634 // LAN Discovery & WebRTC Options
638 // WebRTC Mode & Strategy Options
643 opts.webrtc_relay_only = false;
652 // turn_secret is already zeroed by memset
653
654 // ============================================================================
655 // AUDIO CATEGORY - Audio capture, playback and processing options
656 // ============================================================================
667
668 // ============================================================================
669 // SECURITY CATEGORY - Encryption and authentication options
670 // ============================================================================
672 // encrypt_key is already zeroed by memset
673 // password is already zeroed by memset
674 // encrypt_keyfile is already zeroed by memset
677 // server_key is already zeroed by memset
678 // client_keys is already zeroed by memset
679 // identity_keys array is already zeroed by memset
680 opts.num_identity_keys = 0;
683
684 // ============================================================================
685 // CONFIGURATION CATEGORY - Application configuration options
686 // ============================================================================
687 // config_file is already zeroed by memset
688
689 // ============================================================================
690 // DATABASE CATEGORY - Database and persistent storage options
691 // ============================================================================
692 // discovery_database_path is already zeroed by memset (set above in NETWORK)
693
694 // ============================================================================
695 // Internal/System Fields
696 // ============================================================================
697 // session_string is already zeroed by memset
698 // detected_mode is already zeroed by memset
699
700 return opts;
701}
702
715 // Extract binary-level options before reset
716 binary_level_opts_t binary = extract_binary_level(source);
717
718 // Create fresh options with all defaults
719 options_t opts = options_t_new();
720
721 // Restore binary-level options (overwrite defaults)
722 restore_binary_level(&opts, &binary);
723
724 return opts;
725}
726
727static char *options_get_log_filepath(asciichat_mode_t detected_mode, options_t opts) {
728 // Use static buffers to avoid SAFE_MALLOC and malloc before memory tracking is initialized
729 static char log_filename_buf[256];
730 static char default_log_path_buf[PLATFORM_MAX_PATH_LENGTH];
731 static char tmp_dir_buf[PLATFORM_MAX_PATH_LENGTH];
732 // Use a smaller static buffer to avoid WASM memory issues
733 static char result_buf[512]; // Reduced size for WASM compatibility
734
735 // Determine log filename based on mode
736 const char *log_filename_literal;
737 switch (detected_mode) {
738 case MODE_SERVER:
739 log_filename_literal = "server.log";
740 break;
741 case MODE_CLIENT:
742 log_filename_literal = "client.log";
743 break;
744 case MODE_MIRROR:
745 log_filename_literal = "mirror.log";
746 break;
748 log_filename_literal = "acds.log";
749 break;
750 case MODE_DISCOVERY:
751 log_filename_literal = "discovery.log";
752 break;
753 default:
754 log_filename_literal = "ascii-chat.log";
755 break;
756 }
757 SAFE_STRNCPY(log_filename_buf, log_filename_literal, sizeof(log_filename_buf) - 1);
758 log_filename_buf[sizeof(log_filename_buf) - 1] = '\0';
759
760 char *log_dir = get_log_dir();
761 if (log_dir) {
762 // Build full log file path: log_dir + separator + log_filename
763 safe_snprintf(default_log_path_buf, PLATFORM_MAX_PATH_LENGTH, "%s%s%s", log_dir, PATH_SEPARATOR_STR,
764 log_filename_buf);
765
766 // Validate and normalize the path
767 char *normalized_default_log = NULL;
768 if (path_validate_user_path(default_log_path_buf, PATH_ROLE_LOG_FILE, &normalized_default_log) == ASCIICHAT_OK) {
769 SAFE_SNPRINTF(opts.log_file, OPTIONS_BUFF_SIZE, "%s", normalized_default_log);
770 SAFE_FREE(normalized_default_log);
771 } else {
772 // Validation failed - use the path as-is (validation may fail in debug builds)
773 SAFE_SNPRINTF(opts.log_file, OPTIONS_BUFF_SIZE, "%s", default_log_path_buf);
774 }
775
776 // Free log_dir allocated by get_log_dir() using SAFE_MALLOC
777 SAFE_FREE(log_dir);
778
779 // Copy to static result buffer
780 SAFE_STRNCPY(result_buf, default_log_path_buf, sizeof(result_buf) - 1);
781 result_buf[sizeof(result_buf) - 1] = '\0';
782 return result_buf;
783
784 } else {
785 SAFE_FREE(log_dir);
787 safe_snprintf(default_log_path_buf, PLATFORM_MAX_PATH_LENGTH, "%s%s%s", tmp_dir_buf, PATH_SEPARATOR_STR,
788 "ascii-chat.log");
789 SAFE_STRNCPY(result_buf, default_log_path_buf, sizeof(result_buf) - 1);
790 result_buf[sizeof(result_buf) - 1] = '\0';
791 return result_buf;
792 } else {
793 // Fallback to just the filename
794 SAFE_STRNCPY(result_buf, log_filename_buf, sizeof(result_buf) - 1);
795 result_buf[sizeof(result_buf) - 1] = '\0';
796 return result_buf;
797 }
798 }
799}
800
801// ============================================================================
802// Binary-Level Boolean Option Parser
803// ============================================================================
804
805// Parses binary-level boolean options with support for multiple formats:
806// - --long-name → sets field to true
807// - --long-name=true/false/yes/no/1/0/on/off → parses value
808// - -X (short form) → sets field to true
809// - -X=value (short form with value) → parses value
810static bool parse_binary_bool_arg(const char *arg, bool *field, const char *long_name, char short_name) {
811 if (!arg || !field || !long_name)
812 return false;
813
814 // Check long form: --long-name
815 char long_form[256];
816 snprintf(long_form, sizeof(long_form), "--%s", long_name);
817 if (strcmp(arg, long_form) == 0) {
818 *field = true;
819 return true;
820 }
821
822 // Check long form with =value: --long-name=...
823 char long_form_eq[256];
824 snprintf(long_form_eq, sizeof(long_form_eq), "--%s=", long_name);
825 if (strncmp(arg, long_form_eq, strlen(long_form_eq)) == 0) {
826 const char *value = arg + strlen(long_form_eq);
827 if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 ||
828 strcasecmp(value, "on") == 0) {
829 *field = true;
830 return true;
831 } else if (strcasecmp(value, "false") == 0 || strcasecmp(value, "no") == 0 || strcasecmp(value, "0") == 0 ||
832 strcasecmp(value, "off") == 0) {
833 *field = false;
834 return true;
835 }
836 // If invalid value, still consume it (builder will validate later)
837 return true;
838 }
839
840 // Check short form: -X (if defined)
841 if (short_name != '\0') {
842 char short_form[3];
843 snprintf(short_form, sizeof(short_form), "-%c", short_name);
844 if (strcmp(arg, short_form) == 0) {
845 *field = true;
846 return true;
847 }
848 }
849
850 return false;
851}
852
853// ============================================================================
854// Main Option Parser Entry Point
855// ============================================================================
856
857asciichat_error_t options_init(int argc, char **argv) {
858 log_debug("options_init: starting with argc=%d, argv[0]=%s, argv[1]=%s", argc,
859 (argv && argc > 0 && argv[0]) ? argv[0] : "(null)", (argv && argc > 1 && argv[1]) ? argv[1] : "N/A");
860 // NOTE: --grep filter is initialized in main.c BEFORE any logging starts
861 // This allows ALL logs (including from shared_init) to be filtered
862 // Validate arguments (safety check for tests)
863 if (argc < 0 || argc > 128) {
864 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid argc: %d", argc);
865 }
866 if (argv == NULL) {
867 return SET_ERRNO(ERROR_INVALID_PARAM, "argv is NULL");
868 }
869 // Validate all argv elements are non-NULL up to argc
870 for (int i = 0; i < argc; i++) {
871 if (argv[i] == NULL) {
872 return SET_ERRNO(ERROR_INVALID_PARAM, "argv[%d] is NULL (argc=%d)", i, argc);
873 }
874 }
875 // Initialize RCU options system (must be done before any threads start)
876 // This must happen FIRST, before any other initialization
877 asciichat_error_t rcu_init_result = options_state_init();
878 if (rcu_init_result != ASCIICHAT_OK) {
879 return rcu_init_result;
880 }
881
882 // ========================================================================
883 // STAGE 1: Mode Detection and Binary-Level Option Handling
884 // ========================================================================
885
886 // Check for binary-level actions FIRST (before mode detection)
887 // These actions may take arguments, so we need to check them before mode detection
888 bool show_version = false;
889 bool create_config = false;
890 bool create_manpage = false;
891 bool has_action = false; // Track if any action flag is present
892 const char *config_create_path = NULL;
893 const char *manpage_create_path = NULL;
894
895 // ========================================================================
896 // STAGE 1A: Quick scan for action flags FIRST (they bypass mode detection)
897 // ========================================================================
898 // Quick scan for action flags (they may have arguments)
899 // This must happen BEFORE logging initialization so we can suppress logs before shared_init()
900 // Also scan for --quiet / -q so we can suppress logging from the start
901 // Also scan for --color early so it affects help output colors
902 bool user_quiet = false;
903 int parsed_color_setting = COLOR_SETTING_AUTO; // Store parsed color value until opts is created
904 bool color_setting_found = false;
905 bool check_update_flag_seen = false;
906 bool no_check_update_flag_seen = false;
907
908 // FIRST: Scan entire argv for --help (special case - works before OR after mode)
909 for (int i = 1; i < argc; i++) {
910 if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
911 // Scan backwards to find if a mode was specified before --help
912 asciichat_mode_t help_mode = MODE_DISCOVERY; // Default to discovery mode (binary-level help)
913 for (int j = i - 1; j >= 1; j--) {
914 if (argv[j][0] != '-') {
915 if (strcmp(argv[j], "server") == 0) {
916 help_mode = MODE_SERVER;
917 } else if (strcmp(argv[j], "client") == 0) {
918 help_mode = MODE_CLIENT;
919 } else if (strcmp(argv[j], "mirror") == 0) {
920 help_mode = MODE_MIRROR;
921 } else if (strcmp(argv[j], "discovery-service") == 0) {
922 help_mode = MODE_DISCOVERY_SERVICE;
923 } else if (strcmp(argv[j], "discovery") == 0) {
924 help_mode = MODE_DISCOVERY;
925 }
926 break; // Found first non-flag argument
927 }
928 }
929
930 // Show help for the detected mode (or binary-level if no mode)
931 usage(stdout, help_mode);
932 fflush(NULL);
933 _Exit(0);
934 }
935 }
936
937 // THEN: Scan for other binary-level actions (stops at mode name)
938 for (int i = 1; i < argc; i++) {
939 // Stop scanning at mode name - binary-level options must come before the mode
940 if (argv[i][0] != '-') {
941 bool is_mode =
942 (strcmp(argv[i], "server") == 0 || strcmp(argv[i], "client") == 0 || strcmp(argv[i], "mirror") == 0 ||
943 strcmp(argv[i], "discovery") == 0 || strcmp(argv[i], "discovery-service") == 0);
944 if (is_mode) {
945 break; // Stop processing at mode name
946 }
947 }
948 if (argv[i][0] == '-') {
949 // Handle --quiet, -q, --quiet=value formats
950 bool temp_quiet = false;
951 if (parse_binary_bool_arg(argv[i], &temp_quiet, "quiet", 'q')) {
952 if (temp_quiet) {
953 user_quiet = true;
954 }
955 }
956 // Validate --log-level and --log-file require arguments
957 if (strcmp(argv[i], "--log-level") == 0) {
958 if (i + 1 >= argc || argv[i + 1][0] == '-') {
959 log_error("--log-level requires a value (dev, debug, info, warn, error, fatal)");
960 return ERROR_USAGE;
961 }
962 i++; // Skip the argument in this loop
963 }
964 if (strcmp(argv[i], "-L") == 0 || strcmp(argv[i], "--log-file") == 0) {
965 if (i + 1 >= argc || argv[i + 1][0] == '-') {
966 log_error("%s requires a file path", argv[i]);
967 return ERROR_USAGE;
968 }
969 i++; // Skip the argument in this loop
970 }
971 // --help is handled in the first scan loop above (works before OR after mode)
972 // Parse --color early so it affects help output colors
973 if (strcmp(argv[i], "--color") == 0) {
974 if (i + 1 < argc && argv[i + 1][0] != '-') {
975 // Check if next arg is a mode name (not a color value)
976 const char *next_arg = argv[i + 1];
977 bool is_mode =
978 (strcmp(next_arg, "server") == 0 || strcmp(next_arg, "client") == 0 || strcmp(next_arg, "mirror") == 0 ||
979 strcmp(next_arg, "discovery") == 0 || strcmp(next_arg, "discovery-service") == 0);
980
981 if (is_mode) {
982 // Next arg is a mode, not a color value - default to true
983 parsed_color_setting = COLOR_SETTING_TRUE;
984 color_setting_found = true;
985 } else {
986 // Try to parse as color value
987 char *error_msg = NULL;
988 if (parse_color_setting(next_arg, &parsed_color_setting, &error_msg)) {
989 color_setting_found = true;
990 i++; // Skip the setting argument
991 } else {
992 if (error_msg) {
993 log_error("Error parsing --color: %s", error_msg);
994 free(error_msg);
995 }
996 }
997 }
998 } else {
999 // --color without argument defaults to true (enable colors)
1000 parsed_color_setting = COLOR_SETTING_TRUE;
1001 color_setting_found = true;
1002 }
1003 }
1004 // Check for --color=value format
1005 if (strncmp(argv[i], "--color=", 8) == 0) {
1006 const char *value = argv[i] + 8;
1007 char *error_msg = NULL;
1008 if (parse_color_setting(value, &parsed_color_setting, &error_msg)) {
1009 color_setting_found = true;
1010 } else {
1011 if (error_msg) {
1012 log_error("Error parsing --color: %s", error_msg);
1013 free(error_msg);
1014 }
1015 }
1016 }
1017 if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) {
1018 show_version = true;
1019 has_action = true;
1020 break;
1021 }
1022 if (strcmp(argv[i], "--check-update") == 0) {
1023 check_update_flag_seen = true;
1024 if (no_check_update_flag_seen) {
1025 log_error("Cannot specify both --check-update and --no-check-update");
1026 return ERROR_USAGE;
1027 }
1028 has_action = true;
1030 // action_check_update_immediate() calls _Exit(), so we don't reach here
1031 break;
1032 }
1033 if (strcmp(argv[i], "--no-check-update") == 0) {
1034 no_check_update_flag_seen = true;
1035 if (check_update_flag_seen) {
1036 log_error("Cannot specify both --check-update and --no-check-update");
1037 return ERROR_USAGE;
1038 }
1039 // Flag will be parsed normally later
1040 }
1041 if (strcmp(argv[i], "--config-create") == 0) {
1042 create_config = true;
1043 has_action = true;
1044 // Check for optional [FILE] argument
1045 if (i + 1 < argc && argv[i + 1][0] != '-') {
1046 config_create_path = argv[i + 1];
1047 i++; // Consume the file path argument
1048 }
1049 break;
1050 }
1051 if (strcmp(argv[i], "--man-page-create") == 0) {
1052 create_manpage = true;
1053 has_action = true;
1054 // Check for optional [FILE] argument
1055 if (i + 1 < argc && argv[i + 1][0] != '-') {
1056 manpage_create_path = argv[i + 1];
1057 i++; // Consume the file path argument
1058 }
1059 break;
1060 }
1061 if (strcmp(argv[i], "--completions") == 0) {
1062 has_action = true;
1063 // Handle --completions: generate shell completion scripts
1064 if (i + 1 < argc && argv[i + 1][0] != '-') {
1065 const char *shell_name = argv[i + 1];
1066 i++; // Consume shell name
1067
1068 // Check for optional output file
1069 const char *output_file = NULL;
1070 if (i + 1 < argc && argv[i + 1][0] != '-') {
1071 output_file = argv[i + 1];
1072 i++; // Consume output file
1073 }
1074
1075 action_completions(shell_name, output_file);
1076 // action_completions() calls _Exit(), so we don't reach here
1077 } else {
1078 log_error("--completions requires shell name (bash, fish, zsh, powershell)");
1079 return ERROR_USAGE;
1080 }
1081 break; // Unreachable, but for clarity
1082 }
1083 if (strcmp(argv[i], "--list-webcams") == 0) {
1084 has_action = true;
1086 // action_list_webcams() calls _Exit(), so we don't reach here
1087 break;
1088 }
1089 if (strcmp(argv[i], "--list-microphones") == 0) {
1090 has_action = true;
1092 // action_list_microphones() calls _Exit(), so we don't reach here
1093 break;
1094 }
1095 if (strcmp(argv[i], "--list-speakers") == 0) {
1096 has_action = true;
1098 // action_list_speakers() calls _Exit(), so we don't reach here
1099 break;
1100 }
1101 // Check for --show-capabilities (binary-level action)
1102 if (strcmp(argv[i], "--show-capabilities") == 0) {
1103 has_action = true;
1105 // action_show_capabilities_immediate() calls _Exit(), so we don't reach here
1106 break;
1107 }
1108 }
1109 }
1110 // Store action flag globally for use during cleanup
1111 set_action_flag(has_action);
1112 // ========================================================================
1113 // STAGE 1B: DO MODE DETECTION EARLY (needed for log_init)
1114 // ========================================================================
1115 asciichat_mode_t detected_mode = MODE_DISCOVERY; // Default mode
1116 char detected_session_string[SESSION_STRING_BUFFER_SIZE] = {0};
1117 int mode_index = -1;
1118
1119 asciichat_error_t mode_detect_result =
1120 options_detect_mode(argc, argv, &detected_mode, detected_session_string, &mode_index);
1121 if (mode_detect_result != ASCIICHAT_OK) {
1122 return mode_detect_result;
1123 }
1124
1125 // VALIDATE: Binary-level options must appear BEFORE the mode
1126 // Check if any binary-level options appear after the mode position
1127 if (mode_index > 0) {
1128 for (int i = mode_index + 1; i < argc; i++) {
1129 if (argv[i][0] == '-') {
1130 bool takes_arg = false;
1131 bool takes_optional_arg = false;
1132
1133 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1134 return SET_ERRNO(ERROR_USAGE, "Binary-level option '%s' must appear before the mode '%s', not after it",
1135 argv[i], argv[mode_index]);
1136 }
1137 }
1138 }
1139 }
1140 // ========================================================================
1141 // STAGE 1C: Initialize logging EARLY (before any log_dev calls)
1142 // ========================================================================
1143 // Create local options struct and initialize with defaults
1144 options_t opts = options_t_new(); // Initialize with all defaults
1145 opts.no_check_update = no_check_update_flag_seen;
1146 opts.detected_mode = detected_mode;
1147 char *log_filename = options_get_log_filepath(detected_mode, opts);
1148 SAFE_SNPRINTF(opts.log_file, OPTIONS_BUFF_SIZE, "%s", log_filename);
1149 // Note: log_init() is called earlier in asciichat_shared_init() and will be
1150 // reconfigured with the actual log level and file in main.c after options are fully parsed
1151 // NOTE: --color detection now happens in src/main.c BEFORE asciichat_shared_init()
1152 // This ensures g_color_flag_passed and g_color_flag_value are set before any logging.
1153 //
1154 // NOTE: Timer system and shared subsystems are initialized by src/main.c
1155 // via asciichat_shared_init() BEFORE options_init() is called.
1156 // This allows options_init() to use properly configured logging.
1157
1158 // If an action flag is detected OR user passed --quiet, silence logs for clean output
1159 if (user_quiet || has_action) {
1160 log_set_terminal_output(false); // Suppress console logging for clean action output
1161 }
1162 // Apply parsed color setting from STAGE 1A
1163 if (color_setting_found) {
1164 opts.color = parsed_color_setting;
1165 } else {
1166 opts.color = OPT_COLOR_DEFAULT; // Apply default
1167 }
1168
1169 // If we found version/config-create/create-manpage, handle them immediately (before mode detection)
1170 if (show_version || create_config || create_manpage) {
1171 if (show_version) {
1172 opts.version = true;
1173 options_state_set(&opts);
1174 return ASCIICHAT_OK;
1175 } else if (create_config) {
1176 // Build the schema first so config_create_default can generate options from it
1177 const options_config_t *unified_config = options_preset_unified(NULL, NULL);
1178 if (unified_config) {
1179 asciichat_error_t schema_build_result = config_schema_build_from_configs(&unified_config, 1);
1180 if (schema_build_result != ASCIICHAT_OK) {
1181 // Schema build failed, but continue anyway
1182 (void)schema_build_result;
1183 }
1184 options_config_destroy(unified_config);
1185 }
1186
1187 // Call action handler which handles all output and prompts properly
1188 action_create_config(config_create_path);
1189 // action_create_config() calls _Exit(), so we don't reach here
1190 } else if (create_manpage) {
1191 // Call action handler which handles all output and prompts properly
1192 action_create_manpage(manpage_create_path);
1193 // action_create_manpage() calls _Exit(), so we don't reach here
1194 }
1195 }
1196
1197 // Mode detection and logging already initialized early in STAGE 1B/1C above
1198 // (moved earlier to ensure logging is available before any log_dev() calls)
1199
1200 // Check for binary-level options that can appear before or after mode
1201 // Search entire argv to find --quiet, --log-file, --log-level, -V, etc.
1202 // These are documented as binary-level options that can appear anywhere
1203 for (int i = 1; i < argc; i++) {
1204 if (argv[i][0] == '-') {
1205 // Handle -V and --verbose (stackable verbosity)
1206 if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--verbose") == 0) {
1207 // Check if next argument is a number (optional argument)
1208 if (i + 1 < argc && argv[i + 1][0] != '-') {
1209 // Try to parse as integer count
1210 char *endptr;
1211 long value = strtol(argv[i + 1], &endptr, 10);
1212 if (*endptr == '\0' && value >= 0 && value <= 100) {
1213 opts.verbose_level = (unsigned short int)value;
1214 i++; // Skip next argument
1215 continue;
1216 }
1217 }
1218 // No valid number, just increment
1219 opts.verbose_level++;
1220 }
1221 // Handle binary-level boolean options using the abstraction function
1222 // -q/--quiet, --json, --log-format-console all use the same parser
1223 if (parse_binary_bool_arg(argv[i], &opts.quiet, "quiet", 'q')) {
1224 continue;
1225 }
1226 if (parse_binary_bool_arg(argv[i], &opts.json, "json", '\0')) {
1227 continue;
1228 }
1229 if (parse_binary_bool_arg(argv[i], &opts.log_format_console_only, "log-format-console", '\0')) {
1230 continue;
1231 }
1232 // Handle --log-level LEVEL (set log threshold)
1233 if (strcmp(argv[i], "--log-level") == 0) {
1234 if (i + 1 >= argc) {
1235 log_error("--log-level requires a value (dev, debug, info, warn, error, fatal)");
1236 return ERROR_USAGE;
1237 }
1238 if (argv[i + 1][0] == '-') {
1239 log_error("--log-level requires a value (dev, debug, info, warn, error, fatal)");
1240 return ERROR_USAGE;
1241 }
1242 char *error_msg = NULL;
1243 if (parse_log_level(argv[i + 1], &opts.log_level, &error_msg)) {
1244 i++; // Skip the level argument
1245 } else {
1246 if (error_msg) {
1247 log_error("%s", error_msg);
1248 free(error_msg);
1249 } else {
1250 log_error("invalid log level value: %s", argv[i + 1]);
1251 }
1252 return ERROR_USAGE;
1253 }
1254 }
1255 // Handle -L and --log-file FILE (set log file path)
1256 if ((strcmp(argv[i], "-L") == 0 || strcmp(argv[i], "--log-file") == 0)) {
1257 if (i + 1 >= argc) {
1258 log_error("%s requires a file path", argv[i]);
1259 return ERROR_USAGE;
1260 }
1261 if (argv[i + 1][0] == '-') {
1262 log_error("%s requires a file path", argv[i]);
1263 return ERROR_USAGE;
1264 }
1265 SAFE_STRNCPY(opts.log_file, argv[i + 1], sizeof(opts.log_file));
1266 i++; // Skip the file argument
1267 }
1268 // NOTE: --color is now parsed in STAGE 1A (early, before help processing)
1269 // to ensure help output colors are applied correctly
1270 }
1271 }
1272
1273 if (show_version) {
1274 // Show binary-level version from src/main.c
1275 opts.version = true;
1276 options_state_set(&opts);
1277 return ASCIICHAT_OK;
1278 }
1279
1280 if (create_config) {
1281 // Handle --config-create: create default config file and exit
1282 // Use provided path or default to user config location
1283 char config_path[PLATFORM_MAX_PATH_LENGTH];
1284 if (config_create_path) {
1285 SAFE_STRNCPY(config_path, config_create_path, sizeof(config_path));
1286 } else {
1287 // Use default config path: ~/.ascii-chat/config.toml
1288 char *config_dir = get_config_dir();
1289 if (!config_dir) {
1290 log_error("Error: Failed to determine default config directory");
1291 return ERROR_CONFIG;
1292 }
1293 safe_snprintf(config_path, sizeof(config_path), "%sconfig.toml", config_dir);
1294 SAFE_FREE(config_dir);
1295 }
1296
1297 // Create config with default options
1298 asciichat_error_t result = config_create_default(config_path);
1299 if (result != ASCIICHAT_OK) {
1301 if (HAS_ERRNO(&err_ctx)) {
1302 log_error("Error creating config: %s", err_ctx.context_message);
1303 } else {
1304 log_error("Error: Failed to create config file at %s", config_path);
1305 }
1306 return result;
1307 }
1308
1309 log_plain("Created default config file at: %s", config_path);
1310 return ASCIICHAT_OK; // Return successfully after creating config
1311 }
1312
1313 if (create_manpage) {
1314 // Handle --create-man-page-template: generate merged man page template
1315 // The .1.in file is the existing template to read from (not the output)
1316 const char *existing_template_path = "share/man/man1/ascii-chat.1.in";
1317 options_config_t *config = options_preset_unified(NULL, NULL);
1318 if (!config) {
1319 log_error("Error: Failed to get binary options config");
1320 return ERROR_MEMORY;
1321 }
1322
1323 // Generate merged man page from embedded or filesystem resources
1324 // (existing_template_path and manpage_content_file parameters are no longer supported)
1325 asciichat_error_t err = options_config_generate_manpage_merged(config, "ascii-chat", NULL, existing_template_path,
1326 "Video chat in your terminal");
1327
1328 options_config_destroy(config);
1329
1330 if (err != ASCIICHAT_OK) {
1332 if (HAS_ERRNO(&err_ctx)) {
1333 log_error("%s", err_ctx.context_message);
1334 } else {
1335 log_error("Error: Failed to generate man page");
1336 }
1337 return err;
1338 }
1339
1340 log_plain("Generated man page: %s", existing_template_path);
1341 return ASCIICHAT_OK; // Return successfully after generating man page
1342 }
1343
1344 // ========================================================================
1345 // STAGE 2: Build argv for mode-specific parsing
1346 // ========================================================================
1347 // If mode was found, build argv with only arguments after the mode
1348 // If mode_index == -1, filter out binary-level options from all arguments
1349 int mode_argc = argc;
1350 char **mode_argv = (char **)argv;
1351 char **allocated_mode_argv = NULL; // Track if we need to free mode_argv
1352
1353 if (mode_index == -1) {
1354 // No explicit mode - filter binary-level options from entire argv
1355 int max_mode_argc = argc; // Worst case: no binary opts skipped
1356
1357 if (max_mode_argc > 256) {
1358 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many arguments: %d", max_mode_argc);
1359 }
1360
1361 char **new_mode_argv = SAFE_MALLOC((size_t)(max_mode_argc + 1) * sizeof(char *), char **);
1362 if (!new_mode_argv) {
1363 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate mode_argv");
1364 }
1365 allocated_mode_argv = new_mode_argv;
1366
1367 new_mode_argv[0] = argv[0]; // Copy program name
1368
1369 // Copy all args except binary-level options
1370 int new_argv_idx = 1;
1371 for (int i = 1; i < argc; i++) {
1372 bool takes_arg = false;
1373 bool takes_optional_arg = false;
1374
1375 // Check if this is a binary-level option
1376 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1377 // Skip argument if needed
1378 // NOLINTNEXTLINE(bugprone-branch-clone)
1379 if (takes_arg && i + 1 < argc) {
1380 i++; // Skip required argument
1381 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
1382 i++; // Skip optional argument
1383 }
1384 continue;
1385 }
1386 // Not a binary option, copy to mode_argv
1387 new_mode_argv[new_argv_idx++] = argv[i];
1388 }
1389
1390 mode_argc = new_argv_idx;
1391 new_mode_argv[mode_argc] = NULL;
1392 mode_argv = new_mode_argv;
1393 } else if (mode_index != -1) {
1394 // Mode found at position mode_index
1395 // Build new argv: [program_name, args_before_mode (no binary opts)..., args_after_mode...]
1396 // Binary-level options are not passed to mode-specific parsers
1397
1398 int args_after_mode = argc - mode_index - 1;
1399 // We'll calculate final argc after skipping binary options
1400 int max_mode_argc = 1 + (mode_index - 1) + args_after_mode; // Worst case: no binary opts skipped
1401
1402 if (max_mode_argc > 256) {
1403 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many arguments: %d", max_mode_argc);
1404 }
1405
1406 // Allocate max_mode_argc+1 to accommodate NULL terminator
1407 char **new_mode_argv = SAFE_MALLOC((size_t)(max_mode_argc + 1) * sizeof(char *), char **);
1408 if (!new_mode_argv) {
1409 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate mode_argv");
1410 }
1411 allocated_mode_argv = new_mode_argv; // Track allocation for cleanup
1412
1413 // Copy: [program_name, args_before_mode (excluding binary opts)..., args_after_mode...]
1414 if (mode_index == 0) {
1415 // Mode is at argv[0], use "ascii-chat" as program name
1416 new_mode_argv[0] = "ascii-chat";
1417 } else {
1418 new_mode_argv[0] = argv[0];
1419 }
1420
1421 // Copy args before mode, skipping binary-level options
1422 int new_argv_idx = 1;
1423 for (int i = 1; i < mode_index; i++) {
1424 bool takes_arg = false;
1425 bool takes_optional_arg = false;
1426
1427 // Check if this is a binary-level option
1428 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1429 // Skip argument if needed
1430 if (i + 1 < mode_index && (takes_arg || (takes_optional_arg && argv[i + 1][0] != '-'))) {
1431 i++; // Skip argument
1432 }
1433 continue;
1434 }
1435 // Not a binary option, copy to mode_argv
1436 new_mode_argv[new_argv_idx++] = argv[i];
1437 }
1438 // Copy args after mode, filtering out any binary-level options (they shouldn't appear here)
1439 int args_after_mode_idx = 0;
1440 for (int i = mode_index + 1; i < argc; i++) {
1441 bool takes_arg = false;
1442 bool takes_optional_arg = false;
1443
1444 // Check if this is a binary-level option (shouldn't appear after mode)
1445 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1446 // Skip binary-level option and its argument if needed
1447 if (i + 1 < argc && (takes_arg || (takes_optional_arg && argv[i + 1][0] != '-'))) {
1448 i++; // Skip argument
1449 }
1450 continue; // Skip this option
1451 }
1452 // Not a binary option, copy to mode_argv
1453 new_mode_argv[new_argv_idx + args_after_mode_idx++] = argv[i];
1454 }
1455 // Calculate actual argc (program + filtered args before + filtered args after)
1456 mode_argc = new_argv_idx + args_after_mode_idx;
1457 new_mode_argv[mode_argc] = NULL;
1458
1459 mode_argv = new_mode_argv;
1460 }
1461
1462 // ========================================================================
1463 // STAGE 3: Set Mode-Specific Defaults
1464 // ========================================================================
1465 // Initialize all defaults using options_t_new_preserve_binary() to keep binary-level
1466 // options (--quiet, --verbose, --log-level, etc.) from being reset
1467 opts = options_t_new_preserve_binary(&opts);
1468 // If a session string was detected during mode detection, restore it after options reset
1469 // This ensures discovery mode knows which session to join
1470 if (detected_session_string[0] != '\0') {
1471 SAFE_STRNCPY(opts.session_string, detected_session_string, sizeof(opts.session_string));
1472 log_info("options_init: Detected session string from argv: '%s'", detected_session_string);
1473 }
1474
1475 // Encryption options default to disabled/empty
1476 opts.no_encrypt = 0;
1477 opts.encrypt_key[0] = '\0';
1478 opts.password[0] = '\0';
1479 opts.encrypt_keyfile[0] = '\0';
1480 opts.server_key[0] = '\0';
1481 opts.client_keys[0] = '\0';
1482 opts.palette_custom[0] = '\0';
1483
1484 // Set different default addresses for different modes
1485 if (detected_mode == MODE_CLIENT || detected_mode == MODE_MIRROR || detected_mode == MODE_DISCOVERY) {
1486 // Client/Mirror/Discovery: connects to localhost by default (discovery uses ACDS to find server)
1487 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "localhost");
1488 opts.address6[0] = '\0'; // Client doesn't use address6
1489 } else if (detected_mode == MODE_SERVER) {
1490 // Server: binds to 127.0.0.1 (IPv4) and ::1 (IPv6) by default
1491 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "127.0.0.1");
1492 // address6 is now a positional argument, not an option
1493 opts.address6[0] = '\0';
1494 } else if (detected_mode == MODE_DISCOVERY_SERVICE) {
1495 // ACDS: binds to all interfaces by default
1496 SAFE_SNPRINTF(opts.address, OPTIONS_BUFF_SIZE, "0.0.0.0");
1497 opts.address6[0] = '\0';
1498 }
1499
1500 // ========================================================================
1501 // STAGE 4: Build Dynamic Schema from Unified Options Config
1502 // ========================================================================
1503 // Build the config schema dynamically from the unified config
1504 // This generates TOML keys, CLI flags, categories, and types from builder data
1505 const options_config_t *unified_config = options_preset_unified(NULL, NULL);
1506 if (unified_config) {
1507 asciichat_error_t schema_build_result = config_schema_build_from_configs(&unified_config, 1);
1508 if (schema_build_result != ASCIICHAT_OK) {
1509 // Schema build failed, but continue with static schema as fallback
1510 (void)schema_build_result;
1511 } else {
1512 }
1513 options_config_destroy(unified_config);
1514 } else {
1515 }
1516
1517 // ========================================================================
1518 // STAGE 5: Load Configuration Files
1519 // ========================================================================
1520 // Extract binary-level options BEFORE config loading (config may reset them)
1521 binary_level_opts_t binary_before_config = extract_binary_level(&opts);
1522 // Publish options to RCU before config loading so that config logs get proper colors
1523 // from the parsed --color setting (e.g., --color=true)
1524 asciichat_error_t config_publish_result = options_state_set(&opts);
1525 (void)config_publish_result; // Suppress unused variable warning
1526 // Load config files - now uses detected_mode directly for bitmask validation
1527 // Save flip_x and flip_y as they should not be reset by config files
1528 // Also save encryption settings - they should only be controlled via CLI, not config file
1529 bool saved_flip_x_from_config = opts.flip_x;
1530 bool saved_flip_y_from_config = opts.flip_y;
1531 bool saved_encrypt_enabled = opts.encrypt_enabled;
1532 asciichat_error_t config_result = config_load_system_and_user(detected_mode, false, &opts);
1533 (void)config_result; // Continue with defaults and CLI parsing regardless of result
1534 bool fps_set_by_config = opts.fps != OPT_FPS_DEFAULT;
1535 // Restore binary-level options (don't let config override command-line options)
1536 restore_binary_level(&opts, &binary_before_config);
1537
1538 // Restore flip_x and flip_y - config shouldn't override the defaults
1539 opts.flip_x = saved_flip_x_from_config;
1540 opts.flip_y = saved_flip_y_from_config;
1541
1542 // Restore encrypt_enabled - it should only be set via CLI, not config file
1543 // The config can set key/password which auto-enables encryption, but the encrypt_enabled
1544 // flag itself should stay at its default unless the user explicitly passes --encrypt
1545 opts.encrypt_enabled = saved_encrypt_enabled;
1546
1547 // ========================================================================
1548 // STAGE 6: Parse Command-Line Arguments (Unified)
1549 // ========================================================================
1550 // Extract binary-level options BEFORE applying unified defaults
1551 // (which might override them from config files)
1552 binary_level_opts_t binary_before_defaults = extract_binary_level(&opts);
1553 asciichat_mode_t mode_saved_for_parsing = detected_mode; // CRITICAL: Save before defaults reset
1554 // Get unified config
1555 options_config_t *config = options_preset_unified(NULL, NULL);
1556 if (!config) {
1557 SAFE_FREE(allocated_mode_argv);
1558 return SET_ERRNO(ERROR_CONFIG, "Failed to create options configuration");
1559 }
1560 int remaining_argc;
1561 char **remaining_argv;
1562
1563 // Save flip_x and flip_y before applying defaults (should not be reset by defaults)
1564 bool saved_flip_x = opts.flip_x;
1565 bool saved_flip_y = opts.flip_y;
1566 // Apply defaults from unified config
1567 asciichat_error_t defaults_result = options_config_set_defaults(config, &opts);
1568 if (defaults_result != ASCIICHAT_OK) {
1569 options_config_destroy(config);
1570 SAFE_FREE(allocated_mode_argv);
1571 return defaults_result;
1572 }
1573 // Restore binary-level options (they should never be overridden by defaults)
1574 restore_binary_level(&opts, &binary_before_defaults);
1575
1576 // Restore flip_x and flip_y - they should keep the values from options_t_new()
1577 // unless explicitly set by the user (but defaults shouldn't override them)
1578 opts.flip_x = saved_flip_x;
1579 opts.flip_y = saved_flip_y;
1580
1581 // Restore detected_mode before parsing so mode validation works.
1582 opts.detected_mode = mode_saved_for_parsing;
1583
1584 // Save flip_x and flip_y before parsing - they should not be reset by the parser
1585 bool saved_flip_x_for_parse = opts.flip_x;
1586 bool saved_flip_y_for_parse = opts.flip_y;
1587 // Note: json is already saved via extract_binary_level/restore_binary_level mechanism
1588 // Parse mode-specific arguments
1589 option_mode_bitmask_t mode_bitmask = (1 << mode_saved_for_parsing);
1590 log_debug("About to parse options_config with mode_bitmask=%u, mode_argc=%d", mode_bitmask, mode_argc);
1591 asciichat_error_t result =
1592 options_config_parse(config, mode_argc, mode_argv, &opts, mode_bitmask, &remaining_argc, &remaining_argv);
1593 log_debug("options_config_parse returned: %d", result);
1594 // Restore flip_x and flip_y - they should keep their values unless explicitly overridden
1595 opts.flip_x = saved_flip_x_for_parse;
1596 opts.flip_y = saved_flip_y_for_parse;
1597 // json is already restored via the call to options_state_set which calls restore_binary_level
1598 if (result != ASCIICHAT_OK) {
1599 options_config_destroy(config);
1600 SAFE_FREE(allocated_mode_argv);
1601 // Convert ERROR_CONFIG to ERROR_USAGE for command-line parsing errors
1602 if (result == ERROR_CONFIG) {
1604 const char *error_detail = "invalid options";
1605 if (HAS_ERRNO(&err_ctx)) {
1606 error_detail = err_ctx.context_message;
1607 }
1608 return SET_ERRNO(ERROR_USAGE, "Failed to parse options: %s", error_detail);
1609 }
1610 return result;
1611 }
1612
1613 bool fps_set_by_cli = false;
1614 for (int i = 0; i < mode_argc; i++) {
1615 if (mode_argv[i] &&
1616 (strcmp(mode_argv[i], "--fps") == 0 || strncmp(mode_argv[i], "--fps=", strlen("--fps=")) == 0)) {
1617 fps_set_by_cli = true;
1618 break;
1619 }
1620 }
1621 const char *fps_env = SAFE_GETENV("ASCII_CHAT_FPS");
1622 opts.fps_explicitly_set = fps_set_by_config || fps_set_by_cli || (fps_env && fps_env[0] != '\0');
1623
1624 // ========================================================================
1625 // STAGE 6.5: Publish Parsed Options Early
1626 // ========================================================================
1627 // Publish options to RCU as soon as they're parsed
1628 // This ensures GET_OPTION() works during cleanup even if validation fails
1629 log_debug("Publishing parsed options to RCU before validation");
1630 asciichat_error_t early_publish = options_state_set(&opts);
1631 if (early_publish != ASCIICHAT_OK) {
1632 log_error("Failed to publish parsed options to RCU state early");
1633 options_config_destroy(config);
1634 SAFE_FREE(allocated_mode_argv);
1635 return early_publish;
1636 }
1637 log_debug("Successfully published options to RCU");
1638
1639 // Auto-enable custom palette if palette-chars was set
1640 if (opts.palette_custom[0] != '\0') {
1641 // palette-chars was set - always use PALETTE_CUSTOM (overrides any explicit --palette setting)
1643 opts.palette_custom_set = true;
1644 log_debug("Set PALETTE_CUSTOM because --palette-chars was provided");
1645
1646 // Validate palette characters for UTF-8 correctness
1647 if (!utf8_is_valid(opts.palette_custom)) {
1648 log_error("Error: --palette-chars contains invalid UTF-8 sequences");
1649 options_config_destroy(config);
1650 SAFE_FREE(allocated_mode_argv);
1651 return option_error_invalid();
1652 }
1653
1654 // Check if palette contains non-ASCII characters
1655 bool has_non_ascii = !utf8_is_ascii_only(opts.palette_custom);
1656 if (has_non_ascii) {
1657 // Non-ASCII characters require UTF-8 support
1658 // Check if UTF-8 is explicitly disabled or unavailable
1659 bool utf8_disabled = (opts.force_utf8 == COLOR_SETTING_FALSE);
1660 bool utf8_auto_unavailable = (opts.force_utf8 == COLOR_SETTING_AUTO && !terminal_supports_utf8());
1661
1662 if (utf8_disabled) {
1663 log_error("Error: --palette-chars contains non-ASCII characters but --utf8=false was specified");
1664 log_error(" Remove --utf8=false or use ASCII-only palette characters");
1665 options_config_destroy(config);
1666 SAFE_FREE(allocated_mode_argv);
1667 return option_error_invalid();
1668 }
1669
1670 if (utf8_auto_unavailable) {
1671 log_error("Error: --palette-chars contains non-ASCII characters but terminal does not support UTF-8");
1672 log_error(" Use --utf8=true to force UTF-8 mode or use ASCII-only palette characters");
1673 options_config_destroy(config);
1674 SAFE_FREE(allocated_mode_argv);
1675 return option_error_invalid();
1676 }
1677 }
1678 }
1679
1680 // Auto-enable encryption if key was provided
1681 if (opts.encrypt_key[0] != '\0') {
1682 // A key may be a local file, a remote reference, or inline key material.
1683 // Only validate filesystem paths when the value is actually path-like;
1684 // inline SSH/base64/password keys must not be rejected by stat().
1685 bool key_is_inline = !path_looks_like_path(opts.encrypt_key);
1686 if (!is_remote_key_path(opts.encrypt_key) && !key_is_inline) {
1687 struct stat st;
1688 if (stat(opts.encrypt_key, &st) != 0) {
1689 log_error("Key file not found: %s", opts.encrypt_key);
1690 options_config_destroy(config);
1691 SAFE_FREE(allocated_mode_argv);
1692 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key file not found: %s", opts.encrypt_key);
1693 }
1694 if ((st.st_mode & S_IFMT) != S_IFREG) {
1695 log_error("Key path is not a regular file: %s", opts.encrypt_key);
1696 options_config_destroy(config);
1697 SAFE_FREE(allocated_mode_argv);
1698 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key path is not a regular file: %s", opts.encrypt_key);
1699 }
1700 }
1701 opts.encrypt_enabled = 1;
1702 log_debug("Auto-enabled encryption because --key was provided");
1703 }
1704
1705 // Color filter validation and auto-enable
1706 if (opts.color_filter != COLOR_FILTER_NONE) {
1707 // Color filter requires color to be enabled
1708 if (opts.color == COLOR_SETTING_FALSE) {
1709 log_error("Error: --color-filter cannot be used with --color=false");
1710 options_config_destroy(config);
1711 SAFE_FREE(allocated_mode_argv);
1712 return option_error_invalid();
1713 }
1714 // Auto-enable color when color filter is specified
1716 log_debug("Auto-enabled color because --color-filter was provided");
1717 }
1718
1719 // Detect if splash or status_screen were explicitly set on command line
1720 for (int i = 0; i < mode_argc; i++) {
1721 if (mode_argv[i] &&
1722 (strcmp(mode_argv[i], "--splash-screen") == 0 || strncmp(mode_argv[i], "--splash-screen=", 16) == 0)) {
1723 // Parse the value if provided (--splash-screen=true/false)
1724 bool splash_value = true; // Default to true for plain --splash-screen
1725 if (strncmp(mode_argv[i], "--splash-screen=", 16) == 0) {
1726 const char *value = mode_argv[i] + 16;
1727 if (strcasecmp(value, "false") == 0 || strcasecmp(value, "no") == 0 || strcasecmp(value, "0") == 0 ||
1728 strcasecmp(value, "off") == 0) {
1729 splash_value = false;
1730 }
1731 }
1732 opts.splash_screen = splash_value;
1733 opts.splash_screen_explicitly_set = true;
1734 }
1735 if (mode_argv[i] &&
1736 (strcmp(mode_argv[i], "--status-screen") == 0 || strncmp(mode_argv[i], "--status-screen=", 16) == 0)) {
1737 // Parse the value if provided (--status-screen=true/false)
1738 bool status_value = true; // Default to true for plain --status-screen
1739 if (strncmp(mode_argv[i], "--status-screen=", 16) == 0) {
1740 const char *value = mode_argv[i] + 16;
1741 if (strcasecmp(value, "false") == 0 || strcasecmp(value, "no") == 0 || strcasecmp(value, "0") == 0 ||
1742 strcasecmp(value, "off") == 0) {
1743 status_value = false;
1744 }
1745 }
1746 opts.status_screen = status_value;
1747 opts.status_screen_explicitly_set = true;
1748 }
1749#ifndef NDEBUG
1750 if (mode_argv[i] &&
1751 (strcmp(mode_argv[i], "--sync-state") == 0 || strncmp(mode_argv[i], "--sync-state=", 13) == 0)) {
1753 if (strcmp(mode_argv[i], "--sync-state") == 0) {
1754 if (i + 1 < mode_argc) {
1755 char *endptr;
1756 double val = strtod(mode_argv[i + 1], &endptr);
1757 if (endptr != mode_argv[i + 1] && val > 0.0) {
1758 opts.debug_sync_state_time = val;
1759 i++;
1760 }
1761 }
1762 } else {
1763 const char *value_str = mode_argv[i] + 13;
1764 if (value_str[0] != '\0') {
1765 char *endptr;
1766 double val = strtod(value_str, &endptr);
1767 if (endptr != value_str && val > 0.0) {
1768 opts.debug_sync_state_time = val;
1769 }
1770 }
1771 }
1772 }
1773 if (mode_argv[i] && (strcmp(mode_argv[i], "--backtrace") == 0 || strncmp(mode_argv[i], "--backtrace=", 12) == 0)) {
1775 if (strcmp(mode_argv[i], "--backtrace") == 0) {
1776 if (i + 1 < mode_argc) {
1777 char *endptr;
1778 double val = strtod(mode_argv[i + 1], &endptr);
1779 if (endptr != mode_argv[i + 1] && val > 0.0) {
1780 opts.debug_backtrace_time = val;
1781 i++;
1782 }
1783 }
1784 } else {
1785 const char *value_str = mode_argv[i] + 12;
1786 if (value_str[0] != '\0') {
1787 char *endptr;
1788 double val = strtod(value_str, &endptr);
1789 if (endptr != value_str && val > 0.0) {
1790 opts.debug_backtrace_time = val;
1791 }
1792 }
1793 }
1794 }
1795 if (mode_argv[i] &&
1796 (strcmp(mode_argv[i], "--memory-report") == 0 || strncmp(mode_argv[i], "--memory-report=", 16) == 0)) {
1798 if (strcmp(mode_argv[i], "--memory-report") == 0) {
1799 if (i + 1 < mode_argc) {
1800 char *endptr;
1801 double val = strtod(mode_argv[i + 1], &endptr);
1802 if (endptr != mode_argv[i + 1] && val > 0.0) {
1804 i++;
1805 }
1806 }
1807 } else {
1808 const char *value_str = mode_argv[i] + 16;
1809 if (value_str[0] != '\0') {
1810 char *endptr;
1811 double val = strtod(value_str, &endptr);
1812 if (endptr != value_str && val > 0.0) {
1814 }
1815 }
1816 }
1817 }
1818#endif
1819 }
1820
1821 // Auto-disable splash when grep is used (since it's one-time startup screen)
1822 // UNLESS it was explicitly set by the user
1823 // Status screen is now compatible with --grep since we support auto-loading patterns
1824 bool grep_was_provided = false;
1825 for (int i = 0; i < mode_argc; i++) {
1826 if (mode_argv[i] && (strcmp(mode_argv[i], "--grep") == 0 || strncmp(mode_argv[i], "--grep=", 7) == 0)) {
1827 grep_was_provided = true;
1828 break;
1829 }
1830 }
1831
1832 if (grep_was_provided) {
1833 if (!opts.splash_screen_explicitly_set) {
1834 opts.splash_screen = false;
1835 log_debug("Auto-disabled splash because --grep was provided");
1836 }
1837 }
1838
1839 // Auto-disable splash and status screens when terminal is non-interactive or CLAUDECODE is set
1840 // This ensures clean output when running under LLM automation or non-interactive shells
1841 bool is_terminal_interactive = terminal_is_interactive();
1842 bool is_claudecode_set = platform_getenv("CLAUDECODE") != NULL;
1843 bool should_auto_disable_screens = !is_terminal_interactive || is_claudecode_set;
1844
1845 if (should_auto_disable_screens) {
1846 if (!opts.splash_screen_explicitly_set) {
1847 opts.splash_screen = false;
1848 log_debug("Auto-disabled splash (non-interactive terminal or CLAUDECODE set)");
1849 }
1850 if (!opts.status_screen_explicitly_set) {
1851 opts.status_screen = false;
1852 log_debug("Auto-disabled status screen (non-interactive terminal or CLAUDECODE set)");
1853 }
1854 }
1855
1856// Validate all string options contain valid UTF-8
1857// This prevents crashes and corruption from invalid UTF-8 in any option
1858// DISABLED IN RELEASE BUILDS: utf8_is_valid() has performance issues with musl
1859#ifndef NDEBUG
1860 const char *string_fields[][2] = {{"address", opts.address},
1861 {"address6", opts.address6},
1862 {"encrypt_key", opts.encrypt_key},
1863 {"encrypt_keyfile", opts.encrypt_keyfile},
1864 {"server_key", opts.server_key},
1865 {"client_keys", opts.client_keys},
1866 {"discovery_server", opts.discovery_server},
1867 {"discovery_service_key", opts.discovery_service_key},
1868 {"discovery_database_path", opts.discovery_database_path},
1869 {"log_file", opts.log_file},
1870 {"media_file", opts.media_file},
1871 {"palette_custom", opts.palette_custom},
1872 {"stun_servers", opts.stun_servers},
1873 {"turn_servers", opts.turn_servers},
1874 {"turn_username", opts.turn_username},
1875 {"turn_credential", opts.turn_credential},
1876 {"turn_secret", opts.turn_secret},
1877 {"session_string", opts.session_string},
1878 {NULL, NULL}};
1879
1880 for (int i = 0; string_fields[i][0] != NULL; i++) {
1881 const char *field_name = string_fields[i][0];
1882 const char *field_value = string_fields[i][1];
1883
1884 // Skip empty strings
1885 if (!field_value || field_value[0] == '\0') {
1886 continue;
1887 }
1888
1889 // Validate UTF-8
1890 if (!utf8_is_valid(field_value)) {
1891 log_error("Error: Option --%s contains invalid UTF-8 sequences", field_name);
1892 log_error(" Value: %s", field_value);
1893 options_config_destroy(config);
1894 SAFE_FREE(allocated_mode_argv);
1895 return option_error_invalid();
1896 }
1897 }
1898#endif
1899 // Validate options
1900 log_info("★ VALIDATE_OPTIONS_AND_REPORT: About to call");
1901 result = validate_options_and_report(config, &opts);
1902 log_info("★ VALIDATE_OPTIONS_AND_REPORT: Returned with result=%d", result);
1903 if (result != ASCIICHAT_OK) {
1904 options_config_destroy(config);
1905 SAFE_FREE(allocated_mode_argv);
1906 return result;
1907 }
1908 // Check for unexpected remaining arguments
1909 if (remaining_argc > 0) {
1910 log_error("Error: Unexpected arguments after options:");
1911 for (int i = 0; i < remaining_argc; i++) {
1912 log_error(" %s", remaining_argv[i]);
1913 }
1914 options_config_destroy(config);
1915 SAFE_FREE(allocated_mode_argv);
1916 return option_error_invalid();
1917 }
1918
1919 // Mode-specific post-processing
1920 // Apply mode-specific defaults (port, websocket-port)
1921 log_info("★ APPLY_MODE_SPECIFIC: About to call");
1923 log_info("★ APPLY_MODE_SPECIFIC: Done");
1924 log_dev("Applied mode-specific defaults: port=%d, websocket_port=%d", opts.port, opts.websocket_port);
1925
1926 if (detected_mode == MODE_DISCOVERY_SERVICE) {
1927 // Set default paths if not specified
1928 log_info("★ DISCOVERY_SERVICE: Handling discovery service defaults");
1929 if (opts.discovery_database_path[0] == '\0') {
1930 // Database: Try system-wide location first, fall back to user directories
1931 // Preference: /usr/local/var/ascii-chat/ > ~/.local/share/ascii-chat/ > ~/.config/ascii-chat/
1932 log_info("★ DISCOVERY_SERVICE: About to call get_discovery_database_dir()");
1933 char *db_dir = get_discovery_database_dir();
1934 log_info("★ DISCOVERY_SERVICE: get_discovery_database_dir() returned %p", (void *)db_dir);
1935 if (!db_dir) {
1936 options_config_destroy(config);
1937 SAFE_FREE(allocated_mode_argv);
1938 return SET_ERRNO(ERROR_CONFIG, "Failed to get database directory (tried system and user locations)");
1939 }
1940 safe_snprintf(opts.discovery_database_path, sizeof(opts.discovery_database_path), "%sdiscovery.db", db_dir);
1941 SAFE_FREE(db_dir);
1942 }
1943 }
1944
1945 log_info("★ OPTIONS_CONFIG_DESTROY: About to call");
1946 options_config_destroy(config);
1947 log_info("★ OPTIONS_CONFIG_DESTROY: Done");
1948
1949 // ========================================================================
1950 // STAGE 7: Post-Processing & Validation
1951 // ========================================================================
1952
1953 log_info("★ STAGE 7: Starting post-processing");
1954
1955 // Collect multiple --key flags for multi-key support (server/ACDS only)
1956 // This enables servers to load both SSH and GPG keys and select the right one
1957 // during handshake based on what the client expects
1958 if (detected_mode == MODE_SERVER || detected_mode == MODE_DISCOVERY_SERVICE) {
1959 log_info("★ STAGE 7.1: About to collect identity keys");
1960 int num_keys = options_collect_identity_keys(&opts, argc, argv);
1961 log_info("★ STAGE 7.1: options_collect_identity_keys returned %d", num_keys);
1962 if (num_keys < 0) {
1963 SAFE_FREE(allocated_mode_argv);
1964 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to collect identity keys");
1965 }
1966 // num_keys == 0 is OK (no --key flags provided)
1967 }
1968
1969 log_info("★ STAGE 7.2: About to update dimensions");
1970
1971 // After parsing command line options, update dimensions
1972 // First set any auto dimensions to terminal size, then apply full height logic
1973
1974 // Clear auto flags if dimensions were explicitly parsed from options
1975 // This must happen before update_dimensions_to_terminal_size to prevent auto-detected
1976 // values from overwriting user-specified dimensions
1977 // Note: width=0 and height=0 are special values meaning "auto-detect", so don't clear auto flags for those
1978 if (opts.width != OPT_WIDTH_DEFAULT && opts.width != 0) {
1979 opts.auto_width = false;
1980 }
1981 if (opts.height != OPT_HEIGHT_DEFAULT && opts.height != 0) {
1982 opts.auto_height = false;
1983 }
1985 js_log_options("BEFORE_UPDATE_DIMS");
1986 js_log_options("CALLING_UPDATE_DIM");
1988 js_log_options("AFTER_UPDATE_DIM");
1989
1990 js_log_options("L11");
1991 js_log_options("L12");
1992 js_log_options("L13");
1993 js_log_options("L14");
1994 js_log_options("L15");
1995 js_log_options("CHECKPOINT B");
1996
1997 // SKIP: Check WEBCAM_DISABLED environment variable to enable test pattern mode
1998 // TODO: Debug why logging here causes hang
1999 // const char *webcam_disabled = SAFE_GETENV("WEBCAM_DISABLED");
2000 // if (webcam_disabled &&
2001 // (strcmp(webcam_disabled, "1") == 0 || platform_strcasecmp(webcam_disabled, "true") == 0 ||
2002 // platform_strcasecmp(webcam_disabled, "yes") == 0 || platform_strcasecmp(webcam_disabled, "on") == 0)) {
2003 // opts.test_pattern = true;
2004 // }
2005
2006 js_log_options("CHECKPOINT C");
2007
2008 // Apply --no-compress interaction with audio encoding
2009 js_log_options("Before --no-compress check");
2010 if (opts.no_compress) {
2011 js_log_options("In no_compress block");
2012 opts.encode_audio = false;
2013 js_log_options("Set encode_audio to false");
2014 log_debug("--no-compress set: disabling audio encoding");
2015 js_log_options("After log_debug in no_compress");
2016 }
2017 js_log_options("After --no-compress check");
2018
2019 // Set media_from_stdin flag if media_file is "-"
2020 js_log_options("Before media_from_stdin check");
2021 if (opts.media_file[0] != '\0' && strcmp(opts.media_file, "-") == 0) {
2022 js_log_options("In media_from_stdin block");
2023 opts.media_from_stdin = true;
2024 js_log_options("Set media_from_stdin to true");
2025 log_debug("Media file set to stdin");
2026 js_log_options("After log_debug in media_from_stdin");
2027 }
2028 js_log_options("After media_from_stdin check");
2029
2030 // Validate --seek option
2031 js_log_options("Before --seek validation");
2032 js_log_options("Checking media_seek_timestamp");
2033 if (opts.media_seek_timestamp > 0.0) {
2034 js_log_options("In seek validation block");
2035 // Can't seek stdin
2036 if (opts.media_from_stdin) {
2037 js_log_options("Seek error: media_from_stdin");
2038 log_error("--seek cannot be used with stdin (--file -)");
2039 SAFE_FREE(allocated_mode_argv);
2040 return ERROR_INVALID_PARAM;
2041 }
2042
2043 // Require --file or --url
2044 js_log_options("Checking media_file and media_url for seek");
2045 if (opts.media_file[0] == '\0' && opts.media_url[0] == '\0') {
2046 js_log_options("Seek error: no media_file or media_url");
2047 log_error("--seek requires --file or --url");
2048 SAFE_FREE(allocated_mode_argv);
2049 return ERROR_INVALID_PARAM;
2050 }
2051 js_log_options("Seek validation passed");
2052 }
2053 js_log_options("After --seek validation");
2054
2055 // Validate --pause option
2056 js_log_options("Before --pause validation");
2057 js_log_options("Checking pause flag");
2058 if (opts.pause) {
2059 js_log_options("In pause validation block");
2060 // Require --file or --url (not webcam, not test pattern)
2061 if (opts.media_file[0] == '\0' && opts.media_url[0] == '\0') {
2062 js_log_options("Pause error: no media_file or media_url");
2063 log_error("--pause requires --file or --url");
2064 SAFE_FREE(allocated_mode_argv);
2065 return ERROR_INVALID_PARAM;
2066 }
2067 js_log_options("Pause validation passed");
2068 }
2069 js_log_options("After --pause validation");
2070
2071 js_log_options("Before media_url check");
2072 js_log_options("Checking media_url[0]");
2073 if (opts.media_url[0] != '\0') {
2074 js_log_options("Entering media_url block");
2075 // URL must be a valid HTTP(S) URL (YouTube URLs are HTTPS URLs)
2076 js_log_options("About to call url_is_valid");
2077 js_log_options("Before url_is_valid call");
2078 if (!url_is_valid(opts.media_url)) {
2079 js_log_options("url_is_valid returned false");
2080 log_error("--url must be a valid HTTP(S) URL: %s", opts.media_url);
2081 SAFE_FREE(allocated_mode_argv);
2082 return ERROR_INVALID_PARAM;
2083 }
2084 js_log_options("url_is_valid returned true");
2085 js_log_options("url_is_valid returned, before strstr check");
2086
2087 // Normalize bare URLs by prepending http:// if not present
2088 js_log_options("Before strstr check");
2089 if (!strstr(opts.media_url, "://")) {
2090 js_log_options("URL needs normalization");
2091 char normalized_url[2048];
2092 js_log_options("Before snprintf");
2093 int result = snprintf(normalized_url, sizeof(normalized_url), "http://%s", opts.media_url);
2094 js_log_options("After snprintf");
2095 if (result > 0 && result < (int)sizeof(normalized_url)) {
2096 js_log_options("snprintf successful, before SAFE_STRNCPY");
2097 SAFE_STRNCPY(opts.media_url, normalized_url, sizeof(opts.media_url));
2098 js_log_options("URL normalized");
2099 } else {
2100 js_log_options("snprintf failed");
2101 log_error("Failed to normalize URL (too long): %s", opts.media_url);
2102 SAFE_FREE(allocated_mode_argv);
2103 return ERROR_INVALID_PARAM;
2104 }
2105 }
2106 js_log_options("After normalization check");
2107 }
2108 js_log_options("After media_url block");
2109
2110 // ========================================================================
2111 // STAGE 8: Publish to RCU
2112 // ========================================================================
2113
2114 // Adjust flip_x default: if --file or --url is provided, default flip_x to false
2115 // (file/URL content is typically not horizontally flipped, unlike webcam)
2116 js_log_options("Before flip_x check");
2117 if ((opts.media_file[0] != '\0' || opts.media_url[0] != '\0') && opts.flip_x) {
2118 js_log_options("In flip_x adjustment");
2119 opts.flip_x = false;
2120 js_log_options("flip_x set to false");
2121 log_debug("flip_x auto-adjusted to false for media file/URL");
2122 js_log_options("After log_debug for flip_x");
2123 }
2124 js_log_options("After flip_x check");
2125
2126 // Save the quiet flag before publishing (RCU will be cleaned up before memory report runs)
2127#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
2128 js_log_options("Before quiet_for_memory_report assignment");
2129 bool quiet_for_memory_report = opts.quiet;
2130 js_log_options("After quiet_for_memory_report assignment");
2131#endif
2132
2133 // Publish parsed options to RCU state (replaces options_state_populate_from_globals)
2134 // This makes the options visible to all threads via lock-free reads
2135 js_log_options("STAGE 8 RCU: Before options_state_set");
2136 js_log_options("Before log_info PERF");
2137 js_log_options("After log_info PERF");
2138 js_log_options("Calling options_state_set...");
2139 js_log_options("Before options_state_set call");
2140 asciichat_error_t publish_result = options_state_set(&opts);
2141 js_log_options("After options_state_set call");
2142 js_log_options("options_state_set returned");
2143 js_log_options("Before log_info options_state_set done");
2144 js_log_options("After log_info options_state_set done");
2145 js_log_options("Before publish_result check");
2146 if (publish_result != ASCIICHAT_OK) {
2147 js_log_options("publish_result not OK");
2148 log_error("Failed to publish parsed options to RCU state: %d", publish_result);
2149 SAFE_FREE(allocated_mode_argv);
2150 return publish_result;
2151 }
2152 js_log_options("publish_result is OK");
2153 js_log_options("options_state_set successful");
2154
2155 // Now update debug memory quiet mode with the saved quiet value
2156#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
2157 debug_memory_set_quiet_mode(quiet_for_memory_report);
2158#endif
2159
2160 // ========================================================================
2161 // Apply color scheme to logging
2162 // ========================================================================
2163 // Now that options are parsed, set and apply the selected color scheme to logging
2164 js_log_options("STAGE 8 ColorScheme: Checking color_scheme_name");
2165 js_log_options("Before color_scheme_name[0] check");
2166 if (opts.color_scheme_name[0] != '\0') {
2167 js_log_options("Color scheme name is set");
2168 js_log_options("Before colorscheme_set_active_scheme");
2169 js_log_options("Before colorscheme_set_active_scheme call");
2171 js_log_options("After colorscheme_set_active_scheme call");
2172 js_log_options("colorscheme_set_active_scheme returned");
2173 js_log_options("Before log_info colorscheme done");
2174 js_log_options("After log_info colorscheme done");
2175 js_log_options("Before scheme_result check");
2176 if (scheme_result == ASCIICHAT_OK) {
2177 js_log_options("scheme_result is OK");
2178 js_log_options("Before colorscheme_get_active_scheme");
2180 js_log_options("After colorscheme_get_active_scheme");
2181 js_log_options("Before scheme null check");
2182 if (scheme) {
2183 js_log_options("scheme is not null");
2184 js_log_options("Before log_set_color_scheme");
2185 log_set_color_scheme(scheme);
2186 js_log_options("After log_set_color_scheme");
2187 js_log_options("Color scheme applied");
2188 js_log_options("Before log_debug color scheme");
2189 log_debug("Color scheme applied: %s", opts.color_scheme_name);
2190 js_log_options("After log_debug color scheme");
2191 } else {
2192 js_log_options("scheme is null");
2193 js_log_options("No scheme returned from colorscheme_get_active_scheme");
2194 }
2195 } else {
2196 log_warn("Failed to apply color scheme: %s", opts.color_scheme_name);
2197 }
2198 }
2199
2200 // ========================================================================
2201 // STAGE 8: Execute deferred actions (after all options parsed and published)
2202 // ========================================================================
2203 // Deferred actions (--list-webcams, --list-microphones, --list-speakers, --show-capabilities)
2204 // are executed here after all options are fully parsed and published via RCU.
2205 // This ensures action output reflects the final parsed state (e.g., final dimensions
2206 // for --show-capabilities).
2207 // Re-enable terminal output so deferred actions can print their results.
2208 // It was disabled earlier (STAGE 1C) to suppress log noise during option parsing.
2209 if (has_action) {
2211 }
2213 SAFE_FREE(allocated_mode_argv);
2214 return ASCIICHAT_OK;
2215}
Action option callbacks for ascii-chat.
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
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)
Parse command-line arguments.
Definition builder.c:1815
asciichat_error_t options_config_set_defaults(const options_config_t *config, void *options_struct)
Set default values in options struct.
Definition builder.c:1400
void options_config_destroy(options_config_t *config)
Free options config.
Definition builder.c:631
const color_scheme_t * colorscheme_get_active_scheme(void)
Get currently active color scheme.
asciichat_error_t colorscheme_set_active_scheme(const char *name)
Set the active color scheme.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
TOML configuration file support for ascii-chat.
void debug_memory_set_quiet_mode(bool quiet)
bool is_session_string(const char *str)
Check if a string is a valid session string.
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_GETENV(name)
Definition common.h:434
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
asciichat_error_t config_load_system_and_user(asciichat_mode_t detected_mode, bool strict, options_t *opts)
Load system config first, then user config (user config overrides system)
Definition config.c:1547
asciichat_error_t config_create_default(const char *config_path)
Create default configuration file with all default values.
Definition config.c:1254
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#define HAS_ERRNO(var)
Check if an error occurred and get full context.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_CRYPTO_KEY
Definition error_codes.h:97
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_USAGE
Definition error_codes.h:53
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
log_level_t
Logging levels enumeration.
Definition types.h:29
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define log_file(...)
File-only logging - writes to log file only, no stderr output.
Definition log/log.h:646
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log/log.c:700
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:618
void log_set_color_scheme(const color_scheme_t *scheme)
Set the color scheme for logging output.
Definition log/log.c:1728
#define OPT_STRIP_ANSI_DEFAULT
Default strip ANSI escape sequences flag.
#define OPT_WEBRTC_SKIP_STUN_DEFAULT
Default WebRTC skip STUN flag (false = use STUN)
#define OPT_ACDS_EXPOSE_IP_DEFAULT
Default ACDS expose IP flag (false = private by default)
#define OPT_ENDPOINT_DISCOVERY_SERVICE
Default discovery service endpoint (what to connect to by default)
#define OPT_COLOR_DEFAULT
Default color setting (COLOR_SETTING_AUTO = smart detection)
#define OPT_NO_AUDIO_MIXER_DEFAULT
Default no audio mixer flag (false = enable mixer)
#define OPT_FLIP_Y_DEFAULT
Default vertical flip state (false = no vertical flip)
#define OPT_SHOW_CAPABILITIES_DEFAULT
Default show terminal capabilities flag.
#define OPT_NO_ENCRYPT_DEFAULT
Default no encrypt flag (false = allow encryption)
#define OPT_QUIET_DEFAULT
Default quiet mode flag (false = logging enabled)
asciichat_mode_t
Mode type for options parsing.
#define OPT_MICROPHONE_INDEX_DEFAULT
Default microphone device index (-1 means system default)
#define OPT_TURN_SERVERS_DEFAULT
Default TURN server URLs (comma-separated)
#define OPT_ENCODE_AUDIO_DEFAULT
Default audio encoding state (true = Opus encoding enabled)
#define OPT_COLOR_MODE_DEFAULT
Default color mode (auto-detect terminal capabilities)
#define OPT_PALETTE_TYPE_DEFAULT
Default palette type (standard ASCII art)
#define OPT_RECONNECT_ATTEMPTS_DEFAULT
Default reconnect attempts (-1 means auto/infinite)
#define OPT_SPEAKERS_VOLUME_DEFAULT
Default speakers volume (1.0 = normal volume)
void update_dimensions_to_terminal_size(options_t *opts)
Update dimensions to current terminal size.
#define OPT_NO_AUTH_DEFAULT
Default no auth flag (false = allow authentication)
#define OPT_TEST_PATTERN_DEFAULT
Default test pattern mode (false = use actual webcam)
#define OPT_WEBRTC_ICE_TIMEOUT_MS_DEFAULT
Default WebRTC ICE gathering timeout in milliseconds (10 seconds)
#define OPT_COMPRESSION_LEVEL_DEFAULT
Default compression level (1-9)
#define OPT_ENCRYPT_ENABLED_DEFAULT
Default encrypt enabled flag (true = encryption required)
#define OPT_REQUIRE_SERVER_IDENTITY_DEFAULT
Default require-server-identity setting for ACDS.
#define OPT_ENABLE_KEEPAWAKE_DEFAULT
Default keep-awake (prevent system sleep) flag (false = normal system sleep)
#define OPT_RENDER_MODE_DEFAULT
Default render mode (foreground characters only)
#define OPT_ACDS_PORT_INT_DEFAULT
Default ACDS discovery service port (integer)
#define OPT_AUDIO_ENABLED_DEFAULT
Default audio enabled flag (true = audio enabled by default)
#define OPT_MATRIX_RAIN_DEFAULT
Default Matrix rain effect flag (false = disabled)
#define OPT_FORCE_UTF8_DEFAULT
Default force UTF-8 support setting (auto-detect)
#define OPT_STUN_SERVERS_DEFAULT
Default STUN server URLs (comma-separated)
#define OPT_HELP_DEFAULT
Default help flag (false = don't show help)
#define OPT_ADDRESS6_DEFAULT
Default IPv6 server address.
#define OPT_RENDER_FONT_SIZE_DEFAULT
#define OPT_WEBRTC_RECONNECT_ATTEMPTS_DEFAULT
Default WebRTC reconnection attempts (3 = try initial + 3 retries)
#define OPT_SPLASH_DEFAULT
Default splash screen flag (true = show splash, false = hide splash)
#define OPT_RENDER_THEME_DEFAULT
#define OPT_MEDIA_FROM_STDIN_DEFAULT
Default media from stdin flag (false = not reading from stdin)
#define OPT_AUDIO_ANALYSIS_ENABLED_DEFAULT
Default audio analysis enabled flag.
#define OPT_VERSION_DEFAULT
Default version flag (false = don't show version)
#define OPT_ACDS_DEFAULT
Default ACDS registration flag (false = disabled)
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
#define OPT_NO_COMPRESS_DEFAULT
Default no compression flag (false = enable compression)
asciichat_error_t options_init(int argc, char **argv)
Initialize options by parsing command-line arguments with unified mode detection.
#define OPT_AUTO_HEIGHT_DEFAULT
Default auto-detect height flag (true = auto-detect from terminal)
#define OPT_REQUIRE_CLIENT_VERIFY_DEFAULT
Default require client verify flag (false = not required)
option_mode_bitmask_t
Option mode bitmask.
#define OPT_AUDIO_NO_PLAYBACK_DEFAULT
Default audio playback flag (false = enable playback)
#define OPT_PAUSE_DEFAULT
Default pause media flag (false = play immediately)
#define OPT_REQUIRE_SERVER_VERIFY_DEFAULT
Default require server verify flag (false = not required)
#define OPT_NO_MDNS_ADVERTISE_DEFAULT
Default no mDNS advertise flag (false = advertise enabled)
#define OPT_AUDIO_SOURCE_DEFAULT
Visualize all available sources by default.
#define OPT_STATUS_SCREEN_EXPLICITLY_SET_DEFAULT
Default status screen explicitly set flag (false = use default status screen setting)
#define OPT_COLOR_FILTER_DEFAULT
Default color filter (none - no filtering)
#define OPT_WEBCAM_INDEX_DEFAULT
Default webcam device index.
void usage(FILE *desc, asciichat_mode_t mode)
Print usage information for a specific mode.
#define OPT_HEIGHT_DEFAULT
Default terminal height in characters.
#define OPT_SNAPSHOT_MODE_DEFAULT
Default snapshot mode flag (false = continuous)
#define OPT_COLOR_SCHEME_NAME_DEFAULT
Default color scheme name (pastel)
#define OPT_MEDIA_LOOP_DEFAULT
Default loop media flag (false = play once)
#define OPT_NO_CHECK_UPDATE_DEFAULT
Default no-check-update flag (false = check for updates enabled)
#define OPT_STRETCH_DEFAULT
Default allow aspect ratio distortion flag.
#define OPT_STATUS_SCREEN_DEFAULT
Default status screen flag (true = show status, false = hide status)
#define OPT_LAN_DISCOVERY_DEFAULT
Default LAN discovery flag (false = discovery disabled)
#define OPT_AUTO_WIDTH_DEFAULT
Default auto-detect width flag (true = auto-detect from terminal)
#define OPT_JSON_DEFAULT
Default JSON logging flag (false = text output)
#define OPT_ACDS_INSECURE_DEFAULT
Default ACDS insecure mode flag (false = verify server)
bool has_action_flag(void)
Check if an action flag was detected.
#define OPT_FPS_DEFAULT
Default FPS (frames per second)
#define OPT_WEBRTC_DEFAULT
Default WebRTC mode flag (true = P2P WebRTC, false = direct TCP)
#define OPT_REQUIRE_CLIENT_IDENTITY_DEFAULT
Default require-client-identity setting for ACDS.
#define OPT_DISABLE_KEEPAWAKE_DEFAULT
Default disable keep-awake flag (false = allow system sleep prevention)
#define OPT_ENABLE_UPNP_DEFAULT
Default enable UPnP flag (false = UPnP disabled)
#define OPT_TURN_CREDENTIAL_DEFAULT
Default TURN credential (empty = use ACDS credentials)
#define OPT_WEBRTC_SKIP_HOST_DEFAULT
Default WebRTC skip host candidates flag (false = use host candidates)
bool is_remote_key_path(const char *key_path)
Check if a key path is a remote/virtual key (not a local file)
Definition validation.c:595
#define OPT_VERBOSE_LEVEL_DEFAULT
Default verbose level (0 = not verbose)
#define OPT_SNAPSHOT_DELAY_DEFAULT
Default snapshot delay in seconds.
#define OPT_LOG_LEVEL_DEFAULT
Default log level (LOG_INFO)
#define OPT_MICROPHONE_SENSITIVITY_DEFAULT
Default microphone sensitivity (1.0 = normal volume)
#define OPT_FLIP_X_DEFAULT
Default horizontal flip state (true = horizontally flipped) macOS webcams default to flipped (mirrore...
#define OPT_PORT_INT_DEFAULT
Default TCP port for client/server communication (integer)
int options_collect_identity_keys(options_t *opts, int argc, char *argv[])
Collect multiple –key flags into identity_keys array.
Definition validation.c:527
#define OPT_SPLASH_SCREEN_EXPLICITLY_SET_DEFAULT
Default splash screen explicitly set flag (false = use default splash setting)
#define OPT_MAX_CLIENTS_DEFAULT
Default maximum concurrent clients (server only)
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)
Generate merged man page template preserving manual content.
#define OPT_PREFER_WEBRTC_DEFAULT
Default prefer WebRTC flag (false = try direct TCP first)
options_t options_t_new(void)
Initialize options by parsing command-line arguments.
#define OPT_WEBRTC_DISABLE_TURN_DEFAULT
Default WebRTC disable TURN flag (false = use TURN)
#define OPT_NO_WEBRTC_DEFAULT
Default no WebRTC flag (false = WebRTC enabled)
#define OPT_ADDRESS_DEFAULT
Default server address for client connections.
#define OPT_AUDIO_CAPTURE_SOURCE_DEFAULT
Preserve automatic local audio capture selection by default.
#define OPT_PALETTE_CUSTOM_SET_DEFAULT
Default custom palette set flag (false = not set)
#define OPT_TURN_USERNAME_DEFAULT
Default TURN username (empty = use ACDS credentials)
#define OPT_SPEAKERS_INDEX_DEFAULT
Default speakers device index (-1 means system default)
void update_dimensions_for_full_height(options_t *opts)
Update dimensions for full-height mode.
#define OPT_WEBSOCKET_PORT_SERVER_DEFAULT
Default WebSocket port for server mode (integer)
#define OPT_MEDIA_SEEK_TIMESTAMP_DEFAULT
Default media seek timestamp (start from beginning)
#define OPT_WIDTH_DEFAULT
Default terminal width in characters.
@ MODE_CLIENT
Client mode - network client options.
@ MODE_DISCOVERY_SERVICE
Discovery server mode - session management and WebRTC signaling.
@ MODE_INVALID
Invalid mode.
@ MODE_SERVER
Server mode - network server options.
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network)
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
@ COLOR_SETTING_TRUE
Force colors ON: always colorize regardless of TTY/piping/CLAUDECODE.
@ COLOR_SETTING_FALSE
Force colors OFF: disable all colors and logging colors.
@ COLOR_SETTING_AUTO
Smart detection (default): colors if TTY and not CLAUDECODE and not piping.
@ PALETTE_CUSTOM
User-defined via –palette-chars.
Definition palette.h:98
bool terminal_supports_utf8(void)
Check if terminal supports UTF-8.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define PATH_SEPARATOR_STR
Definition filesystem.h:113
const char * platform_getenv(const char *name)
Get an environment variable value.
Definition wasm/system.c:39
bool platform_get_temp_dir(char *temp_dir, size_t path_size)
Get the system temporary directory path.
Definition util.c:74
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:809
bool utf8_is_ascii_only(const char *str)
Check if a string contains only ASCII characters.
Definition utf8.c:165
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:156
char * get_log_dir(void)
Get log directory path appropriate for current build type.
Definition path.c:537
bool url_is_valid(const char *url)
Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
Definition url.c:81
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Validate and canonicalize a user-supplied filesystem path.
Definition path.c:1020
char * get_discovery_database_dir(void)
Get discovery service database directory with Homebrew-aware system-wide fallback.
Definition path.c:624
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:527
@ PATH_ROLE_LOG_FILE
Definition path.h:318
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Capture stdout/stderr and redirect to logging system.
options_t options_t_new_preserve_binary(const options_t *source)
Create new options struct, preserving binary-level fields from source.
📝 Logging API with multiple log levels and terminal output control
Man page template generation from options builder.
Mode-aware default value getters and mode metadata.
asciichat_error_t validate_options_and_report(const void *config, const void *opts)
Validate options and report errors to stderr.
Common utilities and helpers for option parsing across all modes.
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)
Generate man page template from options builder.
void action_completions(const char *shell_name, const char *output_path)
Generate shell completions and output to stdout or file.
void action_list_microphones(void)
List available microphone devices and exit.
void action_create_config(const char *output_path)
Create default configuration file and exit.
void action_list_webcams(void)
List available webcam devices and exit.
void action_list_speakers(void)
List available speaker devices and exit.
void actions_execute_deferred(void)
Execute the deferred action (if any)
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.
const char * asciichat_suggest_mode(const char *input)
Suggest closest matching mode using fuzzy matching.
Centralized enum and mode string conversion API.
Validation functions for options parsing.
bool parse_color_setting(const char *arg, void *dest, char **error_msg)
Parse color setting option (–color flag)
Definition parsers.c:160
bool parse_log_level(const char *arg, void *dest, char **error_msg)
Parse log level option.
Definition parsers.c:448
Custom option parsers for enum types.
📂 Path Manipulation Utilities
bool terminal_is_interactive(void)
Check if the session is fully interactive.
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
Preset option configurations for ascii-chat modes.
asciichat_error_t options_state_init(void)
Initialize RCU options system.
Definition rcu.c:370
asciichat_error_t options_state_set(const options_t *opts)
Set options from a parsed options struct.
Definition rcu.c:439
asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs)
Build schema dynamically from options builder configs.
Definition schema.c:240
Schema metadata for config file options.
#define SESSION_STRING_BUFFER_SIZE
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)
Create a new options_t struct with all defaults set.
unsigned short int verbose_level
char log_file[OPTIONS_BUFF_SIZE]
char config_file[OPTIONS_BUFF_SIZE]
asciichat_mode_t detected_mode
Color scheme definition.
Definition colorscheme.h:62
Option descriptor.
Definition builder.h:241
option_mode_bitmask_t mode_bitmask
Which modes this option applies to.
Definition builder.h:278
const char * long_name
Long option name (e.g., "port")
Definition builder.h:243
char short_name
Short option char (e.g., 'p', or '\0' if none)
Definition builder.h:244
Options configuration.
Definition builder.h:401
option_descriptor_t * descriptors
Array of option descriptors.
Definition builder.h:402
size_t num_descriptors
Number of descriptors.
Definition builder.h:403
Consolidated options structure.
bool snapshot_mode
Snapshot mode (one frame and exit)
bool splash_screen_explicitly_set
True if splash screen was explicitly set by user.
float speakers_volume
Speaker volume multiplier (0.0-1.0, default 1.0)
int discovery_port
discovery server port (default: 27225)
char media_url[256]
Network URL (HTTP/HTTPS/YouTube/RTSP) - takes priority over media_file.
size_t num_identity_keys
Number of identity keys loaded (0 means single-key mode using encrypt_key)
bool webrtc
Enable WebRTC mode for discovery session (default: true, P2P WebRTC)
audio_capture_source_t audio_capture_source
Local capture/media selection policy.
bool encrypt_enabled
Enable encryption.
terminal_color_mode_t color_mode
Color mode (auto/none/16/256/truecolor)
bool discovery_expose_ip
ACDS: explicitly allow public IP disclosure without verification (opt-in)
char turn_servers[256]
ACDS: Comma-separated list of TURN server URLs.
int force_utf8
UTF-8 support setting (auto/true/false)
int webrtc_ice_timeout_ms
–webrtc-ice-timeout: ICE gathering timeout in milliseconds (default: 10000)
bool debug_backtrace_time_explicit
True if –backtrace was explicitly provided.
bool discovery
Enable discovery session registration (default: false)
double debug_memory_report_interval
Interval in seconds for periodic memory reports (debug builds only)
bool webrtc_relay_only
–webrtc-relay-only: Require TURN relay candidates
bool disable_keepawake
Explicitly disable system sleep prevention (allow OS to sleep)
bool debug_sync_state_time_explicit
True if –debug-state was explicitly provided.
float microphone_sensitivity
Microphone volume multiplier (0.0-1.0, default 1.0)
int compression_level
zstd compression level (1-9)
bool fps_explicitly_set
True if FPS was explicitly configured by the user.
bool fft
Replace the video image with a live audio frequency spectrogram.
bool enable_keepawake
Explicitly enable system sleep prevention.
double debug_sync_state_time
Time parameter for –sync-state option (debug builds only)
bool show_capabilities
Show terminal capabilities and exit.
bool no_mdns_advertise
Disable mDNS service advertisement (server only)
char password[256]
Password string.
int microphone_index
Microphone device index (-1 = default)
char config_file[256]
Config file path (–config)
asciichat_mode_t detected_mode
Mode detected from command-line arguments.
bool strip_ansi
Strip ANSI escape sequences.
bool audio_analysis_enabled
Enable audio analysis (debug)
int websocket_port
WebSocket server port (server/discovery-service only)
bool pause
Start playback paused (toggle with spacebar)
audio_source_t audio_source
Visualization source (all/call/mic/media)
bool json
Enable JSON logging (–json flag)
char stun_servers[256]
ACDS: Comma-separated list of STUN server URLs.
int height
Terminal height in characters (int for OPTION_TYPE_INT compat)
bool enable_upnp
Enable UPnP/NAT-PMP port mapping for direct TCP (opt-in via –upnp)
bool webrtc_skip_stun
–webrtc-skip-stun: Skip Stage 2 (STUN), go to TURN
bool debug_memory_report_interval_explicit
True if –memory-report was explicitly provided.
bool log_format_console_only
Apply log format only to console output.
bool lan_discovery
Enable LAN service discovery via mDNS (client only)
bool audio_enabled
Enable audio streaming.
bool no_encrypt
Disable encryption (opt-out)
bool no_webrtc
–no-webrtc: Disable WebRTC, use Direct TCP only
bool matrix_rain
Matrix digital rain effect (false = disabled)
bool webrtc_disable_turn
–webrtc-disable-turn: Disable Stage 3 (TURN), use STUN only
bool audio_no_playback
Disable speaker playback (debug)
bool auto_height
Auto-detect height from terminal.
log_level_t log_level
Log level threshold.
char turn_credential[256]
ACDS: Credential/password for TURN authentication.
bool require_server_identity
ACDS: require servers to provide signed Ed25519 identity.
char palette_custom[256]
Custom palette characters.
bool status_screen
Show status screen (default: true = show, use –no-status-screen to hide)
int max_clients
Maximum concurrent clients (server only)
bool palette_custom_set
True if custom palette was set.
render_mode_t render_mode
Render mode (foreground/background/half-block)
bool no_auth
Disable authentication layer (–no-auth)
bool splash_screen
Show splash screen (default: true = show, use –no-splash-screen to hide)
char discovery_database_path[256]
~/.ascii-chat/discovery.db)
char server_key[256]
Expected server public key (client)
bool no_audio_mixer
Disable audio mixer (debug)
char encrypt_keyfile[256]
Alternative key file path.
bool webrtc_skip_host
–webrtc-skip-host: Skip host candidates, force STUN/TURN only
bool version
Show version information.
int webcam_index
Webcam device index (0 = first)
bool status_screen_explicitly_set
True if status_screen was explicitly set by user.
bool no_compress
Disable compression entirely.
int webrtc_reconnect_attempts
–webrtc-reconnect-attempts: Number of retry attempts (default: 3)
char address6[256]
IPv6 bind address (server only)
int speakers_index
Speakers device index (-1 = default)
color_filter_t color_filter
Monochromatic color filter (none/black/white/green/etc)
bool prefer_webrtc
–prefer-webrtc: Try WebRTC before Direct TCP
bool quiet
Quiet mode (suppress logs)
double snapshot_delay
Snapshot delay in seconds.
int fps
Target framerate (1-144, default: 60)
bool stretch
Allow aspect ratio distortion.
bool waveform
Replace the video image with a live audio waveform.
int reconnect_attempts
Number of reconnection attempts (-1=infinite, 0=none)
bool test_pattern
Use test pattern instead of webcam.
bool media_from_stdin
Reading from stdin (detected from "--file -")
bool require_client_verify
Client: only connect to servers whose identity was verified by ACDS.
char discovery_server[256]
discovery server address (default: 127.0.0.1)
double debug_backtrace_time
Time parameter for –backtrace option (debug builds only)
int width
Terminal width in characters (int for OPTION_TYPE_INT compat)
bool require_server_verify
Server: only accept clients who verified via ACDS.
palette_type_t palette_type
Selected palette type.
bool flip_y
Flip video vertically (Y-axis)
char color_scheme_name[64]
Color scheme name (e.g., "pastel", "nord")
char turn_secret[256]
ACDS: Shared secret for dynamic TURN credential generation (HMAC-SHA1)
bool media_loop
Loop media file playback.
char discovery_service_key[256]
HTTPS URL)
bool require_client_identity
ACDS: require clients to provide signed Ed25519 identity.
bool discovery_insecure
ACDS: skip server key verification (MITM-vulnerable, requires explicit opt-in)
bool no_check_update
Disable automatic update checks (default: false = checks enabled)
bool encode_audio
Enable Opus audio encoding.
int render_theme
0=dark 1=light 2=auto
char encrypt_key[256]
SSH/GPG key file path (first –key flag, kept for compatibility)
char media_file[256]
Media file path or "-" for stdin.
int color
Color setting (COLOR_SETTING_AUTO/TRUE/FALSE)
char turn_username[256]
ACDS: Username for TURN authentication.
char log_file[256]
Log file path.
unsigned short int verbose_level
Verbosity level (stackable -V)
char address[256]
Server address (client) or bind address (server)
bool flip_x
Flip video horizontally (X-axis). Ignored for webcam on macOS.
char grep_pattern[256]
PCRE2 regex for log filtering.
bool auto_width
Auto-detect width from terminal.
char client_keys[256]
Allowed client keys (server)
double render_font_size
Font size in points (default 12.0, supports e.g. 10.5)
double media_seek_timestamp
Seek to timestamp in seconds before playback.
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
Cross-platform system functions interface for ascii-chat.
🖥️ Cross-platform terminal interface for ascii-chat
@ COLOR_FILTER_NONE
No filtering (default)
Definition terminal.h:601
⏱️ High-precision timing utilities using sokol_time.h and uthash
🌐 Production-Grade URL Validation and Parsing
🔤 UTF-8 Encoding and Decoding Utilities