ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
options/common.c
Go to the documentation of this file.
1
10#include <ascii-chat/options/common.h>
11
12#include <ascii-chat/asciichat_errno.h>
13#include <ascii-chat/common.h>
14#include <ascii-chat/log/logging.h>
15#include <ascii-chat/options/levenshtein.h>
16#include <ascii-chat/options/validation.h>
17#include <ascii-chat/options/builder.h>
18#include <ascii-chat/platform/terminal.h>
19#include <ascii-chat/platform/stat.h>
20#include <ascii-chat/util/parsing.h>
21#include <ascii-chat/util/password.h>
22#include <ascii-chat/util/path.h>
23#include <ascii-chat/util/string.h>
24
25#include <limits.h>
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29#include <sys/stat.h>
30
31// ============================================================================
32// Helper Functions
33// ============================================================================
34
35// Find the most similar option name to an unknown option
36// Returns the best matching option name, or NULL if no good match found
37// Note: Uses LEVENSHTEIN_SUGGESTION_THRESHOLD from levenshtein.h
38const char *find_similar_option(const char *unknown_opt, const struct option *options) {
39 if (!unknown_opt || !options) {
40 return NULL;
41 }
42
43 const char *best_match = NULL;
44 size_t best_distance = SIZE_MAX;
45
46 for (int i = 0; options[i].name != NULL; i++) {
47 size_t dist = levenshtein(unknown_opt, options[i].name);
48 if (dist < best_distance) {
49 best_distance = dist;
50 best_match = options[i].name;
51 }
52 }
53
54 // Only suggest if the distance is within our threshold
55 if (best_distance <= LEVENSHTEIN_SUGGESTION_THRESHOLD) {
56 return best_match;
57 }
58
59 return NULL;
60}
61
62// Helper to format all available modes for an option (used in error messages)
63// Returns comma-separated list like "server, client, mirror" or "global options"
64const char *format_available_modes(option_mode_bitmask_t mode_bitmask) {
65 static char buffer[256];
66 buffer[0] = '\0';
67
68 bool first = true;
69
70 // Check if it's a global/binary option
71 if (mode_bitmask & OPTION_MODE_BINARY) {
72 SAFE_SNPRINTF(buffer, sizeof(buffer), "global options");
73 return buffer;
74 }
75
76 // Build comma-separated list of modes (ordered: default, client, server, mirror, discovery-service)
77 if (mode_bitmask & OPTION_MODE_DISCOVERY) {
78 safe_snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), "%sdefault", first ? "" : ", ");
79 first = false;
80 }
81 if (mode_bitmask & OPTION_MODE_CLIENT) {
82 safe_snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), "%sclient", first ? "" : ", ");
83 first = false;
84 }
85 if (mode_bitmask & OPTION_MODE_SERVER) {
86 safe_snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), "%sserver", first ? "" : ", ");
87 first = false;
88 }
89 if (mode_bitmask & OPTION_MODE_MIRROR) {
90 safe_snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), "%smirror", first ? "" : ", ");
91 first = false;
92 }
93 if (mode_bitmask & OPTION_MODE_DISCOVERY_SVC) {
94 safe_snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), "%sdiscovery-service", first ? "" : ", ");
95 first = false;
96 }
97
98 // Fallback if no modes matched
99 if (buffer[0] == '\0') {
100 SAFE_SNPRINTF(buffer, sizeof(buffer), "unknown mode");
101 }
102
103 return buffer;
104}
105
106// Find similar option across all modes and suggest it with mode information
107// Returns formatted suggestion string with mode info, or NULL if no good match
108const char *find_similar_option_with_mode(const char *unknown_opt, const options_config_t *config,
109 option_mode_bitmask_t current_mode_bitmask) {
110 if (!unknown_opt || !config) {
111 return NULL;
112 }
113
114 // Extract the option name without dashes
115 const char *opt_name = unknown_opt;
116 if (strncmp(opt_name, "--", 2) == 0) {
117 opt_name += 2;
118 } else if (strncmp(opt_name, "-", 1) == 0) {
119 opt_name += 1;
120 } else {
121 return NULL; // Not an option format
122 }
123
124 const option_descriptor_t *best_match = NULL;
125 size_t best_distance = SIZE_MAX;
126
127 // Search through all descriptors
128 for (size_t i = 0; i < config->num_descriptors; i++) {
129 const option_descriptor_t *desc = &config->descriptors[i];
130 if (!desc->long_name)
131 continue;
132
133 // Calculate distance to the long name
134 size_t dist = levenshtein(opt_name, desc->long_name);
135 if (dist < best_distance) {
136 best_distance = dist;
137 best_match = desc;
138 }
139 }
140
141 // Only suggest if the distance is within our threshold
142 if (best_distance > LEVENSHTEIN_SUGGESTION_THRESHOLD || !best_match) {
143 return NULL;
144 }
145
146 // Check if the option is not available in current mode
147 bool available_in_current_mode = (best_match->mode_bitmask & current_mode_bitmask) != 0;
148
149 static char suggestion[256];
150 if (available_in_current_mode) {
151 // Option exists but user typed it wrong - just suggest the correct spelling
152 safe_snprintf(suggestion, sizeof(suggestion), "Did you mean '--%s'?", best_match->long_name);
153 } else {
154 // Option exists but in a different mode - show all available modes
155 const char *modes_str = format_available_modes(best_match->mode_bitmask);
156 safe_snprintf(suggestion, sizeof(suggestion), "Did you mean '--%s' (available in modes: %s)?",
157 best_match->long_name, modes_str);
158 }
159
160 return suggestion;
161}
162
163// Safely parse string to integer with validation
164int strtoint_safe(const char *str) {
165 if (!str || *str == '\0') {
166 return INT_MIN; // Error: NULL or empty string
167 }
168
169 int32_t result = 0;
170 // Use safe parsing utility with full int32 range validation
171 if (parse_int32(str, &result, INT_MIN, INT_MAX) != ASCIICHAT_OK) {
172 return INT_MIN; // Error: invalid input or out of range
173 }
174
175 return (int)result;
176}
177
178// Forward declaration for get_required_argument (defined later in file)
179char *get_required_argument(const char *opt_value, char *buffer, size_t buffer_size, const char *option_name,
180 asciichat_mode_t mode);
181
182// Validate and retrieve required argument for an option
183char *validate_required_argument(const char *optarg, char *argbuf, size_t argbuf_size, const char *option_name,
184 asciichat_mode_t mode) {
185 char *value = get_required_argument(optarg, argbuf, argbuf_size, option_name, mode);
186 if (!value) {
187 (void)option_error_invalid();
188 }
189 return value;
190}
191
192// Validate a positive integer value (internal option parsing helper)
193bool validate_positive_int_opt(const char *value_str, int *out_value, const char *param_name) {
194 if (!value_str || !out_value) {
195 return false;
196 }
197
198 int val = strtoint_safe(value_str);
199 if (val == INT_MIN || val <= 0) {
200 log_error("Invalid %s value '%s'. %s must be a positive integer.", param_name, value_str, param_name);
201 return false;
202 }
203
204 *out_value = val;
205 return true;
206}
207
208// Validate port number (1-65535) (internal option parsing helper)
209bool validate_port_opt(const char *value_str, uint16_t *out_port) {
210 if (!value_str || !out_port) {
211 return false;
212 }
213
214 // Use safe integer parsing with range validation
215 if (parse_port(value_str, out_port) != ASCIICHAT_OK) {
216 log_error("Invalid port value '%s'. Port must be a number between 1 and 65535.", value_str);
217 return false;
218 }
219
220 return true;
221}
222
223// Validate FPS value (1-144) (internal option parsing helper)
224bool validate_fps_opt(const char *value_str, int *out_fps) {
225 if (!value_str || !out_fps) {
226 return false;
227 }
228
229 int fps_val = strtoint_safe(value_str);
230 if (fps_val == INT_MIN || fps_val < 1 || fps_val > 144) {
231 log_error("Invalid FPS value '%s'. FPS must be between 1 and 144.", value_str);
232 return false;
233 }
234
235 *out_fps = fps_val;
236 return true;
237}
238
239// Validate webcam index using the common device index validator
240bool validate_webcam_index(const char *value_str, unsigned short int *out_index) {
241 if (!value_str || !out_index) {
242 return false;
243 }
244
245 char error_msg[BUFFER_SIZE_SMALL];
246 int parsed_index = validate_opt_device_index(value_str, error_msg, sizeof(error_msg));
247 if (parsed_index == INT_MIN) {
248 log_error("Invalid webcam index: %s", error_msg);
249 return false;
250 }
251 // Webcam index doesn't support -1 (default), must be >= 0
252 if (parsed_index < 0) {
253 log_error("Invalid webcam index '%s'. Webcam index must be a non-negative integer.", value_str);
254 return false;
255 }
256
257 *out_index = (unsigned short int)parsed_index;
258 return true;
259}
260
261// Detect default SSH key path for the current user
262asciichat_error_t detect_default_ssh_key(char *key_path, size_t path_size) {
263 // Use expand_path utility to resolve ~/.ssh/id_ed25519
264 char *full_path = expand_path("~/.ssh/id_ed25519");
265 if (!full_path) {
266 return SET_ERRNO(ERROR_CONFIG, "Could not expand SSH key path");
267 }
268
269 // Check if the Ed25519 private key file exists
270 struct stat st;
271 bool found = (stat(full_path, &st) == 0 && S_ISREG(st.st_mode));
272
273 if (found) {
274 SAFE_SNPRINTF(key_path, path_size, "%s", full_path);
275 log_debug("Found default SSH key: %s", full_path);
276 SAFE_FREE(full_path);
277 return ASCIICHAT_OK;
278 }
279
280 log_error("No Ed25519 SSH key found at %s", full_path);
281 SAFE_FREE(full_path);
282 return SET_ERRNO(
283 ERROR_CRYPTO_KEY,
284 "Only Ed25519 keys are supported (modern, secure, fast). Generate a new key with: ssh-keygen -t ed25519");
285}
286
287// ============================================================================
288// Argument Processing Helpers
289// ============================================================================
290
291// Helper function to strip equals sign from optarg if present
292char *strip_equals_prefix(const char *opt_value, char *buffer, size_t buffer_size) {
293 if (!opt_value)
294 return NULL;
295
296 SAFE_SNPRINTF(buffer, buffer_size, "%s", opt_value);
297 char *value_str = buffer;
298 if (value_str[0] == '=') {
299 value_str++; // Skip the equals sign
300 }
301
302 // Return NULL for empty strings (treat as missing argument)
303 if (strlen(value_str) == 0) {
304 return NULL;
305 }
306
307 return value_str;
308}
309
310// Helper function to handle required arguments with consistent error messages
311// Returns NULL on error (caller should check and return error code)
312char *get_required_argument(const char *opt_value, char *buffer, size_t buffer_size, const char *option_name,
313 asciichat_mode_t mode) {
314 // Check if opt_value is NULL or empty
315 if (!opt_value || strlen(opt_value) == 0) {
316 goto error;
317 }
318
319 // Check if getopt_long returned the option name itself as the argument
320 // This happens when a long option requiring an argument is at the end of argv
321 if (opt_value && option_name && strcmp(opt_value, option_name) == 0) {
322 goto error;
323 }
324
325 // Process the argument normally
326 char *value_str = strip_equals_prefix(opt_value, buffer, buffer_size);
327 if (!value_str) {
328 goto error;
329 }
330
331 return value_str;
332
333error:
334 (void)0;
335 const char *mode_name = (mode == MODE_SERVER ? "server" : (mode == MODE_MIRROR ? "mirror" : "client"));
336 log_error("%s: option '--%s' requires an argument", mode_name, option_name);
337 return NULL; // Signal error to caller
338}
339
340// Read password from stdin with prompt (returns allocated string or NULL on error)
341char *read_password_from_stdin(const char *prompt) {
342 char *password_buf = SAFE_MALLOC(PASSWORD_MAX_LEN, char *);
343 if (!password_buf) {
344 return NULL;
345 }
346
347 if (prompt_password_simple(prompt, password_buf, PASSWORD_MAX_LEN) != 0) {
348 SAFE_FREE(password_buf);
349 return NULL;
350 }
351
352 return password_buf; // Caller must free
353}
354
355// ============================================================================
356// Global Variable Definitions
357// ============================================================================
358// Removed: All opt_* global variables moved to RCU options_t struct
359// Access these via GET_OPTION(field_name) for thread-safe lock-free reads
360// Modify via options_set_*() functions for thread-safe updates
361// ============================================================================
362
363// These flags are used during parsing, not part of RCU options
364ASCIICHAT_API bool auto_width = true, auto_height = true;
365
366// Track if --port was explicitly set via command-line flag (for mutual exclusion validation)
368
369// Default weights; must add up to 1.0
370const float weight_red = 0.2989f;
371const float weight_green = 0.5866f;
372const float weight_blue = 0.1145f;
373
374// Color channel lookup tables (used by image.c for palette precomputation)
375// Size must match ASCII_LUMINANCE_LEVELS (256) from ascii.h
376unsigned short int RED[256];
377unsigned short int GREEN[256];
378unsigned short int BLUE[256];
379unsigned short int GRAY[256];
380
381// ============================================================================
382// Shared Option Parsers (Client + Mirror Common Options)
383// ============================================================================
384
385asciichat_error_t parse_color_mode_option(const char *value_str, options_t *opts) {
386 if (!value_str || !opts) {
387 return ERROR_INVALID_PARAM;
388 }
389
390 if (strcmp(value_str, "auto") == 0 || strcmp(value_str, "a") == 0) {
391 opts->color_mode = COLOR_MODE_AUTO;
392 } else if (strcmp(value_str, "none") == 0 || strcmp(value_str, "mono") == 0) {
393 opts->color_mode = COLOR_MODE_NONE;
394 } else if (strcmp(value_str, "16") == 0 || strcmp(value_str, "16color") == 0 || strcmp(value_str, "ansi") == 0) {
395 opts->color_mode = COLOR_MODE_16_COLOR;
396 } else if (strcmp(value_str, "256") == 0 || strcmp(value_str, "256color") == 0) {
397 opts->color_mode = COLOR_MODE_256_COLOR;
398 } else if (strcmp(value_str, "truecolor") == 0 || strcmp(value_str, "24bit") == 0 || strcmp(value_str, "tc") == 0 ||
399 strcmp(value_str, "rgb") == 0 || strcmp(value_str, "true") == 0) {
400 opts->color_mode = COLOR_MODE_TRUECOLOR;
401 } else {
402 log_error("Invalid color mode '%s'. Valid modes: auto, none, 16, 256, truecolor", value_str);
403 return ERROR_INVALID_PARAM;
404 }
405
406 return ASCIICHAT_OK;
407}
408
409asciichat_error_t parse_render_mode_option(const char *value_str, options_t *opts) {
410 if (!value_str || !opts) {
411 return ERROR_INVALID_PARAM;
412 }
413
414 if (strcmp(value_str, "foreground") == 0 || strcmp(value_str, "fg") == 0) {
415 opts->render_mode = RENDER_MODE_FOREGROUND;
416 } else if (strcmp(value_str, "background") == 0 || strcmp(value_str, "bg") == 0) {
417 opts->render_mode = RENDER_MODE_BACKGROUND;
418 } else if (strcmp(value_str, "half-block") == 0 || strcmp(value_str, "halfblock") == 0) {
419 opts->render_mode = RENDER_MODE_HALF_BLOCK;
420 } else {
421 log_error("Invalid render mode '%s'. Valid modes: foreground, background, half-block", value_str);
422 return ERROR_INVALID_PARAM;
423 }
424
425 return ASCIICHAT_OK;
426}
427
428asciichat_error_t parse_palette_option(const char *value_str, options_t *opts) {
429 if (!value_str || !opts) {
430 return ERROR_INVALID_PARAM;
431 }
432
433 if (strcmp(value_str, "standard") == 0) {
434 opts->palette_type = PALETTE_STANDARD;
435 } else if (strcmp(value_str, "blocks") == 0) {
436 opts->palette_type = PALETTE_BLOCKS;
437 } else if (strcmp(value_str, "digital") == 0) {
438 opts->palette_type = PALETTE_DIGITAL;
439 } else if (strcmp(value_str, "minimal") == 0) {
440 opts->palette_type = PALETTE_MINIMAL;
441 } else if (strcmp(value_str, "cool") == 0) {
442 opts->palette_type = PALETTE_COOL;
443 } else if (strcmp(value_str, "custom") == 0) {
444 opts->palette_type = PALETTE_CUSTOM;
445 } else {
446 log_error("Invalid palette '%s'. Valid palettes: standard, blocks, digital, minimal, cool, custom", value_str);
447 return ERROR_INVALID_PARAM;
448 }
449
450 return ASCIICHAT_OK;
451}
452
453asciichat_error_t parse_palette_chars_option(const char *value_str, options_t *opts) {
454 if (!value_str || !opts) {
455 return ERROR_INVALID_PARAM;
456 }
457
458 if (strlen(value_str) >= sizeof(opts->palette_custom)) {
459 log_error("Invalid palette-chars: too long (%zu chars, max %zu)", strlen(value_str),
460 sizeof(opts->palette_custom) - 1);
461 return ERROR_INVALID_PARAM;
462 }
463
464 SAFE_STRNCPY(opts->palette_custom, value_str, sizeof(opts->palette_custom));
465 opts->palette_custom[sizeof(opts->palette_custom) - 1] = '\0';
466 opts->palette_custom_set = true;
467 opts->palette_type = PALETTE_CUSTOM;
468
469 return ASCIICHAT_OK;
470}
471
472asciichat_error_t parse_width_option(const char *value_str, options_t *opts) {
473 if (!opts) {
474 return ERROR_INVALID_PARAM;
475 }
476
477 int width_val;
478 if (!validate_positive_int_opt(value_str, &width_val, "width")) {
479 return ERROR_INVALID_PARAM;
480 }
481
482 opts->width = width_val;
483 opts->auto_width = false;
484
485 return ASCIICHAT_OK;
486}
487
488asciichat_error_t parse_height_option(const char *value_str, options_t *opts) {
489 if (!opts) {
490 return ERROR_INVALID_PARAM;
491 }
492
493 int height_val;
494 if (!validate_positive_int_opt(value_str, &height_val, "height")) {
495 return ERROR_INVALID_PARAM;
496 }
497
498 opts->height = height_val;
499 opts->auto_height = false;
500
501 return ASCIICHAT_OK;
502}
503
504asciichat_error_t parse_webcam_index_option(const char *value_str, options_t *opts) {
505 if (!opts) {
506 return ERROR_INVALID_PARAM;
507 }
508
509 unsigned short int index_val;
510 if (!validate_webcam_index(value_str, &index_val)) {
511 return ERROR_INVALID_PARAM;
512 }
513
514 opts->webcam_index = index_val;
515
516 return ASCIICHAT_OK;
517}
518
519asciichat_error_t parse_snapshot_delay_option(const char *value_str, options_t *opts) {
520 if (!value_str || !opts) {
521 return ERROR_INVALID_PARAM;
522 }
523
524 char *endptr;
525 float delay = strtof(value_str, &endptr);
526 if (endptr == value_str || *endptr != '\0' || delay < 0.0f) {
527 log_error("Invalid snapshot delay '%s'. Must be a non-negative number.", value_str);
528 return ERROR_INVALID_PARAM;
529 }
530
531 opts->snapshot_delay = delay;
532
533 return ASCIICHAT_OK;
534}
535
536asciichat_error_t parse_log_level_option(const char *value_str, options_t *opts) {
537 if (!opts) {
538 return ERROR_INVALID_PARAM;
539 }
540
541 char error_msg[BUFFER_SIZE_SMALL];
542 int log_level = validate_opt_log_level(value_str, error_msg, sizeof(error_msg));
543
544 if (log_level == -1) {
545 log_error("%s", error_msg);
546 return ERROR_INVALID_PARAM;
547 }
548
549 opts->log_level = (log_level_t)log_level;
550
551 return ASCIICHAT_OK;
552}
553
554// ============================================================================
555// Option Formatting Utilities
556// ============================================================================
557
558const char *options_get_type_placeholder(option_type_t type) {
559 switch (type) {
560 case OPTION_TYPE_INT:
561 return "INTEGER";
562 case OPTION_TYPE_DOUBLE:
563 return "NUMBER";
564 case OPTION_TYPE_STRING:
565 return "STRING";
566 case OPTION_TYPE_CALLBACK:
567 return "VALUE";
568 case OPTION_TYPE_BOOL:
569 return "[BOOLEAN]";
570 case OPTION_TYPE_ACTION:
571 default:
572 return "";
573 }
574}
575
576int options_format_default_value(option_type_t type, const void *default_value, char *buf, size_t bufsize) {
577 if (!default_value || !buf || bufsize == 0) {
578 return 0;
579 }
580
581 switch (type) {
582 case OPTION_TYPE_BOOL:
583 return safe_snprintf(buf, bufsize, "%s", *(const bool *)default_value ? "true" : "false");
584 case OPTION_TYPE_INT: {
585 int int_val = 0;
586 memcpy(&int_val, default_value, sizeof(int));
587 return safe_snprintf(buf, bufsize, "%d", int_val);
588 }
589 case OPTION_TYPE_STRING:
590 return safe_snprintf(buf, bufsize, "%s", *(const char *const *)default_value);
591 case OPTION_TYPE_DOUBLE: {
592 double double_val = 0.0;
593 memcpy(&double_val, default_value, sizeof(double));
594 return safe_snprintf(buf, bufsize, "%.2f", double_val);
595 }
596 default:
597 // OPTION_TYPE_CALLBACK and OPTION_TYPE_ACTION don't have defaults to display
598 return 0;
599 }
600}
601
602// ============================================================================
603// Terminal Dimension Utilities
604// ============================================================================
605
607 if (!opts) {
608 return;
609 }
610
611 unsigned short int term_width, term_height;
612
613 // Note: Logging is not available during options_init, so we can't use log_debug here
614 asciichat_error_t result = get_terminal_size(&term_width, &term_height);
615 if (result == ASCIICHAT_OK) {
616 // If both dimensions are auto, set height to terminal height and let
617 // aspect_ratio calculate width
618 if (opts->auto_height && opts->auto_width) {
619 opts->height = term_height;
620 opts->width = term_width; // Also set width when both are auto
621 }
622 // If only height is auto, use full terminal height
623 else if (opts->auto_height) {
624 opts->height = term_height;
625 }
626 // If only width is auto, use full terminal width
627 else if (opts->auto_width) {
628 opts->width = term_width;
629 }
630 } else {
631 // Terminal size detection failed, but we can still continue with defaults
632 }
633}
634
636 if (!opts) {
637 return;
638 }
639
640 unsigned short int term_width, term_height;
641 // Get current terminal size (get_terminal_size already handles ioctl first, then $COLUMNS/$LINES fallback)
642 asciichat_error_t terminal_result = get_terminal_size(&term_width, &term_height);
643 if (terminal_result == ASCIICHAT_OK) {
644 // Use INFO level so this is visible without -v flag (important for debugging dimension issues)
645 log_dev("Terminal size detected: %ux%u (auto_width=%d, auto_height=%d)", term_width, term_height, opts->auto_width,
646 opts->auto_height);
647 if (opts->auto_width) {
648 opts->width = term_width;
649 log_debug("Auto-width: set width to %u", opts->width);
650 }
651 if (opts->auto_height) {
652 opts->height = term_height;
653 log_debug("Auto-height: set height to %u", opts->height);
654 }
655 log_debug("Final dimensions: %ux%u", opts->width, opts->height);
656 } else {
657 // Terminal detection failed - keep the default values set in options_init()
658 log_warn("TERMINAL_DETECT_FAIL: Could not detect terminal size, using defaults: %ux%u", opts->width, opts->height);
659 }
660}
661
662// ============================================================================
663// Generic Usage Function (Unified Implementation)
664// ============================================================================
665
669typedef struct {
670 asciichat_mode_t mode;
671 const char *program_name;
672 const char *description;
674
675static const mode_metadata_t mode_info[] = {
676 {MODE_SERVER, "ascii-chat server", "host a server mixing video and audio for ascii-chat clients"},
677 {MODE_CLIENT, "ascii-chat client", "connect to an ascii-chat server"},
678 {MODE_MIRROR, "ascii-chat mirror", "use the webcam or files or urls without network connections"},
679 {MODE_DISCOVERY_SERVICE, "ascii-chat discovery-service", "secure p2p session signalling"},
680 {MODE_DISCOVERY, "💻📸 ascii-chat 🔡💬", "Video chat in your terminal"},
681};
682
683void usage(FILE *desc, asciichat_mode_t mode) {
684 if (!desc) {
685 return;
686 }
687
688 // Find mode metadata
689 const mode_metadata_t *metadata = NULL;
690 for (size_t i = 0; i < sizeof(mode_info) / sizeof(mode_info[0]); i++) {
691 if (mode_info[i].mode == mode) {
692 metadata = &mode_info[i];
693 break;
694 }
695 }
696
697 if (!metadata) {
698 (void)fprintf(desc, "error: Unknown mode\n");
699 return;
700 }
701
702 // Get unified config
703 const options_config_t *config = options_preset_unified(metadata->program_name, metadata->description);
704 if (!config) {
705 (void)fprintf(desc, "Error: Failed to create options config\n");
706 return;
707 }
708
709 options_print_help_for_mode(config, mode, metadata->program_name, metadata->description, desc);
710
712}
713
714// ============================================================================
715// Options Validation Helper
716// ============================================================================
717
718asciichat_error_t validate_options_and_report(const void *config, const void *opts) {
719 if (!config || !opts) {
720 return SET_ERRNO(ERROR_INVALID_PARAM, "Config or options is NULL");
721 }
722
723 char *error_message = NULL;
724 // Cast opaque config pointer to actual type
725 const options_config_t *config_typed = (const options_config_t *)config;
726 asciichat_error_t result = options_config_validate(config_typed, opts, &error_message);
727 if (result != ASCIICHAT_OK) {
728 if (error_message) {
729 log_error("%s", error_message);
730 free(error_message);
731 }
732 }
733 return result;
734}
735
736// ============================================================================
737// Print Project Links
738// ============================================================================
739
740void print_project_links(FILE *desc) {
741 if (!desc) {
742 return;
743 }
744
745 (void)fprintf(desc, "🔗 %s\n", colored_string(LOG_COLOR_GREY, "https://ascii-chat.com"));
746 (void)fprintf(desc, "🔗 %s\n", colored_string(LOG_COLOR_GREY, "https://github.com/zfogg/ascii-chat"));
747}
asciichat_error_t options_config_validate(const options_config_t *config, const void *options_struct, char **error_message)
Definition builder.c:1775
void options_config_destroy(options_config_t *config)
Definition builder.c:601
int buffer_size
Size of circular buffer.
Definition grep.c:84
void options_print_help_for_mode(const options_config_t *config, asciichat_mode_t mode, const char *program_name, const char *description, FILE *desc)
Print help for a specific mode or binary level.
Definition help.c:1094
size_t levenshtein(const char *a, const char *b)
Definition levenshtein.c:74
unsigned short int GREEN[256]
char * strip_equals_prefix(const char *opt_value, char *buffer, size_t buffer_size)
int options_format_default_value(option_type_t type, const void *default_value, char *buf, size_t bufsize)
ASCIICHAT_API bool auto_width
void print_project_links(FILE *desc)
const char * format_available_modes(option_mode_bitmask_t mode_bitmask)
char * validate_required_argument(const char *optarg, char *argbuf, size_t argbuf_size, const char *option_name, asciichat_mode_t mode)
const char * find_similar_option(const char *unknown_opt, const struct option *options)
asciichat_error_t parse_render_mode_option(const char *value_str, options_t *opts)
int strtoint_safe(const char *str)
void update_dimensions_to_terminal_size(options_t *opts)
asciichat_error_t parse_width_option(const char *value_str, options_t *opts)
asciichat_error_t detect_default_ssh_key(char *key_path, size_t path_size)
const float weight_blue
unsigned short int GRAY[256]
char * read_password_from_stdin(const char *prompt)
bool validate_port_opt(const char *value_str, uint16_t *out_port)
asciichat_error_t parse_height_option(const char *value_str, options_t *opts)
bool validate_fps_opt(const char *value_str, int *out_fps)
asciichat_error_t parse_palette_chars_option(const char *value_str, options_t *opts)
asciichat_error_t parse_palette_option(const char *value_str, options_t *opts)
const float weight_red
ASCIICHAT_API bool auto_height
asciichat_error_t parse_log_level_option(const char *value_str, options_t *opts)
unsigned short int RED[256]
void usage(FILE *desc, asciichat_mode_t mode)
unsigned short int BLUE[256]
const char * find_similar_option_with_mode(const char *unknown_opt, const options_config_t *config, option_mode_bitmask_t current_mode_bitmask)
asciichat_error_t validate_options_and_report(const void *config, const void *opts)
bool validate_positive_int_opt(const char *value_str, int *out_value, const char *param_name)
const char * options_get_type_placeholder(option_type_t type)
asciichat_error_t parse_color_mode_option(const char *value_str, options_t *opts)
asciichat_error_t parse_snapshot_delay_option(const char *value_str, options_t *opts)
const float weight_green
char * get_required_argument(const char *opt_value, char *buffer, size_t buffer_size, const char *option_name, asciichat_mode_t mode)
asciichat_error_t parse_webcam_index_option(const char *value_str, options_t *opts)
bool port_explicitly_set_via_flag
bool validate_webcam_index(const char *value_str, unsigned short int *out_index)
void update_dimensions_for_full_height(options_t *opts)
asciichat_error_t parse_int32(const char *str, int32_t *out_value, int32_t min_value, int32_t max_value)
Definition parsing.c:303
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
Definition parsing.c:251
int prompt_password_simple(const char *prompt, char *password, size_t max_len)
Definition password.c:70
char * expand_path(const char *path)
Definition path.c:471
asciichat_error_t get_terminal_size(unsigned short int *width, unsigned short int *height)
options_config_t * options_preset_unified(const char *program_name, const char *description)
Build unified options config with ALL options (binary + all modes)
Definition presets.c:51
Mode metadata for usage display.
const char * program_name
Program name shown in usage.
asciichat_mode_t mode
Application mode.
const char * description
One-line description of mode.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
const char * colored_string(log_color_t color, const char *text)
int validate_opt_device_index(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:457
int validate_opt_log_level(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:235