10#include <ascii-chat/options/common.h>
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>
39 if (!unknown_opt || !options) {
43 const char *best_match = NULL;
44 size_t best_distance = SIZE_MAX;
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) {
50 best_match = options[i].name;
55 if (best_distance <= LEVENSHTEIN_SUGGESTION_THRESHOLD) {
65 static char buffer[256];
71 if (mode_bitmask & OPTION_MODE_BINARY) {
72 SAFE_SNPRINTF(buffer,
sizeof(buffer),
"global options");
77 if (mode_bitmask & OPTION_MODE_DISCOVERY) {
78 safe_snprintf(buffer + strlen(buffer),
sizeof(buffer) - strlen(buffer),
"%sdefault", first ?
"" :
", ");
81 if (mode_bitmask & OPTION_MODE_CLIENT) {
82 safe_snprintf(buffer + strlen(buffer),
sizeof(buffer) - strlen(buffer),
"%sclient", first ?
"" :
", ");
85 if (mode_bitmask & OPTION_MODE_SERVER) {
86 safe_snprintf(buffer + strlen(buffer),
sizeof(buffer) - strlen(buffer),
"%sserver", first ?
"" :
", ");
89 if (mode_bitmask & OPTION_MODE_MIRROR) {
90 safe_snprintf(buffer + strlen(buffer),
sizeof(buffer) - strlen(buffer),
"%smirror", first ?
"" :
", ");
93 if (mode_bitmask & OPTION_MODE_DISCOVERY_SVC) {
94 safe_snprintf(buffer + strlen(buffer),
sizeof(buffer) - strlen(buffer),
"%sdiscovery-service", first ?
"" :
", ");
99 if (buffer[0] ==
'\0') {
100 SAFE_SNPRINTF(buffer,
sizeof(buffer),
"unknown mode");
109 option_mode_bitmask_t current_mode_bitmask) {
110 if (!unknown_opt || !config) {
115 const char *opt_name = unknown_opt;
116 if (strncmp(opt_name,
"--", 2) == 0) {
118 }
else if (strncmp(opt_name,
"-", 1) == 0) {
124 const option_descriptor_t *best_match = NULL;
125 size_t best_distance = SIZE_MAX;
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)
134 size_t dist =
levenshtein(opt_name, desc->long_name);
135 if (dist < best_distance) {
136 best_distance = dist;
142 if (best_distance > LEVENSHTEIN_SUGGESTION_THRESHOLD || !best_match) {
147 bool available_in_current_mode = (best_match->mode_bitmask & current_mode_bitmask) != 0;
149 static char suggestion[256];
150 if (available_in_current_mode) {
152 safe_snprintf(suggestion,
sizeof(suggestion),
"Did you mean '--%s'?", best_match->long_name);
156 safe_snprintf(suggestion,
sizeof(suggestion),
"Did you mean '--%s' (available in modes: %s)?",
157 best_match->long_name, modes_str);
165 if (!str || *str ==
'\0') {
171 if (
parse_int32(str, &result, INT_MIN, INT_MAX) != ASCIICHAT_OK) {
180 asciichat_mode_t mode);
184 asciichat_mode_t mode) {
187 (void)option_error_invalid();
194 if (!value_str || !out_value) {
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);
210 if (!value_str || !out_port) {
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);
225 if (!value_str || !out_fps) {
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);
241 if (!value_str || !out_index) {
245 char error_msg[BUFFER_SIZE_SMALL];
247 if (parsed_index == INT_MIN) {
248 log_error(
"Invalid webcam index: %s", error_msg);
252 if (parsed_index < 0) {
253 log_error(
"Invalid webcam index '%s'. Webcam index must be a non-negative integer.", value_str);
257 *out_index = (
unsigned short int)parsed_index;
264 char *full_path =
expand_path(
"~/.ssh/id_ed25519");
266 return SET_ERRNO(ERROR_CONFIG,
"Could not expand SSH key path");
271 bool found = (stat(full_path, &st) == 0 && S_ISREG(st.st_mode));
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);
280 log_error(
"No Ed25519 SSH key found at %s", full_path);
281 SAFE_FREE(full_path);
284 "Only Ed25519 keys are supported (modern, secure, fast). Generate a new key with: ssh-keygen -t ed25519");
296 SAFE_SNPRINTF(buffer,
buffer_size,
"%s", opt_value);
297 char *value_str = buffer;
298 if (value_str[0] ==
'=') {
303 if (strlen(value_str) == 0) {
313 asciichat_mode_t mode) {
315 if (!opt_value || strlen(opt_value) == 0) {
321 if (opt_value && option_name && strcmp(opt_value, option_name) == 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);
342 char *password_buf = SAFE_MALLOC(PASSWORD_MAX_LEN,
char *);
348 SAFE_FREE(password_buf);
376unsigned short int RED[256];
386 if (!value_str || !opts) {
387 return ERROR_INVALID_PARAM;
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;
402 log_error(
"Invalid color mode '%s'. Valid modes: auto, none, 16, 256, truecolor", value_str);
403 return ERROR_INVALID_PARAM;
410 if (!value_str || !opts) {
411 return ERROR_INVALID_PARAM;
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;
421 log_error(
"Invalid render mode '%s'. Valid modes: foreground, background, half-block", value_str);
422 return ERROR_INVALID_PARAM;
429 if (!value_str || !opts) {
430 return ERROR_INVALID_PARAM;
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;
446 log_error(
"Invalid palette '%s'. Valid palettes: standard, blocks, digital, minimal, cool, custom", value_str);
447 return ERROR_INVALID_PARAM;
454 if (!value_str || !opts) {
455 return ERROR_INVALID_PARAM;
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;
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;
474 return ERROR_INVALID_PARAM;
479 return ERROR_INVALID_PARAM;
482 opts->width = width_val;
483 opts->auto_width =
false;
490 return ERROR_INVALID_PARAM;
495 return ERROR_INVALID_PARAM;
498 opts->height = height_val;
499 opts->auto_height =
false;
506 return ERROR_INVALID_PARAM;
509 unsigned short int index_val;
511 return ERROR_INVALID_PARAM;
514 opts->webcam_index = index_val;
520 if (!value_str || !opts) {
521 return ERROR_INVALID_PARAM;
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;
531 opts->snapshot_delay = delay;
538 return ERROR_INVALID_PARAM;
541 char error_msg[BUFFER_SIZE_SMALL];
544 if (log_level == -1) {
545 log_error(
"%s", error_msg);
546 return ERROR_INVALID_PARAM;
549 opts->log_level = (log_level_t)log_level;
560 case OPTION_TYPE_INT:
562 case OPTION_TYPE_DOUBLE:
564 case OPTION_TYPE_STRING:
566 case OPTION_TYPE_CALLBACK:
568 case OPTION_TYPE_BOOL:
570 case OPTION_TYPE_ACTION:
577 if (!default_value || !buf || bufsize == 0) {
582 case OPTION_TYPE_BOOL:
583 return safe_snprintf(buf, bufsize,
"%s", *(
const bool *)default_value ?
"true" :
"false");
584 case OPTION_TYPE_INT: {
586 memcpy(&int_val, default_value,
sizeof(
int));
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));
611 unsigned short int term_width, term_height;
615 if (result == ASCIICHAT_OK) {
618 if (opts->auto_height && opts->auto_width) {
619 opts->height = term_height;
620 opts->width = term_width;
623 else if (opts->auto_height) {
624 opts->height = term_height;
627 else if (opts->auto_width) {
628 opts->width = term_width;
640 unsigned short int term_width, term_height;
642 asciichat_error_t terminal_result =
get_terminal_size(&term_width, &term_height);
643 if (terminal_result == ASCIICHAT_OK) {
645 log_dev(
"Terminal size detected: %ux%u (auto_width=%d, auto_height=%d)", term_width, term_height, opts->auto_width,
647 if (opts->auto_width) {
648 opts->width = term_width;
649 log_debug(
"Auto-width: set width to %u", opts->width);
651 if (opts->auto_height) {
652 opts->height = term_height;
653 log_debug(
"Auto-height: set height to %u", opts->height);
655 log_debug(
"Final dimensions: %ux%u", opts->width, opts->height);
658 log_warn(
"TERMINAL_DETECT_FAIL: Could not detect terminal size, using defaults: %ux%u", opts->width, opts->height);
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"},
683void usage(FILE *desc, asciichat_mode_t mode) {
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];
698 (void)fprintf(desc,
"error: Unknown mode\n");
705 (void)fprintf(desc,
"Error: Failed to create options config\n");
719 if (!config || !opts) {
720 return SET_ERRNO(ERROR_INVALID_PARAM,
"Config or options is NULL");
723 char *error_message = NULL;
725 const options_config_t *config_typed = (
const options_config_t *)config;
727 if (result != ASCIICHAT_OK) {
729 log_error(
"%s", error_message);
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"));
asciichat_error_t options_config_validate(const options_config_t *config, const void *options_struct, char **error_message)
void options_config_destroy(options_config_t *config)
int buffer_size
Size of circular buffer.
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.
size_t levenshtein(const char *a, const char *b)
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)
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)
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)
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)
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
int prompt_password_simple(const char *prompt, char *password, size_t max_len)
char * expand_path(const char *path)
options_config_t * options_preset_unified(const char *program_name, const char *description)
Build unified options config with ALL options (binary + all modes)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
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)
int validate_opt_log_level(const char *value_str, char *error_msg, size_t error_msg_size)