ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
Config Module

The toml text file configuration file module. More...

Files

file  config.c
 📋 TOML configuration file parser with schema validation and CLI override support
 
file  config.h
 TOML configuration file support for ascii-chat.
 

Functions

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.
 
asciichat_error_t config_create_default (const char *config_path, const options_t *opts)
 Create default configuration file with all default values.
 
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)
 

Detailed Description

The toml text file configuration file module.

This module provides functionality for loading configuration from TOML files (typically located at ~/.ascii-chat/config.toml). Configuration values are applied to global options, but command-line arguments always take precedence over config file values.

The interface provides:

Note
Configuration Priority: Command-line arguments override config file values. Config file values override default values. The config file is loaded before CLI argument parsing to ensure this precedence.
Configuration File Location: The config file is loaded from the ascii-chat configuration directory:
  • Unix: $XDG_CONFIG_HOME/ascii-chat/config.toml if set, otherwise ~/.ascii-chat/config.toml
  • Windows: APPDATA%\ascii-chat\config.toml if set, otherwise ~\.ascii-chat\config.toml
Error Handling: Config file parsing errors are non-fatal. If the file is missing, malformed, or contains invalid values, warnings are printed to stderr and the application continues with default values. Invalid individual values are skipped with warnings, but valid values are still applied.
Validation: All configuration values are validated using the same validation functions used by CLI argument parsing, ensuring consistency between config file and CLI option handling.
Warning
Password Storage: While passwords can be stored in the config file (via crypto.password), this is strongly discouraged for security reasons. A warning is printed if a password is found in the config file. Use CLI --password or environment variables instead.
File Permissions: Users should secure their config file to prevent unauthorized access, especially if it contains sensitive information like encryption keys or passwords.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Configuration README

Command-line argument parsing and TOML-based configuration file management for flexible server and client setup.

Overview

ascii-chat supports two configuration methods:

  1. Command-line arguments: Quick configuration for ad-hoc usage
  2. TOML configuration files: Persistent, structured configuration

Command-line arguments override configuration file settings, allowing:

  • Default configuration in ~/.config/ascii-chat/config.toml
  • Per-session overrides via command-line flags
  • Easy scripting and automation

TOML Configuration Files

ascii-chat uses TOML (Tom's Obvious, Minimal Language) for configuration files, parsed by the tomlc17 library.

File Format

Complete example configuration:

# ascii-chat Configuration File
# Location: ~/.config/ascii-chat/config.toml
[server]
# Bind address ("::" for all IPv6/IPv4, "0.0.0.0" for IPv4 only)
address = "::"
# Server port (default: 27224)
port = 27224
# Maximum simultaneous clients (1-9)
max_clients = 9
# Server identity key (SSH Ed25519 format)
key_file = "~/.ssh/ascii-chat-server"
# Authorized client public keys (SSH format or github:username)
# Leave empty to allow all clients (less secure)
client_keys = [
"github:alice",
"github:bob",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... charlie@example.com"
]
[client]
# Server address to connect to
address = "localhost"
# Server port
port = 27224
# Expected server public key (for TOFU verification)
# Leave empty to accept any server (prompts for confirmation)
server_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
# Client identity key
key_file = "~/.ssh/id_ed25519"
# Connection timeout (seconds)
timeout = 10
[video]
# Terminal width (auto-detected if not specified)
width = 160
# Terminal height (auto-detected if not specified)
height = 45
# Enable color output (true/false)
color = true
# Use half-block characters for 2x vertical resolution
half_blocks = true
# ASCII palette (characters from dark to bright)
palette = " .:-=+*#%@"
# Webcam device index (0 = first camera)
webcam_device = 0
# Capture resolution (before scaling to terminal size)
capture_width = 1920
capture_height = 1080
# Target framerate
fps = 30
[audio]
# Enable audio capture/playback
enabled = true
# Sample rate (Hz)
sample_rate = 48000
# Audio channels (1=mono, 2=stereo)
channels = 1
# PortAudio buffer size (frames)
buffer_size = 512
# Audio input device (-1 = default)
input_device = -1
# Audio output device (-1 = default)
output_device = -1
[logging]
# Log file path (empty = stderr only)
file = "/tmp/ascii-chat.log"
# Log level (debug, info, warn, error, fatal)
level = "info"
# Enable color in log output
color = true

Loading Configuration

Load Config File:

#include "config.h"
#include "options.h"
options_t opts;
asciichat_error_t err = config_load(&opts, "~/.config/ascii-chat/config.toml");
if (err != ASCIICHAT_OK) {
log_error("Failed to load config file");
return err;
}
// Configuration is now available in opts structure
log_info("Server address: %s:%d", opts.address, opts.port);
log_info("Max clients: %d", opts.max_clients);
log_info("Video: %dx%d @ %d FPS", opts.width, opts.height, opts.fps);
// Cleanup
options_destroy(&opts);
TOML configuration file support for ascii-chat.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ASCIICHAT_OK
Definition error_codes.h:48
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
⚙️ Command-line options parsing and configuration management for ascii-chat
Consolidated options structure.
Definition options.h:439
char port[256]
Server port number.
Definition options.h:464
unsigned short int width
Terminal width in characters.
Definition options.h:454
unsigned short int height
Terminal height in characters.
Definition options.h:455
int max_clients
Maximum concurrent clients (server only)
Definition options.h:465
int fps
Target framerate (1-144, 0=use default)
Definition options.h:511
char address[256]
Server address (client) or bind address (server)
Definition options.h:462

Apply Defaults:

// Initialize with default values
options_t opts = {0};
apply_default_options(&opts);

Command-Line Options

Server Options

# Start server with defaults
./ascii-chat server
# Bind to specific address and port
./ascii-chat server :: --port 27224
# Bind to IPv4 only
./ascii-chat server 0.0.0.0 --port 27224
# Use server identity key
./ascii-chat server --key ~/.ssh/ascii-chat-server
# Authorize specific clients
./ascii-chat server --client-keys github:alice,github:bob
# Limit maximum clients
./ascii-chat server --max-clients 4
# Enable color and set log file
./ascii-chat server --color --log-file /tmp/server.log
# Use configuration file
./ascii-chat --config ~/.config/ascii-chat/server.toml server

Server-specific flags:

  • [address1] [address2] (positional): Bind addresses, 0-2 IPv4/IPv6 addresses (default: 127.0.0.1 and ::1)
  • --port, -p PORT: Listen port (default: 27224)
  • --key KEY_FILE: Server identity key (SSH Ed25519)
  • --client-keys KEYS: Comma-separated authorized client keys
  • --max-clients N: Maximum simultaneous clients (1-9, default: 9)

Client Options

# Connect to localhost (default)
./ascii-chat client
# Connect to specific server
./ascii-chat client example.com:27224
# Specify terminal dimensions
./ascii-chat client --width 160 --height 45
# Enable color and half-blocks
./ascii-chat client --color --half-blocks
# Use custom ASCII palette
./ascii-chat client --palette " .:-=+*#%@"
# Enable audio
./ascii-chat client --audio
# Verify server identity
./ascii-chat client --server-key ~/.ssh/server_key.pub
# Use client identity key
./ascii-chat client --key ~/.ssh/id_ed25519
# Snapshot mode (capture and exit)
./ascii-chat client --snapshot --snapshot-delay 10
# Use configuration file
./ascii-chat --config ~/.config/ascii-chat/client.toml client example.com

Client-specific flags:

  • [address][:port] (positional): Server address with optional port (default: localhost:27224)
  • --port, -p PORT: Server port (default: 27224) - conflicts with port in positional argument
  • --width, -w WIDTH: Terminal width (default: auto-detect)
  • --height, -h HEIGHT: Terminal height (default: auto-detect)
  • --color, -c: Enable color output
  • --half-blocks: Use half-block characters (2x vertical resolution)
  • --palette, -P PALETTE: ASCII palette type (standard, blocks, digital, minimal, cool, custom)
  • --palette-chars, -C CHARS: Custom palette characters string
  • --audio, -A: Enable audio capture/playback
  • --server-key KEY: Expected server public key (TOFU)
  • --key KEY_FILE: Client identity key
  • --snapshot: Snapshot mode (capture once and exit)
  • --snapshot-delay SECONDS: Snapshot duration before exit

Common Options

Available for both server and client:

  • --config FILE: Configuration file path
  • –log-file FILE: Log file path
  • –help, -?: Show help message
  • –version, -v: Show version information

Default Config Locations

All ascii-chat data files (config.toml, known_hosts) are stored in a single directory:

Linux/macOS:

  • $XDG_CONFIG_HOME/ascii-chat/ (if XDG_CONFIG_HOME is set)
  • ~/.ascii-chat/ (default when XDG_CONFIG_HOME is not set)

Windows:

  • APPDATA%\ascii-chat\ (if APPDATA is set)
  • ~\.ascii-chat\ (default when APPDATA is not set)

Creating a Default Config:

# Create config at default location
./ascii-chat --config-create
# Create config at custom location
./ascii-chat --config-create ~/.my-config.toml
# Overwrite existing config (scripted - supports piped input)
echo "y" | ./ascii-chat --config-create
yes | ./ascii-chat --config-create /tmp/server.toml

Using a Config File:

./ascii-chat --config ~/.ascii-chat/config.toml server
./ascii-chat --config ~/.ascii-chat/config.toml client example.com

Overriding Config with CLI:

# Config file sets port=27224, but override it with --port
./ascii-chat --config ~/.ascii-chat/config.toml client --port 8080

Configuration Precedence

Settings are applied in this order (later overrides earlier):

  1. Built-in defaults - Hardcoded default values
  2. Config file - Values from TOML configuration (if specified with –config)
  3. Environment variables - LOG_LEVEL, etc.
  4. Command-line arguments - Highest priority

Example:

# Config file has port=27224
# Environment has LOG_LEVEL=1
# Command line has --port 8080
# Result: port=8080 (CLI wins), log_level=1 (from env)

Configuration Validation

The configuration system automatically validates all settings:

Automatic Validation:

  • Port range validation (1-65535)
  • Webcam index validation
  • File path validation (key files, config files)
  • Log level validation (0-4)
  • IP address format validation
  • Dimensions validation (width: 20-500, height: 10-200)
  • Client limits (max_clients: 1-9, grid layout constraint)
  • Tilde expansion for home directory (~/)
  • SSH keys must be Ed25519 format
  • Palette strings (must have at least 2 characters, ordered dark → bright)
  • UTF-8 validation for multi-byte characters

Error Reporting:

asciichat_error_t err = config_load(&opts, "~/.ascii-chat/config.toml");
if (err != ASCIICHAT_OK) {
if (HAS_ERRNO(&ctx)) {
log_error("Invalid configuration: %s", ctx.context_message);
}
return err;
}
#define HAS_ERRNO(var)
Check if an error occurred and get full context.
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)

Example validation code:

asciichat_error_t validate_config(const options_t *opts) {
if (opts->port < 1 || opts->port > 65535) {
return SET_ERRNO(ERROR_CONFIG_INVALID,
"Port must be 1-65535 (got %d)", opts->port);
}
if (opts->max_clients < 1 || opts->max_clients > 9) {
return SET_ERRNO(ERROR_CONFIG_INVALID,
"max_clients must be 1-9 (got %d)", opts->max_clients);
}
if (opts->palette && strlen(opts->palette) < 2) {
return SET_ERRNO(ERROR_CONFIG_INVALID,
"Palette must have at least 2 characters");
}
return ASCIICHAT_OK;
}
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.

Usage Examples

Parsing Command-Line and Config File

int main(int argc, char **argv) {
options_t opts;
// Parse command-line arguments
err = options_parse(&opts, argc, argv);
if (err != ASCIICHAT_OK) {
fprintf(stderr, "Usage: %s [server|client] [OPTIONS]\n", argv[0]);
return 1;
}
// Load configuration file if specified
if (opts.config_file) {
err = config_load(&opts, opts.config_file);
if (err != ASCIICHAT_OK) {
log_error("Failed to load config: %s", opts.config_file);
options_destroy(&opts);
return 1;
}
}
// Validate configuration
err = validate_config(&opts);
if (err != ASCIICHAT_OK) {
if (HAS_ERRNO(&ctx)) {
log_error("Invalid configuration: %s", ctx.context_message);
}
options_destroy(&opts);
return 1;
}
// Use configuration...
if (opts.mode == MODE_SERVER) {
run_server(&opts);
} else {
run_client(&opts);
}
// Cleanup
options_destroy(&opts);
return 0;
}
int main(int argc, char **argv)
Definition acds/main.c:55
@ MODE_SERVER
Server mode - network server options.
Definition options.h:427

Setting Defaults

void options_set_defaults(options_t *opts) {
memset(opts, 0, sizeof(*opts));
// Network defaults
opts->port = DEFAULT_PORT; // 27224
opts->address = strdup("::"); // Dual-stack IPv6/IPv4
// Video defaults
opts->width = DEFAULT_WIDTH; // 80 (or auto-detect)
opts->height = DEFAULT_HEIGHT; // 24 (or auto-detect)
opts->fps = DEFAULT_FPS; // 30
opts->color = true;
opts->half_blocks = false;
opts->palette = strdup(" .:-=+*#%@");
// Audio defaults
opts->audio = false; // Disabled by default for client
opts->sample_rate = SAMPLE_RATE; // 48000
opts->audio_channels = AUDIO_CHANNELS; // 1 (mono)
// Logging defaults
opts->log_color = true;
}
#define AUDIO_CHANNELS
Number of audio channels (1 = mono)
@ LOG_INFO
Definition log/logging.h:62
log_level_t log_level
Log level threshold.
Definition options.h:536
See also
Options for command-line argument parsing
options.h
config.h

Function Documentation

◆ config_create_default()

asciichat_error_t config_create_default ( const char *  config_path,
const options_t opts 
)

#include <config.c>

Create default configuration file with all default values.

Parameters
config_pathPath to config file to create (NULL uses default location)
optsOptions structure with values to write (use defaults for a "default" config)
Returns
ASCIICHAT_OK on success, error code on failure

Creates a new configuration file at the specified path (or default location if config_path is NULL) with all configuration options set to values from opts.

The created file includes:

  • Version comment at the top (current ascii-chat version)
  • All supported configuration sections with values from opts
  • Comments explaining each option
Note
The function will create the directory structure if needed.
If the file already exists, it will not be overwritten (returns error).

Definition at line 999 of file config.c.

999 {
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}
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_FCLOSE(fp)
Definition common.h:330
#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.
@ ERROR_MEMORY
Definition error_codes.h:53
@ ERROR_CONFIG
Definition error_codes.h:54
#define log_plain_stderr(...)
Plain logging to stderr with newline.
#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)
int errno
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
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
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
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
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 client_keys[256]
Allowed client keys (server)
Definition options.h:547

References options_state::address, ASCIICHAT_OK, options_state::audio_enabled, options_state::client_keys, defer, DIR_PERM_PRIVATE, options_state::encrypt_enabled, options_state::encrypt_key, options_state::encrypt_keyfile, errno, ERROR_CONFIG, ERROR_MEMORY, expand_path(), options_state::force_utf8, get_config_dir(), options_state::height, options_state::log_file, log_plain_stderr, options_state::microphone_index, options_state::no_encrypt, options_state::password, PATH_DELIM, PATH_ROLE_CONFIG_FILE, path_validate_user_path(), platform_fopen(), platform_prompt_yes_no(), platform_strdup(), options_state::port, options_state::quiet, SAFE_FCLOSE, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), options_state::server_key, SET_ERRNO, SET_ERRNO_SYS, options_state::show_capabilities, options_state::snapshot_delay, options_state::snapshot_mode, options_state::speakers_index, options_state::stretch, options_state::test_pattern, options_state::webcam_flip, options_state::webcam_index, and options_state::width.

Referenced by options_init().

◆ config_load_and_apply()

asciichat_error_t config_load_and_apply ( bool  is_client,
const char *  config_path,
bool  strict,
options_t opts 
)

#include <config.c>

Main function to load configuration from file and apply to global options.

Load configuration from TOML file and apply to global options.

Parameters
is_clienttrue if loading client configuration, false for server configuration
config_pathOptional path to config file (NULL uses default location)
strictIf true, errors are fatal; if false, errors are non-fatal warnings
Returns
ASCIICHAT_OK on success, error code on failure (if strict) or non-fatal (if !strict)

This is the main entry point for configuration loading. It:

  1. Expands the config file path (default location or custom path)
  2. Checks if the file exists and is a regular file
  3. Parses the TOML file using tomlc17
  4. Applies configuration from each section (network, client, palette, crypto, logging)
  5. Frees resources and returns

Configuration file errors are non-fatal if strict is false:

  • Missing file: Returns ASCIICHAT_OK (config file is optional)
  • Not a regular file: Warns and returns ASCIICHAT_OK
  • Parse errors: Warns and returns ASCIICHAT_OK
  • Invalid values: Individual values are skipped with warnings

If strict is true, any error causes immediate return with error code.

Note
This function should be called before options_init() parses command-line arguments to ensure CLI arguments can override config file values.
Configuration warnings are printed to stderr because logging may not be initialized yet when this function is called.
Parameters
is_clienttrue if loading client configuration, false for server configuration
config_pathOptional path to config file (NULL uses default location)
strictIf true, errors are fatal; if false, errors are non-fatal warnings
Returns
ASCIICHAT_OK on success, error code on failure (if strict) or non-fatal (if !strict)

Loads configuration from the specified path (or default location if config_path is NULL) and applies values to global options.

Default config file location (when config_path is NULL):

  • Unix: $XDG_CONFIG_HOME/ascii-chat/config.toml if set, otherwise ~/.ascii-chat/config.toml
  • Windows: APPDATA%\ascii-chat\config.toml if set, otherwise ~.ascii-chat\config.toml

Only applies configuration values that haven't already been set (though in practice, CLI arguments will override config values anyway since this is called before CLI parsing).

Supported configuration sections:

  • [network]: port
  • [server]: bind_ipv4, bind_ipv6
  • [client]: address, width, height, webcam_index, webcam_flip, color_mode, render_mode, fps, stretch, quiet, snapshot_mode, snapshot_delay, test_pattern, show_capabilities, force_utf8
  • [audio]: enabled, device
  • [palette]: type, chars
  • [crypto]: encrypt_enabled, key, password, keyfile, no_encrypt, server_key (client only), client_keys (server only)
  • [logging] or root: log_file
Note
This function should be called before options_init() parses command-line arguments, so that CLI arguments can override config file values.
If strict is false and the config file doesn't exist, is not a regular file, or fails to parse, the function returns ASCIICHAT_OK (non-fatal). Individual invalid values are skipped with warnings, but valid values are still applied.
If strict is true, any error (file not found, parse error, etc.) causes the function to return an error code immediately.
Warning
Configuration warnings are printed directly to stderr because logging may not be initialized yet when this function is called.

Definition at line 842 of file config.c.

842 {
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}
#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

References ASCIICHAT_OK, CONFIG_DEBUG, CONFIG_WARN, defer, ERROR_CONFIG, expand_path(), get_config_dir(), PATH_ROLE_CONFIG_FILE, path_validate_user_path(), platform_strdup(), SAFE_FREE, SAFE_MALLOC, safe_snprintf(), and SET_ERRNO.

Referenced by config_load_system_and_user().

◆ config_load_system_and_user()

asciichat_error_t config_load_system_and_user ( bool  is_client,
const char *  user_config_path,
bool  strict,
options_t opts 
)

#include <config.h>

Load system config first, then user config (user config overrides system)

Parameters
is_clienttrue if loading client configuration, false for server configuration
user_config_pathOptional path to user config file (NULL uses default location)
strictIf true, user config errors are fatal; system config is always non-strict
optsOptions structure to write configuration to
Returns
ASCIICHAT_OK on success, error code on failure

Loads configuration from two locations in order:

  1. System config: ${INSTALL_PREFIX}/etc/ascii-chat/config.toml (non-strict, optional)
  2. User config: custom path or default location (strictness as specified)

User config values override system config values. Both override defaults. This function should be called before options_init() parses command-line arguments.

Note
System config is always loaded non-strict (missing file is not an error)
User config strictness follows the strict parameter

Definition at line 1207 of file config.c.

1208 {
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 SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:412
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 CLEAR_ERRNO()
Clear the current error state.

References ASCIICHAT_INSTALL_PREFIX, ASCIICHAT_OK, CLEAR_ERRNO, CONFIG_DEBUG, config_load_and_apply(), and SAFE_SNPRINTF.

Referenced by options_init().