ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
config.c
Go to the documentation of this file.
1
7#include "options/config.h"
8#include "options/options.h"
10#include "util/path.h"
11#include "common.h"
12#include "platform/terminal.h"
13#include "platform/system.h"
14#include "platform/question.h"
15#include "crypto/crypto.h"
16#include "log/logging.h"
17#include "version.h"
18#include "tooling/defer/defer.h"
19
20#include "tomlc17.h"
21#include <string.h>
22#include <stdlib.h>
23#include <stdio.h>
24#include <errno.h>
25#include <sys/stat.h>
26#ifdef _WIN32
27#include <io.h>
28#include <direct.h>
29#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
30#define mkdir(path, mode) _mkdir(path)
31#else
32#include <unistd.h>
33#endif
34
47#define CONFIG_WARN(fmt, ...) \
48 do { \
49 (void)fprintf(stderr, "WARNING: Config file: " fmt "\n", ##__VA_ARGS__); \
50 (void)fflush(stderr); \
51 } while (0)
52
59#define CONFIG_DEBUG(fmt, ...) \
60 do { \
61 /* Debug messages are only shown in debug builds after logging is initialized */ \
62 /* Use log_debug which safely checks initialization itself */ \
63 log_debug(fmt, ##__VA_ARGS__); \
64 } while (0)
65
78static bool config_address_set = false;
80static bool config_address6_set = false;
82static bool config_port_set = false;
84static bool config_width_set = false;
86static bool config_height_set = false;
88static bool config_webcam_index_set = false;
90static bool config_webcam_flip_set = false;
92static bool config_color_mode_set = false;
94static bool config_render_mode_set = false;
96static bool config_palette_set = false;
98static bool config_palette_chars_set = false;
100static bool config_audio_enabled_set = false;
102static bool config_microphone_index_set = false;
104static bool config_speakers_index_set = false;
106static bool config_stretch_set = false;
108static bool config_quiet_set = false;
110static bool config_snapshot_mode_set = false;
112static bool config_mirror_mode_set = false;
114static bool config_snapshot_delay_set = false;
116static bool config_log_file_set = false;
118static bool config_encrypt_enabled_set = false;
120static bool config_encrypt_key_set = false;
122static bool config_password_set = false;
124static bool config_encrypt_keyfile_set = false;
126static bool config_no_encrypt_set = false;
128static bool config_server_key_set = false;
130static bool config_client_keys_set = false;
131
134/* Validation functions are now provided by options/validation.h */
135
150static const char *get_toml_string(toml_datum_t datum) {
151 if (datum.type == TOML_STRING) {
152 return datum.u.s;
153 }
154 return NULL;
155}
156
188static void apply_network_config(toml_datum_t toptab, bool is_client, options_t *opts) {
189 if (is_client) {
190 // Client: Get connect address from client.address or network.address (legacy)
191 toml_datum_t address = toml_seek(toptab, "client.address");
192 const char *address_str = get_toml_string(address);
193 if (!address_str || strlen(address_str) == 0) {
194 // Fallback to legacy network.address
195 toml_datum_t network = toml_seek(toptab, "network");
196 if (network.type == TOML_TABLE) {
197 address = toml_seek(toptab, "network.address");
198 address_str = get_toml_string(address);
199 }
200 }
201 if (address_str && strlen(address_str) > 0 && !config_address_set) {
202 char parsed_addr[OPTIONS_BUFF_SIZE];
203 char error_msg[256];
204 if (validate_opt_ip_address(address_str, parsed_addr, sizeof(parsed_addr), is_client, error_msg,
205 sizeof(error_msg)) == 0) {
206 SAFE_SNPRINTF(opts->address, OPTIONS_BUFF_SIZE, "%s", parsed_addr);
207 config_address_set = true;
208 } else {
209 CONFIG_WARN("%s (skipping client address)", error_msg);
210 }
211 }
212 } else {
213 // Server: Get bind addresses from server.bind_ipv4 and server.bind_ipv6
214 toml_datum_t server = toml_seek(toptab, "server");
215 if (server.type == TOML_TABLE) {
216 // IPv4 bind address - seek from the server table, not root
217 toml_datum_t bind_ipv4 = toml_get(server, "bind_ipv4");
218 const char *ipv4_str = get_toml_string(bind_ipv4);
219 if (ipv4_str && strlen(ipv4_str) > 0 && !config_address_set) {
220 char parsed_addr[OPTIONS_BUFF_SIZE];
221 char error_msg[256];
222 if (validate_opt_ip_address(ipv4_str, parsed_addr, sizeof(parsed_addr), is_client, error_msg,
223 sizeof(error_msg)) == 0) {
224 SAFE_SNPRINTF(opts->address, OPTIONS_BUFF_SIZE, "%s", parsed_addr);
225 config_address_set = true;
226 } else {
227 const char *errmsg = (strlen(error_msg) > 0) ? error_msg : "Invalid IPv4 address";
228 CONFIG_WARN("Invalid server.bind_ipv4 value '%s': %s (skipping)", ipv4_str, errmsg);
229 }
230 }
231
232 // IPv6 bind address - seek from the server table, not root
233 toml_datum_t bind_ipv6 = toml_get(server, "bind_ipv6");
234 const char *ipv6_str = get_toml_string(bind_ipv6);
235 if (ipv6_str && strlen(ipv6_str) > 0 && !config_address6_set) {
236 char parsed_addr[OPTIONS_BUFF_SIZE];
237 char error_msg[256];
238 if (validate_opt_ip_address(ipv6_str, parsed_addr, sizeof(parsed_addr), is_client, error_msg,
239 sizeof(error_msg)) == 0) {
240 SAFE_SNPRINTF(opts->address6, OPTIONS_BUFF_SIZE, "%s", parsed_addr);
241 config_address6_set = true;
242 } else {
243 const char *errmsg = (strlen(error_msg) > 0) ? error_msg : "Invalid IPv6 address";
244 CONFIG_WARN("Invalid server.bind_ipv6 value '%s': %s (skipping)", ipv6_str, errmsg);
245 }
246 }
247 }
248
249 // Fallback to legacy network.address if no server-specific bind addresses found
250 if (!config_address_set) {
251 toml_datum_t network = toml_seek(toptab, "network");
252 if (network.type == TOML_TABLE) {
253 toml_datum_t address = toml_get(network, "address");
254 const char *address_str = get_toml_string(address);
255 if (address_str && strlen(address_str) > 0) {
256 char parsed_addr[OPTIONS_BUFF_SIZE];
257 char error_msg[256];
258 if (validate_opt_ip_address(address_str, parsed_addr, sizeof(parsed_addr), is_client, error_msg,
259 sizeof(error_msg)) == 0) {
260 SAFE_SNPRINTF(opts->address, OPTIONS_BUFF_SIZE, "%s", parsed_addr);
261 config_address_set = true;
262 } else {
263 CONFIG_WARN("%s (skipping network.address)", error_msg);
264 }
265 }
266 }
267 }
268 }
269
270 // Port (shared between server and client)
271 toml_datum_t network = toml_seek(toptab, "network");
272 if (network.type != TOML_TABLE) {
273 return; // No network section
274 }
275
276 // Port (can be string or integer in TOML)
277 toml_datum_t port = toml_seek(toptab, "network.port");
278 if (port.type == TOML_STRING && !config_port_set) {
279 const char *port_str = port.u.s;
280 char error_msg[256];
281 if (validate_opt_port(port_str, error_msg, sizeof(error_msg)) == 0) {
282 SAFE_SNPRINTF(opts->port, OPTIONS_BUFF_SIZE, "%s", port_str);
283 config_port_set = true;
284 } else {
285 CONFIG_WARN("%s (skipping network.port)", error_msg);
286 }
287 } else if (port.type == TOML_INT64 && !config_port_set) {
288 int64_t port_val = port.u.int64;
289 if (port_val >= 1 && port_val <= 65535) {
290 SAFE_SNPRINTF(opts->port, OPTIONS_BUFF_SIZE, "%lld", (long long)port_val);
291 config_port_set = true;
292 } else {
293 CONFIG_WARN("Invalid port value %lld (must be 1-65535, skipping network.port)", (long long)port_val);
294 }
295 }
296}
297
325static void apply_client_config(toml_datum_t toptab, bool is_client, options_t *opts) {
326 if (!is_client) {
327 return; // Server doesn't use client config
328 }
329
330 toml_datum_t client = toml_seek(toptab, "client");
331 if (client.type != TOML_TABLE) {
332 return; // No client section
333 }
334
335 // Width
336 toml_datum_t width = toml_seek(toptab, "client.width");
337 if (width.type == TOML_INT64 && !config_width_set && !opts->auto_width) {
338 int64_t width_val = width.u.int64;
339 if (width_val > 0) {
340 opts->width = (unsigned short int)width_val;
341 opts->auto_width = false;
342 config_width_set = true;
343 }
344 } else if (width.type == TOML_STRING && !config_width_set) {
345 const char *width_str = width.u.s;
346 char error_msg[256];
347 int width_val = validate_opt_positive_int(width_str, error_msg, sizeof(error_msg));
348 if (width_val > 0) {
349 opts->width = (unsigned short int)width_val;
350 opts->auto_width = false;
351 config_width_set = true;
352 } else {
353 CONFIG_WARN("%s (skipping client.width)", error_msg);
354 }
355 }
356
357 // Height
358 toml_datum_t height = toml_seek(toptab, "client.height");
359 if (height.type == TOML_INT64 && !config_height_set && !opts->auto_height) {
360 int64_t height_val = height.u.int64;
361 if (height_val > 0) {
362 opts->height = (unsigned short int)height_val;
363 opts->auto_height = false;
364 config_height_set = true;
365 }
366 } else if (height.type == TOML_STRING && !config_height_set) {
367 const char *height_str = height.u.s;
368 char error_msg[256];
369 int height_val = validate_opt_positive_int(height_str, error_msg, sizeof(error_msg));
370 if (height_val > 0) {
371 opts->height = (unsigned short int)height_val;
372 opts->auto_height = false;
373 config_height_set = true;
374 } else {
375 CONFIG_WARN("%s (skipping client.height)", error_msg);
376 }
377 }
378
379 // Webcam index
380 toml_datum_t webcam_index = toml_seek(toptab, "client.webcam_index");
381 if (webcam_index.type == TOML_INT64 && !config_webcam_index_set) {
382 int64_t idx = webcam_index.u.int64;
383 if (idx >= 0) {
384 opts->webcam_index = (unsigned short int)idx;
385 config_webcam_index_set = true;
386 }
387 } else if (webcam_index.type == TOML_STRING && !config_webcam_index_set) {
388 const char *idx_str = webcam_index.u.s;
389 char error_msg[256];
390 int idx = validate_opt_non_negative_int(idx_str, error_msg, sizeof(error_msg));
391 if (idx >= 0) {
392 opts->webcam_index = (unsigned short int)idx;
393 config_webcam_index_set = true;
394 } else {
395 CONFIG_WARN("%s (skipping client.webcam_index)", error_msg);
396 }
397 }
398
399 // Webcam flip
400 toml_datum_t webcam_flip = toml_seek(toptab, "client.webcam_flip");
401 if (webcam_flip.type == TOML_BOOLEAN && !config_webcam_flip_set) {
402 opts->webcam_flip = webcam_flip.u.boolean;
403 config_webcam_flip_set = true;
404 }
405
406 // Color mode
407 toml_datum_t color_mode = toml_seek(toptab, "client.color_mode");
408 const char *color_mode_str = get_toml_string(color_mode);
409 if (color_mode_str && !config_color_mode_set) {
410 char error_msg[256];
411 int mode = validate_opt_color_mode(color_mode_str, error_msg, sizeof(error_msg));
412 if (mode >= 0) {
413 opts->color_mode = (terminal_color_mode_t)mode;
414 config_color_mode_set = true;
415 } else {
416 CONFIG_WARN("%s (skipping client.color_mode)", error_msg);
417 }
418 }
419
420 // Render mode
421 toml_datum_t render_mode = toml_seek(toptab, "client.render_mode");
422 const char *render_mode_str = get_toml_string(render_mode);
423 if (render_mode_str && !config_render_mode_set) {
424 char error_msg[256];
425 int mode = validate_opt_render_mode(render_mode_str, error_msg, sizeof(error_msg));
426 if (mode >= 0) {
427 opts->render_mode = (render_mode_t)mode;
428 config_render_mode_set = true;
429 } else {
430 CONFIG_WARN("%s (skipping client.render_mode)", error_msg);
431 }
432 }
433
434 // FPS (NOTE: FPS is still a global variable g_max_fps, not in options_t)
435 toml_datum_t fps = toml_seek(toptab, "client.fps");
436 extern int g_max_fps; // From common.c
437 if (fps.type == TOML_INT64) {
438 int64_t fps_val = fps.u.int64;
439 if (fps_val >= 1 && fps_val <= 144) {
440 g_max_fps = (int)fps_val;
441 } else {
442 CONFIG_WARN("Invalid FPS value %lld (must be 1-144, skipping client.fps)", (long long)fps_val);
443 }
444 } else if (fps.type == TOML_STRING) {
445 const char *fps_str = fps.u.s;
446 char error_msg[256];
447 int fps_val = validate_opt_fps(fps_str, error_msg, sizeof(error_msg));
448 if (fps_val > 0) {
449 g_max_fps = fps_val;
450 } else {
451 CONFIG_WARN("%s (skipping client.fps)", error_msg);
452 }
453 }
454
455 // Stretch
456 toml_datum_t stretch = toml_seek(toptab, "client.stretch");
457 if (stretch.type == TOML_BOOLEAN && !config_stretch_set) {
458 opts->stretch = stretch.u.boolean ? 1 : 0;
459 config_stretch_set = true;
460 }
461
462 // Quiet
463 toml_datum_t quiet = toml_seek(toptab, "client.quiet");
464 if (quiet.type == TOML_BOOLEAN && !config_quiet_set) {
465 opts->quiet = quiet.u.boolean ? 1 : 0;
466 config_quiet_set = true;
467 }
468
469 // Snapshot mode
470 toml_datum_t snapshot_mode = toml_seek(toptab, "client.snapshot_mode");
471 if (snapshot_mode.type == TOML_BOOLEAN && !config_snapshot_mode_set) {
472 opts->snapshot_mode = snapshot_mode.u.boolean ? 1 : 0;
473 config_snapshot_mode_set = true;
474 }
475
476 // Snapshot delay
477 toml_datum_t snapshot_delay = toml_seek(toptab, "client.snapshot_delay");
478 if (snapshot_delay.type == TOML_FP64 && !config_snapshot_delay_set) {
479 double delay = snapshot_delay.u.fp64;
480 if (delay >= 0.0) {
481 opts->snapshot_delay = (float)delay;
482 config_snapshot_delay_set = true;
483 } else {
484 CONFIG_WARN("Invalid snapshot_delay value %.2f (must be non-negative, skipping)", delay);
485 }
486 } else if (snapshot_delay.type == TOML_STRING && !config_snapshot_delay_set) {
487 const char *delay_str = snapshot_delay.u.s;
488 char error_msg[256];
489 float delay = validate_opt_float_non_negative(delay_str, error_msg, sizeof(error_msg));
490 if (delay >= 0.0f) {
491 opts->snapshot_delay = delay;
492 config_snapshot_delay_set = true;
493 } else {
494 CONFIG_WARN("%s (skipping client.snapshot_delay)", error_msg);
495 }
496 }
497}
498
517static void apply_audio_config(toml_datum_t toptab, bool is_client, options_t *opts) {
518 if (!is_client) {
519 return; // Server doesn't use audio config
520 }
521
522 toml_datum_t audio = toml_seek(toptab, "audio");
523 if (audio.type != TOML_TABLE) {
524 return; // No audio section
525 }
526
527 // Audio enabled
528 toml_datum_t audio_enabled = toml_seek(toptab, "audio.enabled");
529 if (audio_enabled.type == TOML_BOOLEAN && !config_audio_enabled_set) {
530 opts->audio_enabled = audio_enabled.u.boolean ? 1 : 0;
531 config_audio_enabled_set = true;
532 }
533
534 // Microphone index
535 toml_datum_t microphone_index = toml_seek(toptab, "audio.microphone_index");
536 if (microphone_index.type == TOML_INT64 && !config_microphone_index_set) {
537 int64_t mic_idx = microphone_index.u.int64;
538 if (mic_idx >= -1) {
539 opts->microphone_index = (int)mic_idx;
540 config_microphone_index_set = true;
541 }
542 } else if (microphone_index.type == TOML_STRING && !config_microphone_index_set) {
543 const char *mic_str = microphone_index.u.s;
544 char error_msg[256];
545 int mic_idx = validate_opt_device_index(mic_str, error_msg, sizeof(error_msg));
546 if (mic_idx != INT_MIN) {
547 opts->microphone_index = mic_idx;
548 config_microphone_index_set = true;
549 } else {
550 CONFIG_WARN("%s (skipping audio.microphone_index)", error_msg);
551 }
552 }
553
554 // Speakers index
555 toml_datum_t speakers_index = toml_seek(toptab, "audio.speakers_index");
556 if (speakers_index.type == TOML_INT64 && !config_speakers_index_set) {
557 int64_t spk_idx = speakers_index.u.int64;
558 if (spk_idx >= -1) {
559 opts->speakers_index = (int)spk_idx;
560 config_speakers_index_set = true;
561 }
562 } else if (speakers_index.type == TOML_STRING && !config_speakers_index_set) {
563 const char *spk_str = speakers_index.u.s;
564 char error_msg[256];
565 int spk_idx = validate_opt_device_index(spk_str, error_msg, sizeof(error_msg));
566 if (spk_idx != INT_MIN) {
567 opts->speakers_index = spk_idx;
568 config_speakers_index_set = true;
569 } else {
570 CONFIG_WARN("%s (skipping audio.speakers_index)", error_msg);
571 }
572 }
573}
574
589static void apply_palette_config_from_toml(toml_datum_t toptab, options_t *opts) {
590 toml_datum_t palette = toml_seek(toptab, "palette");
591 if (palette.type != TOML_TABLE) {
592 return; // No palette section
593 }
594
595 // Palette type
596 toml_datum_t palette_type = toml_seek(toptab, "palette.type");
597 const char *palette_type_str = get_toml_string(palette_type);
598 if (palette_type_str && !config_palette_set) {
599 char error_msg[256];
600 int type = validate_opt_palette(palette_type_str, error_msg, sizeof(error_msg));
601 if (type >= 0) {
602 opts->palette_type = (palette_type_t)type;
603 config_palette_set = true;
604 } else {
605 CONFIG_WARN("%s (skipping palette.type)", error_msg);
606 }
607 }
608
609 // Palette chars (custom palette)
610 toml_datum_t palette_chars = toml_seek(toptab, "palette.chars");
611 const char *palette_chars_str = get_toml_string(palette_chars);
612 if (palette_chars_str && !config_palette_chars_set) {
613 if (strlen(palette_chars_str) < sizeof(opts->palette_custom)) {
614 SAFE_STRNCPY(opts->palette_custom, palette_chars_str, sizeof(opts->palette_custom));
615 opts->palette_custom[sizeof(opts->palette_custom) - 1] = '\0';
616 opts->palette_custom_set = true;
617 opts->palette_type = PALETTE_CUSTOM; // Automatically set to custom
618 config_palette_chars_set = true;
619 } else {
620 CONFIG_WARN("Invalid palette.chars: too long (%zu chars, max %zu, skipping)", strlen(palette_chars_str),
621 sizeof(opts->palette_custom) - 1);
622 }
623 }
624}
625
648static asciichat_error_t apply_crypto_config(toml_datum_t toptab, bool is_client, options_t *opts) {
649 toml_datum_t crypto = toml_seek(toptab, "crypto");
650 if (crypto.type != TOML_TABLE) {
651 return ASCIICHAT_OK; // No crypto section
652 }
653
654 // Encrypt enabled
655 toml_datum_t encrypt_enabled = toml_seek(toptab, "crypto.encrypt_enabled");
656 if (encrypt_enabled.type == TOML_BOOLEAN && !config_encrypt_enabled_set) {
657 opts->encrypt_enabled = encrypt_enabled.u.boolean ? 1 : 0;
658 config_encrypt_enabled_set = true;
659 }
660
661 // Key
662 toml_datum_t key = toml_seek(toptab, "crypto.key");
663 const char *key_str = get_toml_string(key);
664 if (key_str && strlen(key_str) > 0 && !config_encrypt_key_set) {
665 if (path_looks_like_path(key_str)) {
666 char *normalized_key = NULL;
667 asciichat_error_t key_result = path_validate_user_path(key_str, PATH_ROLE_KEY_PRIVATE, &normalized_key);
668 if (key_result != ASCIICHAT_OK) {
669 SAFE_FREE(normalized_key);
670 return key_result;
671 }
672 SAFE_SNPRINTF(opts->encrypt_key, OPTIONS_BUFF_SIZE, "%s", normalized_key);
673 SAFE_FREE(normalized_key);
674 } else {
675 SAFE_SNPRINTF(opts->encrypt_key, OPTIONS_BUFF_SIZE, "%s", key_str);
676 }
677 opts->encrypt_enabled = 1; // Auto-enable encryption when key provided
678 config_encrypt_key_set = true;
679 }
680
681 // Password (WARNING: storing passwords in config file is insecure!)
682 // We only load it if explicitly set, but warn user
683 toml_datum_t password = toml_seek(toptab, "crypto.password");
684 const char *password_str = get_toml_string(password);
685 if (password_str && strlen(password_str) > 0 && !config_password_set) {
686 char error_msg[256];
687 if (validate_opt_password(password_str, error_msg, sizeof(error_msg)) == 0) {
688 CONFIG_WARN("Password stored in config file is insecure! Use CLI --password instead.");
689 SAFE_SNPRINTF(opts->password, OPTIONS_BUFF_SIZE, "%s", password_str);
690 opts->encrypt_enabled = 1; // Auto-enable encryption when password provided
691 config_password_set = true;
692 } else {
693 CONFIG_WARN("%s (skipping crypto.password)", error_msg);
694 }
695 }
696
697 // Keyfile
698 toml_datum_t keyfile = toml_seek(toptab, "crypto.keyfile");
699 const char *keyfile_str = get_toml_string(keyfile);
700 if (keyfile_str && strlen(keyfile_str) > 0 && !config_encrypt_keyfile_set) {
701 if (path_looks_like_path(keyfile_str)) {
702 char *normalized_keyfile = NULL;
703 asciichat_error_t keyfile_result =
704 path_validate_user_path(keyfile_str, PATH_ROLE_KEY_PRIVATE, &normalized_keyfile);
705 if (keyfile_result != ASCIICHAT_OK) {
706 SAFE_FREE(normalized_keyfile);
707 return keyfile_result;
708 }
709 SAFE_SNPRINTF(opts->encrypt_keyfile, OPTIONS_BUFF_SIZE, "%s", normalized_keyfile);
710 SAFE_FREE(normalized_keyfile);
711 } else {
712 SAFE_SNPRINTF(opts->encrypt_keyfile, OPTIONS_BUFF_SIZE, "%s", keyfile_str);
713 }
714 opts->encrypt_enabled = 1; // Auto-enable encryption when keyfile provided
715 config_encrypt_keyfile_set = true;
716 }
717
718 // No encrypt
719 toml_datum_t no_encrypt = toml_seek(toptab, "crypto.no_encrypt");
720 if (no_encrypt.type == TOML_BOOLEAN && !config_no_encrypt_set) {
721 if (no_encrypt.u.boolean) {
722 opts->no_encrypt = 1;
723 opts->encrypt_enabled = 0;
724 config_no_encrypt_set = true;
725 }
726 }
727
728 // Server key (client only)
729 if (is_client) {
730 toml_datum_t server_key = toml_seek(toptab, "crypto.server_key");
731 const char *server_key_str = get_toml_string(server_key);
732 if (server_key_str && strlen(server_key_str) > 0 && !config_server_key_set) {
733 if (path_looks_like_path(server_key_str)) {
734 char *normalized_server_key = NULL;
735 asciichat_error_t server_key_result =
736 path_validate_user_path(server_key_str, PATH_ROLE_KEY_PUBLIC, &normalized_server_key);
737 if (server_key_result != ASCIICHAT_OK) {
738 SAFE_FREE(normalized_server_key);
739 return server_key_result;
740 }
741 SAFE_SNPRINTF(opts->server_key, OPTIONS_BUFF_SIZE, "%s", normalized_server_key);
742 SAFE_FREE(normalized_server_key);
743 } else {
744 SAFE_SNPRINTF(opts->server_key, OPTIONS_BUFF_SIZE, "%s", server_key_str);
745 }
746 config_server_key_set = true;
747 }
748 }
749
750 // Client keys (server only)
751 if (!is_client) {
752 toml_datum_t client_keys = toml_seek(toptab, "crypto.client_keys");
753 const char *client_keys_str = get_toml_string(client_keys);
754 if (client_keys_str && strlen(client_keys_str) > 0 && !config_client_keys_set) {
755 if (path_looks_like_path(client_keys_str)) {
756 char *normalized_client_keys = NULL;
757 asciichat_error_t client_keys_result =
758 path_validate_user_path(client_keys_str, PATH_ROLE_CLIENT_KEYS, &normalized_client_keys);
759 if (client_keys_result != ASCIICHAT_OK) {
760 SAFE_FREE(normalized_client_keys);
761 return client_keys_result;
762 }
763 SAFE_SNPRINTF(opts->client_keys, OPTIONS_BUFF_SIZE, "%s", normalized_client_keys);
764 SAFE_FREE(normalized_client_keys);
765 } else {
766 SAFE_SNPRINTF(opts->client_keys, OPTIONS_BUFF_SIZE, "%s", client_keys_str);
767 }
768 config_client_keys_set = true;
769 }
770 }
771
772 return ASCIICHAT_OK;
773}
774
788static asciichat_error_t apply_log_config(toml_datum_t toptab, options_t *opts) {
789 // Log file can be in root or in a [logging] section
790 toml_datum_t log_file = toml_seek(toptab, "log_file");
791 if (log_file.type == TOML_UNKNOWN) {
792 log_file = toml_seek(toptab, "logging.log_file");
793 }
794
795 const char *log_file_str = get_toml_string(log_file);
796 if (log_file_str && strlen(log_file_str) > 0 && !config_log_file_set) {
797 char *normalized_log = NULL;
798 asciichat_error_t log_result = path_validate_user_path(log_file_str, PATH_ROLE_LOG_FILE, &normalized_log);
799 if (log_result != ASCIICHAT_OK) {
800 SAFE_FREE(normalized_log);
801 return log_result;
802 }
803 SAFE_SNPRINTF(opts->log_file, OPTIONS_BUFF_SIZE, "%s", normalized_log);
804 SAFE_FREE(normalized_log);
805 config_log_file_set = true;
806 }
807 return ASCIICHAT_OK;
808}
809
842asciichat_error_t config_load_and_apply(bool is_client, const char *config_path, bool strict, options_t *opts) {
843 char *config_path_expanded = NULL;
844 defer(SAFE_FREE(config_path_expanded));
845
846 if (config_path) {
847 // Use custom path provided
848 config_path_expanded = expand_path(config_path);
849 if (!config_path_expanded) {
850 // If expansion fails, try using as-is (might already be absolute)
851 config_path_expanded = platform_strdup(config_path);
852 }
853 } else {
854 // Use default location with XDG support
855 char *config_dir = get_config_dir();
856 defer(SAFE_FREE(config_dir));
857 if (config_dir) {
858 size_t len = strlen(config_dir) + strlen("config.toml") + 1;
859 config_path_expanded = SAFE_MALLOC(len, char *);
860 if (config_path_expanded) {
861#ifdef _WIN32
862 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
863#else
864 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
865#endif
866 }
867 }
868
869 // Fallback to ~/.ascii-chat/config.toml
870 if (!config_path_expanded) {
871 config_path_expanded = expand_path("~/.ascii-chat/config.toml");
872 }
873 }
874
875 if (!config_path_expanded) {
876 if (strict) {
877 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
878 }
879 return ASCIICHAT_OK;
880 }
881
882 char *validated_config_path = NULL;
883 asciichat_error_t validate_result =
884 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
885 if (validate_result != ASCIICHAT_OK) {
886 return validate_result;
887 }
888 SAFE_FREE(config_path_expanded);
889 config_path_expanded = validated_config_path;
890
891 // Determine display path for error messages (before any early returns)
892 const char *display_path = config_path ? config_path : config_path_expanded;
893
894 // Check if config file exists
895 struct stat st;
896 if (stat(config_path_expanded, &st) != 0) {
897 if (strict) {
898 return SET_ERRNO(ERROR_CONFIG, "Config file does not exist: '%s'", display_path);
899 }
900 // File doesn't exist, that's OK - not required (non-strict mode)
901 return ASCIICHAT_OK;
902 }
903
904 // Verify it's a regular file
905 if (!S_ISREG(st.st_mode)) {
906 if (strict) {
907 return SET_ERRNO(ERROR_CONFIG, "Config file exists but is not a regular file: '%s'", display_path);
908 }
909 CONFIG_WARN("Config file exists but is not a regular file: '%s' (skipping)", display_path);
910 return ASCIICHAT_OK;
911 }
912
913 // Parse TOML file
914 toml_result_t result = toml_parse_file_ex(config_path_expanded);
915 defer(toml_free(result));
916
917 if (!result.ok) {
918 // result.errmsg is an array, so check its first character
919 const char *errmsg = (strlen(result.errmsg) > 0) ? result.errmsg : "Unknown parse error";
920
921 if (strict) {
922 // For strict mode, return detailed error message directly
923 // Note: SET_ERRNO stores the message in context, but asciichat_error_string() only returns generic codes
924 // So we need to format the error message ourselves here
925 char error_buffer[512];
926 safe_snprintf(error_buffer, sizeof(error_buffer), "Failed to parse config file '%s': %s", display_path, errmsg);
927 return SET_ERRNO(ERROR_CONFIG, "%s", error_buffer);
928 }
929 CONFIG_WARN("Failed to parse config file '%s': %s (skipping)", display_path, errmsg);
930 return ASCIICHAT_OK; // Non-fatal error
931 }
932
933 // Apply configuration from each section
934 apply_network_config(result.toptab, is_client, opts);
935 apply_client_config(result.toptab, is_client, opts);
936 apply_audio_config(result.toptab, is_client, opts);
937 apply_palette_config_from_toml(result.toptab, opts);
938 asciichat_error_t crypto_result = apply_crypto_config(result.toptab, is_client, opts);
939 if (crypto_result != ASCIICHAT_OK) {
940 return crypto_result;
941 }
942 asciichat_error_t log_result = apply_log_config(result.toptab, opts);
943 if (log_result != ASCIICHAT_OK) {
944 return log_result;
945 }
946
947 // Reset all config flags for next call
948 config_address_set = false;
949 config_address6_set = false;
950 config_port_set = false;
951 config_width_set = false;
952 config_height_set = false;
953 config_webcam_index_set = false;
954 config_webcam_flip_set = false;
955 config_color_mode_set = false;
956 config_render_mode_set = false;
957 config_palette_set = false;
958 config_palette_chars_set = false;
959 config_audio_enabled_set = false;
960 config_microphone_index_set = false;
961 config_speakers_index_set = false;
962 config_stretch_set = false;
963 config_quiet_set = false;
964 config_snapshot_mode_set = false;
965 config_mirror_mode_set = false;
966 config_snapshot_delay_set = false;
967 config_log_file_set = false;
968 config_encrypt_enabled_set = false;
969 config_encrypt_key_set = false;
970 config_password_set = false;
971 config_encrypt_keyfile_set = false;
972 config_no_encrypt_set = false;
973 config_server_key_set = false;
974 config_client_keys_set = false;
975
976 CONFIG_DEBUG("Loaded configuration from %s", display_path);
977 return ASCIICHAT_OK;
978}
979
999asciichat_error_t config_create_default(const char *config_path, const options_t *opts) {
1000 char *config_path_expanded = NULL;
1001 defer(SAFE_FREE(config_path_expanded));
1002
1003 if (config_path) {
1004 // Use custom path provided
1005 config_path_expanded = expand_path(config_path);
1006 if (!config_path_expanded) {
1007 // If expansion fails, try using as-is (might already be absolute)
1008 config_path_expanded = platform_strdup(config_path);
1009 }
1010 } else {
1011 // Use default location with XDG support
1012 char *config_dir = get_config_dir();
1013 defer(SAFE_FREE(config_dir));
1014 if (config_dir) {
1015 size_t len = strlen(config_dir) + strlen("config.toml") + 1;
1016 config_path_expanded = SAFE_MALLOC(len, char *);
1017 if (config_path_expanded) {
1018#ifdef _WIN32
1019 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
1020#else
1021 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
1022#endif
1023 }
1024 }
1025
1026 // Fallback to ~/.ascii-chat/config.toml
1027 if (!config_path_expanded) {
1028 config_path_expanded = expand_path("~/.ascii-chat/config.toml");
1029 }
1030 }
1031
1032 if (!config_path_expanded) {
1033 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1034 }
1035
1036 char *validated_config_path = NULL;
1037 asciichat_error_t validate_result =
1038 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1039 if (validate_result != ASCIICHAT_OK) {
1040 SAFE_FREE(config_path_expanded);
1041 return validate_result;
1042 }
1043 SAFE_FREE(config_path_expanded);
1044 config_path_expanded = validated_config_path;
1045
1046 // Check if file already exists
1047 struct stat st;
1048 if (stat(config_path_expanded, &st) == 0) {
1049 // File exists - ask user if they want to overwrite
1050 log_plain_stderr("Config file already exists: %s", config_path_expanded);
1051
1052 bool overwrite = platform_prompt_yes_no("Overwrite", false); // Default to No
1053 if (!overwrite) {
1054 log_plain_stderr("Config file creation cancelled.");
1055 return SET_ERRNO(ERROR_CONFIG, "User cancelled overwrite");
1056 }
1057
1058 // User confirmed overwrite - continue to create file (will overwrite existing)
1059 log_plain_stderr("Overwriting existing config file...");
1060 }
1061
1062 // Create directory if needed
1063 char *dir_path = platform_strdup(config_path_expanded);
1064 if (!dir_path) {
1065 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
1066 }
1067
1068 // Find the last path separator
1069 char *last_sep = strrchr(dir_path, PATH_DELIM);
1070
1071 if (last_sep) {
1072 *last_sep = '\0';
1073 // Create directory (similar to known_hosts.c approach)
1074#ifdef _WIN32
1075 // Windows: Create directory (creates only one level)
1076 int mkdir_result = _mkdir(dir_path);
1077#else
1078 // POSIX: Create directory with DIR_PERM_PRIVATE permissions
1079 int mkdir_result = mkdir(dir_path, DIR_PERM_PRIVATE);
1080#endif
1081 if (mkdir_result != 0 && errno != EEXIST) {
1082 // mkdir failed and it's not because the directory already exists
1083 // Verify if directory actually exists despite the error (Windows compatibility)
1084 struct stat test_st;
1085 if (stat(dir_path, &test_st) != 0) {
1086 // Directory doesn't exist and we couldn't create it
1087 asciichat_error_t err = SET_ERRNO_SYS(ERROR_CONFIG, "Failed to create config directory: %s", dir_path);
1088 SAFE_FREE(dir_path);
1089 return err;
1090 }
1091 // Directory exists despite error, proceed
1092 }
1093 }
1094 SAFE_FREE(dir_path);
1095
1096 // Create file with default values
1097 FILE *f = platform_fopen(config_path_expanded, "w");
1098 defer(SAFE_FCLOSE(f));
1099 if (!f) {
1100 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to create config file: %s", config_path_expanded);
1101 }
1102
1103 // Write version comment
1104 (void)fprintf(f, "# ascii-chat configuration file\n");
1105 (void)fprintf(f, "# Generated by ascii-chat v%d.%d.%d-%s\n", ASCII_CHAT_VERSION_MAJOR, ASCII_CHAT_VERSION_MINOR,
1106 ASCII_CHAT_VERSION_PATCH, ASCII_CHAT_GIT_VERSION);
1107 (void)fprintf(f, "#\n");
1108 (void)fprintf(f, "# If you upgrade ascii-chat and this version comment changes, you may need to\n");
1109 (void)fprintf(f, "# delete and regenerate this file with: ascii-chat --config-create\n");
1110 (void)fprintf(f, "#\n\n");
1111
1112 // Write network section with defaults
1113 (void)fprintf(f, "[network]\n");
1114 (void)fprintf(f, "# Port number (1-65535, shared between server and client)\n");
1115 (void)fprintf(f, "#port = %s\n\n", opts->port);
1116
1117 // Write server section with bind addresses
1118 (void)fprintf(f, "[server]\n");
1119 (void)fprintf(f, "# IPv4 bind address (default: 127.0.0.1)\n");
1120 (void)fprintf(f, "#bind_ipv4 = \"127.0.0.1\"\n");
1121 (void)fprintf(f, "# IPv6 bind address (default: ::1 for IPv6-only, or :: for dual-stack)\n");
1122 (void)fprintf(f, "#bind_ipv6 = \"::1\"\n");
1123 (void)fprintf(f, "# Legacy bind address (fallback if bind_ipv4/bind_ipv6 not set)\n");
1124 (void)fprintf(f, "#address = \"::\"\n\n");
1125
1126 // Write client section with defaults
1127 (void)fprintf(f, "[client]\n");
1128 (void)fprintf(f, "# Server address to connect to\n");
1129 (void)fprintf(f, "#address = \"%s\"\n", opts->address);
1130 (void)fprintf(f, "# Alternative: set via network.address (legacy)\n");
1131 (void)fprintf(f, "#network.address = \"%s\"\n\n", opts->address);
1132 (void)fprintf(f, "# Terminal width in characters (0 = auto-detect)\n");
1133 (void)fprintf(f, "#width = %hu\n", opts->width);
1134 (void)fprintf(f, "# Terminal height in characters (0 = auto-detect)\n");
1135 (void)fprintf(f, "#height = %hu\n", opts->height);
1136 (void)fprintf(f, "# Webcam device index (0 = first webcam)\n");
1137 (void)fprintf(f, "#webcam_index = %hu\n", opts->webcam_index);
1138 (void)fprintf(f, "# Flip webcam image horizontally\n");
1139 (void)fprintf(f, "#webcam_flip = %s\n", opts->webcam_flip ? "true" : "false");
1140 (void)fprintf(f, "# Color mode: \"none\", \"16\", \"256\", \"truecolor\" (or \"auto\" for auto-detect)\n");
1141 (void)fprintf(f, "#color_mode = \"auto\"\n");
1142 (void)fprintf(f, "# Render mode: \"foreground\", \"background\", \"half-block\"\n");
1143 (void)fprintf(f, "#render_mode = \"foreground\"\n");
1144 (void)fprintf(f, "# Frames per second (1-144, default: 30 for Windows, 60 for Unix)\n");
1145#if defined(_WIN32)
1146 (void)fprintf(f, "#fps = 30\n");
1147#else
1148 (void)fprintf(f, "#fps = 60\n");
1149#endif
1150 (void)fprintf(f, "# Stretch video to terminal size (without preserving aspect ratio)\n");
1151 (void)fprintf(f, "#stretch = %s\n", opts->stretch ? "true" : "false");
1152 (void)fprintf(f, "# Quiet mode (disable console logging)\n");
1153 (void)fprintf(f, "#quiet = %s\n", opts->quiet ? "true" : "false");
1154 (void)fprintf(f, "# Snapshot mode (capture one frame and exit)\n");
1155 (void)fprintf(f, "#snapshot_mode = %s\n", opts->snapshot_mode ? "true" : "false");
1156 (void)fprintf(f, "# Snapshot delay in seconds (for webcam warmup)\n");
1157 (void)fprintf(f, "#snapshot_delay = %.1f\n", (double)opts->snapshot_delay);
1158 (void)fprintf(f, "# Use test pattern instead of real webcam\n");
1159 (void)fprintf(f, "#test_pattern = %s\n", opts->test_pattern ? "true" : "false");
1160 (void)fprintf(f, "# Show terminal capabilities and exit\n");
1161 (void)fprintf(f, "#show_capabilities = %s\n", opts->show_capabilities ? "true" : "false");
1162 (void)fprintf(f, "# Force UTF-8 support\n");
1163 (void)fprintf(f, "#force_utf8 = %s\n\n", opts->force_utf8 ? "true" : "false");
1164
1165 // Write audio section (client only)
1166 (void)fprintf(f, "[audio]\n");
1167 (void)fprintf(f, "# Enable audio streaming\n");
1168 (void)fprintf(f, "#enabled = %s\n", opts->audio_enabled ? "true" : "false");
1169 (void)fprintf(f, "# Microphone device index (-1 = use default)\n");
1170 (void)fprintf(f, "#microphone_index = %d\n", opts->microphone_index);
1171 (void)fprintf(f, "# Speakers device index (-1 = use default)\n");
1172 (void)fprintf(f, "#speakers_index = %d\n\n", opts->speakers_index);
1173
1174 // Write palette section
1175 (void)fprintf(f, "[palette]\n");
1176 (void)fprintf(f, "# Palette type: \"blocks\", \"half-blocks\", \"chars\", \"custom\"\n");
1177 (void)fprintf(f, "#type = \"half-blocks\"\n");
1178 (void)fprintf(f, "# Custom palette characters (only used if type = \"custom\")\n");
1179 (void)fprintf(f, "#chars = \" ...',;:clodxkO0KXNWM\"\n\n");
1180
1181 // Write crypto section
1182 (void)fprintf(f, "[crypto]\n");
1183 (void)fprintf(f, "# Enable encryption\n");
1184 (void)fprintf(f, "#encrypt_enabled = %s\n", opts->encrypt_enabled ? "true" : "false");
1185 (void)fprintf(f, "# Encryption key identifier (e.g., \"gpg:keyid\" or \"github:username\")\n");
1186 (void)fprintf(f, "#key = \"%s\"\n", opts->encrypt_key);
1187 (void)fprintf(f, "# Password for encryption (WARNING: storing passwords in config files is insecure!)\n");
1188 (void)fprintf(f, "# Use CLI --password or environment variables instead.\n");
1189 (void)fprintf(f, "#password = \"%s\"\n", opts->password);
1190 (void)fprintf(f, "# Key file path\n");
1191 (void)fprintf(f, "#keyfile = \"%s\"\n", opts->encrypt_keyfile);
1192 (void)fprintf(f, "# Disable encryption (opt-out)\n");
1193 (void)fprintf(f, "#no_encrypt = %s\n", opts->no_encrypt ? "true" : "false");
1194 (void)fprintf(f, "# Server public key (client only)\n");
1195 (void)fprintf(f, "#server_key = \"%s\"\n", opts->server_key);
1196 (void)fprintf(f, "# Client keys directory (server only)\n");
1197 (void)fprintf(f, "#client_keys = \"%s\"\n\n", opts->client_keys);
1198
1199 // Write logging section
1200 (void)fprintf(f, "[logging]\n");
1201 (void)fprintf(f, "# Log file path (empty string = no file logging)\n");
1202 (void)fprintf(f, "#log_file = \"%s\"\n", opts->log_file);
1203
1204 return ASCIICHAT_OK;
1205}
1206
1207asciichat_error_t config_load_system_and_user(bool is_client, const char *user_config_path, bool strict,
1208 options_t *opts) {
1209 // Fallback for ASCIICHAT_INSTALL_PREFIX if paths.h hasn't been generated yet
1210 // (prevents defer tool compilation errors during initial builds)
1211#ifndef ASCIICHAT_INSTALL_PREFIX
1212#define ASCIICHAT_INSTALL_PREFIX "/usr"
1213#endif
1214
1215 // Build system config path: ${INSTALL_PREFIX}/etc/ascii-chat/config.toml
1216 char system_config_path[1024];
1217#ifdef _WIN32
1218 SAFE_SNPRINTF(system_config_path, sizeof(system_config_path), "%s\\etc\\ascii-chat\\config.toml",
1220#else
1221 SAFE_SNPRINTF(system_config_path, sizeof(system_config_path), "%s/etc/ascii-chat/config.toml",
1223#endif
1224
1225 // Load system config first (non-strict - it's optional)
1226 CONFIG_DEBUG("Attempting to load system config from: %s", system_config_path);
1227 asciichat_error_t system_result = config_load_and_apply(is_client, system_config_path, false, opts);
1228 if (system_result == ASCIICHAT_OK) {
1229 CONFIG_DEBUG("System config loaded successfully");
1230 } else {
1231 CONFIG_DEBUG("System config not loaded (this is normal if file doesn't exist)");
1232 // Clear the error context since this failure is expected/non-fatal
1233 CLEAR_ERRNO();
1234 }
1235
1236 // Load user config second (with user-specified strictness)
1237 // User config values will override system config values
1238 CONFIG_DEBUG("Loading user config (strict=%s)", strict ? "true" : "false");
1239 asciichat_error_t user_result = config_load_and_apply(is_client, user_config_path, strict, opts);
1240
1241 // Return user config result - errors in user config should be reported
1242 return user_result;
1243}
#define ASCIICHAT_INSTALL_PREFIX
#define CONFIG_DEBUG(fmt,...)
Print configuration debug message.
Definition config.c:59
#define CONFIG_WARN(fmt,...)
Print configuration warning to stderr.
Definition config.c:47
TOML configuration file support for ascii-chat.
Defer macro definition for source-to-source transformation.
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:412
#define SAFE_FCLOSE(fp)
Definition common.h:330
asciichat_error_t config_create_default(const char *config_path, const options_t *opts)
Create default configuration file with all default values.
Definition config.c:999
asciichat_error_t config_load_system_and_user(bool is_client, const char *user_config_path, bool strict, options_t *opts)
Load system config first, then user config (user config overrides system)
Definition config.c:1207
asciichat_error_t config_load_and_apply(bool is_client, const char *config_path, bool strict, options_t *opts)
Main function to load configuration from file and apply to global options.
Definition config.c:842
#define defer(action)
Defer a cleanup action until function scope exit.
Definition defer.h:36
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
#define CLEAR_ERRNO()
Clear the current error state.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_MEMORY
Definition error_codes.h:53
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_CONFIG
Definition error_codes.h:54
int g_max_fps
Runtime configurable maximum frame rate (can be overridden via environment or command line)
Definition common.c:30
#define log_plain_stderr(...)
Plain logging to stderr with newline.
#define log_file(...)
File-only logging - writes to log file only, no stderr output.
int validate_opt_device_index(const char *value_str, char *error_msg, size_t error_msg_size)
Validate device index (-1 for default, 0+ for specific device)
Definition validation.c:422
int validate_opt_color_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Validate color mode string.
Definition validation.c:94
int validate_opt_fps(const char *value_str, char *error_msg, size_t error_msg_size)
Validate FPS value (1-144)
Definition validation.c:349
int validate_opt_palette(const char *value_str, char *error_msg, size_t error_msg_size)
Validate palette type string.
Definition validation.c:156
int validate_opt_positive_int(const char *value_str, char *error_msg, size_t error_msg_size)
Validate positive integer.
Definition validation.c:50
int validate_opt_password(const char *value_str, char *error_msg, size_t error_msg_size)
Validate password (8-256 characters)
Definition validation.c:454
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
Definition options.h:176
float validate_opt_float_non_negative(const char *value_str, char *error_msg, size_t error_msg_size)
Validate non-negative float value.
Definition validation.c:276
int validate_opt_non_negative_int(const char *value_str, char *error_msg, size_t error_msg_size)
Validate non-negative integer.
Definition validation.c:72
int validate_opt_render_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Validate render mode string.
Definition validation.c:128
int validate_opt_port(const char *value_str, char *error_msg, size_t error_msg_size)
Validate port number (1-65535)
Definition validation.c:26
int validate_opt_ip_address(const char *value_str, char *parsed_address, size_t address_size, bool is_client, char *error_msg, size_t error_msg_size)
Validate IP address or hostname.
Definition validation.c:224
palette_type_t
Built-in palette type enumeration.
Definition palette.h:84
@ PALETTE_CUSTOM
User-defined via –palette-chars.
Definition palette.h:96
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition system.h:649
FILE * platform_fopen(const char *filename, const char *mode)
Safe file open stream (fopen replacement)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
bool platform_prompt_yes_no(const char *prompt, bool default_yes)
Prompt the user for a yes/no answer.
#define PATH_DELIM
Platform-specific path separator character.
Definition system.h:605
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
render_mode_t
Render mode preferences.
Definition terminal.h:467
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:424
int errno
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:439
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:498
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:183
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:223
@ PATH_ROLE_CONFIG_FILE
Definition path.h:254
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:258
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:257
@ PATH_ROLE_LOG_FILE
Definition path.h:255
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:256
📝 Logging API with multiple log levels and terminal output control
Validation functions for options parsing.
⚙️ Command-line options parsing and configuration management for ascii-chat
📂 Path Manipulation Utilities
Cross-platform interactive prompting utilities.
Consolidated options structure.
Definition options.h:439
terminal_color_mode_t color_mode
Color mode (auto/none/16/256/truecolor)
Definition options.h:507
char port[256]
Server port number.
Definition options.h:464
unsigned short int no_encrypt
Disable encryption (opt-out)
Definition options.h:545
char password[256]
Password string.
Definition options.h:543
int microphone_index
Microphone device index (-1 = default)
Definition options.h:517
unsigned short int force_utf8
Force UTF-8 support.
Definition options.h:510
unsigned short int quiet
Quiet mode (suppress logs)
Definition options.h:530
unsigned short int encrypt_enabled
Enable encryption.
Definition options.h:541
unsigned short int webcam_index
Webcam device index (0 = first)
Definition options.h:499
unsigned short int width
Terminal width in characters.
Definition options.h:454
bool auto_height
Auto-detect height from terminal.
Definition options.h:457
char palette_custom[256]
Custom palette characters.
Definition options.h:582
unsigned short int height
Terminal height in characters.
Definition options.h:455
bool palette_custom_set
True if custom palette was set.
Definition options.h:583
render_mode_t render_mode
Render mode (foreground/background/half-block)
Definition options.h:508
char server_key[256]
Expected server public key (client)
Definition options.h:546
unsigned short int show_capabilities
Show terminal capabilities and exit.
Definition options.h:509
char encrypt_keyfile[256]
Alternative key file path.
Definition options.h:544
char address6[256]
IPv6 bind address (server only)
Definition options.h:463
int speakers_index
Speakers device index (-1 = default)
Definition options.h:518
double snapshot_delay
Snapshot delay in seconds.
Definition options.h:533
unsigned short int audio_enabled
Enable audio streaming.
Definition options.h:516
bool test_pattern
Use test pattern instead of webcam.
Definition options.h:501
bool webcam_flip
Flip webcam image horizontally.
Definition options.h:500
unsigned short int snapshot_mode
Snapshot mode (one frame and exit)
Definition options.h:532
palette_type_t palette_type
Selected palette type.
Definition options.h:581
unsigned short int stretch
Allow aspect ratio distortion.
Definition options.h:525
char encrypt_key[256]
SSH/GPG key file path.
Definition options.h:542
char log_file[256]
Log file path.
Definition options.h:535
char address[256]
Server address (client) or bind address (server)
Definition options.h:462
bool auto_width
Auto-detect width from terminal.
Definition options.h:456
char client_keys[256]
Allowed client keys (server)
Definition options.h:547
Cross-platform system functions interface for ascii-chat.
🖥️ Cross-platform terminal interface for ascii-chat
Common SIMD utilities and structures.