38#pragma GCC diagnostic push
39#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
40static struct log_context_t {
45 _Atomic
size_t current_size;
46 _Atomic
bool terminal_output_enabled;
47 _Atomic
bool level_manually_set;
48 _Atomic
bool force_stderr;
49 _Atomic
bool terminal_locked;
50 _Atomic
uint64_t terminal_owner_thread;
51 _Atomic
unsigned int flush_delay_ms;
53 _Atomic
bool rotation_mutex_initialized;
60 .terminal_output_enabled =
true,
61 .level_manually_set =
false,
62 .force_stderr =
false,
63 .terminal_locked =
false,
64 .terminal_owner_thread = 0,
66 .rotation_mutex_initialized =
false,
68#pragma GCC diagnostic pop
71static const char *level_strings[] = {
"DEV",
"DEBUG",
"INFO",
"WARN",
"ERROR",
"FATAL"};
77#define LOG_COLOR_COUNT 7
82 {
"\x1b[34m",
"\x1b[36m",
"\x1b[32m",
"\x1b[33m",
"\x1b[31m",
"\x1b[35m",
"\x1b[0m"},
84 {
"\x1b[94m",
"\x1b[96m",
"\x1b[92m",
"\x1b[33m",
"\x1b[31m",
"\x1b[35m",
"\x1b[0m"},
86 {
"\x1b[38;2;150;100;205m",
"\x1b[38;2;30;205;255m",
"\x1b[38;2;50;205;100m",
"\x1b[38;2;205;185;40m",
87 "\x1b[38;2;205;30;100m",
"\x1b[38;2;205;80;205m",
"\x1b[0m"},
92#define LOGGING_INTERNAL_ERROR(error, message, ...) \
94 asciichat_set_errno_with_message(error, NULL, 0, NULL, message, ##__VA_ARGS__); \
95 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
96 safe_fprintf(stderr, "%s%s%s: %s", log_level_color(LOG_COLOR_ERROR), msg_header, log_level_color(LOG_COLOR_RESET), \
98 platform_write(g_log.file, msg_header, strlen(msg_header)); \
99 platform_write(g_log.file, message, strlen(message)); \
100 platform_write(g_log.file, "\n", 1); \
101 platform_print_backtrace(0); \
104#define LOGGING_INTERNAL_ERROR(error, message, ...) \
106 asciichat_set_errno_with_message(error, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__); \
107 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
108 safe_fprintf(stderr, "%s%s%s: %s", log_level_color(LOG_COLOR_ERROR), msg_header, log_level_color(LOG_COLOR_RESET), \
110 platform_write(g_log.file, msg_header, strlen(msg_header)); \
111 platform_write(g_log.file, message, strlen(message)); \
112 platform_write(g_log.file, "\n", 1); \
113 platform_print_backtrace(0); \
119static bool g_terminal_caps_initialized =
false;
120static bool g_terminal_caps_detecting =
false;
123static bool g_shutdown_saved_terminal_output =
true;
124static bool g_shutdown_in_progress =
false;
129 (void)clock_gettime(CLOCK_REALTIME, &ts);
135 size_t len = strftime(time_buf, 32,
"%H:%M:%S", &tm_info);
136 if (len == 0 || len >= 32) {
142 long microseconds = ts.tv_nsec / 1000;
143 if (microseconds < 0)
145 if (microseconds > 999999)
146 microseconds = 999999;
148 int result = snprintf(time_buf + len, 32 - len,
".%06ld", microseconds);
149 if (result < 0 || result >= (
int)(32 - len)) {
154 return len + (size_t)result;
164 va_copy(args_copy, args);
165 int size = vsnprintf(NULL, 0, format, args_copy);
175 int result = vsnprintf(message, (
size_t)size + 1, format, args);
218 log_warn(
"Invalid LOG_LEVEL: %s", env_level);
226static void rotate_log_locked(
void) {
227 int file = atomic_load(&g_log.file);
228 size_t current_size = atomic_load(&g_log.current_size);
230 if (file < 0 || file == STDERR_FILENO || strlen(g_log.filename) == 0) {
239 atomic_store(&g_log.file, -1);
244 safe_fprintf(stderr,
"Failed to open log file for tail rotation: %s\n", g_log.filename);
247 atomic_store(&g_log.file, fd);
248 atomic_store(&g_log.current_size, 0);
254 if (current_size < keep_size) {
258 atomic_store(&g_log.file, fd);
259 atomic_store(&g_log.current_size, 0);
262 if (lseek(read_file, (off_t)(current_size - keep_size), SEEK_SET) == (off_t)-1) {
266 atomic_store(&g_log.file, fd);
267 atomic_store(&g_log.current_size, 0);
279 int result = snprintf(temp_filename,
sizeof(temp_filename),
"%s.tmp", g_log.filename);
280 if (result <= 0 || result >= (
int)
sizeof(temp_filename)) {
284 atomic_store(&g_log.file, fd);
293 atomic_store(&g_log.file, fd);
294 atomic_store(&g_log.current_size, 0);
302 while ((bytes_read =
platform_read(read_file, buffer,
sizeof(buffer))) > 0) {
303 ssize_t written =
platform_write(temp_file, buffer, (
size_t)bytes_read);
304 if (written != bytes_read) {
307 unlink(temp_filename);
310 atomic_store(&g_log.file, fd);
311 atomic_store(&g_log.current_size, 0);
314 new_size += (size_t)bytes_read;
321 if (rename(temp_filename, g_log.filename) != 0) {
322 unlink(temp_filename);
325 atomic_store(&g_log.file, fd);
326 atomic_store(&g_log.current_size, 0);
334 atomic_store(&g_log.file, STDERR_FILENO);
335 g_log.filename[0] =
'\0';
336 atomic_store(&g_log.current_size, 0);
339 atomic_store(&g_log.file, new_fd);
340 atomic_store(&g_log.current_size, new_size);
348 if (log_msg_len > 0 && log_msg_len < (
int)
sizeof(
log_msg)) {
354static void maybe_rotate_log(
void) {
356 if (!atomic_load(&g_log.rotation_mutex_initialized)) {
362 size_t used = 0, capacity = 0;
365 if (used > capacity * 9 / 10) {
375 size_t current_size = atomic_load(&g_log.current_size);
388 if (!atomic_load(&g_log.rotation_mutex_initialized)) {
390 atomic_store(&g_log.rotation_mutex_initialized,
true);
394 atomic_store(&g_log.force_stderr, force_stderr);
395 bool preserve_terminal_output = atomic_load(&g_log.terminal_output_enabled);
398 int old_file = atomic_load(&g_log.file);
399 if (atomic_load(&g_log.initialized) && old_file >= 0 && old_file != STDERR_FILENO) {
401 atomic_store(&g_log.file, -1);
405 const char *env_level_str =
SAFE_GETENV(
"LOG_LEVEL");
407 atomic_store(&g_log.level, (
int)parse_log_level_from_env());
409 atomic_store(&g_log.level, (
int)level);
412 atomic_store(&g_log.level_manually_set,
false);
413 atomic_store(&g_log.current_size, 0);
416 SAFE_STRNCPY(g_log.filename, filename,
sizeof(g_log.filename) - 1);
422 atomic_store(&g_log.file, -1);
425 if (preserve_terminal_output) {
426 safe_fprintf(stderr,
"Mmap logging failed for %s, using stderr only (lock-free)\n", filename);
428 atomic_store(&g_log.file, STDERR_FILENO);
429 g_log.filename[0] =
'\0';
434 atomic_store(&g_log.file, (fd >= 0) ? fd : STDERR_FILENO);
436 if (preserve_terminal_output) {
437 safe_fprintf(stderr,
"Failed to open log file: %s\n", filename);
439 g_log.filename[0] =
'\0';
443 atomic_store(&g_log.file, STDERR_FILENO);
444 g_log.filename[0] =
'\0';
447 atomic_store(&g_log.initialized,
true);
448 atomic_store(&g_log.terminal_output_enabled, preserve_terminal_output);
452 g_terminal_caps_initialized =
false;
466 int old_file = atomic_load(&g_log.file);
467 if (old_file >= 0 && old_file != STDERR_FILENO) {
470 atomic_store(&g_log.file, -1);
471 atomic_store(&g_log.initialized,
false);
474 if (atomic_load(&g_log.rotation_mutex_initialized)) {
476 atomic_store(&g_log.rotation_mutex_initialized,
false);
481 atomic_store(&g_log.level, (
int)level);
482 atomic_store(&g_log.level_manually_set,
true);
496 if (enabled && opts && opts->
quiet) {
499 atomic_store(&g_log.terminal_output_enabled, enabled);
503 return atomic_load(&g_log.terminal_output_enabled);
507 atomic_store(&g_log.force_stderr, enabled);
511 return atomic_load(&g_log.force_stderr);
515 bool previous_state = atomic_exchange(&g_log.terminal_locked,
true);
517 return previous_state;
521 atomic_store(&g_log.terminal_locked, previous_state);
522 if (!previous_state) {
523 atomic_store(&g_log.terminal_owner_thread, 0);
528 atomic_store(&g_log.flush_delay_ms, delay_ms);
534 int file = atomic_load(&g_log.file);
535 if (file >= 0 && file != STDERR_FILENO && strlen(g_log.filename) > 0) {
537 if (fstat(file, &st) == 0 && st.st_size >
MAX_LOG_SIZE) {
538 atomic_store(&g_log.current_size, (
size_t)st.st_size);
546static void write_to_log_file_atomic(
const char *buffer,
int length) {
547 if (length <= 0 || buffer == NULL) {
555 int file = atomic_load(&g_log.file);
556 if (file < 0 || file == STDERR_FILENO) {
563 atomic_fetch_add(&g_log.current_size, (
size_t)written);
571static int format_log_header(
char *buffer,
size_t buffer_size,
log_level_t level,
const char *timestamp,
572 const char *file,
int line,
const char *func,
bool use_colors,
bool newline) {
575 if (use_colors && colors == NULL) {
578 const char *color = use_colors ? colors[level] :
"";
581 const char *level_string = level_strings[level];
582 if (level_string == level_strings[
LOG_INFO]) {
583 level_string =
"INFO ";
584 }
else if (level_string == level_strings[
LOG_WARN]) {
585 level_string =
"WARN ";
586 }
else if (level_string == level_strings[
LOG_DEV]) {
587 level_string =
"DEV ";
590 const char *newline_or_not = newline ?
"\n" :
"";
595 (void)newline_or_not;
602 result = snprintf(buffer, buffer_size,
"[%s%s%s] [%s%s%s] ", color, timestamp, reset, color, level_string, reset);
604 result = snprintf(buffer, buffer_size,
"[%s] [%s] ", timestamp, level_strings[level]);
612 const char *file_color = colors[3];
613 const char *line_color = colors[5];
614 const char *func_color = colors[0];
615 result = snprintf(buffer, buffer_size,
"[%s%s%s] [%s%s%s] %s%s%s:%s%d%s in %s%s%s(): %s%s", color, timestamp, reset,
616 color, level_string, reset, file_color, rel_file, reset, line_color, line, reset, func_color,
617 func, reset, reset, newline_or_not);
619 result = snprintf(buffer, buffer_size,
"[%s] [%s] %s:%d in %s(): %s", timestamp, level_strings[level], rel_file,
620 line, func, newline_or_not);
624 if (result <= 0 || result >= (
int)buffer_size) {
639static void write_to_terminal_atomic(
log_level_t level,
const char *timestamp,
const char *file,
int line,
640 const char *func,
const char *fmt, va_list args) {
644 if (atomic_load(&g_log.force_stderr)) {
645 output_stream = stderr;
649 int fd = output_stream == stderr ? STDERR_FILENO : STDOUT_FILENO;
652 char header_buffer[512];
653 bool use_colors = isatty(fd);
656 format_log_header(header_buffer,
sizeof(header_buffer), level, timestamp, file, line, func, use_colors,
false);
657 if (header_len <= 0 || header_len >= (
int)
sizeof(header_buffer)) {
663 if (!atomic_load(&g_log.terminal_output_enabled)) {
668 if (atomic_load(&g_log.terminal_locked)) {
669 uint64_t owner = atomic_load(&g_log.terminal_owner_thread);
677 if (colors == NULL) {
679 (void)vfprintf(output_stream, fmt, args);
684 (void)vfprintf(output_stream, fmt, args);
689 (void)vfprintf(output_stream, fmt, args);
692 (void)fflush(output_stream);
697 if (!atomic_load(&g_log.initialized)) {
701 if (level < (
log_level_t)atomic_load(&g_log.level)) {
714 vsnprintf(msg_buffer,
sizeof(msg_buffer), fmt, args);
720 if (atomic_load(&g_log.terminal_output_enabled) && !atomic_load(&g_log.terminal_locked)) {
725 if (atomic_load(&g_log.force_stderr)) {
726 output_stream = stderr;
730 int fd = output_stream == stderr ? STDERR_FILENO : STDOUT_FILENO;
731 bool use_colors = isatty(fd);
733 char header_buffer[512];
735 format_log_header(header_buffer,
sizeof(header_buffer), level, time_buf, file, line, func, use_colors,
false);
737 if (header_len > 0 && header_len < (
int)
sizeof(header_buffer)) {
744 safe_fprintf(output_stream,
"%s%s\n", header_buffer, msg_buffer);
747 safe_fprintf(output_stream,
"%s%s\n", header_buffer, msg_buffer);
749 (void)fflush(output_stream);
766 int header_len = format_log_header(log_buffer,
sizeof(log_buffer), level, time_buf, file, line, func,
false,
false);
767 if (header_len <= 0 || header_len >= (
int)
sizeof(log_buffer)) {
773 int msg_len = header_len;
774 int formatted_len = vsnprintf(log_buffer + header_len,
sizeof(log_buffer) - (
size_t)header_len, fmt, args);
775 if (formatted_len < 0) {
781 msg_len += formatted_len;
782 if (msg_len >= (
int)
sizeof(log_buffer)) {
783 msg_len =
sizeof(log_buffer) - 1;
784 log_buffer[msg_len] =
'\0';
787 if (msg_len > 0 && msg_len < (
int)
sizeof(log_buffer) - 1) {
788 log_buffer[msg_len++] =
'\n';
789 log_buffer[msg_len] =
'\0';
795 int file_fd = atomic_load(&g_log.file);
796 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
797 write_to_log_file_atomic(log_buffer, msg_len);
801 va_list args_terminal;
802 va_start(args_terminal, fmt);
803 write_to_terminal_atomic(level, time_buf, file, line, func, fmt, args_terminal);
804 va_end(args_terminal);
808 if (!atomic_load(&g_log.initialized)) {
819 int msg_len = vsnprintf(log_buffer,
sizeof(log_buffer), fmt, args);
822 if (msg_len <= 0 || msg_len >= (
int)
sizeof(log_buffer)) {
831 int file_fd = atomic_load(&g_log.file);
832 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
833 write_to_log_file_atomic(log_buffer, msg_len);
834 write_to_log_file_atomic(
"\n", 1);
839 if (!atomic_load(&g_log.terminal_output_enabled)) {
842 if (atomic_load(&g_log.terminal_locked)) {
843 uint64_t owner = atomic_load(&g_log.terminal_owner_thread);
850 (void)fflush(stdout);
854static void log_plain_stderr_internal_atomic(
const char *fmt, va_list args,
bool add_newline) {
856 int msg_len = vsnprintf(log_buffer,
sizeof(log_buffer), fmt, args);
858 if (msg_len <= 0 || msg_len >= (
int)
sizeof(log_buffer)) {
867 int file_fd = atomic_load(&g_log.file);
868 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
869 write_to_log_file_atomic(log_buffer, msg_len);
871 write_to_log_file_atomic(
"\n", 1);
877 if (!atomic_load(&g_log.terminal_output_enabled)) {
880 if (atomic_load(&g_log.terminal_locked)) {
881 uint64_t owner = atomic_load(&g_log.terminal_owner_thread);
892 (void)fflush(stderr);
896 if (!atomic_load(&g_log.initialized)) {
905 log_plain_stderr_internal_atomic(fmt, args,
true);
910 if (!atomic_load(&g_log.initialized)) {
919 log_plain_stderr_internal_atomic(fmt, args,
false);
924 if (!atomic_load(&g_log.initialized)) {
931 int msg_len = vsnprintf(log_buffer,
sizeof(log_buffer), fmt, args);
934 if (msg_len <= 0 || msg_len >= (
int)
sizeof(log_buffer)) {
942 int file_fd = atomic_load(&g_log.file);
943 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
944 write_to_log_file_atomic(log_buffer, msg_len);
945 write_to_log_file_atomic(
"\n", 1);
953 return "serverโclient";
955 return "clientโserver";
963 const char *file,
int line,
const char *func,
const char *fmt,
970 va_copy(args_copy, args);
979 return current_error;
985 log_msg(
LOG_WARN, file, line, func,
"Skipping remote log message: invalid socket descriptor");
989 log_msg(
LOG_WARN, file, line, func,
"Failed to send remote log message: %s", asciichat_error_string(send_result));
993 const char *direction_label = log_network_direction_label(direction);
994 log_msg(level, file, line, func,
"[NET %s] %s", direction_label, formatted);
1003 va_start(args, fmt);
1005 log_network_message_internal(sockfd, crypto_ctx, level, direction, NULL, 0, NULL, fmt, args);
1012 const char *fmt, ...) {
1014 va_start(args, fmt);
1016 log_network_message_internal(sockfd, crypto_ctx, level, direction, file, line, func, fmt, args);
1026static void init_terminal_capabilities(
void) {
1029 if (g_terminal_caps_detecting) {
1033 if (!g_terminal_caps_initialized) {
1035 g_terminal_caps_detecting =
true;
1046 g_terminal_caps_initialized =
true;
1049 g_terminal_caps_detecting =
false;
1056 if (g_terminal_caps_detecting) {
1064 g_terminal_caps_detecting =
true;
1066 g_terminal_caps_detecting =
false;
1067 g_terminal_caps_initialized =
true;
1070 log_debug(
"Terminal capabilities: color_level=%d, capabilities=0x%x, utf8=%s, fps=%d", g_terminal_caps.
color_level,
1080 init_terminal_capabilities();
1092 return (
const char **)level_colors[mode];
1097 if (colors == NULL) {
1101 return colors[color];
1125 log_info(
"Lock-free mmap logging enabled: %s", log_path);
1132 log_info(
"Lock-free mmap logging disabled");
1141 if (g_shutdown_in_progress) {
1146 g_shutdown_saved_terminal_output = atomic_load(&g_log.terminal_output_enabled);
1147 atomic_store(&g_log.terminal_output_enabled,
false);
1148 g_shutdown_in_progress =
true;
1152 if (!g_shutdown_in_progress) {
1157 atomic_store(&g_log.terminal_output_enabled, g_shutdown_saved_terminal_output);
1158 g_shutdown_in_progress =
false;
๐ Cross-platform abstraction layer umbrella header for ascii-chat
#define SAFE_STRNCPY(dst, src, size)
#define SAFE_MALLOC(size, cast)
#define SAFE_GETENV(name)
#define PLATFORM_MAX_PATH_LENGTH
unsigned long long uint64_t
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
#define GET_ERRNO()
Get current error code (0 if no error)
asciichat_error_t
Error and exit codes - unified status values (0-255)
void log_truncate_if_large(void)
Manually truncate large log files.
void log_plain_stderr_nonewline_msg(const char *fmt,...)
Plain logging to stderr without trailing newline.
void log_plain_msg(const char *fmt,...)
Plain logging without timestamps or levels.
#define log_warn(...)
Log a WARN message.
bool log_get_terminal_output(void)
Get current terminal output setting.
asciichat_error_t log_enable_mmap(const char *log_path)
Enable lock-free mmap-based logging.
void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
Log a message at a specific level.
void log_shutdown_end(void)
End shutdown phase - restore previous logging settings.
void log_destroy(void)
Destroy the logging system and close log file.
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
enum remote_log_direction remote_log_direction_t
Remote log packet direction enumeration.
log_level_t log_get_level(void)
Get the current minimum log level.
const char ** log_get_color_array(void)
Get the appropriate color array based on terminal capabilities.
const char * log_level_color(log_color_t color)
Get color string for a given color enum.
void log_set_flush_delay(unsigned int delay_ms)
Set the delay between flushing buffered log entries.
#define LOG_MSG_BUFFER_SIZE
Maximum size of a single log message (including formatting)
char * format_message(const char *format, va_list args)
Format a message using va_list.
log_color_t
Color enum for logging - indexes into color arrays.
void log_shutdown_begin(void)
Begin shutdown phase - disable console logging but keep file logging.
#define log_error(...)
Log an ERROR message.
bool log_get_force_stderr(void)
Get current force_stderr setting.
void log_set_level(log_level_t level)
Set the minimum log level.
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
Initialize the logging system.
asciichat_error_t log_net_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *file, int line, const char *func, const char *fmt,...)
Log a message to all destinations (network, file, and terminal).
bool log_lock_terminal(void)
Lock terminal output for exclusive access by the calling thread.
asciichat_error_t log_network_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *fmt,...)
Send a formatted log message over the network.
log_level_t
Logging levels enumeration.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
size_t get_current_time_formatted(char *time_buf)
Get current time as formatted string.
asciichat_error_t log_enable_mmap_sized(const char *log_path, size_t max_size)
Enable lock-free mmap logging with custom file size.
#define MAX_LOG_SIZE
Maximum log file size in bytes (3MB) before rotation.
void log_file_msg(const char *fmt,...)
Log to file only, no stderr output.
void log_disable_mmap(void)
Disable mmap logging and return to mutex-based logging.
#define LOG_TIMESTAMP_BUFFER_SIZE
Maximum size of a timestamp string.
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
#define LOG_MMAP_MSG_BUFFER_SIZE
Maximum size of a log message in mmap mode.
void log_plain_stderr_msg(const char *fmt,...)
Plain logging to stderr with newline.
void log_redetect_terminal_capabilities(void)
Re-detect terminal capabilities after logging is initialized.
void log_unlock_terminal(bool previous_state)
Release terminal lock and flush buffered messages.
#define DEFAULT_LOG_LEVEL
Default log level for debug builds (DEBUG and above)
@ REMOTE_LOG_DIRECTION_SERVER_TO_CLIENT
@ REMOTE_LOG_DIRECTION_CLIENT_TO_SERVER
asciichat_error_t packet_send_remote_log(socket_t sockfd, const crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, uint16_t flags, const char *message)
Send a remote log packet with optional encryption context.
const options_t * options_get(void)
Get current options (lock-free read)
bool shutdown_is_requested(void)
Check if shutdown has been requested.
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
#define LOGGING_INTERNAL_ERROR(error, message,...)
Lock-free memory-mapped text logging with crash safety.
void log_mmap_rotate(void)
Rotate the mmap log (tail-keeping rotation)
bool log_mmap_is_active(void)
Check if mmap logging is active.
void log_mmap_write(int level, const char *file, int line, const char *func, const char *fmt,...)
Write a log entry directly to the mmap'd file (lock-free)
bool log_mmap_get_usage(size_t *used, size_t *capacity)
Get current mmap log usage.
void log_mmap_destroy(void)
Shutdown mmap logging.
asciichat_error_t log_mmap_init_simple(const char *log_path, size_t max_size)
Initialize mmap logging with simple parameters.
Cross-platform mutex interface for ascii-chat.
โ๏ธ Command-line options parsing and configuration management for ascii-chat
Packet protocol implementation with encryption and compression support.
๐ Path Manipulation Utilities
Cryptographic context structure.
Consolidated options structure.
unsigned short int quiet
Quiet mode (suppress logs)
Complete terminal capabilities structure.
terminal_color_mode_t color_level
Detected color support level (terminal_color_mode_t)
uint8_t desired_fps
Client's desired frame rate (1-144 FPS)
bool utf8_support
True if terminal supports UTF-8 encoding.
uint32_t capabilities
Capability flags bitmask (terminal_capability_flags_t)
uint32_t color_count
Maximum number of colors (16, 256, or 16777216)
bool detection_reliable
True if detection is confident (reliable detection)
Cross-platform system functions interface for ascii-chat.
๐ฅ๏ธ Cross-platform terminal interface for ascii-chat
Test logging control utilities.
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash