ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
options.c
Go to the documentation of this file.
1
47
48/* Storage capacities in elements; text buffers include the terminating NUL. */
49#define OPTIONS_OPT_BUFFER_SIZE 64
50#define OPTIONS_LOG_FILENAME_BUFFER_SIZE 256
51#define OPTIONS_RESULT_BUFFER_SIZE 512
52#define OPTIONS_LONG_FORM_BUFFER_SIZE 256
53#define OPTIONS_LONG_FORM_EQ_BUFFER_SIZE 256
54#define OPTIONS_NORMALIZED_URL_BUFFER_SIZE 2048
55
56#include <ascii-chat/options/builder/internal.h>
58#include <ctype.h>
59#include <sys/stat.h>
61#include <ascii-chat/source_root.h>
62#include <ascii-chat/options/rcu.h> // RCU-based thread-safe options
69#include <ascii-chat/options/registry/mode_defaults.h> // Mode-aware defaults
70
72#include <ascii-chat/options/strings.h> // Enum/mode string conversions
75#include <ascii-chat/common.h>
77#include <ascii-chat/log/log.h>
78#include <ascii-chat/log/grep.h>
80#include <libplatform/system.h>
81#include <libplatform/filesystem.h>
82#include <libplatform/systemd.h>
83#include <libplatform/terminal.h>
86#include <ascii-chat/util/url.h>
88
89#ifdef _WIN32
90#include <io.h>
91#else
92#include <unistd.h>
93#endif
94
95#include <stdio.h>
96#include <stdlib.h>
97#include <string.h>
98
99static void js_log_options(const char *msg) {
100 (void)msg;
101}
102
103// ============================================================================
104// Global Action Flag Tracking
105// ============================================================================
106
108static bool g_action_flag = false;
109
114static void set_action_flag(bool action_present) {
115 g_action_flag = action_present;
116}
117
122bool has_action_flag(void) {
123 return g_action_flag;
124}
125
126// ============================================================================
127// Mode Detection Helper
128// ============================================================================
129
153static bool is_binary_level_option_with_args(const char *arg, bool *out_takes_arg, bool *out_takes_optional_arg) {
154 if (!arg) {
155 SET_ERRNO(ERROR_INVALID_PARAM, "arg is null");
156 return false;
157 }
158
159 if (out_takes_arg)
160 *out_takes_arg = false;
161 if (out_takes_optional_arg)
162 *out_takes_optional_arg = false;
163
164 // Extract option name (without leading dashes and without value after '=')
165 const char *opt_name = arg;
166 if (arg[0] == '-') {
167 opt_name = arg + (arg[1] == '-' ? 2 : 1); // Skip - or --
168 }
169
170 // For --option=value format, stop at the '=' sign
171 const char *equals = strchr(opt_name, '=');
172 size_t opt_len = equals ? (size_t)(equals - opt_name) : strlen(opt_name);
173 char opt_buffer[OPTIONS_OPT_BUFFER_SIZE];
174 if (opt_len >= sizeof(opt_buffer)) {
175 opt_len = sizeof(opt_buffer) - 1;
176 }
177 SAFE_STRNCPY(opt_buffer, opt_name, opt_len + 1);
178 opt_name = opt_buffer;
179
180 // Check option against binary-level options
181 if (strcmp(opt_name, "help") == 0 || strcmp(arg, "-h") == 0) {
182 return true;
183 }
184 if (strcmp(opt_name, "version") == 0 || strcmp(arg, "-v") == 0) {
185 return true;
186 }
187 if (strcmp(opt_name, "verbose") == 0 || strcmp(arg, "-V") == 0) {
188 if (out_takes_optional_arg)
189 *out_takes_optional_arg = true;
190 return true;
191 }
192 if (strcmp(opt_name, "quiet") == 0 || strcmp(arg, "-q") == 0) {
193 return true;
194 }
195 if (strcmp(opt_name, "log-file") == 0 || strcmp(arg, "-L") == 0) {
196 if (out_takes_arg)
197 *out_takes_arg = true;
198 return true;
199 }
200 if (strcmp(opt_name, "log-level") == 0) {
201 if (out_takes_arg)
202 *out_takes_arg = true;
203 return true;
204 }
205 if (strcmp(opt_name, "config") == 0) {
206 if (out_takes_arg)
207 *out_takes_arg = true;
208 return true;
209 }
210 if (strcmp(opt_name, "config-create") == 0) {
211 if (out_takes_optional_arg)
212 *out_takes_optional_arg = true;
213 return true;
214 }
215 if (strcmp(opt_name, "color") == 0) {
216 // --color takes an optional argument (auto/true/false)
217 // Can be used as --color (defaults to true) or --color=auto or --color auto
218 if (out_takes_optional_arg)
219 *out_takes_optional_arg = true;
220 return true;
221 }
222 if (strcmp(opt_name, "check-update") == 0) {
223 return true;
224 }
225 if (strcmp(opt_name, "no-check-update") == 0) {
226 return true;
227 }
228 if (strcmp(opt_name, "json") == 0) {
229 return true;
230 }
231 if (strcmp(opt_name, "log-format-console") == 0) {
232 return true;
233 }
234
235 return false;
236}
237
250static bool is_mirror_mode_option(const char *opt_name, char short_name) {
251 // Get unified config
252 options_config_t *config = options_preset_unified(NULL, NULL);
253 if (!config) {
254 return false;
255 }
256
257 // Check if this option exists and applies to mirror mode
258 bool found = false;
259 for (size_t i = 0; i < config->num_descriptors; i++) {
260 const option_descriptor_t *desc = &config->descriptors[i];
261
262 // Check by long name
263 if (opt_name && desc->long_name && strcmp(opt_name, desc->long_name) == 0) {
264 // Check if this option is EXCLUSIVE to mirror mode (not shared with other modes)
265 if ((desc->mode_bitmask & (1 << MODE_MIRROR)) && (desc->mode_bitmask == (1 << MODE_MIRROR))) {
266 found = true;
267 }
268 break;
269 }
270
271 // Check by short name
272 if (short_name != '\0' && desc->short_name == short_name) {
273 // Check if this option is EXCLUSIVE to mirror mode (not shared with other modes)
274 if ((desc->mode_bitmask & (1 << MODE_MIRROR)) && (desc->mode_bitmask == (1 << MODE_MIRROR))) {
275 found = true;
276 }
277 break;
278 }
279 }
280
282 return found;
283}
284
285static asciichat_error_t options_detect_mode(int argc, char **argv, asciichat_mode_t *out_mode,
286 char *out_session_string, int *out_mode_index) {
287 if (out_mode == NULL || out_mode_index == NULL) {
288 return SET_ERRNO(ERROR_INVALID_PARAM, "Output parameters must not be NULL");
289 }
290
291 *out_mode_index = -1;
292 if (out_session_string) {
293 out_session_string[0] = '\0';
294 }
295
296 // Check if argv[0] itself is a mode name (for test compatibility)
297 // This handles the case where tests pass ["client", "-p", "80"] without a binary name
298 const char *const mode_names_check[] = {"server", "client", "mirror", "discovery-service",
299 "mcp-server", "api-server", NULL};
302 for (int i = 0; mode_names_check[i] != NULL; i++) {
303 if (strcmp(argv[0], mode_names_check[i]) == 0) {
304 log_dev("Mode detected from argv[0]: %s", argv[0]);
305 *out_mode = mode_values_check[i];
306 *out_mode_index = 0;
307 return ASCIICHAT_OK;
308 }
309 }
310
311 log_dev("argv[0] '%s' is not a mode name, looking for first non-option argument", argv[0]);
312
313 // Find the first non-option argument (potential mode or session string)
314 // Also detect mirror-specific options to infer mode if no positional arg found
315 int first_positional_idx = -1;
316 bool has_mirror_specific_option = false;
317 for (int i = 1; i < argc; i++) {
318 // Skip options and their arguments
319 if (argv[i][0] == '-') {
320 bool takes_arg = false;
321 bool takes_optional_arg = false;
322
323 // Check if this is a binary-level option
324 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
325 // For --option=value format, don't skip the next argument
326 // (the value is part of the current argument)
327 const char *equals = strchr(argv[i], '=');
328 if (!equals) {
329 // No equals sign, so value might be in next argument
330 if (takes_arg && i + 1 < argc) {
331 i++; // Skip required argument
332 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
333 // For optional arguments, skip if it doesn't look like a mode/session string
334 const char *next_arg = argv[i + 1];
335 bool looks_like_mode_or_session = false;
336
337 // Check if it looks like a mode name
338 const char *const mode_names[] = {"server", "client", "mirror", "discovery-service",
339 "mcp-server", "api-server", NULL};
340 for (int j = 0; mode_names[j] != NULL; j++) {
341 if (strcmp(next_arg, mode_names[j]) == 0) {
342 looks_like_mode_or_session = true;
343 break;
344 }
345 }
346
347 // Check if it looks like a session string (word-word-word)
348 if (!looks_like_mode_or_session && is_session_string(next_arg)) {
349 looks_like_mode_or_session = true;
350 }
351
352 // If it doesn't look like a mode/session, it's the optional argument for this option
353 if (!looks_like_mode_or_session) {
354 i++; // Skip optional argument
355 }
356 }
357 }
358 } else {
359 // Extract option name (without leading dashes)
360 const char *opt_name = argv[i];
361 char short_name = '\0';
362
363 if (opt_name[0] == '-') {
364 if (opt_name[1] == '-') {
365 // Long option: skip "--"
366 opt_name = opt_name + 2;
367 } else {
368 // Short option: extract char and store
369 short_name = opt_name[1];
370 opt_name = opt_name + 1;
371 }
372 }
373
374 // Check if this option is mirror-specific by querying mirror config
375 if (is_mirror_mode_option(opt_name, short_name)) {
376 has_mirror_specific_option = true;
377 // Skip the argument if this option takes one
378 if (i + 1 < argc && argv[i + 1][0] != '-') {
379 i++; // Skip required argument
380 }
381 } else {
382 // Other mode-specific option - skip argument if it takes one
383 if (i + 1 < argc && argv[i + 1][0] != '-') {
384 i++; // Skip potential argument
385 }
386 }
387 }
388 continue;
389 }
390
391 // Found a positional argument
392 first_positional_idx = i;
393 break;
394 }
395
396 // If no positional argument found, check for mirror-specific options
397 if (first_positional_idx == -1) {
398 if (has_mirror_specific_option) {
399 // User specified mirror-specific option (-f or -u) without explicit mode keyword
400 *out_mode = MODE_MIRROR;
401 *out_mode_index = -1;
402 return ASCIICHAT_OK;
403 }
404 // Otherwise default to discovery mode (start new session)
405 *out_mode = MODE_DISCOVERY;
406 *out_mode_index = -1;
407 return ASCIICHAT_OK;
408 }
409
410 const char *positional = argv[first_positional_idx];
411
412 // Try to match against known modes
413 const char *const mode_names[] = {"server", "client", "mirror", "discovery-service",
414 "mcp-server", "api-server", "default", NULL};
417
418 for (int i = 0; mode_names[i] != NULL; i++) {
419 if (strcmp(positional, mode_names[i]) == 0) {
420 if (mode_values[i] == MODE_INVALID) {
421 return SET_ERRNO(ERROR_USAGE,
422 "The default mode cannot be specified explicitly. Just run 'ascii-chat' without a mode.");
423 }
424 *out_mode = mode_values[i];
425 *out_mode_index = first_positional_idx;
426 return ASCIICHAT_OK;
427 }
428 }
429
430 // Not a known mode - check if it's a session string (word-word-word pattern)
431 if (is_session_string(positional)) {
432 *out_mode = MODE_DISCOVERY;
433 *out_mode_index = first_positional_idx;
434 if (out_session_string) {
435 SAFE_STRNCPY(out_session_string, positional, SESSION_STRING_BUFFER_SIZE);
436 }
437 return ASCIICHAT_OK;
438 }
439
440 // Unknown positional argument - suggest closest match
441 const char *suggestion = asciichat_suggest_mode(positional);
442 if (suggestion) {
443 return SET_ERRNO(ERROR_USAGE, "Unknown mode '%s'. Did you mean '%s'?", positional, suggestion);
444 }
445 return SET_ERRNO(ERROR_USAGE, "Unknown mode or invalid argument: %s", positional);
446}
447
448// ============================================================================
449// Default Initialization
450// ============================================================================
451
466typedef struct {
468 unsigned short int verbose_level;
469 bool quiet;
471 bool help;
475 int color; // Color setting (COLOR_SETTING_AUTO/TRUE/FALSE) - binary-level option parsed early
476 bool json; // JSON logging format - binary-level option
477 bool log_format_console_only; // Apply log template only to console - binary-level option
478 bool no_check_update; // Disable the startup update check
491
492static inline binary_level_opts_t extract_binary_level(const options_t *opts) {
493 binary_level_opts_t binary = {0};
494 if (!opts)
495 return binary;
496 binary.log_level = opts->log_level;
497 binary.verbose_level = opts->verbose_level;
498 binary.quiet = opts->quiet;
499 SAFE_STRNCPY(binary.log_file, opts->log_file, sizeof(binary.log_file));
500 binary.help = opts->help;
501 binary.version = opts->version;
502 SAFE_STRNCPY(binary.config_file, opts->config_file, sizeof(binary.config_file));
503 binary.detected_mode = opts->detected_mode;
504 binary.color = opts->color; // Save color setting (parsed in STAGE 1A)
505 binary.json = opts->json; // Save JSON logging flag (binary-level option)
506 binary.log_format_console_only = opts->log_format_console_only; // Save log format console setting
507 binary.no_check_update = opts->no_check_update;
508 SAFE_STRNCPY(binary.otel_log_level, opts->otel_log_level, sizeof(binary.otel_log_level));
509 SAFE_STRNCPY(binary.otel_endpoint, opts->otel_endpoint, sizeof(binary.otel_endpoint));
510 SAFE_STRNCPY(binary.otel_ca_file, opts->otel_ca_file, sizeof(binary.otel_ca_file));
515 binary.otel_timeout = opts->otel_timeout;
517
518 return binary;
519}
520
521static inline void restore_binary_level(options_t *opts, const binary_level_opts_t *binary) {
522 if (!opts || !binary)
523 return;
524 opts->log_level = binary->log_level;
525 opts->verbose_level = binary->verbose_level;
526 opts->quiet = binary->quiet;
527 SAFE_STRNCPY(opts->log_file, binary->log_file, sizeof(opts->log_file));
528 opts->help = binary->help;
529 opts->version = binary->version;
530 SAFE_STRNCPY(opts->config_file, binary->config_file, sizeof(opts->config_file));
531 opts->detected_mode = binary->detected_mode;
532 opts->color = binary->color; // Restore color setting (parsed in STAGE 1A)
533 opts->json = binary->json; // Restore JSON logging flag (binary-level option)
534 opts->log_format_console_only = binary->log_format_console_only; // Restore log format console setting
535 opts->no_check_update = binary->no_check_update;
536 SAFE_STRNCPY(opts->otel_log_level, binary->otel_log_level, sizeof(opts->otel_log_level));
537 SAFE_STRNCPY(opts->otel_endpoint, binary->otel_endpoint, sizeof(opts->otel_endpoint));
538 SAFE_STRNCPY(opts->otel_ca_file, binary->otel_ca_file, sizeof(opts->otel_ca_file));
543 opts->otel_timeout = binary->otel_timeout;
545}
546
548 options_t opts;
549
550 // Zero-initialize all fields first
551 memset(&opts, 0, sizeof(opts));
552 opts.mode_arg_index = -1;
553
554 // ============================================================================
555 // GENERAL CATEGORY - General-purpose options
556 // ============================================================================
557 opts.help = OPT_HELP_DEFAULT;
563 opts.fps_explicitly_set = false;
567
568 // ============================================================================
569 // LOGGING CATEGORY - Binary-level logging and output control options
570 // ============================================================================
571 // log_file is mode-dependent at startup and intentionally left empty here
576 SAFE_STRNCPY(opts.otel_enabled_telemetry, "traces,logs,metrics", sizeof(opts.otel_enabled_telemetry));
577 opts.otel_realtime_sample_rate = 0.01;
579 opts.otel_metrics_interval = 10000;
580 SAFE_STRNCPY(opts.otel_log_level, "info", sizeof(opts.otel_log_level));
581 opts.otel_timeout = 5000;
582 opts.otel_queue_capacity = 4096;
583 if (platform_systemd_is_managed()) {
584 opts.splash_screen = false;
585 opts.status_screen = false;
586 opts.no_check_update = true;
587 SAFE_STRNCPY(opts.log_file, "/dev/null", sizeof(opts.log_file));
588 opts.otel_endpoint[0] = '\0';
589 }
590 opts.grep_pattern[0] = '\0'; // Explicitly ensure grep_pattern is empty
591 opts.json = OPT_JSON_DEFAULT;
593 // log_template and log_format are set by get_default_log_template()
594
595 // ============================================================================
596 // TERMINAL CATEGORY - Terminal display options
597 // ============================================================================
607 if (platform_systemd_is_managed())
609
610 // ============================================================================
611 // DISPLAY CATEGORY - Display and rendering options
612 // ============================================================================
616 opts.stabilize = true;
617 opts.stabilize_explicit = false;
619 // palette_custom is already zeroed by memset
622 opts.fps = OPT_FPS_DEFAULT;
626 opts.waveform = false;
627 opts.fft = false;
628 SAFE_STRNCPY(opts.fft_scale, "-62:-6", sizeof(opts.fft_scale));
631
632 // ============================================================================
633 // WEBCAM CATEGORY - Webcam and capture device options
634 // ============================================================================
638
639 // ============================================================================
640 // MEDIA CATEGORY - Media file streaming and playback options
641 // ============================================================================
642 // media_file is already zeroed by memset
643 // media_url is already zeroed by memset
648 // yt_dlp_options is already zeroed by memset
649
650 // Render-to-file options
651 // render_file is already zeroed by memset
653 // render_font is already zeroed by memset
655
656 // ============================================================================
657 // NETWORK CATEGORY - Network connectivity and protocol options
658 // ============================================================================
659 SAFE_STRNCPY(opts.address, OPT_ADDRESS_DEFAULT, sizeof(opts.address));
667 // Discovery Service (ACDS) Options
675 // discovery_service_key is already zeroed by memset
676 opts.discovery_database_path[0] = '\0'; // Explicitly ensure discovery_database_path is empty
677 // LAN Discovery & WebRTC Options
681 // WebRTC Mode & Strategy Options
686 opts.webrtc_relay_only = false;
695 // turn_secret is already zeroed by memset
696
697 // ============================================================================
698 // AUDIO CATEGORY - Audio capture, playback and processing options
699 // ============================================================================
700 opts.audio_fft_size = 1024;
701 opts.test_audio = false;
702 opts.audio_spectral = true;
703 opts.audio_spectral_mixer = false;
704 opts.audio_spectral_gate = true;
708 opts.microphone_channels = 1;
709 opts.microphone_channel = 0;
717
718 // ============================================================================
719 // SECURITY CATEGORY - Encryption and authentication options
720 // ============================================================================
722 // encrypt_key is already zeroed by memset
723 // password is already zeroed by memset
724 // encrypt_keyfile is already zeroed by memset
727 // server_key is already zeroed by memset
728 // client_keys is already zeroed by memset
729 // identity_keys array is already zeroed by memset
730 opts.num_identity_keys = 0;
733
734 // ============================================================================
735 // CONFIGURATION CATEGORY - Application configuration options
736 // ============================================================================
737 // config_file is already zeroed by memset
738
739 // ============================================================================
740 // DATABASE CATEGORY - Database and persistent storage options
741 // ============================================================================
742 // discovery_database_path is already zeroed by memset (set above in NETWORK)
743
744 // ============================================================================
745 // Internal/System Fields
746 // ============================================================================
747 // session_string is already zeroed by memset
748 // detected_mode is already zeroed by memset
749
750 return opts;
751}
752
766 // Extract binary-level options before reset
767 binary_level_opts_t binary = extract_binary_level(source);
768
769 // Create fresh options with all defaults
770 options_t opts = options_t_new();
771
772 // Restore binary-level options (overwrite defaults)
773 restore_binary_level(&opts, &binary);
774
775 return opts;
776}
777
778static char *options_get_log_filepath(asciichat_mode_t detected_mode, options_t *opts) {
779 // Use static buffers to avoid SAFE_MALLOC and malloc before memory tracking is initialized
780 static char log_filename_buf[OPTIONS_LOG_FILENAME_BUFFER_SIZE];
781 static char default_log_path_buf[PLATFORM_MAX_PATH_LENGTH];
782 static char tmp_dir_buf[PLATFORM_MAX_PATH_LENGTH];
783 // Use a smaller static buffer to avoid WASM memory issues
784 static char result_buf[OPTIONS_RESULT_BUFFER_SIZE]; // Reduced size for WASM compatibility
785
786 // Determine log filename based on mode
787 const char *log_filename_literal;
788 switch (detected_mode) {
789 case MODE_SERVER:
790 log_filename_literal = "server.log";
791 break;
792 case MODE_CLIENT:
793 log_filename_literal = "client.log";
794 break;
795 case MODE_MIRROR:
796 log_filename_literal = "mirror.log";
797 break;
798 case MODE_MCP_SERVER:
799 log_filename_literal = "mcp-server.log";
800 break;
801 case MODE_API_SERVER:
802 log_filename_literal = "api-server.log";
803 break;
805 log_filename_literal = "acds.log";
806 break;
807 case MODE_DISCOVERY:
808 log_filename_literal = "discovery.log";
809 break;
810 default:
811 log_filename_literal = "ascii-chat.log";
812 break;
813 }
814 SAFE_STRNCPY(log_filename_buf, log_filename_literal, sizeof(log_filename_buf) - 1);
815 log_filename_buf[sizeof(log_filename_buf) - 1] = '\0';
816
817 /* options_init() runs before the tracked allocator is initialized. Debug
818 * builds already have a stable checkout path, so avoid get_log_dir()'s
819 * allocation and path-validation work during this early phase. */
820#ifndef NDEBUG
821 const char *debug_log_dir = ASCIICHAT_SOURCE_ROOT;
822 if (debug_log_dir && *debug_log_dir) {
823 safe_snprintf(default_log_path_buf, PLATFORM_MAX_PATH_LENGTH, "%s%s%s", debug_log_dir, PATH_SEPARATOR_STR,
824 log_filename_buf);
825 SAFE_SNPRINTF(opts->log_file, OPTIONS_BUFF_SIZE, "%s", default_log_path_buf);
826 SAFE_STRNCPY(result_buf, default_log_path_buf, sizeof(result_buf) - 1);
827 result_buf[sizeof(result_buf) - 1] = '\0';
828 return result_buf;
829 }
830#endif
831
832 char *log_dir = get_log_dir();
833 if (log_dir) {
834 // Build full log file path: log_dir + separator + log_filename
835 safe_snprintf(default_log_path_buf, PLATFORM_MAX_PATH_LENGTH, "%s%s%s", log_dir, PATH_SEPARATOR_STR,
836 log_filename_buf);
837
838 // Validate and normalize the path
839 char *normalized_default_log = NULL;
840 if (path_validate_user_path(default_log_path_buf, PATH_ROLE_LOG_FILE, &normalized_default_log) == ASCIICHAT_OK) {
841 SAFE_SNPRINTF(opts->log_file, OPTIONS_BUFF_SIZE, "%s", normalized_default_log);
842 SAFE_FREE(normalized_default_log);
843 } else {
844 // Validation failed - use the path as-is (validation may fail in debug builds)
845 SAFE_SNPRINTF(opts->log_file, OPTIONS_BUFF_SIZE, "%s", default_log_path_buf);
846 }
847
848 // Free log_dir allocated by get_log_dir() using SAFE_MALLOC
849 SAFE_FREE(log_dir);
850
851 // Copy to static result buffer
852 SAFE_STRNCPY(result_buf, default_log_path_buf, sizeof(result_buf) - 1);
853 result_buf[sizeof(result_buf) - 1] = '\0';
854 return result_buf;
855
856 } else {
857 SAFE_FREE(log_dir);
858 if (platform_get_temp_dir(tmp_dir_buf, PLATFORM_MAX_PATH_LENGTH)) {
859 safe_snprintf(default_log_path_buf, PLATFORM_MAX_PATH_LENGTH, "%s%s%s", tmp_dir_buf, PATH_SEPARATOR_STR,
860 "ascii-chat.log");
861 SAFE_STRNCPY(result_buf, default_log_path_buf, sizeof(result_buf) - 1);
862 result_buf[sizeof(result_buf) - 1] = '\0';
863 return result_buf;
864 } else {
865 // Fallback to just the filename
866 SAFE_STRNCPY(result_buf, log_filename_buf, sizeof(result_buf) - 1);
867 result_buf[sizeof(result_buf) - 1] = '\0';
868 return result_buf;
869 }
870 }
871}
872
873// ============================================================================
874// Binary-Level Boolean Option Parser
875// ============================================================================
876
877// Parses binary-level boolean options with support for multiple formats:
878// - --long-name → sets field to true
879// - --long-name=true/false/yes/no/1/0/on/off → parses value
880// - -X (short form) → sets field to true
881// - -X=value (short form with value) → parses value
882static bool parse_binary_bool_arg(const char *arg, bool *field, const char *long_name, char short_name) {
883 if (!arg || !field || !long_name)
884 return false;
885
886 // Check long form: --long-name
887 char long_form[OPTIONS_LONG_FORM_BUFFER_SIZE];
888 snprintf(long_form, sizeof(long_form), "--%s", long_name);
889 if (strcmp(arg, long_form) == 0) {
890 *field = true;
891 return true;
892 }
893
894 // Check long form with =value: --long-name=...
895 char long_form_eq[OPTIONS_LONG_FORM_EQ_BUFFER_SIZE];
896 snprintf(long_form_eq, sizeof(long_form_eq), "--%s=", long_name);
897 if (strncmp(arg, long_form_eq, strlen(long_form_eq)) == 0) {
898 const char *value = arg + strlen(long_form_eq);
899 if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 ||
900 strcasecmp(value, "on") == 0) {
901 *field = true;
902 return true;
903 } else if (strcasecmp(value, "false") == 0 || strcasecmp(value, "no") == 0 || strcasecmp(value, "0") == 0 ||
904 strcasecmp(value, "off") == 0) {
905 *field = false;
906 return true;
907 }
908 // If invalid value, still consume it (builder will validate later)
909 return true;
910 }
911
912 // Check short form: -X (if defined)
913 if (short_name != '\0') {
914 char short_form[3];
915 snprintf(short_form, sizeof(short_form), "-%c", short_name);
916 if (strcmp(arg, short_form) == 0) {
917 *field = true;
918 return true;
919 }
920 }
921
922 return false;
923}
924
925// ============================================================================
926// Option Initialization Stages
927// ============================================================================
928
929// State collected before mode detection and full command-line parsing.
945
946// argv entries borrow the original strings; only the pointer array is owned.
952typedef struct {
953 int argc;
954 char **argv;
955 char **allocated;
962
963static asciichat_error_t validate_options_argv(int argc, char **argv) {
964 // Validate arguments (safety check for tests)
965 if (argc < 0 || argc > 128) {
966 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid argc: %d", argc);
967 }
968 if (argv == NULL) {
969 return SET_ERRNO(ERROR_INVALID_PARAM, "argv is NULL");
970 }
971 // Validate all argv elements are non-NULL up to argc
972 for (int i = 0; i < argc; i++) {
973 if (argv[i] == NULL) {
974 return SET_ERRNO(ERROR_INVALID_PARAM, "argv[%d] is NULL (argc=%d)", i, argc);
975 }
976 }
977 return ASCIICHAT_OK;
978}
979
980static void handle_help_option(int argc, char **argv) {
981 // FIRST: Scan entire argv for --help (special case - works before OR after mode)
982 for (int i = 1; i < argc; i++) {
983 if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
984 // Scan backwards to find if a mode was specified before --help
985 asciichat_mode_t help_mode = MODE_DISCOVERY; // Default to discovery mode (binary-level help)
986 for (int j = i - 1; j >= 1; j--) {
987 if (argv[j][0] != '-') {
988 if (strcmp(argv[j], "server") == 0) {
989 help_mode = MODE_SERVER;
990 } else if (strcmp(argv[j], "client") == 0) {
991 help_mode = MODE_CLIENT;
992 } else if (strcmp(argv[j], "mirror") == 0) {
993 help_mode = MODE_MIRROR;
994 } else if (strcmp(argv[j], "mcp-server") == 0) {
995 help_mode = MODE_MCP_SERVER;
996 } else if (strcmp(argv[j], "api-server") == 0) {
997 help_mode = MODE_API_SERVER;
998 } else if (strcmp(argv[j], "discovery-service") == 0) {
999 help_mode = MODE_DISCOVERY_SERVICE;
1000 } else if (strcmp(argv[j], "discovery") == 0) {
1001 help_mode = MODE_DISCOVERY;
1002 }
1003 break; // Found first non-flag argument
1004 }
1005 }
1006
1007 // Show help for the detected mode (or binary-level if no mode)
1008 usage(stdout, help_mode);
1009 fflush(NULL);
1010 asciichat_coverage_flush();
1011 _Exit(0);
1012 }
1013 }
1014}
1015
1016static asciichat_error_t scan_binary_actions(int argc, char **argv, binary_actions_t *actions) {
1017 // THEN: Scan for other binary-level actions (stops at mode name)
1018 for (int i = 1; i < argc; i++) {
1019 // Stop scanning at mode name - binary-level options must come before the mode
1020 if (argv[i][0] != '-') {
1021 bool is_mode =
1022 (strcmp(argv[i], "server") == 0 || strcmp(argv[i], "client") == 0 || strcmp(argv[i], "mirror") == 0 ||
1023 strcmp(argv[i], "discovery") == 0 || strcmp(argv[i], "discovery-service") == 0);
1024 if (is_mode) {
1025 break; // Stop processing at mode name
1026 }
1027 }
1028 if (argv[i][0] == '-') {
1029 // Handle --quiet, -q, --quiet=value formats
1030 bool temp_quiet = false;
1031 if (parse_binary_bool_arg(argv[i], &temp_quiet, "quiet", 'q')) {
1032 if (temp_quiet) {
1033 actions->user_quiet = true;
1034 }
1035 }
1036 // Validate --log-level and --log-file require arguments
1037 if (strcmp(argv[i], "--log-level") == 0) {
1038 if (i + 1 >= argc || argv[i + 1][0] == '-') {
1039 log_error("--log-level requires a value (dev, debug, info, warn, error, fatal)");
1040 return ERROR_USAGE;
1041 }
1042 i++; // Skip the argument in this loop
1043 }
1044 if (strcmp(argv[i], "-L") == 0 || strcmp(argv[i], "--log-file") == 0) {
1045 if (i + 1 >= argc || argv[i + 1][0] == '-') {
1046 log_error("%s requires a file path", argv[i]);
1047 return ERROR_USAGE;
1048 }
1049 i++; // Skip the argument in this loop
1050 }
1051 // --help is handled in the first scan loop above (works before OR after mode)
1052 // Parse --color early so it affects help output colors
1053 if (strcmp(argv[i], "--color") == 0) {
1054 if (i + 1 < argc && argv[i + 1][0] != '-') {
1055 // Check if next arg is a mode name (not a color value)
1056 const char *next_arg = argv[i + 1];
1057 bool is_mode =
1058 (strcmp(next_arg, "server") == 0 || strcmp(next_arg, "client") == 0 || strcmp(next_arg, "mirror") == 0 ||
1059 strcmp(next_arg, "discovery") == 0 || strcmp(next_arg, "discovery-service") == 0);
1060
1061 if (is_mode) {
1062 // Next arg is a mode, not a color value - default to true
1064 actions->color_setting_found = true;
1065 } else {
1066 // Try to parse as color value
1067 char *error_msg = NULL;
1068 if (parse_color_setting(next_arg, &actions->parsed_color_setting, &error_msg)) {
1069 actions->color_setting_found = true;
1070 i++; // Skip the setting argument
1071 } else {
1072 if (error_msg) {
1073 log_error("Error parsing --color: %s", error_msg);
1074 SAFE_FREE(error_msg);
1075 }
1076 }
1077 }
1078 } else {
1079 // --color without argument defaults to true (enable colors)
1081 actions->color_setting_found = true;
1082 }
1083 }
1084 // Check for --color=value format
1085 if (strncmp(argv[i], "--color=", 8) == 0) {
1086 const char *value = argv[i] + 8;
1087 char *error_msg = NULL;
1088 if (parse_color_setting(value, &actions->parsed_color_setting, &error_msg)) {
1089 actions->color_setting_found = true;
1090 } else {
1091 if (error_msg) {
1092 log_error("Error parsing --color: %s", error_msg);
1093 SAFE_FREE(error_msg);
1094 }
1095 }
1096 }
1097 if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) {
1098 actions->show_version = true;
1099 actions->has_action = true;
1100 break;
1101 }
1102 if (strcmp(argv[i], "--check-update") == 0) {
1103 actions->check_update_flag_seen = true;
1104 if (actions->no_check_update_flag_seen) {
1105 log_error("Cannot specify both --check-update and --no-check-update");
1106 return ERROR_USAGE;
1107 }
1108 actions->has_action = true;
1110 // action_check_update_immediate() calls _Exit(), so we don't reach here
1111 break;
1112 }
1113 if (strcmp(argv[i], "--no-check-update") == 0) {
1114 actions->no_check_update_flag_seen = true;
1115 if (actions->check_update_flag_seen) {
1116 log_error("Cannot specify both --check-update and --no-check-update");
1117 return ERROR_USAGE;
1118 }
1119 // Flag will be parsed normally later
1120 }
1121 if (strcmp(argv[i], "--config-create") == 0) {
1122 actions->create_config = true;
1123 actions->has_action = true;
1124 // Check for optional [FILE] argument
1125 if (i + 1 < argc && argv[i + 1][0] != '-') {
1126 actions->config_create_path = argv[i + 1];
1127 i++; // Consume the file path argument
1128 }
1129 break;
1130 }
1131 if (strcmp(argv[i], "--man-page-create") == 0) {
1132 actions->create_manpage = true;
1133 actions->has_action = true;
1134 // Check for optional [FILE] argument
1135 if (i + 1 < argc && argv[i + 1][0] != '-') {
1136 actions->manpage_create_path = argv[i + 1];
1137 i++; // Consume the file path argument
1138 }
1139 break;
1140 }
1141 if (strcmp(argv[i], "--completions") == 0) {
1142 actions->has_action = true;
1143 // Handle --completions: generate shell completion scripts
1144 if (i + 1 < argc && argv[i + 1][0] != '-') {
1145 const char *shell_name = argv[i + 1];
1146 i++; // Consume shell name
1147
1148 // Check for optional output file
1149 const char *output_file = NULL;
1150 if (i + 1 < argc && argv[i + 1][0] != '-') {
1151 output_file = argv[i + 1];
1152 i++; // Consume output file
1153 }
1154
1155 action_completions(shell_name, output_file);
1156 // action_completions() calls _Exit(), so we don't reach here
1157 } else {
1158 log_error("--completions requires shell name (bash, fish, zsh, powershell)");
1159 return ERROR_USAGE;
1160 }
1161 break; // Unreachable, but for clarity
1162 }
1163 if (strncmp(argv[i], "--", 2) == 0) {
1164 option_action_fn action = options_action_callback(argv[i] + 2, true);
1165 if (action) {
1166 actions->has_action = true;
1167 action();
1168 break;
1169 }
1170 }
1171 }
1172 }
1173 return ASCIICHAT_OK;
1174}
1175
1176static asciichat_error_t validate_binary_option_position(int argc, char **argv, int mode_index) {
1177 // VALIDATE: Binary-level options must appear BEFORE the mode
1178 // Check if any binary-level options appear after the mode position
1179 if (mode_index > 0) {
1180 for (int i = mode_index + 1; i < argc; i++) {
1181 if (argv[i][0] == '-') {
1182 bool takes_arg = false;
1183 bool takes_optional_arg = false;
1184
1185 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1186 return SET_ERRNO(ERROR_USAGE, "Binary-level option '%s' must appear before the mode '%s', not after it",
1187 argv[i], argv[mode_index]);
1188 }
1189 }
1190 }
1191 }
1192 return ASCIICHAT_OK;
1193}
1194
1201static asciichat_error_t validate_early_color_mode(int argc, char **argv) {
1202 for (int i = 1; i < argc; i++) {
1203 const char *value = NULL;
1204 if (strcmp(argv[i], "--color-mode") == 0) {
1205 if (i + 1 >= argc || argv[i + 1][0] == '-') {
1206 return SET_ERRNO(ERROR_USAGE, "--color-mode requires a value (auto, none, 16, 256, or truecolor)");
1207 }
1208 value = argv[++i];
1209 } else if (strncmp(argv[i], "--color-mode=", 13) == 0) {
1210 value = argv[i] + 13;
1211 }
1212
1213 if (value) {
1214 terminal_color_mode_t parsed = TERM_COLOR_AUTO;
1215 char *error_msg = NULL;
1216 if (!parse_color_mode(value, &parsed, &error_msg)) {
1217 asciichat_error_t result = SET_ERRNO(ERROR_USAGE, "%s", error_msg ? error_msg : "Invalid color mode");
1218 SAFE_FREE(error_msg);
1219 return result;
1220 }
1221 SAFE_FREE(error_msg);
1222 }
1223 }
1224 return ASCIICHAT_OK;
1225}
1226
1227static void initialize_binary_options(options_t *opts, asciichat_mode_t detected_mode,
1228 const binary_actions_t *actions) {
1229 log_debug("initialize_binary_options: before systemd");
1230 opts->no_check_update = actions->no_check_update_flag_seen || platform_systemd_is_managed();
1231 log_debug("initialize_binary_options: after systemd");
1232 opts->detected_mode = detected_mode;
1233 if (actions->has_action) {
1234 // Immediate output actions must not allocate or validate a log path before
1235 // the diagnostic allocator has completed initialization.
1236#ifdef _WIN32
1237 SAFE_STRNCPY(opts->log_file, "NUL", sizeof(opts->log_file));
1238#else
1239 SAFE_STRNCPY(opts->log_file, "/dev/null", sizeof(opts->log_file));
1240#endif
1241 } else if (!platform_systemd_is_managed()) {
1242 log_debug("initialize_binary_options: before log path");
1243 char *log_filename = options_get_log_filepath(detected_mode, opts);
1244 log_debug("initialize_binary_options: after log path");
1245 SAFE_SNPRINTF(opts->log_file, OPTIONS_BUFF_SIZE, "%s", log_filename);
1246 } else {
1247 SAFE_STRNCPY(opts->log_file, "/dev/null", sizeof(opts->log_file));
1248 }
1249 // Note: log_init() is called earlier in asciichat_shared_init() and will be
1250 // reconfigured with the actual log level and file in main.c after options are fully parsed
1251 // NOTE: --color detection now happens in src/main.c BEFORE asciichat_shared_init()
1252 // This ensures g_color_flag_passed and g_color_flag_value are set before any logging.
1253 //
1254 // NOTE: Timer system and shared subsystems are initialized by src/main.c
1255 // via asciichat_shared_init() BEFORE options_init() is called.
1256 // This allows options_init() to use properly configured logging.
1257
1258 // If an action flag is detected OR user passed --quiet, silence logs for clean output
1259 if (actions->user_quiet || actions->has_action) {
1260 log_set_terminal_output(false); // Suppress console logging for clean action output
1261 }
1262 // Apply parsed color setting from STAGE 1A
1263 if (actions->color_setting_found) {
1264 opts->color = actions->parsed_color_setting;
1265 } else {
1266 opts->color = platform_systemd_is_managed() ? COLOR_SETTING_FALSE : OPT_COLOR_DEFAULT;
1267 }
1268}
1269
1270static asciichat_error_t handle_immediate_actions(options_t *opts, const binary_actions_t *actions, bool *handled) {
1271 // Handle output actions before loading configuration files and parsing mode options.
1272 if (actions->show_version || actions->create_config || actions->create_manpage) {
1273 if (actions->show_version) {
1274 opts->version = true;
1275 options_state_set(opts);
1276 *handled = true;
1277 return ASCIICHAT_OK;
1278 } else if (actions->create_config) {
1279 // Build the schema first so config_create_default can generate options from it
1280 const options_config_t *unified_config = options_preset_unified(NULL, NULL);
1281 if (unified_config) {
1282 asciichat_error_t schema_build_result = config_schema_build_from_configs(&unified_config, 1);
1283 if (schema_build_result != ASCIICHAT_OK) {
1284 // Schema build failed, but continue anyway
1285 (void)schema_build_result;
1286 }
1287 options_config_destroy(unified_config);
1288 }
1289
1290 // Call action handler which handles all output and prompts properly
1292 // action_create_config() calls _Exit(), so we don't reach here
1293 } else if (actions->create_manpage) {
1294 // Call action handler which handles all output and prompts properly
1296 // action_create_manpage() calls _Exit(), so we don't reach here
1297 }
1298 }
1299 return ASCIICHAT_OK;
1300}
1301
1302static asciichat_error_t parse_binary_options(options_t *opts, int argc, char **argv) {
1303 // Check for binary-level options that can appear before or after mode
1304 // Search entire argv to find --quiet, --log-file, --log-level, -V, etc.
1305 // These are documented as binary-level options that can appear anywhere
1306 for (int i = 1; i < argc; i++) {
1307 if (argv[i][0] == '-') {
1308 // Handle -V and --verbose (stackable verbosity)
1309 if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--verbose") == 0) {
1310 // Check if next argument is a number (optional argument)
1311 if (i + 1 < argc && argv[i + 1][0] != '-') {
1312 // Try to parse as integer count
1313 char *endptr;
1314 long value = strtol(argv[i + 1], &endptr, 10);
1315 if (*endptr == '\0' && value >= 0 && value <= 100) {
1316 opts->verbose_level = (unsigned short int)value;
1317 i++; // Skip next argument
1318 continue;
1319 }
1320 }
1321 // No valid number, just increment
1322 opts->verbose_level++;
1323 }
1324 // Handle binary-level boolean options using the abstraction function
1325 // -q/--quiet, --json, --log-format-console all use the same parser
1326 if (parse_binary_bool_arg(argv[i], &opts->quiet, "quiet", 'q')) {
1327 continue;
1328 }
1329 if (parse_binary_bool_arg(argv[i], &opts->json, "json", '\0')) {
1330 continue;
1331 }
1332 if (parse_binary_bool_arg(argv[i], &opts->log_format_console_only, "log-format-console", '\0')) {
1333 continue;
1334 }
1335 // Handle --log-level LEVEL (set log threshold)
1336 if (strcmp(argv[i], "--log-level") == 0) {
1337 if (i + 1 >= argc) {
1338 log_error("--log-level requires a value (dev, debug, info, warn, error, fatal)");
1339 return ERROR_USAGE;
1340 }
1341 if (argv[i + 1][0] == '-') {
1342 log_error("--log-level requires a value (dev, debug, info, warn, error, fatal)");
1343 return ERROR_USAGE;
1344 }
1345 char *error_msg = NULL;
1346 if (parse_log_level(argv[i + 1], &opts->log_level, &error_msg)) {
1347 i++; // Skip the level argument
1348 } else {
1349 if (error_msg) {
1350 log_error("%s", error_msg);
1351 SAFE_FREE(error_msg);
1352 } else {
1353 log_error("invalid log level value: %s", argv[i + 1]);
1354 }
1355 return ERROR_USAGE;
1356 }
1357 }
1358 if (strcmp(argv[i], "--config") == 0 || strncmp(argv[i], "--config=", 9) == 0) {
1359 const char *path = argv[i] + 9;
1360 if (strcmp(argv[i], "--config") == 0) {
1361 if (i + 1 >= argc || argv[i + 1][0] == '-')
1362 return SET_ERRNO(ERROR_USAGE, "--config requires a file path");
1363 path = argv[++i];
1364 }
1365 if (!path[0] || strlen(path) >= sizeof(opts->config_file))
1366 return SET_ERRNO(ERROR_USAGE, "Invalid --config file path");
1367 SAFE_STRNCPY(opts->config_file, path, sizeof(opts->config_file));
1368 continue;
1369 }
1370 // Handle -L and --log-file FILE (set log file path)
1371 if ((strcmp(argv[i], "-L") == 0 || strcmp(argv[i], "--log-file") == 0)) {
1372 if (i + 1 >= argc) {
1373 log_error("%s requires a file path", argv[i]);
1374 return ERROR_USAGE;
1375 }
1376 if (argv[i + 1][0] == '-') {
1377 log_error("%s requires a file path", argv[i]);
1378 return ERROR_USAGE;
1379 }
1380 SAFE_STRNCPY(opts->log_file, argv[i + 1], sizeof(opts->log_file));
1381 i++; // Skip the file argument
1382 }
1383 // NOTE: --color is now parsed in STAGE 1A (early, before help processing)
1384 // to ensure help output colors are applied correctly
1385 }
1386 }
1387 return ASCIICHAT_OK;
1388}
1389
1390static asciichat_error_t handle_fallback_actions(options_t *opts, const binary_actions_t *actions) {
1391 if (actions->show_version) {
1392 // Show binary-level version from src/main.c
1393 opts->version = true;
1394 options_state_set(opts);
1395 return ASCIICHAT_OK;
1396 }
1397
1398 if (actions->create_config) {
1399 // Handle --config-create: create default config file and exit
1400 // Use provided path or default to user config location
1401 char config_path[PLATFORM_MAX_PATH_LENGTH];
1402 if (actions->config_create_path) {
1403 SAFE_STRNCPY(config_path, actions->config_create_path, sizeof(config_path));
1404 } else {
1405 // Use default config path: ~/.ascii-chat/config.toml
1406 char *config_dir = get_config_dir();
1407 if (!config_dir) {
1408 log_error("Failed to determine default config directory");
1409 return ERROR_CONFIG;
1410 }
1411 safe_snprintf(config_path, sizeof(config_path), "%sconfig.toml", config_dir);
1412 SAFE_FREE(config_dir);
1413 }
1414
1415 // Create config with default options
1416 asciichat_error_t result = config_create_default(config_path);
1417 if (result != ASCIICHAT_OK) {
1419 if (HAS_ERRNO(&err_ctx)) {
1420 log_error("Error creating config: %s", err_ctx.context_message);
1421 } else {
1422 log_error("Failed to create config file at %s", config_path);
1423 }
1424 return result;
1425 }
1426
1427 log_plain("Created default config file at: %s", config_path);
1428 return ASCIICHAT_OK; // Return successfully after creating config
1429 }
1430
1431 if (actions->create_manpage) {
1432 // Handle --create-man-page-template: generate merged man page template
1433 // The .1.in file is the existing template to read from (not the output)
1434 const char *existing_template_path = "share/man/man1/ascii-chat.1.in";
1435 options_config_t *config = options_preset_unified(NULL, NULL);
1436 if (!config) {
1437 log_error("Failed to get binary options config");
1438 return ERROR_MEMORY;
1439 }
1440
1441 // Generate merged man page from embedded or filesystem resources
1442 // (existing_template_path and manpage_content_file parameters are no longer supported)
1443 asciichat_error_t err = options_config_generate_manpage_merged(config, "ascii-chat", NULL, existing_template_path,
1444 "Video chat in your terminal");
1445
1446 options_config_destroy(config);
1447
1448 if (err != ASCIICHAT_OK) {
1450 if (HAS_ERRNO(&err_ctx)) {
1451 log_error("%s", err_ctx.context_message);
1452 } else {
1453 log_error("Failed to generate man page");
1454 }
1455 return err;
1456 }
1457
1458 log_plain("Generated man page: %s", existing_template_path);
1459 return ASCIICHAT_OK; // Return successfully after generating man page
1460 }
1461 return ASCIICHAT_OK;
1462}
1463
1464static asciichat_error_t build_mode_argv(int argc, char **argv, int mode_index, mode_arguments_t *args) {
1465 if (mode_index == -1) {
1466 // No explicit mode - filter binary-level options from entire argv
1467 int max_mode_argc = argc; // Worst case: no binary opts skipped
1468
1469 if (max_mode_argc > 256) {
1470 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many arguments: %d", max_mode_argc);
1471 }
1472
1473 char **new_mode_argv = SAFE_MALLOC((size_t)(max_mode_argc + 1) * sizeof(char *), char **);
1474 if (!new_mode_argv) {
1475 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate mode_argv");
1476 }
1477 args->allocated = new_mode_argv;
1478
1479 new_mode_argv[0] = argv[0]; // Copy program name
1480
1481 // Copy all args except binary-level options
1482 int new_argv_idx = 1;
1483 for (int i = 1; i < argc; i++) {
1484 bool takes_arg = false;
1485 bool takes_optional_arg = false;
1486
1487 // Check if this is a binary-level option
1488 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1489 // Skip argument if needed
1490 // NOLINTNEXTLINE(bugprone-branch-clone)
1491 if (takes_arg && i + 1 < argc) {
1492 i++; // Skip required argument
1493 } else if (takes_optional_arg && i + 1 < argc && argv[i + 1][0] != '-') {
1494 i++; // Skip optional argument
1495 }
1496 continue;
1497 }
1498 // Not a binary option, copy to mode_argv
1499 new_mode_argv[new_argv_idx++] = argv[i];
1500 }
1501
1502 args->argc = new_argv_idx;
1503 new_mode_argv[args->argc] = NULL;
1504 args->argv = new_mode_argv;
1505 } else if (mode_index != -1) {
1506 // Mode found at position mode_index
1507 // Build new argv: [program_name, args_before_mode (no binary opts)..., args_after_mode...]
1508 // Binary-level options are not passed to mode-specific parsers
1509
1510 int args_after_mode = argc - mode_index - 1;
1511 // We'll calculate final argc after skipping binary options
1512 int max_mode_argc = 1 + (mode_index - 1) + args_after_mode; // Worst case: no binary opts skipped
1513
1514 if (max_mode_argc > 256) {
1515 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many arguments: %d", max_mode_argc);
1516 }
1517
1518 // Allocate max_mode_argc+1 to accommodate NULL terminator
1519 char **new_mode_argv = SAFE_MALLOC((size_t)(max_mode_argc + 1) * sizeof(char *), char **);
1520 if (!new_mode_argv) {
1521 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate mode_argv");
1522 }
1523 args->allocated = new_mode_argv; // Track allocation for cleanup
1524
1525 // Copy: [program_name, args_before_mode (excluding binary opts)..., args_after_mode...]
1526 if (mode_index == 0) {
1527 // Mode is at argv[0], use "ascii-chat" as program name
1528 new_mode_argv[0] = "ascii-chat";
1529 } else {
1530 new_mode_argv[0] = argv[0];
1531 }
1532
1533 // Copy args before mode, skipping binary-level options
1534 int new_argv_idx = 1;
1535 for (int i = 1; i < mode_index; i++) {
1536 bool takes_arg = false;
1537 bool takes_optional_arg = false;
1538
1539 // Check if this is a binary-level option
1540 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1541 // Skip argument if needed
1542 if (!strchr(argv[i], '=') && i + 1 < mode_index &&
1543 (takes_arg || (takes_optional_arg && argv[i + 1][0] != '-'))) {
1544 i++; // Skip argument
1545 }
1546 continue;
1547 }
1548 // Not a binary option, copy to mode_argv
1549 new_mode_argv[new_argv_idx++] = argv[i];
1550 }
1551 // Copy args after mode, filtering out any binary-level options (they shouldn't appear here)
1552 int args_after_mode_idx = 0;
1553 for (int i = mode_index + 1; i < argc; i++) {
1554 bool takes_arg = false;
1555 bool takes_optional_arg = false;
1556
1557 // Check if this is a binary-level option (shouldn't appear after mode)
1558 if (is_binary_level_option_with_args(argv[i], &takes_arg, &takes_optional_arg)) {
1559 // Skip binary-level option and its argument if needed
1560 if (!strchr(argv[i], '=') && i + 1 < argc && (takes_arg || (takes_optional_arg && argv[i + 1][0] != '-'))) {
1561 i++; // Skip argument
1562 }
1563 continue; // Skip this option
1564 }
1565 // Not a binary option, copy to mode_argv
1566 new_mode_argv[new_argv_idx + args_after_mode_idx++] = argv[i];
1567 }
1568 // Calculate actual argc (program + filtered args before + filtered args after)
1569 args->argc = new_argv_idx + args_after_mode_idx;
1570 new_mode_argv[args->argc] = NULL;
1571
1572 args->argv = new_mode_argv;
1573 }
1574 return ASCIICHAT_OK;
1575}
1576
1577static void initialize_mode_defaults(options_t *opts, asciichat_mode_t detected_mode,
1578 const char *detected_session_string) {
1579 // Initialize all defaults using options_t_new_preserve_binary() to keep binary-level
1580 // options (--quiet, --verbose, --log-level, etc.) from being reset
1581 (*opts) = options_t_new_preserve_binary(opts);
1582 if (detected_mode == MODE_MCP_SERVER || detected_mode == MODE_API_SERVER) {
1583 opts->port = OPT_MCP_PORT_DEFAULT;
1584 // Zero means use the available CPU count unless max-clients is configured.
1585 opts->max_clients = 0;
1586 }
1587 // If a session string was detected during mode detection, restore it after options reset
1588 // This ensures discovery mode knows which session to join
1589 if (detected_session_string[0] != '\0') {
1590 SAFE_STRNCPY(opts->session_string, detected_session_string, sizeof(opts->session_string));
1591 log_info("Detected session string from argv: '%s'", detected_session_string);
1592 }
1593
1594 // Encryption options default to disabled/empty
1595 opts->no_encrypt = 0;
1596 opts->encrypt_key[0] = '\0';
1597 opts->password[0] = '\0';
1598 opts->encrypt_keyfile[0] = '\0';
1599 opts->server_key[0] = '\0';
1600 opts->client_keys[0] = '\0';
1601 opts->palette_custom[0] = '\0';
1602
1603 // Set different default addresses for different modes
1604 if (detected_mode == MODE_CLIENT || detected_mode == MODE_MIRROR || detected_mode == MODE_DISCOVERY) {
1605 // Client/Mirror/Discovery: connects to localhost by default (discovery uses ACDS to find server)
1606 SAFE_SNPRINTF(opts->address, OPTIONS_BUFF_SIZE, "localhost");
1607 opts->address6[0] = '\0'; // Client doesn't use address6
1608 } else if (detected_mode == MODE_SERVER || detected_mode == MODE_MCP_SERVER) {
1609 // Server: binds to 127.0.0.1 (IPv4) and ::1 (IPv6) by default
1610 SAFE_SNPRINTF(opts->address, OPTIONS_BUFF_SIZE, "127.0.0.1");
1611 // address6 is now a positional argument, not an option
1612 opts->address6[0] = '\0';
1613 } else if (detected_mode == MODE_DISCOVERY_SERVICE) {
1614 // ACDS: binds to all interfaces by default
1615 SAFE_SNPRINTF(opts->address, OPTIONS_BUFF_SIZE, "0.0.0.0");
1616 opts->address6[0] = '\0';
1617 }
1618}
1619
1620static void build_options_schema(void) {
1621 // Build the config schema dynamically from the unified config
1622 // This generates TOML keys, CLI flags, categories, and types from builder data
1623 const options_config_t *unified_config = options_preset_unified(NULL, NULL);
1624 if (unified_config) {
1625 asciichat_error_t schema_build_result = config_schema_build_from_configs(&unified_config, 1);
1626 if (schema_build_result != ASCIICHAT_OK) {
1627 // Schema build failed, but continue with static schema as fallback
1628 (void)schema_build_result;
1629 }
1630 options_config_destroy(unified_config);
1631 }
1632}
1633
1634static asciichat_error_t load_options_config(options_t *opts, asciichat_mode_t detected_mode, bool *fps_set_by_config) {
1635 // Isolated MCP renderers never load user/system configuration.
1636 if (SAFE_GETENV("ASCII_CHAT_MCP_WORKER")) {
1637 *fps_set_by_config = false;
1638 return ASCIICHAT_OK;
1639 }
1640 // Extract binary-level options BEFORE config loading (config may reset them)
1641 binary_level_opts_t binary_before_config = extract_binary_level(opts);
1642 // Publish options to RCU before config loading so that config logs get proper colors
1643 // from the parsed --color setting (e.g., --color=true)
1644 asciichat_error_t config_publish_result = options_state_set(opts);
1645 (void)config_publish_result; // Suppress unused variable warning
1646 // Load config files - now uses detected_mode directly for bitmask validation
1647 // Preserve the existing vertical flip configuration behavior.
1648 bool saved_flip_y_from_config = opts->flip_y;
1649 asciichat_error_t config_result = config_load_system_and_user(detected_mode, false, opts);
1650 (void)config_result; // Continue with defaults and CLI parsing regardless of result
1651 if (binary_before_config.config_file[0]) {
1652 config_result = config_load_and_apply(detected_mode, binary_before_config.config_file, true, opts);
1653 if (config_result != ASCIICHAT_OK)
1654 return config_result;
1655 }
1656 *fps_set_by_config = opts->fps != OPT_FPS_DEFAULT;
1657 // Telemetry CLI values are parsed again after config and environment resolution.
1658 SAFE_STRNCPY(binary_before_config.otel_log_level, opts->otel_log_level, sizeof(binary_before_config.otel_log_level));
1659 SAFE_STRNCPY(binary_before_config.otel_endpoint, opts->otel_endpoint, sizeof(binary_before_config.otel_endpoint));
1660 SAFE_STRNCPY(binary_before_config.otel_ca_file, opts->otel_ca_file, sizeof(binary_before_config.otel_ca_file));
1662 sizeof(binary_before_config.otel_enabled_telemetry));
1663 binary_before_config.otel_realtime_sample_rate = opts->otel_realtime_sample_rate;
1665 binary_before_config.otel_metrics_interval = opts->otel_metrics_interval;
1666 binary_before_config.otel_timeout = opts->otel_timeout;
1667 binary_before_config.otel_queue_capacity = opts->otel_queue_capacity;
1668 // Restore binary-level options (don't let config override command-line options)
1669 restore_binary_level(opts, &binary_before_config);
1670
1671 // Preserve the existing vertical flip default.
1672 opts->flip_y = saved_flip_y_from_config;
1673 return ASCIICHAT_OK;
1674}
1675
1676static asciichat_error_t parse_mode_options(options_t *opts, options_config_t *config, asciichat_mode_t detected_mode,
1677 int mode_argc, char **mode_argv, int *remaining_argc,
1678 char ***remaining_argv) {
1679 // Extract binary-level options BEFORE applying unified defaults
1680 // (which might override them from config files)
1681 binary_level_opts_t binary_before_defaults = extract_binary_level(opts);
1682 asciichat_mode_t mode_saved_for_parsing = detected_mode; // Save before defaults reset
1683 // Save flip_x and flip_y before applying defaults (should not be reset by defaults)
1684 bool saved_flip_x = opts->flip_x;
1685 bool saved_flip_y = opts->flip_y;
1686 if (SAFE_GETENV("ASCII_CHAT_FLIP_X"))
1687 opts->flip_x = OPT_FLIP_X_DEFAULT;
1688 // Apply defaults from unified config
1689 asciichat_error_t defaults_result = options_config_set_defaults(config, opts);
1690 if (defaults_result != ASCIICHAT_OK) {
1691 return defaults_result;
1692 }
1693 // An enabled environment flag overrides the opposite config-file choice.
1694 const char *encrypt_env = SAFE_GETENV("ASCII_CHAT_ENCRYPT");
1695 const char *no_encrypt_env = SAFE_GETENV("ASCII_CHAT_NO_ENCRYPT");
1696 if (encrypt_env && !no_encrypt_env && opts->encrypt_enabled)
1697 opts->no_encrypt = false;
1698 if (no_encrypt_env && !encrypt_env && opts->no_encrypt)
1699 opts->encrypt_enabled = false;
1700
1701 bool cli_encrypt = false, cli_no_encrypt = false;
1702 bool cli_flip_x = false, cli_flip_y = false;
1703 for (int i = 1; i < mode_argc; ++i) {
1704 if (strcmp(mode_argv[i], "--") == 0)
1705 break;
1706 cli_encrypt |= strcmp(mode_argv[i], "--encrypt") == 0 || strcmp(mode_argv[i], "-E") == 0 ||
1707 strncmp(mode_argv[i], "--encrypt=", 10) == 0;
1708 cli_no_encrypt |= strcmp(mode_argv[i], "--no-encrypt") == 0 || strncmp(mode_argv[i], "--no-encrypt=", 13) == 0;
1709 cli_flip_x |= strcmp(mode_argv[i], "--flip-x") == 0 || strncmp(mode_argv[i], "--flip-x=", 9) == 0;
1710 cli_flip_y |= strcmp(mode_argv[i], "--flip-y") == 0 || strncmp(mode_argv[i], "--flip-y=", 9) == 0;
1711 }
1712 // Bare boolean CLI flags toggle from false, independent of lower layers.
1713 if (cli_encrypt)
1714 opts->encrypt_enabled = false;
1715 if (cli_no_encrypt)
1716 opts->no_encrypt = false;
1717 // Restore binary-level options (they should never be overridden by defaults)
1718 restore_binary_level(opts, &binary_before_defaults);
1719
1720 // Restore flip_x and flip_y - they should keep the values from options_t_new()
1721 // unless explicitly set by the user (but defaults shouldn't override them)
1722 if (!SAFE_GETENV("ASCII_CHAT_FLIP_X"))
1723 opts->flip_x = saved_flip_x;
1724 if (cli_flip_x)
1725 opts->flip_x = false;
1726 opts->flip_y = saved_flip_y;
1727
1728 // Restore detected_mode before parsing so mode validation works.
1729 opts->detected_mode = mode_saved_for_parsing;
1730
1731 // Apply telemetry environment values after config restoration, before CLI.
1732 // Using the normal value parsers retains range checks and error reporting.
1733 for (size_t i = 0; i < config->num_descriptors; ++i) {
1734 const option_descriptor_t *desc = &config->descriptors[i];
1735 if (!desc->long_name || strncmp(desc->long_name, "otel-", 5) || !desc->env_var_name)
1736 continue;
1737 const char *value = SAFE_GETENV(desc->env_var_name);
1738 if (value && desc->type >= 0 && desc->type < NUM_OPTION_TYPES) {
1739 asciichat_error_t env_result = g_builder_handlers[desc->type].apply_cli((char *)opts + desc->offset, value, desc);
1740 if (env_result != ASCIICHAT_OK)
1741 return env_result;
1742 }
1743 }
1744 // Save flip_x and flip_y before parsing - they should not be reset by the parser
1745 bool saved_flip_x_for_parse = opts->flip_x;
1746 bool saved_flip_y_for_parse = opts->flip_y;
1747 // Note: json is already saved via extract_binary_level/restore_binary_level mechanism
1748 // Parse mode-specific arguments
1749 option_mode_bitmask_t mode_bitmask = (1 << mode_saved_for_parsing);
1750 log_debug("About to parse options_config with mode_bitmask=%u, mode_argc=%d", mode_bitmask, mode_argc);
1751 asciichat_error_t result =
1752 options_config_parse(config, mode_argc, mode_argv, opts, mode_bitmask, remaining_argc, remaining_argv);
1753 log_debug("options_config_parse returned: %d", result);
1754 if (cli_encrypt && !cli_no_encrypt && opts->encrypt_enabled)
1755 opts->no_encrypt = false;
1756 if (cli_no_encrypt && !cli_encrypt && opts->no_encrypt)
1757 opts->encrypt_enabled = false;
1758 // Restore flip_x and flip_y - they should keep their values unless explicitly overridden
1759 if (!cli_flip_x && !SAFE_GETENV("ASCII_CHAT_FLIP_X"))
1760 opts->flip_x = saved_flip_x_for_parse;
1761 opts->flip_x_explicit |= cli_flip_x || SAFE_GETENV("ASCII_CHAT_FLIP_X") != NULL;
1762 if (!cli_flip_y)
1763 opts->flip_y = saved_flip_y_for_parse;
1764 // json is already restored via the call to options_state_set which calls restore_binary_level
1765 if (result != ASCIICHAT_OK) {
1766 // Convert ERROR_CONFIG to ERROR_USAGE for command-line parsing errors
1767 if (result == ERROR_CONFIG) {
1769 const char *error_detail = "invalid options";
1770 if (HAS_ERRNO(&err_ctx)) {
1771 error_detail = err_ctx.context_message;
1772 }
1773 return SET_ERRNO(ERROR_USAGE, "Failed to parse options: %s", error_detail);
1774 }
1775 return result;
1776 }
1777 return ASCIICHAT_OK;
1778}
1779
1780static void record_explicit_fps(options_t *opts, int mode_argc, char **mode_argv, bool fps_set_by_config) {
1781 bool fps_set_by_cli = false;
1782 for (int i = 0; i < mode_argc; i++) {
1783 if (mode_argv[i] &&
1784 (strcmp(mode_argv[i], "--fps") == 0 || strncmp(mode_argv[i], "--fps=", strlen("--fps=")) == 0)) {
1785 fps_set_by_cli = true;
1786 break;
1787 }
1788 }
1789 const char *fps_env = SAFE_GETENV("ASCII_CHAT_FPS");
1790 opts->fps_explicitly_set = fps_set_by_config || fps_set_by_cli || (fps_env && fps_env[0] != '\0');
1791}
1792
1793static void record_explicit_stabilize(options_t *opts, int mode_argc, char **mode_argv) {
1794 if (!opts)
1795 return;
1796 for (int i = 0; i < mode_argc; ++i) {
1797 if (mode_argv[i] && (!strcmp(mode_argv[i], "--stabilize") || !strncmp(mode_argv[i], "--stabilize=", 12))) {
1798 opts->stabilize_explicit = true;
1799 return;
1800 }
1801 }
1802 const char *env = SAFE_GETENV("ASCII_CHAT_STABILIZE");
1803 opts->stabilize_explicit = env && env[0] != '\0';
1804}
1805
1806static asciichat_error_t publish_options_for_validation(options_t *opts) {
1807 // Publish options to RCU as soon as they're parsed
1808 // This ensures GET_OPTION() works during cleanup even if validation fails
1809 if (opts->test_audio)
1810 opts->audio_enabled = true;
1811 log_debug("Publishing parsed options to RCU before validation");
1812 asciichat_error_t early_publish = options_state_set(opts);
1813 if (early_publish != ASCIICHAT_OK) {
1814 log_error("Failed to publish parsed options to RCU state early");
1815
1816 return early_publish;
1817 }
1818 log_debug("Successfully published options to RCU");
1819 return ASCIICHAT_OK;
1820}
1821
1822static asciichat_error_t validate_custom_palette(options_t *opts) {
1823 // Auto-enable custom palette if palette-chars was set
1824 if (opts->palette_custom[0] != '\0') {
1825 // palette-chars was set - always use PALETTE_CUSTOM (overrides any explicit --palette setting)
1827 opts->palette_custom_set = true;
1828 log_debug("Set PALETTE_CUSTOM because --palette-chars was provided");
1829
1830 // Validate palette characters for UTF-8 correctness
1831 if (!utf8_is_valid(opts->palette_custom)) {
1832 log_error("--palette-chars contains invalid UTF-8 sequences");
1833
1834 return option_error_invalid();
1835 }
1836
1837 // Check if palette contains non-ASCII characters
1838 bool has_non_ascii = !utf8_is_ascii_only(opts->palette_custom);
1839 if (has_non_ascii) {
1840 // Non-ASCII characters require UTF-8 support
1841 // Check if UTF-8 is explicitly disabled or unavailable
1842 bool utf8_disabled = (opts->force_utf8 == COLOR_SETTING_FALSE);
1843 bool utf8_auto_unavailable = (opts->force_utf8 == COLOR_SETTING_AUTO && !terminal_supports_utf8());
1844
1845 if (utf8_disabled) {
1846 log_error("--palette-chars contains non-ASCII characters but --utf8=false was specified");
1847 log_error(" Remove --utf8=false or use ASCII-only palette characters");
1848
1849 return option_error_invalid();
1850 }
1851
1852 if (utf8_auto_unavailable) {
1853 log_error("--palette-chars contains non-ASCII characters but terminal does not support UTF-8");
1854 log_error(" Use --utf8=true to force UTF-8 mode or use ASCII-only palette characters");
1855
1856 return option_error_invalid();
1857 }
1858 }
1859 }
1860 return ASCIICHAT_OK;
1861}
1862
1863static asciichat_error_t validate_encryption_key(options_t *opts) {
1864 crypto_policy_t policy = {
1865 .encrypt = opts->encrypt_enabled,
1866 .no_encrypt = opts->no_encrypt,
1867 .no_auth = opts->no_auth,
1868 .has_password = opts->password[0] != '\0',
1869 .has_identity = opts->encrypt_key[0] != '\0' || opts->num_identity_keys > 0,
1870 .has_verification_key = opts->server_key[0] != '\0' || opts->client_keys[0] != '\0',
1871 };
1872 uint8_t mode;
1873 asciichat_error_t result = crypto_policy_resolve(&policy, &mode);
1874 if (result != ASCIICHAT_OK)
1875 return result;
1876
1877 // Validate identity paths without changing the encryption policy.
1878 if (opts->encrypt_key[0] != '\0') {
1879 // A key may be a local file, a remote reference, or inline key material.
1880 // Only validate filesystem paths when the value is actually path-like;
1881 // inline SSH/base64/password keys must not be rejected by stat().
1882 bool key_is_inline = !path_looks_like_path(opts->encrypt_key);
1883 if (!is_remote_key_path(opts->encrypt_key) && !key_is_inline) {
1884 struct stat st;
1885 if (stat(opts->encrypt_key, &st) != 0) {
1886 log_error("Key file not found: %s", opts->encrypt_key);
1887
1888 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key file not found: %s", opts->encrypt_key);
1889 }
1890 if ((st.st_mode & S_IFMT) != S_IFREG) {
1891 log_error("Key path is not a regular file: %s", opts->encrypt_key);
1892
1893 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key path is not a regular file: %s", opts->encrypt_key);
1894 }
1895 }
1896 // Identity keys select authentication independently of payload encryption.
1897 }
1898 return ASCIICHAT_OK;
1899}
1900
1901static asciichat_error_t validate_color_filter(options_t *opts) {
1902 // Color filter validation and auto-enable
1903 if (opts->color_filter != COLOR_FILTER_NONE) {
1904 // Color filter requires color to be enabled
1905 if (opts->color == COLOR_SETTING_FALSE) {
1906 log_error("--color-filter cannot be used with --color=false");
1907
1908 return option_error_invalid();
1909 }
1910 // Auto-enable color when color filter is specified
1911 opts->color = COLOR_SETTING_TRUE;
1912 log_debug("Auto-enabled color because --color-filter was provided");
1913 }
1914 return ASCIICHAT_OK;
1915}
1916
1917static void record_explicit_screen_and_debug_options(options_t *opts, int mode_argc, char **mode_argv) {
1918 // Detect if splash or status_screen were explicitly set on command line
1919 for (int i = 0; i < mode_argc; i++) {
1920 if (mode_argv[i] &&
1921 (strcmp(mode_argv[i], "--splash-screen") == 0 || strncmp(mode_argv[i], "--splash-screen=", 16) == 0)) {
1922 // Parse the value if provided (--splash-screen=true/false)
1923 bool splash_value = true; // Default to true for plain --splash-screen
1924 if (strncmp(mode_argv[i], "--splash-screen=", 16) == 0) {
1925 const char *value = mode_argv[i] + 16;
1926 if (strcasecmp(value, "false") == 0 || strcasecmp(value, "no") == 0 || strcasecmp(value, "0") == 0 ||
1927 strcasecmp(value, "off") == 0) {
1928 splash_value = false;
1929 }
1930 }
1931 opts->splash_screen = splash_value;
1932 opts->splash_screen_explicitly_set = true;
1933 }
1934 if (mode_argv[i] &&
1935 (strcmp(mode_argv[i], "--status-screen") == 0 || strncmp(mode_argv[i], "--status-screen=", 16) == 0)) {
1936 // Parse the value if provided (--status-screen=true/false)
1937 bool status_value = true; // Default to true for plain --status-screen
1938 if (strncmp(mode_argv[i], "--status-screen=", 16) == 0) {
1939 const char *value = mode_argv[i] + 16;
1940 if (strcasecmp(value, "false") == 0 || strcasecmp(value, "no") == 0 || strcasecmp(value, "0") == 0 ||
1941 strcasecmp(value, "off") == 0) {
1942 status_value = false;
1943 }
1944 }
1945 opts->status_screen = status_value;
1946 opts->status_screen_explicitly_set = true;
1947 }
1948
1949#ifndef NDEBUG
1950 if (mode_argv[i] &&
1951 (strcmp(mode_argv[i], "--sync-state") == 0 || strncmp(mode_argv[i], "--sync-state=", 13) == 0 ||
1952 strcmp(mode_argv[i], "--errno-stacks") == 0 || strncmp(mode_argv[i], "--errno-stacks=", 15) == 0)) {
1953 bool errno_report = strncmp(mode_argv[i], "--errno-stacks", 14) == 0;
1954 double *report_time = errno_report ? &opts->debug_errno_stacks_time : &opts->debug_sync_state_time;
1955 if (errno_report)
1957 else
1958 opts->debug_sync_state_time_explicit = true;
1959 if (strcmp(mode_argv[i], "--sync-state") == 0 || strcmp(mode_argv[i], "--errno-stacks") == 0) {
1960 if (i + 1 < mode_argc) {
1961 char *endptr;
1962 double val = strtod(mode_argv[i + 1], &endptr);
1963 if (endptr != mode_argv[i + 1] && val > 0.0) {
1964 *report_time = val;
1965 i++;
1966 }
1967 }
1968 } else {
1969 const char *value_str = strchr(mode_argv[i], '=') + 1;
1970 if (value_str[0] != '\0') {
1971 char *endptr;
1972 double val = strtod(value_str, &endptr);
1973 if (endptr != value_str && val > 0.0) {
1974 *report_time = val;
1975 }
1976 }
1977 }
1978 }
1979 if (mode_argv[i] && (strcmp(mode_argv[i], "--backtrace") == 0 || strncmp(mode_argv[i], "--backtrace=", 12) == 0)) {
1980 opts->debug_backtrace_time_explicit = true;
1981 if (strcmp(mode_argv[i], "--backtrace") == 0) {
1982 if (i + 1 < mode_argc) {
1983 char *endptr;
1984 double val = strtod(mode_argv[i + 1], &endptr);
1985 if (endptr != mode_argv[i + 1] && val > 0.0) {
1986 opts->debug_backtrace_time = val;
1987 i++;
1988 }
1989 }
1990 } else {
1991 const char *value_str = mode_argv[i] + 12;
1992 if (value_str[0] != '\0') {
1993 char *endptr;
1994 double val = strtod(value_str, &endptr);
1995 if (endptr != value_str && val > 0.0) {
1996 opts->debug_backtrace_time = val;
1997 }
1998 }
1999 }
2000 }
2001 if (mode_argv[i] &&
2002 (strcmp(mode_argv[i], "--memory-report") == 0 || strncmp(mode_argv[i], "--memory-report=", 16) == 0)) {
2004 if (strcmp(mode_argv[i], "--memory-report") == 0) {
2005 if (i + 1 < mode_argc) {
2006 char *endptr;
2007 double val = strtod(mode_argv[i + 1], &endptr);
2008 if (endptr != mode_argv[i + 1] && val > 0.0) {
2009 opts->debug_memory_report_interval = val;
2010 i++;
2011 }
2012 }
2013 } else {
2014 const char *value_str = mode_argv[i] + 16;
2015 if (value_str[0] != '\0') {
2016 char *endptr;
2017 double val = strtod(value_str, &endptr);
2018 if (endptr != value_str && val > 0.0) {
2019 opts->debug_memory_report_interval = val;
2020 }
2021 }
2022 }
2023 }
2024#endif
2025 }
2026}
2027
2028static void apply_screen_defaults(options_t *opts, int mode_argc, char **mode_argv) {
2029 // Auto-disable splash when grep is used (since it's one-time startup screen)
2030 // UNLESS it was explicitly set by the user
2031 // Status screen is now compatible with --grep since we support auto-loading patterns
2032 bool grep_was_provided = false;
2033 for (int i = 0; i < mode_argc; i++) {
2034 if (mode_argv[i] && (strcmp(mode_argv[i], "--grep") == 0 || strncmp(mode_argv[i], "--grep=", 7) == 0)) {
2035 grep_was_provided = true;
2036 break;
2037 }
2038 }
2039
2040 if (grep_was_provided) {
2041 if (!opts->splash_screen_explicitly_set) {
2042 opts->splash_screen = false;
2043 log_debug("Auto-disabled splash because --grep was provided");
2044 }
2045 }
2046
2047 // Auto-disable splash and status screens when terminal is non-interactive or CLAUDECODE is set
2048 // This ensures clean output when running under LLM automation or non-interactive shells
2049 bool is_terminal_interactive = terminal_is_interactive();
2050 bool is_claudecode_set = platform_getenv("CLAUDECODE") != NULL;
2051 bool should_auto_disable_screens = !is_terminal_interactive || is_claudecode_set;
2052
2053 if (should_auto_disable_screens) {
2054 if (!opts->splash_screen_explicitly_set) {
2055 opts->splash_screen = false;
2056 log_debug("Auto-disabled splash (non-interactive terminal or CLAUDECODE set)");
2057 }
2058 if (!opts->status_screen_explicitly_set) {
2059 opts->status_screen = false;
2060 log_debug("Auto-disabled status screen (non-interactive terminal or CLAUDECODE set)");
2061 }
2062 }
2063}
2064
2065static asciichat_error_t validate_option_strings(const options_t *opts) {
2066// Validate all string options contain valid UTF-8
2067// This prevents crashes and corruption from invalid UTF-8 in any option
2068// DISABLED IN RELEASE BUILDS: utf8_is_valid() has performance issues with musl
2069#ifndef NDEBUG
2070 const char *string_fields[][2] = {{"address", opts->address},
2071 {"address6", opts->address6},
2072 {"encrypt_key", opts->encrypt_key},
2073 {"encrypt_keyfile", opts->encrypt_keyfile},
2074 {"server_key", opts->server_key},
2075 {"client_keys", opts->client_keys},
2076 {"discovery_server", opts->discovery_server},
2077 {"discovery_service_key", opts->discovery_service_key},
2078 {"discovery_database_path", opts->discovery_database_path},
2079 {"log_file", opts->log_file},
2080 {"media_file", opts->media_file},
2081 {"palette_custom", opts->palette_custom},
2082 {"stun_servers", opts->stun_servers},
2083 {"turn_servers", opts->turn_servers},
2084 {"turn_username", opts->turn_username},
2085 {"turn_credential", opts->turn_credential},
2086 {"turn_secret", opts->turn_secret},
2087 {"session_string", opts->session_string},
2088 {NULL, NULL}};
2089
2090 for (int i = 0; string_fields[i][0] != NULL; i++) {
2091 const char *field_name = string_fields[i][0];
2092 const char *field_value = string_fields[i][1];
2093
2094 // Skip empty strings
2095 if (!field_value || field_value[0] == '\0') {
2096 continue;
2097 }
2098
2099 // Validate UTF-8
2100 if (!utf8_is_valid(field_value)) {
2101 log_error("Option --%s contains invalid UTF-8 sequences", field_name);
2102 log_error(" Value: %s", field_value);
2103
2104 return option_error_invalid();
2105 }
2106 }
2107#else
2108 (void)(*opts);
2109#endif
2110 return ASCIICHAT_OK;
2111}
2112
2113static asciichat_error_t validate_parsed_options(options_t *opts, options_config_t *config, int remaining_argc,
2114 char **remaining_argv) {
2115 // Validate options
2116 log_debug("★ VALIDATE_OPTIONS_AND_REPORT: About to call");
2117 asciichat_error_t result = validate_options_and_report(config, opts);
2118 log_debug("★ VALIDATE_OPTIONS_AND_REPORT: Returned with result=%s", asciichat_error_format(NULL, result));
2119 if (result != ASCIICHAT_OK) {
2120 return result;
2121 }
2122 // Check for unexpected remaining arguments
2123 if (remaining_argc > 0) {
2124 log_error("Unexpected arguments after options:");
2125 for (int i = 0; i < remaining_argc; i++) {
2126 log_error(" %s", remaining_argv[i]);
2127 }
2128
2129 return option_error_invalid();
2130 }
2131 return ASCIICHAT_OK;
2132}
2133
2134static asciichat_error_t finalize_mode_defaults(options_t *opts, asciichat_mode_t detected_mode) {
2135 // Mode-specific post-processing
2136 // Apply mode-specific defaults (port, websocket-port)
2137 log_debug("★ APPLY_MODE_SPECIFIC: About to call");
2139 log_debug("★ APPLY_MODE_SPECIFIC: Done");
2140 log_dev("Applied mode-specific defaults: port=%d, websocket_port=%d", opts->port, opts->websocket_port);
2141
2142 if (detected_mode == MODE_DISCOVERY_SERVICE) {
2143 // Set default paths if not specified
2144 log_debug("★ DISCOVERY_SERVICE: Handling discovery service defaults");
2145 if (opts->discovery_database_path[0] == '\0') {
2146 // Database: Try system-wide location first, fall back to user directories
2147 // Preference: /usr/local/var/ascii-chat/ > ~/.local/share/ascii-chat/ > ~/.config/ascii-chat/
2148 log_debug("★ DISCOVERY_SERVICE: About to call get_discovery_database_dir()");
2149 char *db_dir = get_discovery_database_dir();
2150 log_debug("★ DISCOVERY_SERVICE: get_discovery_database_dir() returned %p", (void *)db_dir);
2151 if (!db_dir) {
2152
2153 return SET_ERRNO(ERROR_CONFIG, "Failed to get database directory (tried system and user locations)");
2154 }
2155 safe_snprintf(opts->discovery_database_path, sizeof(opts->discovery_database_path), "%sdiscovery.db", db_dir);
2156 SAFE_FREE(db_dir);
2157 }
2158 }
2159 return ASCIICHAT_OK;
2160}
2161
2162static asciichat_error_t collect_server_identity_keys(options_t *opts, asciichat_mode_t detected_mode, int argc,
2163 char **argv) {
2164 // Collect multiple --key flags for multi-key support (server/ACDS only)
2165 // This enables servers to load both SSH and GPG keys and select the right one
2166 // during handshake based on what the client expects
2167 if (detected_mode == MODE_SERVER || detected_mode == MODE_DISCOVERY_SERVICE) {
2168 log_debug("★ STAGE 7.1: About to collect identity keys");
2169 int num_keys = options_collect_identity_keys(opts, argc, argv);
2170 log_debug("★ STAGE 7.1: options_collect_identity_keys returned %d", num_keys);
2171 if (num_keys < 0) {
2172
2173 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to collect identity keys");
2174 }
2175 // num_keys == 0 is OK (no --key flags provided)
2176 }
2177 return ASCIICHAT_OK;
2178}
2179
2180static void finalize_dimensions(options_t *opts) {
2181 log_debug("★ STAGE 7.2: About to update dimensions");
2182
2183 // After parsing command line options, update dimensions
2184 // First set any auto dimensions to terminal size, then apply full height logic
2185
2186 // Clear auto flags if dimensions were explicitly parsed from options
2187 // This must happen before update_dimensions_to_terminal_size to prevent auto-detected
2188 // values from overwriting user-specified dimensions
2189 // Note: width=0 and height=0 are special values meaning "auto-detect", so don't clear auto flags for those
2190 if (opts->width != OPT_WIDTH_DEFAULT && opts->width != 0) {
2191 opts->auto_width = false;
2192 }
2193 if (opts->height != OPT_HEIGHT_DEFAULT && opts->height != 0) {
2194 opts->auto_height = false;
2195 }
2197 js_log_options("BEFORE_UPDATE_DIMS");
2198 js_log_options("CALLING_UPDATE_DIM");
2200 js_log_options("AFTER_UPDATE_DIM");
2201
2202 js_log_options("L11");
2203 js_log_options("L12");
2204 js_log_options("L13");
2205 js_log_options("L14");
2206 js_log_options("L15");
2207 js_log_options("CHECKPOINT B");
2208
2209 // SKIP: Check WEBCAM_DISABLED environment variable to enable test pattern mode
2210 // TODO: Debug why logging here causes hang
2211 // const char *webcam_disabled = SAFE_GETENV("WEBCAM_DISABLED");
2212 // if (webcam_disabled &&
2213 // (strcmp(webcam_disabled, "1") == 0 || platform_strcasecmp(webcam_disabled, "true") == 0 ||
2214 // platform_strcasecmp(webcam_disabled, "yes") == 0 || platform_strcasecmp(webcam_disabled, "on") == 0)) {
2215 // opts.test_pattern = true;
2216 // }
2217
2218 js_log_options("CHECKPOINT C");
2219}
2220
2221static asciichat_error_t finalize_media_options(options_t *opts) {
2222 if (opts->test_pattern && opts->media_file[0] != '\0') {
2223 return SET_ERRNO(ERROR_USAGE, "--test-pattern and --file are mutually exclusive; choose one video source");
2224 }
2225 if (opts->test_pattern && opts->media_url[0] != '\0') {
2226 return SET_ERRNO(ERROR_USAGE, "--test-pattern and --url are mutually exclusive; choose one video source");
2227 }
2228 if (opts->test_pattern && (opts->waveform || opts->fft)) {
2229 return SET_ERRNO(ERROR_USAGE,
2230 "--test-pattern cannot be used with --waveform or --fft; choose video or audio visualization");
2231 }
2232
2233 // Apply --no-compress interaction with audio encoding
2234 js_log_options("Before --no-compress check");
2235 if (opts->no_compress) {
2236 js_log_options("In no_compress block");
2237 opts->encode_audio = false;
2238 js_log_options("Set encode_audio to false");
2239 log_debug("--no-compress set: disabling audio encoding");
2240 js_log_options("After log_debug in no_compress");
2241 }
2242 js_log_options("After --no-compress check");
2243
2244 // Set media_from_stdin flag if media_file is "-"
2245 js_log_options("Before media_from_stdin check");
2246 if (opts->media_file[0] != '\0' && strcmp(opts->media_file, "-") == 0) {
2247 js_log_options("In media_from_stdin block");
2248 opts->media_from_stdin = true;
2249 js_log_options("Set media_from_stdin to true");
2250 log_debug("Media file set to stdin");
2251 js_log_options("After log_debug in media_from_stdin");
2252 }
2253 js_log_options("After media_from_stdin check");
2254
2255 // Validate --seek option
2256 js_log_options("Before --seek validation");
2257 js_log_options("Checking media_seek_timestamp");
2258 if (opts->media_seek_timestamp > 0.0) {
2259 js_log_options("In seek validation block");
2260 // Can't seek stdin
2261 if (opts->media_from_stdin) {
2262 js_log_options("Seek error: media_from_stdin");
2263 log_error("--seek cannot be used with stdin (--file -)");
2264
2265 return ERROR_INVALID_PARAM;
2266 }
2267
2268 // Require --file or --url
2269 js_log_options("Checking media_file and media_url for seek");
2270 if (opts->media_file[0] == '\0' && opts->media_url[0] == '\0') {
2271 js_log_options("Seek error: no media_file or media_url");
2272 log_error("--seek requires --file or --url");
2273
2274 return ERROR_INVALID_PARAM;
2275 }
2276 js_log_options("Seek validation passed");
2277 }
2278 js_log_options("After --seek validation");
2279
2280 // Validate --pause option
2281 js_log_options("Before --pause validation");
2282 js_log_options("Checking pause flag");
2283 if (opts->pause) {
2284 js_log_options("In pause validation block");
2285 // Require --file or --url (not webcam, not test pattern)
2286 if (opts->media_file[0] == '\0' && opts->media_url[0] == '\0') {
2287 js_log_options("Pause error: no media_file or media_url");
2288 log_error("--pause requires --file or --url");
2289
2290 return ERROR_INVALID_PARAM;
2291 }
2292 js_log_options("Pause validation passed");
2293 }
2294 js_log_options("After --pause validation");
2295
2296 js_log_options("Before media_url check");
2297 js_log_options("Checking media_url[0]");
2298 if (opts->media_url[0] != '\0') {
2299 js_log_options("Entering media_url block");
2300 // URL must be a valid HTTP(S) URL (YouTube URLs are HTTPS URLs)
2301 js_log_options("About to call url_is_valid");
2302 js_log_options("Before url_is_valid call");
2303 if (!url_is_valid(opts->media_url)) {
2304 js_log_options("url_is_valid returned false");
2305 log_error("--url must be a valid HTTP(S) URL: %s", opts->media_url);
2306
2307 return ERROR_INVALID_PARAM;
2308 }
2309 js_log_options("url_is_valid returned true");
2310 js_log_options("url_is_valid returned, before strstr check");
2311
2312 // Normalize bare URLs by prepending http:// if not present
2313 js_log_options("Before strstr check");
2314 if (!strstr(opts->media_url, "://")) {
2315 js_log_options("URL needs normalization");
2316 char normalized_url[OPTIONS_NORMALIZED_URL_BUFFER_SIZE];
2317 js_log_options("Before snprintf");
2318 int result = snprintf(normalized_url, sizeof(normalized_url), "http://%s", opts->media_url);
2319 js_log_options("After snprintf");
2320 if (result > 0 && result < (int)sizeof(normalized_url)) {
2321 js_log_options("snprintf successful, before SAFE_STRNCPY");
2322 SAFE_STRNCPY(opts->media_url, normalized_url, sizeof(opts->media_url));
2323 js_log_options("URL normalized");
2324 } else {
2325 js_log_options("snprintf failed");
2326 log_error("Failed to normalize URL (too long): %s", opts->media_url);
2327
2328 return ERROR_INVALID_PARAM;
2329 }
2330 }
2331 js_log_options("After normalization check");
2332 }
2333 js_log_options("After media_url block");
2334 return ASCIICHAT_OK;
2335}
2336
2337static asciichat_error_t publish_final_options(options_t *opts, asciichat_mode_t detected_mode, int mode_index) {
2338 opts->media_input = opts->media_file[0] != '\0' || opts->media_url[0] != '\0';
2340
2341 // Save the quiet flag before publishing (RCU will be cleaned up before memory report runs)
2342#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
2343 js_log_options("Before quiet_for_memory_report assignment");
2344 bool quiet_for_memory_report = opts->quiet;
2345 js_log_options("After quiet_for_memory_report assignment");
2346#endif
2347
2348 // Publish parsed options to RCU state (replaces options_state_populate_from_globals)
2349 // This makes the options visible to all threads via lock-free reads
2350 js_log_options("STAGE 8 RCU: Before options_state_set");
2351 js_log_options("Before log_info PERF");
2352 js_log_options("After log_info PERF");
2353 js_log_options("Calling options_state_set...");
2354 js_log_options("Before options_state_set call");
2355 opts->mode_arg_index = detected_mode == MODE_DISCOVERY ? -1 : mode_index;
2356 asciichat_error_t publish_result = options_state_set(opts);
2357 js_log_options("After options_state_set call");
2358 js_log_options("options_state_set returned");
2359 js_log_options("Before log_info options_state_set done");
2360 js_log_options("After log_info options_state_set done");
2361 js_log_options("Before publish_result check");
2362 if (publish_result != ASCIICHAT_OK) {
2363 js_log_options("publish_result not OK");
2364 log_error("Failed to publish parsed options to RCU state: %d", publish_result);
2365
2366 return publish_result;
2367 }
2368 js_log_options("publish_result is OK");
2369 js_log_options("options_state_set successful");
2370
2371 // Now update debug memory quiet mode with the saved quiet value
2372#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
2373 debug_memory_set_quiet_mode(quiet_for_memory_report);
2374#endif
2375 return ASCIICHAT_OK;
2376}
2377
2378static void apply_logging_color_scheme(const options_t *opts) {
2379 // Now that options are parsed, set and apply the selected color scheme to logging
2380 js_log_options("STAGE 8 ColorScheme: Checking color_scheme_name");
2381 js_log_options("Before color_scheme_name[0] check");
2382 if (opts->color_scheme_name[0] != '\0') {
2383 js_log_options("Color scheme name is set");
2384 js_log_options("Before colorscheme_set_active_scheme");
2385 js_log_options("Before colorscheme_set_active_scheme call");
2387 js_log_options("After colorscheme_set_active_scheme call");
2388 js_log_options("colorscheme_set_active_scheme returned");
2389 js_log_options("Before log_info colorscheme done");
2390 js_log_options("After log_info colorscheme done");
2391 js_log_options("Before scheme_result check");
2392 if (scheme_result == ASCIICHAT_OK) {
2393 js_log_options("scheme_result is OK");
2394 js_log_options("Before colorscheme_get_active_scheme");
2396 js_log_options("After colorscheme_get_active_scheme");
2397 js_log_options("Before scheme null check");
2398 if (scheme) {
2399 js_log_options("scheme is not null");
2400 js_log_options("Before log_set_color_scheme");
2401 log_set_color_scheme(scheme);
2402 js_log_options("After log_set_color_scheme");
2403 js_log_options("Color scheme applied");
2404 js_log_options("Before log_debug color scheme");
2405 log_debug("Color scheme applied: %s", opts->color_scheme_name);
2406 js_log_options("After log_debug color scheme");
2407 } else {
2408 js_log_options("scheme is null");
2409 js_log_options("No scheme returned from colorscheme_get_active_scheme");
2410 }
2411 } else {
2412 log_warn("Failed to apply color scheme: %s", opts->color_scheme_name);
2413 }
2414 }
2415}
2416
2417asciichat_error_t options_init(int argc, char **argv) {
2418 log_debug("starting with argc=%d, argv[0]=%s, argv[1]=%s", argc, (argv && argc > 0 && argv[0]) ? argv[0] : "(null)",
2419 (argv && argc > 1 && argv[1]) ? argv[1] : "N/A");
2420 asciichat_error_t result = validate_options_argv(argc, argv);
2421 if (result != ASCIICHAT_OK) {
2422 return result;
2423 }
2424 result = options_state_init();
2425 if (result != ASCIICHAT_OK) {
2426 return result;
2427 }
2428 log_debug("options_init: state initialized");
2429
2430 // Discover binary actions before mode parsing so help and early exits need no media setup.
2431 handle_help_option(argc, argv);
2432 binary_actions_t actions = {.parsed_color_setting = COLOR_SETTING_AUTO};
2433 result = scan_binary_actions(argc, argv, &actions);
2434 if (result != ASCIICHAT_OK) {
2435 return result;
2436 }
2437 log_debug("options_init: binary actions scanned");
2438 set_action_flag(actions.has_action);
2439
2440 asciichat_mode_t detected_mode = MODE_DISCOVERY;
2441 char detected_session_string[SESSION_STRING_BUFFER_SIZE] = {0};
2442 int mode_index = -1;
2443 result = options_detect_mode(argc, argv, &detected_mode, detected_session_string, &mode_index);
2444 if (result != ASCIICHAT_OK) {
2445 return result;
2446 }
2447 result = validate_binary_option_position(argc, argv, mode_index);
2448 if (result != ASCIICHAT_OK) {
2449 return result;
2450 }
2451 result = validate_early_color_mode(argc, argv);
2452 if (result != ASCIICHAT_OK) {
2453 return result;
2454 }
2455
2456 options_t opts = options_t_new();
2457 log_debug("options_init: options constructed");
2458 initialize_binary_options(&opts, detected_mode, &actions);
2459 log_debug("options_init: binary options initialized");
2460 bool handled = false;
2461 result = handle_immediate_actions(&opts, &actions, &handled);
2462 if (result != ASCIICHAT_OK || handled) {
2463 return result;
2464 }
2465 result = parse_binary_options(&opts, argc, argv);
2466 if (result != ASCIICHAT_OK) {
2467 return result;
2468 }
2469 if (actions.show_version || actions.create_config || actions.create_manpage) {
2470 result = handle_fallback_actions(&opts, &actions);
2471 if (result != ASCIICHAT_OK) {
2472 return result;
2473 }
2474 return ASCIICHAT_OK;
2475 }
2476
2477 // Mode parsing owns the temporary argv and schema until the shared cleanup path.
2478 mode_arguments_t mode_args = {.argc = argc, .argv = argv};
2479 options_config_t *config = NULL;
2480 result = build_mode_argv(argc, argv, mode_index, &mode_args);
2481 if (result != ASCIICHAT_OK) {
2482 goto cleanup;
2483 }
2484 initialize_mode_defaults(&opts, detected_mode, detected_session_string);
2485 build_options_schema();
2486 // Apply defaults, config, environment, then CLI values in increasing precedence.
2487 bool fps_set_by_config = false;
2488 result = load_options_config(&opts, detected_mode, &fps_set_by_config);
2489 if (result != ASCIICHAT_OK)
2490 goto cleanup;
2491 config = options_preset_unified(NULL, NULL);
2492 if (!config) {
2493 result = SET_ERRNO(ERROR_CONFIG, "Failed to create options configuration");
2494 goto cleanup;
2495 }
2496 int remaining_argc;
2497 char **remaining_argv;
2498 result = parse_mode_options(&opts, config, detected_mode, mode_args.argc, mode_args.argv, &remaining_argc,
2499 &remaining_argv);
2500 if (result != ASCIICHAT_OK) {
2501 goto cleanup;
2502 }
2503 record_explicit_fps(&opts, mode_args.argc, mode_args.argv, fps_set_by_config);
2504 result = validate_custom_palette(&opts);
2505 if (result != ASCIICHAT_OK) {
2506 goto cleanup;
2507 }
2508 result = validate_glyph_palette(&opts);
2509 if (result != ASCIICHAT_OK)
2510 goto cleanup;
2511 // Remaining validators consume a published snapshot.
2512 result = publish_options_for_validation(&opts);
2513 if (result != ASCIICHAT_OK) {
2514 goto cleanup;
2515 }
2516 result = validate_encryption_key(&opts);
2517 if (result != ASCIICHAT_OK) {
2518 goto cleanup;
2519 }
2520 result = validate_color_filter(&opts);
2521 if (result != ASCIICHAT_OK) {
2522 goto cleanup;
2523 }
2524 record_explicit_screen_and_debug_options(&opts, mode_args.argc, mode_args.argv);
2525 record_explicit_stabilize(&opts, mode_args.argc, mode_args.argv);
2526 apply_screen_defaults(&opts, mode_args.argc, mode_args.argv);
2527 result = validate_option_strings(&opts);
2528 if (result != ASCIICHAT_OK) {
2529 goto cleanup;
2530 }
2531 result = validate_parsed_options(&opts, config, remaining_argc, remaining_argv);
2532 if (result != ASCIICHAT_OK) {
2533 goto cleanup;
2534 }
2535 result = finalize_mode_defaults(&opts, detected_mode);
2536 if (result != ASCIICHAT_OK) {
2537 goto cleanup;
2538 }
2539 log_debug("★ OPTIONS_CONFIG_DESTROY: About to call");
2540 options_config_destroy(config);
2541 config = NULL;
2542 log_debug("★ OPTIONS_CONFIG_DESTROY: Done");
2543
2544 log_debug("★ STAGE 7: Starting post-processing");
2545 // Resolve identity, dimensions, and media before exposing the final options to actions.
2546 result = collect_server_identity_keys(&opts, detected_mode, argc, argv);
2547 if (result != ASCIICHAT_OK) {
2548 goto cleanup;
2549 }
2550 finalize_dimensions(&opts);
2551 result = finalize_media_options(&opts);
2552 if (result != ASCIICHAT_OK) {
2553 goto cleanup;
2554 }
2555 result = publish_final_options(&opts, detected_mode, mode_index);
2556 if (result != ASCIICHAT_OK) {
2557 goto cleanup;
2558 }
2559 apply_logging_color_scheme(&opts);
2560
2561 // Deferred actions must see the final published options and may print output.
2562 if (actions.has_action) {
2564 }
2566
2567cleanup:
2568 if (config) {
2569 options_config_destroy(config);
2570 }
2571 SAFE_FREE(mode_args.allocated);
2572 return result;
2573}
2574
2576 if (!opts->flip_x_explicit)
2577 opts->flip_x = !opts->media_input && !opts->test_pattern && !opts->waveform && !opts->fft;
2578}
option_action_fn options_action_callback(const char *name, bool immediate)
Definition actions.c:716
void action_check_update_immediate(void)
Execute update check immediately (for early binary-level execution).
Definition actions.c:323
action_args_t args
Definition actions.c:78
void action_create_manpage(const char *output_path)
Generate man page template from options builder.
Definition actions.c:488
void action_completions(const char *shell_name, const char *output_path)
Generate shell completions and output to stdout or file.
Definition actions.c:584
deferred_action_t action
Definition actions.c:77
void action_create_config(const char *output_path)
Create default configuration file and exit.
Definition actions.c:537
void actions_execute_deferred(void)
Execute the deferred action (if any).
Definition actions.c:682
Action option callbacks for ascii-chat.
void(* option_action_fn)(void)
Definition actions.h:222
const char * asciichat_error_format(const char *message, asciichat_error_t code)
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
#define HAS_ERRNO(context)
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.
asciichat_error_t options_config_set_defaults(const options_config_t *config, void *options_struct)
Set default values in options struct.
void options_config_destroy(options_config_t *config)
Free options config.
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
bool is_session_string(const char *str)
Check if a string is a valid session string.
audio_visualization_source_t source
Definition fft.c:24
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_GETENV(name)
Definition common.h:449
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
unsigned char uint8_t
Definition common.h:60
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).
asciichat_error_t config_load_and_apply(asciichat_mode_t detected_mode, const char *config_path, bool strict, options_t *opts)
Main function to load configuration from file and apply to global options.
asciichat_error_t config_create_default(const char *config_path)
Create default configuration file with all default values.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_CRYPTO_KEY
Definition error_codes.h:94
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_CONFIG
Definition error_codes.h:52
@ ERROR_USAGE
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_dev(...)
Log a DEV message (most verbose, development only).
Definition log/log.h:581
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
log_level_t
Logging levels enumeration.
Definition types.h:29
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log.c:746
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:665
void log_set_color_scheme(const color_scheme_t *scheme)
Set the color scheme for logging output.
Definition log.c:1969
#define OPT_STRIP_ANSI_DEFAULT
Default strip ANSI escape sequences flag.
Definition options.h:451
#define OPT_WEBRTC_SKIP_STUN_DEFAULT
Default WebRTC skip STUN flag (false = use STUN).
Definition options.h:678
#define OPT_ACDS_EXPOSE_IP_DEFAULT
Default ACDS expose IP flag (false = private by default).
Definition options.h:600
#define OPT_ENDPOINT_DISCOVERY_SERVICE
Default discovery service endpoint (what to connect to by default).
Definition options.h:560
#define OPT_COLOR_DEFAULT
Default color setting (COLOR_SETTING_AUTO = smart detection).
Definition options.h:454
#define OPT_NO_AUDIO_MIXER_DEFAULT
Default no audio mixer flag (false = enable mixer).
Definition options.h:518
#define OPT_FLIP_Y_DEFAULT
Default vertical flip state (false = no vertical flip).
Definition options.h:505
#define OPT_SHOW_CAPABILITIES_DEFAULT
Default show terminal capabilities flag.
Definition options.h:445
#define OPT_NO_ENCRYPT_DEFAULT
Default no encrypt flag (false = allow encryption).
Definition options.h:739
#define OPT_QUIET_DEFAULT
Default quiet mode flag (false = logging enabled).
Definition options.h:348
asciichat_mode_t
Mode type for options parsing.
Definition options.h:919
#define OPT_MICROPHONE_INDEX_DEFAULT
Default microphone device index (-1 means system default).
Definition options.h:711
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
#define OPT_TURN_SERVERS_DEFAULT
Default TURN server URLs (comma-separated).
Definition options.h:664
#define OPT_ENCODE_AUDIO_DEFAULT
Default audio encoding state (true = Opus encoding enabled).
Definition options.h:729
#define OPT_COLOR_MODE_DEFAULT
Default color mode (auto-detect terminal capabilities).
Definition options.h:433
#define OPT_PALETTE_TYPE_DEFAULT
Default palette type (standard ASCII art).
Definition options.h:467
#define OPT_RECONNECT_ATTEMPTS_DEFAULT
Default reconnect attempts (-1 means auto/infinite).
Definition options.h:582
#define OPT_SPEAKERS_VOLUME_DEFAULT
Default speakers volume (1.0 = normal volume).
Definition options.h:720
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).
Definition options.h:742
#define OPT_TEST_PATTERN_DEFAULT
Default test pattern mode (false = use actual webcam).
Definition options.h:515
#define OPT_OTEL_ENDPOINT_DEFAULT
Debug and Dev builds export to the local Spaniel Compose collector.
Definition options.h:354
#define OPT_WEBRTC_ICE_TIMEOUT_MS_DEFAULT
Default WebRTC ICE gathering timeout in milliseconds (10 seconds).
Definition options.h:687
#define OPT_COMPRESSION_LEVEL_DEFAULT
Default compression level (1-9).
Definition options.h:585
#define OPT_ENCRYPT_ENABLED_DEFAULT
Default encrypt enabled flag (true = encryption required).
Definition options.h:736
#define OPT_REQUIRE_SERVER_IDENTITY_DEFAULT
Default require-server-identity setting for ACDS.
Definition options.h:606
#define OPT_ENABLE_KEEPAWAKE_DEFAULT
Default keep-awake (prevent system sleep) flag (false = normal system sleep).
Definition options.h:335
#define OPT_RENDER_MODE_DEFAULT
Default render mode (foreground characters only).
Definition options.h:464
#define OPT_ACDS_PORT_INT_DEFAULT
Default ACDS discovery service port (integer).
Definition options.h:594
#define OPT_AUDIO_ENABLED_DEFAULT
Default audio enabled flag (true = audio enabled by default).
Definition options.h:703
#define OPT_MATRIX_RAIN_DEFAULT
Default Matrix rain effect flag (false = disabled).
Definition options.h:496
#define OPT_FORCE_UTF8_DEFAULT
Default force UTF-8 support setting (auto-detect).
Definition options.h:448
#define OPT_STUN_SERVERS_DEFAULT
Default STUN server URLs (comma-separated).
Definition options.h:661
#define OPT_HELP_DEFAULT
Default help flag (false = don't show help).
Definition options.h:317
asciichat_error_t validate_glyph_palette(const options_t *opts)
Definition validation.c:25
#define OPT_ADDRESS6_DEFAULT
Default IPv6 server address.
Definition options.h:544
#define OPT_RENDER_FONT_SIZE_DEFAULT
Definition options.h:775
#define OPT_WEBRTC_RECONNECT_ATTEMPTS_DEFAULT
Default WebRTC reconnection attempts (3 = try initial + 3 retries).
Definition options.h:690
#define OPT_SPLASH_DEFAULT
Default splash screen flag (true = show splash, false = hide splash).
Definition options.h:323
#define OPT_RENDER_THEME_DEFAULT
Definition options.h:773
#define OPT_MEDIA_FROM_STDIN_DEFAULT
Default media from stdin flag (false = not reading from stdin).
Definition options.h:528
#define OPT_AUDIO_ANALYSIS_ENABLED_DEFAULT
Default audio analysis enabled flag.
Definition options.h:723
#define OPT_VERSION_DEFAULT
Default version flag (false = don't show version).
Definition options.h:320
#define OPT_ACDS_DEFAULT
Default ACDS registration flag (false = disabled).
Definition options.h:591
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
Definition options.h:310
#define OPT_NO_COMPRESS_DEFAULT
Default no compression flag (false = enable compression).
Definition options.h:588
asciichat_error_t options_init(int argc, char **argv)
Initialize options by parsing command-line arguments with unified mode detection.
Definition options.c:2417
#define OPT_AUTO_HEIGHT_DEFAULT
Default auto-detect height flag (true = auto-detect from terminal).
Definition options.h:430
#define ASCII_RENDERER_DEFAULT
Definition options.h:967
#define OPT_REQUIRE_CLIENT_VERIFY_DEFAULT
Default require client verify flag (false = not required).
Definition options.h:748
option_mode_bitmask_t
Option mode bitmask.
Definition options.h:936
#define OPT_AUDIO_NO_PLAYBACK_DEFAULT
Default audio playback flag (false = enable playback).
Definition options.h:726
#define OPT_PAUSE_DEFAULT
Default pause media flag (false = play immediately).
Definition options.h:534
#define OPT_REQUIRE_SERVER_VERIFY_DEFAULT
Default require server verify flag (false = not required).
Definition options.h:745
#define OPT_NO_MDNS_ADVERTISE_DEFAULT
Default no mDNS advertise flag (false = advertise enabled).
Definition options.h:615
#define OPT_AUDIO_SOURCE_DEFAULT
Visualize all available sources by default.
Definition options.h:706
#define OPT_STATUS_SCREEN_EXPLICITLY_SET_DEFAULT
Default status screen explicitly set flag (false = use default status screen setting).
Definition options.h:332
#define OPT_COLOR_FILTER_DEFAULT
Default color filter (none - no filtering).
Definition options.h:461
#define OPT_WEBCAM_INDEX_DEFAULT
Default webcam device index.
Definition options.h:512
void usage(FILE *desc, asciichat_mode_t mode)
Print usage information for a specific mode.
#define OPT_HEIGHT_DEFAULT
Default terminal height in characters.
Definition options.h:424
#define OPT_SNAPSHOT_MODE_DEFAULT
Default snapshot mode flag (false = continuous).
Definition options.h:479
#define OPT_MCP_PORT_DEFAULT
Definition options.h:576
#define OPT_COLOR_SCHEME_NAME_DEFAULT
Default color scheme name (pastel).
Definition options.h:370
#define OPT_MEDIA_LOOP_DEFAULT
Default loop media flag (false = play once).
Definition options.h:525
#define OPT_NO_CHECK_UPDATE_DEFAULT
Default no-check-update flag (false = check for updates enabled).
Definition options.h:341
#define OPT_STRETCH_DEFAULT
Default allow aspect ratio distortion flag.
Definition options.h:473
#define OPT_STATUS_SCREEN_DEFAULT
Default status screen flag (true = show status, false = hide status).
Definition options.h:329
#define OPT_LAN_DISCOVERY_DEFAULT
Default LAN discovery flag (false = discovery disabled).
Definition options.h:612
#define OPT_AUTO_WIDTH_DEFAULT
Default auto-detect width flag (true = auto-detect from terminal).
Definition options.h:427
#define OPT_JSON_DEFAULT
Default JSON logging flag (false = text output).
Definition options.h:367
#define OPT_ACDS_INSECURE_DEFAULT
Default ACDS insecure mode flag (false = verify server).
Definition options.h:603
bool has_action_flag(void)
Check if an action flag was detected.
Definition options.c:122
#define OPT_FPS_DEFAULT
Default FPS (frames per second).
Definition options.h:476
#define OPT_WEBRTC_DEFAULT
Default WebRTC mode flag (true = P2P WebRTC, false = direct TCP).
Definition options.h:669
#define OPT_REQUIRE_CLIENT_IDENTITY_DEFAULT
Default require-client-identity setting for ACDS.
Definition options.h:609
#define OPT_DISABLE_KEEPAWAKE_DEFAULT
Default disable keep-awake flag (false = allow system sleep prevention).
Definition options.h:338
#define OPT_ENABLE_UPNP_DEFAULT
Default enable UPnP flag (false = UPnP disabled).
Definition options.h:618
#define OPT_TURN_CREDENTIAL_DEFAULT
Default TURN credential (empty = use ACDS credentials).
Definition options.h:696
#define OPT_WEBRTC_SKIP_HOST_DEFAULT
Default WebRTC skip host candidates flag (false = use host candidates).
Definition options.h:684
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:612
#define OPT_VERBOSE_LEVEL_DEFAULT
Default verbose level (0 = not verbose).
Definition options.h:358
#define OPT_SNAPSHOT_DELAY_DEFAULT
Default snapshot delay in seconds.
Definition options.h:492
#define OPT_LOG_LEVEL_DEFAULT
Default log level (LOG_INFO).
Definition options.h:361
#define OPT_MICROPHONE_SENSITIVITY_DEFAULT
Default microphone sensitivity (1.0 = normal volume).
Definition options.h:717
#define OPT_FLIP_X_DEFAULT
Webcam input is mirrored unless an orientation is explicitly selected.
Definition options.h:502
#define OPT_PORT_INT_DEFAULT
Default TCP port for client/server communication (integer).
Definition options.h:567
int options_collect_identity_keys(options_t *opts, int argc, char *argv[])
Collect multiple –key flags into identity_keys array.
Definition validation.c:540
#define OPT_SPLASH_SCREEN_EXPLICITLY_SET_DEFAULT
Default splash screen explicitly set flag (false = use default splash setting).
Definition options.h:326
#define OPT_MAX_CLIENTS_DEFAULT
Default maximum concurrent clients (server only).
Definition options.h:579
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.
Definition manpage.c:688
#define OPT_PREFER_WEBRTC_DEFAULT
Default prefer WebRTC flag (false = try direct TCP first).
Definition options.h:672
options_t options_t_new(void)
Initialize options by parsing command-line arguments.
Definition options.c:547
#define OPT_WEBRTC_DISABLE_TURN_DEFAULT
Default WebRTC disable TURN flag (false = use TURN).
Definition options.h:681
#define OPT_NO_WEBRTC_DEFAULT
Default no WebRTC flag (false = WebRTC enabled).
Definition options.h:675
#define OPT_ADDRESS_DEFAULT
Default server address for client connections.
Definition options.h:541
#define OPT_AUDIO_CAPTURE_SOURCE_DEFAULT
Preserve automatic local audio capture selection by default.
Definition options.h:708
#define OPT_PALETTE_CUSTOM_SET_DEFAULT
Default custom palette set flag (false = not set).
Definition options.h:470
#define OPT_TURN_USERNAME_DEFAULT
Default TURN username (empty = use ACDS credentials).
Definition options.h:693
#define OPT_SPEAKERS_INDEX_DEFAULT
Default speakers device index (-1 means system default).
Definition options.h:714
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).
Definition options.h:570
#define OPT_MEDIA_SEEK_TIMESTAMP_DEFAULT
Default media seek timestamp (start from beginning).
Definition options.h:531
#define OPT_WIDTH_DEFAULT
Default terminal width in characters.
Definition options.h:413
@ MODE_CLIENT
Client mode - network client options.
Definition options.h:921
@ MODE_MCP_SERVER
Model Context Protocol rendering server.
Definition options.h:925
@ MODE_DISCOVERY_SERVICE
Discovery server mode - session management and WebRTC signaling.
Definition options.h:923
@ MODE_API_SERVER
Image-to-ASCII HTTP API server.
Definition options.h:926
@ MODE_INVALID
Invalid mode.
Definition options.h:927
@ MODE_SERVER
Server mode - network server options.
Definition options.h:920
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network).
Definition options.h:922
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
Definition options.h:924
@ COLOR_SETTING_TRUE
Force colors ON: always colorize regardless of TTY/piping/CLAUDECODE.
Definition options.h:247
@ COLOR_SETTING_FALSE
Force colors OFF: disable all colors and logging colors.
Definition options.h:249
@ COLOR_SETTING_AUTO
Smart detection (default): colors if TTY and not CLAUDECODE and not piping.
Definition options.h:245
@ PALETTE_CUSTOM
User-defined via –palette-chars.
Definition palette.h:104
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
const char * platform_getenv(const char *name)
Get an environment variable value.
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
bool utf8_is_ascii_only(const char *str)
Check if a string contains only ASCII characters.
Definition utf8.c:137
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:128
char * get_log_dir(void)
Get log directory path appropriate for current build type.
Definition path.c:291
bool url_is_valid(const char *url)
URL validation using shared parsing and ICU hostname checks.
Definition url.c:159
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:712
char * get_discovery_database_dir(void)
Get discovery service database directory with Homebrew-aware system-wide fallback.
Definition path.c:374
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:281
@ PATH_ROLE_LOG_FILE
Definition path.h:320
Capture stdout/stderr and redirect to logging system.
📝 Logging API with multiple log levels and terminal output control
Man page template generation from options builder.
void apply_mode_specific_defaults(options_t *opts)
Apply mode-specific defaults to an options struct after mode detection.
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.
TOML configuration file support for ascii-chat.
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.
#define OPTIONS_RESULT_BUFFER_SIZE
Definition options.c:51
void options_resolve_flip_x(options_t *opts)
Definition options.c:2575
#define OPTIONS_NORMALIZED_URL_BUFFER_SIZE
Definition options.c:54
#define OPTIONS_LONG_FORM_BUFFER_SIZE
Definition options.c:52
options_t options_t_new_preserve_binary(const options_t *source)
Create a new options_t struct with all defaults set.
Definition options.c:765
#define OPTIONS_OPT_BUFFER_SIZE
Definition options.c:49
#define OPTIONS_LONG_FORM_EQ_BUFFER_SIZE
Definition options.c:53
#define OPTIONS_LOG_FILENAME_BUFFER_SIZE
Definition options.c:50
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
bool parse_color_setting(const char *arg, void *dest, char **error_msg)
Parse color setting option (–color flag).
Definition parsers.c:110
bool parse_log_level(const char *arg, void *dest, char **error_msg)
Parse log level option.
Definition parsers.c:399
bool parse_color_mode(const char *arg, void *dest, char **error_msg)
Parse terminal color level option.
Definition parsers.c:160
Custom option parsers for enum types.
📂 Path Manipulation Utilities
asciichat_error_t crypto_policy_resolve(const crypto_policy_t *policy, uint8_t *mode)
Definition policy.c:4
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:58
Preset option configurations for ascii-chat modes.
asciichat_error_t options_state_init(void)
Initialize RCU options system.
Definition rcu.c:380
asciichat_error_t options_state_set(const options_t *opts)
Set options from a parsed options struct.
Definition rcu.c:450
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:252
Schema metadata for config file options.
🔤 String Manipulation and Shell Escaping Utilities
#define SESSION_STRING_BUFFER_SIZE
const char * manpage_create_path
Definition options.c:936
const char * config_create_path
Definition options.c:935
bool check_update_flag_seen
Definition options.c:940
int parsed_color_setting
Definition options.c:938
bool create_manpage
Definition options.c:933
bool no_check_update_flag_seen
Definition options.c:941
bool color_setting_found
Definition options.c:939
Create a new options_t struct with all defaults set.
Definition options.c:466
int otel_realtime_max_per_second
Definition options.c:484
log_level_t log_level
Definition options.c:467
unsigned short int verbose_level
Definition options.c:468
char otel_ca_file[OPTIONS_BUFF_SIZE]
Definition options.c:481
char log_file[OPTIONS_BUFF_SIZE]
Definition options.c:470
char config_file[OPTIONS_BUFF_SIZE]
Definition options.c:473
bool log_format_console_only
Definition options.c:477
char otel_log_level[OPTIONS_BUFF_SIZE]
Definition options.c:479
char otel_enabled_telemetry[OPTIONS_BUFF_SIZE]
Definition options.c:482
asciichat_mode_t detected_mode
Definition options.c:474
double otel_realtime_sample_rate
Definition options.c:483
char otel_endpoint[OPTIONS_BUFF_SIZE]
Definition options.c:480
Color scheme definition.
Definition colorscheme.h:76
The useful part of crypto policy is in transport_secure, encrypt, no_encrypt, no_auth: together they ...
Definition policy.h:12
The useful part of binary actions is in show_version, create_config, create_manpage,...
Definition options.c:952
char ** allocated
Definition options.c:955
The complete registry entry for one command-line/configuration option.
Definition builder.h:249
option_mode_bitmask_t mode_bitmask
Which modes this option applies to.
Definition builder.h:286
size_t offset
offsetof(struct, field) - where to store value
Definition builder.h:256
const char * env_var_name
Environment variable fallback (or NULL).
Definition builder.h:268
const char * long_name
Long option name (e.g., "port").
Definition builder.h:251
char short_name
Short option char (e.g., 'p', or '\0' if none).
Definition builder.h:252
option_type_t type
Value type.
Definition builder.h:255
Immutable parsing tables produced from an options builder.
Definition builder.h:414
option_descriptor_t * descriptors
Array of option descriptors.
Definition builder.h:415
size_t num_descriptors
Number of descriptors.
Definition builder.h:416
char otel_ca_file[256]
Definition options.h:1159
bool snapshot_mode
Snapshot mode (one frame and exit).
Definition options.h:1149
int otel_realtime_max_per_second
Definition options.h:1162
bool splash_screen_explicitly_set
True if splash screen was explicitly set by user.
Definition options.h:1238
float speakers_volume
Speaker volume multiplier (0.0-1.0, default 1.0).
Definition options.h:1131
int discovery_port
discovery server port (default: 27225)
Definition options.h:1040
char media_url[256]
Network URL (HTTP/HTTPS/YouTube/RTSP) - takes priority over media_file.
Definition options.h:1080
double otel_realtime_sample_rate
Definition options.h:1161
bool flip_x_explicit
User selected an orientation for this process lifetime.
Definition options.h:1104
size_t num_identity_keys
Number of identity keys loaded (0 means single-key mode using encrypt_key).
Definition options.h:1191
bool webrtc
Enable WebRTC mode for discovery session (default: true, P2P WebRTC).
Definition options.h:1045
audio_capture_source_t audio_capture_source
Local capture/media selection policy.
Definition options.h:1122
bool encrypt_enabled
Enable encryption.
Definition options.h:1178
int otel_metrics_interval
Definition options.h:1163
terminal_color_mode_t color_mode
Color mode (auto/none/16/256/truecolor).
Definition options.h:1092
bool audio_spectral_gate
Definition options.h:1116
bool discovery_expose_ip
ACDS: explicitly allow public IP disclosure without verification (opt-in).
Definition options.h:1200
char turn_servers[256]
ACDS: Comma-separated list of TURN server URLs.
Definition options.h:1221
int force_utf8
UTF-8 support setting (auto/true/false).
Definition options.h:1097
int webrtc_ice_timeout_ms
–webrtc-ice-timeout: ICE gathering timeout in milliseconds (default: 10000)
Definition options.h:1213
bool debug_backtrace_time_explicit
True if –backtrace was explicitly provided.
Definition options.h:1251
bool discovery
Enable discovery session registration (default: false).
Definition options.h:1032
double debug_memory_report_interval
Interval in seconds for periodic memory reports.
Definition options.h:1252
bool webrtc_relay_only
–webrtc-relay-only: Require TURN relay candidates
Definition options.h:1210
bool disable_keepawake
Explicitly disable system sleep prevention (allow OS to sleep).
Definition options.h:1173
int otel_queue_capacity
Definition options.h:1165
bool test_audio
Replace microphone capture with a repeating audible test signal.
Definition options.h:1112
int microphone_channel
First zero-based input channel; -1 downmixes all inputs.
Definition options.h:1127
bool debug_sync_state_time_explicit
True if –sync-state was explicitly provided.
Definition options.h:1249
float microphone_sensitivity
Microphone volume multiplier (0.0-1.0, default 1.0).
Definition options.h:1130
int microphone_channels
Selected microphone channels: 1 (speech) or 2 (stereo).
Definition options.h:1126
int compression_level
zstd compression level (1-9)
Definition options.h:1058
bool fps_explicitly_set
True if FPS was explicitly configured by the user.
Definition options.h:1100
bool fft
Replace the video image with a live audio frequency spectrogram.
Definition options.h:1124
bool enable_keepawake
Explicitly enable system sleep prevention.
Definition options.h:1172
double debug_sync_state_time
Time parameter for –sync-state.
Definition options.h:1248
char otel_endpoint[256]
Definition options.h:1158
bool show_capabilities
Show terminal capabilities and exit.
Definition options.h:1096
bool no_mdns_advertise
Disable mDNS service advertisement (server only).
Definition options.h:1053
char password[256]
Password string.
Definition options.h:1180
int microphone_index
Microphone device index (-1 = default).
Definition options.h:1128
char config_file[256]
Config file path (–config).
Definition options.h:991
asciichat_mode_t detected_mode
Mode detected from command-line arguments.
Definition options.h:983
bool strip_ansi
Strip ANSI escape sequences.
Definition options.h:1154
bool audio_analysis_enabled
Enable audio analysis (debug).
Definition options.h:1132
int websocket_port
WebSocket server port (server/discovery-service only).
Definition options.h:1010
bool pause
Start playback paused (toggle with spacebar).
Definition options.h:1084
audio_source_t audio_source
Visualization source (all/call/mic/media).
Definition options.h:1121
bool json
Enable JSON logging (–json flag).
Definition options.h:1169
char stun_servers[256]
ACDS: Comma-separated list of STUN server URLs.
Definition options.h:1220
int height
Terminal height in characters (int for OPTION_TYPE_INT compat).
Definition options.h:997
bool enable_upnp
Opt-in TCP/WebSocket and WebRTC UDP mapping via –port-forwarding.
Definition options.h:1219
bool debug_errno_stacks_time_explicit
True if –errno-stacks was provided.
Definition options.h:1247
bool webrtc_skip_stun
–webrtc-skip-stun: Skip Stage 2 (STUN), go to TURN
Definition options.h:1209
bool debug_memory_report_interval_explicit
True if –memory-report was explicitly provided.
Definition options.h:1253
bool log_format_console_only
Apply log format only to console output.
Definition options.h:1171
bool lan_discovery
Enable LAN service discovery via mDNS (client only).
Definition options.h:1052
double debug_errno_stacks_time
Delay before the error report.
Definition options.h:1246
char session_string[(38+1)]
Definition options.h:1026
bool audio_enabled
Enable audio streaming.
Definition options.h:1120
bool no_encrypt
Disable encryption (opt-out).
Definition options.h:1182
bool no_webrtc
–no-webrtc: Disable WebRTC, use Direct TCP only
Definition options.h:1207
bool matrix_rain
Matrix digital rain effect (false = disabled).
Definition options.h:1152
bool webrtc_disable_turn
–webrtc-disable-turn: Disable Stage 3 (TURN), use STUN only
Definition options.h:1211
bool audio_no_playback
Disable speaker playback (debug).
Definition options.h:1133
bool auto_height
Auto-detect height from terminal.
Definition options.h:999
log_level_t log_level
Log level threshold.
Definition options.h:1167
char turn_credential[256]
ACDS: Credential/password for TURN authentication.
Definition options.h:1223
bool require_server_identity
ACDS: require servers to provide signed Ed25519 identity.
Definition options.h:1196
int audio_fft_size
Definition options.h:1119
char palette_custom[256]
Custom palette characters.
Definition options.h:1230
bool status_screen
Show status screen (default: true = show, use –no-status-screen to hide).
Definition options.h:1239
char otel_log_level[256]
Definition options.h:1157
int max_clients
Maximum concurrent clients (server only).
Definition options.h:1025
bool palette_custom_set
True if custom palette was set.
Definition options.h:1231
bool no_auth
Disable authentication layer (–no-auth).
Definition options.h:1183
terminal_render_mode_t render_mode
Render mode (foreground/background/half-block).
Definition options.h:1095
bool splash_screen
Show splash screen (default: true = show, use –no-splash-screen to hide).
Definition options.h:1237
char discovery_database_path[256]
~/.ascii-chat/discovery.db)
Definition options.h:1046
char server_key[256]
Expected server public key (client).
Definition options.h:1184
bool media_input
Active input is a file/URL rather than the webcam.
Definition options.h:1105
bool no_audio_mixer
Disable audio mixer (debug).
Definition options.h:1073
bool audio_spectral_mixer
Explicit post-mix processing; disabled by default.
Definition options.h:1115
char encrypt_keyfile[256]
Alternative key file path.
Definition options.h:1181
bool webrtc_skip_host
–webrtc-skip-host: Skip host candidates, force STUN/TURN only
Definition options.h:1212
bool version
Show version information.
Definition options.h:990
int webcam_index
Webcam device index (0 = first).
Definition options.h:1070
bool status_screen_explicitly_set
True if status_screen was explicitly set by user.
Definition options.h:1240
bool stabilize
Reduce frame-to-frame character flicker.
Definition options.h:1138
bool no_compress
Disable compression entirely.
Definition options.h:1059
int webrtc_reconnect_attempts
–webrtc-reconnect-attempts: Number of retry attempts (default: 3)
Definition options.h:1214
char address6[256]
IPv6 bind address (server only).
Definition options.h:1008
int speakers_index
Speakers device index (-1 = default).
Definition options.h:1129
color_filter_t color_filter
Monochromatic color filter (none/black/white/green/etc).
Definition options.h:1093
bool help
Show help message.
Definition options.h:989
bool prefer_webrtc
–prefer-webrtc: Try WebRTC before Direct TCP
Definition options.h:1206
bool quiet
Quiet mode (suppress logs).
Definition options.h:1147
double snapshot_delay
Snapshot delay in seconds.
Definition options.h:1150
bool stabilize_explicit
Runtime preference was explicitly changed.
Definition options.h:1139
bool audio_spectral
Definition options.h:1114
int fps
Target framerate (0 = auto, 1-144).
Definition options.h:1099
bool stretch
Allow aspect ratio distortion.
Definition options.h:1141
ascii_renderer_t renderer
Character selection algorithm.
Definition options.h:1140
bool waveform
Replace the video image with a live audio waveform.
Definition options.h:1123
int reconnect_attempts
Number of reconnection attempts (-1=infinite, 0=none).
Definition options.h:1065
bool test_pattern
Use test pattern instead of webcam.
Definition options.h:1071
char fft_scale[32]
Amplitude scale for the FFT visualization.
Definition options.h:1125
int port
Server port number.
Definition options.h:1009
bool media_from_stdin
Reading from stdin (detected from "--file -").
Definition options.h:1082
bool require_client_verify
Client: only connect to servers whose identity was verified by ACDS.
Definition options.h:1199
char discovery_server[256]
discovery server address (default: 127.0.0.1)
Definition options.h:1033
double debug_backtrace_time
Time parameter for –backtrace option.
Definition options.h:1250
int width
Terminal width in characters (int for OPTION_TYPE_INT compat).
Definition options.h:996
int mode_arg_index
Explicit mode token in original argv, or -1 for implicit discovery/session strings.
Definition options.h:984
bool require_server_verify
Server: only accept clients who verified via ACDS.
Definition options.h:1198
palette_type_t palette_type
Selected palette type.
Definition options.h:1229
bool flip_y
Flip video vertically (Y-axis).
Definition options.h:1106
char color_scheme_name[64]
Color scheme name (e.g., "pastel", "nord").
Definition options.h:1094
char turn_secret[256]
ACDS: Shared secret for dynamic TURN credential generation (HMAC-SHA1).
Definition options.h:1224
bool media_loop
Loop media file playback.
Definition options.h:1081
char discovery_service_key[256]
HTTPS URL).
Definition options.h:1043
bool require_client_identity
ACDS: require clients to provide signed Ed25519 identity.
Definition options.h:1197
bool discovery_insecure
ACDS: skip server key verification (MITM-vulnerable, requires explicit opt-in).
Definition options.h:1201
bool no_check_update
Disable automatic update checks (default: false = checks enabled).
Definition options.h:1241
bool encode_audio
Enable Opus audio encoding.
Definition options.h:1060
int render_theme
0=dark 1=light 2=auto
Definition options.h:1262
char encrypt_key[256]
SSH/GPG key file path (first –key flag, kept for compatibility).
Definition options.h:1179
char media_file[256]
Media file path or "-" for stdin.
Definition options.h:1079
char otel_enabled_telemetry[256]
Definition options.h:1160
int color
Color setting (COLOR_SETTING_AUTO/TRUE/FALSE).
Definition options.h:1091
char turn_username[256]
ACDS: Username for TURN authentication.
Definition options.h:1222
char log_file[256]
Log file path.
Definition options.h:1166
unsigned short int verbose_level
Verbosity level (stackable -V).
Definition options.h:1148
char address[256]
Server address (client) or bind address (server).
Definition options.h:1007
bool flip_x
Effective horizontal orientation.
Definition options.h:1103
char grep_pattern[256]
PCRE2 regex for log filtering.
Definition options.h:1168
bool auto_width
Auto-detect width from terminal.
Definition options.h:998
char client_keys[256]
Allowed client keys (server).
Definition options.h:1185
double render_font_size
Font size in points (0 selects an automatic size).
Definition options.h:1264
double media_seek_timestamp
Seek to timestamp in seconds before playback.
Definition options.h:1083
⏱️ High-precision timing utilities using sokol_time.h and uthash
🌐 Production-Grade URL Validation and Parsing
🔤 UTF-8 Encoding and Decoding Utilities