Skip to content

Instantly share code, notes, and snippets.

@grejc
Last active September 20, 2026 03:09
Show Gist options
  • Select an option

  • Save grejc/3817f0875b9cea61941b48e465e712db to your computer and use it in GitHub Desktop.

Select an option

Save grejc/3817f0875b9cea61941b48e465e712db to your computer and use it in GitHub Desktop.
Biblioteca de utilidades para C
/// @file utils.h
/// @brief Lib of utils written in C with love and hate by @grejc
///
/// Features:
/// - Terminal colors, TrueColor & ANSI detection [54:360]
/// - Styled print_error & print_success macros [363:487]
/// - Styled test macros & semantic assertions [489:1157]
/// - Box utils [1159:1281]
/// - Table utils (incluindo alignment, word-wrap e TableAuto) [1283:1845]
/// - Modern Test Runner Engine (accumulator & summary table) [1847:2163]
/// - Terminal menu & multiselect checkboxes [2165:2564]
/// - Progress Bars & Indeterminate Spinners [2567:2947]
/// - ArgParser (Python Style) with `--help` automatic (GOD DAMN bro, that was HARD) [2950:4205]
/// - init
/// - add_flag
/// - add_option
/// - add_option_choices
/// - add_pos
/// - add_subcommand
/// - get_subcommand
/// - get_int
/// - get_float
/// - get_bool
///
/// I promise to myself never touch in this code again!
/// And obviously, I failed --- 6 --- times.
#ifndef GREJC_UTILS_H
#define GREJC_UTILS_H
#pragma once
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include <ctype.h>
#include <errno.h>
#include <float.h>
#include <limits.h>
#include <math.h>
#include <poll.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
// ============================================================================
// TERMINAL UTILS
// ============================================================================
/// @brief Clears the entire terminal screen.
///
/// Sends the ANSI escape sequence `\033[H\033[J` which moves the cursor to
/// the home position (1,1) and clears the screen from that point onward.
#define TERMINAL_CLEAN_SCREEN() printf( "\033[H\033[J" )
/// @brief Moves the cursor to an absolute position on the terminal.
///
/// @param row Row number (1-indexed).
/// @param col Column number (1-indexed).
#define TERMINAL_MOVE_CURSOR( row, col ) printf( "\033[%d;%dH", row, col )
/// @brief Moves the cursor up by `n` rows.
///
/// @param n Number of rows to move up.
#define TERMINAL_MOVE_CURSOR_UP( n ) printf( "\033[%dA", n )
/// @brief Moves the cursor down by `n` rows.
///
/// @param n Number of rows to move down.
#define TERMINAL_MOVE_CURSOR_DOWN( n ) printf( "\033[%dB", n )
/// @brief Moves the cursor left by `n` columns.
///
/// @param n Number of columns to move left.
#define TERMINAL_MOVE_CURSOR_LEFT( n ) printf( "\033[%dD", n )
/// @brief Moves the cursor right by `n` columns.
///
/// @param n Number of columns to move right.
#define TERMINAL_MOVE_CURSOR_RIGHT( n ) printf( "\033[%dC", n )
/// @brief Moves the cursor to the home position (1, 1).
#define TERMINAL_MOVE_CURSOR_HOME() printf( "\033[H" )
/// @brief Moves the cursor to the bottom row of the terminal.
#define TERMINAL_MOVE_CURSOR_END() printf( "\033[9999;1H" )
/// @defgroup terminal_colors Terminal Color Codes
///
/// ANSI escape sequences for changing terminal text color.
/// Use these constants with printf or fprintf to colorize output.
/// Always reset with @ref TERMINAL_COLOR_RESET after the colored text.
///
/// @{
/// @name Color Constants
/// @{
#define TERMINAL_COLOR_RESET "\033[0m"
#define TERMINAL_COLOR_BLACK "\033[30m"
#define TERMINAL_COLOR_RED "\033[31m"
#define TERMINAL_COLOR_GREEN "\033[32m"
#define TERMINAL_COLOR_YELLOW "\033[33m"
#define TERMINAL_COLOR_BLUE "\033[34m"
#define TERMINAL_COLOR_MAGENTA "\033[35m"
#define TERMINAL_COLOR_CYAN "\033[36m"
#define TERMINAL_COLOR_WHITE "\033[37m"
/// @}
/// @}
/// @brief Stores the dimensions of the terminal window.
typedef struct term_dim_s {
unsigned width; ///< Width in columns.
unsigned height; ///< Height in rows.
} term_dim_t;
/// @brief Retrieves the current terminal dimensions via an ioctl call.
///
/// Uses `TIOCGWINSZ` on `STDOUT_FILENO` to obtain the terminal size.
/// Falls back to 80x24 if the ioctl fails or returns zero.
///
/// @return A @ref term_dim_t struct with the current terminal width and height.
static inline term_dim_t _get_terminal_dimensions( void ) {
struct winsize w;
term_dim_t td;
if ( ioctl( STDOUT_FILENO, TIOCGWINSZ, &w ) == 0 ) {
td.width = w.ws_col > 0 ? w.ws_col : 80;
td.height = w.ws_row > 0 ? w.ws_row : 24;
} else {
td.width = 80;
td.height = 24;
}
return td;
}
// ----------------------------------------------------------------------------
// TERMINAL COLOR MODE & ANSI CAPABILITY DETECTION
// ----------------------------------------------------------------------------
/// @brief Modo de controle de emissão de cores e sequências de escape ANSI.
typedef enum {
TERMINAL_COLOR_AUTO = 0, ///< Detecta automaticamente via isatty(), NO_COLOR e TERM (Padrão).
TERMINAL_COLOR_ALWAYS, ///< Força a emissão de códigos ANSI mesmo em pipes ou arquivos.
TERMINAL_COLOR_NEVER ///< Desativa completamente qualquer sequência de escape ANSI.
} TerminalColorMode;
static TerminalColorMode _terminal_color_mode = TERMINAL_COLOR_AUTO;
/// @brief Define o modo global de emissão de cores para o terminal.
///
/// Permite que a aplicação force a exibição de cores em ambientes sem TTY
/// (ex: pipelines de CI configurados para ANSI) ou desligue as cores em definitivo.
///
/// @param mode O novo modo (@ref TERMINAL_COLOR_AUTO, @ref TERMINAL_COLOR_ALWAYS, @ref TERMINAL_COLOR_NEVER).
static inline void terminal_set_color_mode( TerminalColorMode mode ) {
_terminal_color_mode = mode;
}
/// @brief Obtém o modo global de cores configurado atualmente.
/// @return O valor ativo de @ref TerminalColorMode.
static inline TerminalColorMode terminal_get_color_mode( void ) {
return _terminal_color_mode;
}
/// @brief Avalia se o fluxo (stream) especificado suporta a emissão de códigos ANSI.
///
/// Regras de validação em modo @ref TERMINAL_COLOR_AUTO:
/// 1. Se @p stream for NULL, retorna false.
/// 2. Se a variável de ambiente `NO_COLOR` estiver presente e não vazia (conforme https://no-color.org), retorna false.
/// 3. Se a variável de ambiente `TERM` for "dumb", retorna false.
/// 4. Se `isatty(fileno(stream))` retornar 0 (fluxo redirecionado para pipe ou arquivo), retorna false.
/// 5. Caso contrário, retorna true.
///
/// Se o modo for @ref TERMINAL_COLOR_ALWAYS, retorna true (desde que @p stream seja válido).
/// Se o modo for @ref TERMINAL_COLOR_NEVER, retorna false incondicionalmente.
///
/// @param stream Ponteiro para o fluxo FILE (ex: stdout, stderr, ou arquivo de log).
/// @return true se cores forem suportadas e permitidas no fluxo, false caso contrário.
static inline bool terminal_supports_color( FILE *stream ) {
if ( _terminal_color_mode == TERMINAL_COLOR_NEVER ) {
return false;
}
if ( stream == NULL ) {
return false;
}
if ( _terminal_color_mode == TERMINAL_COLOR_ALWAYS ) {
return true;
}
const char *nc = getenv( "NO_COLOR" );
if ( nc != NULL && nc[0] != '\0' ) {
return false;
}
const char *term = getenv( "TERM" );
if ( term != NULL && strcmp( term, "dumb" ) == 0 ) {
return false;
}
int fd = fileno( stream );
if ( fd < 0 || !isatty( fd ) ) {
return false;
}
return true;
}
/// @brief Retorna a sequência de controle ANSI fornecida se o stream especificado suportar cores, ou "" caso contrário.
/// @param stream Fluxo alvo (ex: stderr).
/// @param ansi_code A sequência ANSI literal.
/// @return O ponteiro @p ansi_code se o stream suportar cores, ou "" caso contrário.
static inline const char *terminal_color_stream( FILE *stream, const char *ansi_code ) {
if ( ansi_code == NULL ) {
return "";
}
if ( terminal_supports_color( stream ) ) {
return ansi_code;
}
return "";
}
/// @brief Retorna a sequência de controle ANSI fornecida se stdout suportar cores, ou string vazia ("") caso contrário.
///
/// Facilita a interpolação fluida em chamadas `printf`:
/// @code{.c}
/// printf("%sTexto em Vermelho%s\n",
/// terminal_color(TERMINAL_COLOR_RED),
/// terminal_color(TERMINAL_COLOR_RESET));
/// @endcode
///
/// @param ansi_code A sequência ANSI literal (ex: TERMINAL_COLOR_RED).
/// @return O ponteiro @p ansi_code se cores estiverem ativas, ou "" se desativadas ou se @p ansi_code for NULL.
static inline const char *terminal_color( const char *ansi_code ) {
return terminal_color_stream( stdout, ansi_code );
}
// ----------------------------------------------------------------------------
// EXTENDED COLORS (TrueColor 24-bit RGB & 256 Colors) & TYPOGRAPHY STYLES
// ----------------------------------------------------------------------------
/// @defgroup terminal_styles Estilos Tipográficos ANSI Estendidos
/// @{
#define TERMINAL_STYLE_RESET "\033[0m"
#define TERMINAL_STYLE_BOLD "\033[1m"
#define TERMINAL_STYLE_DIM "\033[2m"
#define TERMINAL_STYLE_ITALIC "\033[3m"
#define TERMINAL_STYLE_UNDERLINE "\033[4m"
#define TERMINAL_STYLE_BLINK "\033[5m"
#define TERMINAL_STYLE_INVERSE "\033[7m"
#define TERMINAL_STYLE_STRIKE "\033[9m"
/// @}
/// @brief Formata uma sequência ANSI TrueColor 24-bit para a cor de primeiro plano (foreground).
///
/// Produz o código `\033[38;2;R;G;Bm`. Se as cores estiverem desativadas em stdout via
/// @ref terminal_supports_color, gera uma string vazia ("").
///
/// @param buf Buffer de destino para receber a string formatada (mínimo recomendado: 32 bytes).
/// @param buf_sz Capacidade total de @p buf em bytes.
/// @param r Componente vermelho [0-255].
/// @param g Componente verde [0-255].
/// @param b Componente azul [0-255].
static inline void terminal_rgb( char *buf, size_t buf_sz, uint8_t r, uint8_t g, uint8_t b ) {
if ( buf == NULL || buf_sz == 0 ) {
return;
}
if ( !terminal_supports_color( stdout ) ) {
buf[0] = '\0';
return;
}
snprintf( buf, buf_sz, "\033[38;2;%u;%u;%um", (unsigned)r, (unsigned)g, (unsigned)b );
}
/// @brief Formata uma sequência ANSI TrueColor 24-bit para a cor de fundo (background).
///
/// Produz o código `\033[48;2;R;G;Bm`. Se as cores estiverem desativadas em stdout, gera string vazia ("").
///
/// @param buf Buffer de destino para receber a string formatada.
/// @param buf_sz Capacidade total de @p buf em bytes.
/// @param r Componente vermelho [0-255].
/// @param g Componente verde [0-255].
/// @param b Componente azul [0-255].
static inline void terminal_bg_rgb( char *buf, size_t buf_sz, uint8_t r, uint8_t g, uint8_t b ) {
if ( buf == NULL || buf_sz == 0 ) {
return;
}
if ( !terminal_supports_color( stdout ) ) {
buf[0] = '\0';
return;
}
snprintf( buf, buf_sz, "\033[48;2;%u;%u;%um", (unsigned)r, (unsigned)g, (unsigned)b );
}
/// @brief Formata uma sequência ANSI de 256 cores para o primeiro plano (foreground: `\033[38;5;<n>m`).
/// @param buf Buffer de destino (mínimo: 16 bytes).
/// @param buf_sz Capacidade total do buffer.
/// @param color_code Código de cor no mapa de 256 cores [0-255].
static inline void terminal_color256( char *buf, size_t buf_sz, uint8_t color_code ) {
if ( buf == NULL || buf_sz == 0 ) {
return;
}
if ( !terminal_supports_color( stdout ) ) {
buf[0] = '\0';
return;
}
snprintf( buf, buf_sz, "\033[38;5;%um", (unsigned)color_code );
}
/// @brief Formata uma sequência ANSI de 256 cores para o fundo (background: `\033[48;5;<n>m`).
/// @param buf Buffer de destino.
/// @param buf_sz Capacidade total do buffer.
/// @param color_code Código de cor no mapa de 256 cores [0-255].
static inline void terminal_bg_color256( char *buf, size_t buf_sz, uint8_t color_code ) {
if ( buf == NULL || buf_sz == 0 ) {
return;
}
if ( !terminal_supports_color( stdout ) ) {
buf[0] = '\0';
return;
}
snprintf( buf, buf_sz, "\033[48;5;%um", (unsigned)color_code );
}
#if defined( __GNUC__ ) || defined( __clang__ )
static inline void terminal_print_rgb( uint8_t r, uint8_t g, uint8_t b, const char *fmt, ... )
__attribute__( ( format( printf, 4, 5 ) ) );
#endif
/// @brief Imprime uma mensagem formatada na saída padrão (stdout) com a cor RGB especificada,
/// restaurando automaticamente a cor original ao final.
///
/// Se o terminal não suportar cores, a mensagem é impressa normalmente sem os códigos de escape.
///
/// @param r Componente vermelho [0-255].
/// @param g Componente verde [0-255].
/// @param b Componente azul [0-255].
/// @param fmt String de formatação padrão estilo printf.
/// @param ... Argumentos variáveis de formatação.
static inline void terminal_print_rgb( uint8_t r, uint8_t g, uint8_t b, const char *fmt, ... ) {
if ( fmt == NULL ) {
return;
}
va_list args;
if ( terminal_supports_color( stdout ) ) {
printf( "\033[38;2;%u;%u;%um", (unsigned)r, (unsigned)g, (unsigned)b );
va_start( args, fmt );
vprintf( fmt, args );
va_end( args );
printf( "\033[0m" );
} else {
va_start( args, fmt );
vprintf( fmt, args );
va_end( args );
}
fflush( stdout );
}
/// @brief Prints a formatted error message to an output stream, with an
/// optional program exit.
///
/// If @p output_stream is NULL, the message is printed to stdout with ANSI
/// color codes for visual emphasis. If a valid FILE pointer is provided, the
/// message is printed without colors (suitable for file logging).
///
/// When @p exit_code is not -1, the program terminates with that exit code
/// immediately after printing.
///
/// @param error_reason A short string identifying the reason for the error
/// (e.g. "File not found").
/// @param message A detailed error description.
/// @param output_stream FILE pointer to redirect output, or NULL for colored
/// stdout output.
/// @param exit_code Exit code passed to `exit()`. If -1, the program does
/// **not** exit.
///
/// Example:
/// @code{.c}
/// print_error("Error reason", "Error message", NULL, 1);
/// @endcode
///
/// Output (colored when output_stream is NULL):
/// @code
/// [ERROR]{LINE -> 45}: Error reason
/// -> Error message
/// @endcode
#define print_error( error_reason, message, output_stream, exit_code ) \
do { \
FILE *_pe_stream = ( output_stream ); \
FILE *_pe_dest = ( _pe_stream == NULL ) ? stdout : _pe_stream; \
if ( ( _pe_stream != NULL ) ? terminal_supports_color( _pe_stream ) : \
( terminal_supports_color( stdout ) || \
( _terminal_color_mode != TERMINAL_COLOR_NEVER && \
( !getenv( "NO_COLOR" ) || getenv( "NO_COLOR" )[0] == '\0' ) && \
( !getenv( "TERM" ) || strcmp( getenv( "TERM" ), "dumb" ) != 0 ) ) ) ) { \
fprintf( _pe_dest, "[%sERROR%s]{%sLINE \u2192 %d%s}:\t%s%s\n\t%s%s%s\n", TERMINAL_COLOR_RED, \
TERMINAL_COLOR_RESET, TERMINAL_COLOR_BLUE, __LINE__, TERMINAL_COLOR_RESET, TERMINAL_COLOR_RED, \
( error_reason ), TERMINAL_COLOR_YELLOW, ( message ), TERMINAL_COLOR_RESET ); \
} else { \
fprintf( _pe_dest, "[ERROR]{LINE \u2192 %d}: %s\n\t%s\n", __LINE__, ( error_reason ), ( message ) ); \
} \
if ( ( exit_code ) != -1 ) \
exit( exit_code ); \
} while ( 0 )
#define PRINT_SUCCESS_VA_NUM_ARGS( ... ) PRINT_SUCCESS_VA_NUM_ARGS_IMPL( __VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0 )
#define PRINT_SUCCESS_VA_NUM_ARGS_IMPL( _1, _2, _3, _4, _5, _6, _7, _8, N, ... ) N
#define PRINT_SUCCESS_DISPATCH( N ) PRINT_SUCCESS_DISPATCH_IMPL( N )
#define PRINT_SUCCESS_DISPATCH_IMPL( N ) print_success_##N
#define print_success_2( output_stream, message ) \
do { \
FILE *_ps_stream = ( output_stream ); \
FILE *_ps_dest = ( _ps_stream == NULL ) ? stdout : _ps_stream; \
if ( ( _ps_stream != NULL ) ? terminal_supports_color( _ps_stream ) : \
( terminal_supports_color( stdout ) || \
( _terminal_color_mode != TERMINAL_COLOR_NEVER && \
( !getenv( "NO_COLOR" ) || getenv( "NO_COLOR" )[0] == '\0' ) && \
( !getenv( "TERM" ) || strcmp( getenv( "TERM" ), "dumb" ) != 0 ) ) ) ) { \
fprintf( _ps_dest, "[%sSUCCESS%s]{%sLINE \u2192 %d%s}:\n\t%s%s%s\n", TERMINAL_COLOR_GREEN, \
TERMINAL_COLOR_RESET, TERMINAL_COLOR_BLUE, __LINE__, TERMINAL_COLOR_RESET, TERMINAL_COLOR_GREEN, \
message, TERMINAL_COLOR_RESET ); \
} else { \
fprintf( _ps_dest, "[SUCCESS]{LINE \u2192 %d}:\n\t%s\n", __LINE__, message ); \
} \
} while ( 0 )
#define print_success_3( output_stream, fmt, ... ) \
do { \
FILE *_ps_stream = ( output_stream ); \
FILE *_ps_dest = ( _ps_stream == NULL ) ? stdout : _ps_stream; \
if ( ( _ps_stream != NULL ) ? terminal_supports_color( _ps_stream ) : \
( terminal_supports_color( stdout ) || \
( _terminal_color_mode != TERMINAL_COLOR_NEVER && \
( !getenv( "NO_COLOR" ) || getenv( "NO_COLOR" )[0] == '\0' ) && \
( !getenv( "TERM" ) || strcmp( getenv( "TERM" ), "dumb" ) != 0 ) ) ) ) { \
fprintf( _ps_dest, "[%sSUCCESS%s]{%sLINE \u2192 %d%s}:\n\t%s", TERMINAL_COLOR_GREEN, TERMINAL_COLOR_RESET, \
TERMINAL_COLOR_BLUE, __LINE__, TERMINAL_COLOR_RESET, TERMINAL_COLOR_GREEN ); \
fprintf( _ps_dest, fmt, __VA_ARGS__ ); \
fprintf( _ps_dest, "%s\n", TERMINAL_COLOR_RESET ); \
} else { \
fprintf( _ps_dest, "[SUCCESS]{LINE \u2192 %d}:\n\t", __LINE__ ); \
fprintf( _ps_dest, fmt, __VA_ARGS__ ); \
fprintf( _ps_dest, "\n" ); \
} \
} while ( 0 )
#define print_success_4( output_stream, fmt, ... ) print_success_3( output_stream, fmt, __VA_ARGS__ )
#define print_success_5( output_stream, fmt, ... ) print_success_3( output_stream, fmt, __VA_ARGS__ )
#define print_success_6( output_stream, fmt, ... ) print_success_3( output_stream, fmt, __VA_ARGS__ )
#define print_success_7( output_stream, fmt, ... ) print_success_3( output_stream, fmt, __VA_ARGS__ )
#define print_success_8( output_stream, fmt, ... ) print_success_3( output_stream, fmt, __VA_ARGS__ )
/// @brief Prints a success message to an output stream.
///
/// If @p output_stream is NULL, the message is printed to stdout with a green
/// color. If a valid FILE pointer is provided, the message is printed without
/// colors.
///
/// @param output_stream FILE pointer to redirect output, or NULL for colored
/// stdout output.
/// @param message The success message to display (printf-style format).
/// @param ... Optional variadic arguments for @p message.
///
/// @par Overloads (based on argument count):
/// | Args | Signature | Behaviour |
/// |------|-----------------------------------------|---------------------|
/// | 2 | `print_success(output_stream, message)` | Literal message. |
/// | 3+ | `print_success(output_stream, fmt, ...)`| printf-style msg. |
///
/// Example:
/// @code{.c}
/// print_success(NULL, "Operation completed");
/// print_success(NULL, "Processed %d items in %.2fs", count, elapsed);
/// @endcode
///
/// Output (colored when output_stream is NULL):
/// @code
/// [SUCCESS]{LINE -> 45}:
/// Operation completed
/// @endcode
#define print_success( ... ) PRINT_SUCCESS_DISPATCH( PRINT_SUCCESS_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
// ============================================================================
// TEST UTILS (Based on assert.h - Enhanced Version)
// ============================================================================
#ifdef NDEBUG
#define test( ... ) ( (void)0 )
#define test_op( ... ) ( (void)0 )
#define test_str_eq( ... ) ( (void)0 )
#define test_str_ne( ... ) ( (void)0 )
#define test_str_contains( ... ) ( (void)0 )
#define test_float_eq( ... ) ( (void)0 )
#define test_double_eq( ... ) ( (void)0 )
#else
/// @brief Resolves the current function name in a portable way.
///
/// Uses `__func__` (C99+), `__PRETTY_FUNCTION__` (GCC), or `"unknown"` as a
/// fallback. The result is embedded in the failure report of @ref test and
/// @ref test_op. @see GREJC_FAIL_REPORT_BASE, @see GREJC_FAIL_REPORT_OP_BASE
#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L
#define GREJC_TEST_FUNCTION __func__
#elif defined __GNUC__
#define GREJC_TEST_FUNCTION __extension__ __PRETTY_FUNCTION__
#else
#define GREJC_TEST_FUNCTION "unknown"
#endif
/// @name Type-Specific Value Printers
///
/// Static helper functions invoked by @ref GREJC_PRINT_VAL to format a value
/// of a known type to stderr. Each handles one C type and is selected at
/// compile time via `_Generic` dispatch.
///
/// @{
static inline void _grejc_p_bool( _Bool x ) { fprintf( stderr, "%s", x ? "true" : "false" ); }
static inline void _grejc_p_char( char x ) { fprintf( stderr, "'%c' (0x%02X)", x, x ); }
static inline void _grejc_p_schar( signed char x ) { fprintf( stderr, "%d", x ); }
static inline void _grejc_p_uchar( unsigned char x ) { fprintf( stderr, "%u", x ); }
static inline void _grejc_p_short( short x ) { fprintf( stderr, "%d", x ); }
static inline void _grejc_p_ushort( unsigned short x ) { fprintf( stderr, "%u", x ); }
static inline void _grejc_p_int( int x ) { fprintf( stderr, "%d", x ); }
static inline void _grejc_p_uint( unsigned int x ) { fprintf( stderr, "%u", x ); }
static inline void _grejc_p_long( long x ) { fprintf( stderr, "%ld", x ); }
static inline void _grejc_p_ulong( unsigned long x ) { fprintf( stderr, "%lu", x ); }
static inline void _grejc_p_llong( long long x ) { fprintf( stderr, "%lld", x ); }
static inline void _grejc_p_ullong( unsigned long long x ) { fprintf( stderr, "%llu", x ); }
static inline void _grejc_p_float( float x ) { fprintf( stderr, "%f", x ); }
static inline void _grejc_p_double( double x ) { fprintf( stderr, "%f", x ); }
static inline void _grejc_p_str( char *x ) { fprintf( stderr, "\"%s\"", x ? x : "NULL" ); }
static inline void _grejc_p_cstr( const char *x ) { fprintf( stderr, "\"%s\"", x ? x : "NULL" ); }
static inline void _grejc_p_ptr( const void *x ) { fprintf( stderr, "%p", x ); }
/// @}
/// @brief Resolves any expression to a human-readable C type name at compile
/// time via `_Generic`.
///
/// The returned string is used in the failure report of @ref test (the "Result
/// Type" line). Pointers and unrecognised struct types fall through to the
/// `default` label.
///
/// @param x Any expression whose type is to be named.
/// @return A string literal describing the type of @p x.
#define GREJC_TYPE_STR( x ) \
_Generic( ( x ), \
_Bool: "bool", \
char: "char", \
signed char: "signed char", \
unsigned char: "unsigned char", \
short: "short", \
unsigned short: "unsigned short", \
int: "int", \
unsigned int: "unsigned int", \
long: "long", \
unsigned long: "unsigned long", \
long long: "long long", \
unsigned long long: "unsigned long long", \
float: "float", \
double: "double", \
char *: "string (char*)", \
const char *: "string (const char*)", \
default: "pointer / structure reference" )
/// @brief Prints a typed value to stderr in a human-readable format.
///
/// Uses `_Generic` to select the correct type-specific printer function
/// (from the @ref _grejc_p_* family) at compile time, then immediately
/// invokes it with @p x.
///
/// Booleans are printed as `true`/`false`, characters as `'c' (0x63)`,
/// strings as `"..."` (or `NULL`), and pointers with `%p`. All other types
/// use their natural decimal or floating-point format.
///
/// @param x The value to print.
#define GREJC_PRINT_VAL( x ) \
_Generic( ( x ), \
_Bool: _grejc_p_bool, \
char: _grejc_p_char, \
signed char: _grejc_p_schar, \
unsigned char: _grejc_p_uchar, \
short: _grejc_p_short, \
unsigned short: _grejc_p_ushort, \
int: _grejc_p_int, \
unsigned int: _grejc_p_uint, \
long: _grejc_p_long, \
unsigned long: _grejc_p_ulong, \
long long: _grejc_p_llong, \
unsigned long long: _grejc_p_ullong, \
float: _grejc_p_float, \
double: _grejc_p_double, \
char *: _grejc_p_str, \
const char *: _grejc_p_cstr, \
default: _grejc_p_ptr )( x )
/// @brief Prints the standard failure-report header for @ref test (without
/// exit or user message).
///
/// Emits the coloured diagnostic block (file, line, expression text, resolved
/// type, evaluated value, enclosing function) to stderr. The caller is
/// responsible for printing any optional message and calling `exit(1)`.
///
/// @param expr_str Stringified expression text.
/// @param type_str Human-readable type name (from @ref GREJC_TYPE_STR).
/// @param print_stmt Single statement (semicolon-terminated) that prints the
/// evaluated value to stderr (typically @ref
/// GREJC_PRINT_VAL).
#define GREJC_FAIL_REPORT_BASE( expr_str, type_str, print_stmt ) \
do { \
fprintf( stderr, "[%sTEST FAILED%s]{%s%s:%d%s}:\n", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET, \
TERMINAL_COLOR_BLUE, __FILE__, __LINE__, TERMINAL_COLOR_RESET ); \
fprintf( stderr, "\t%sExpression:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED, expr_str, \
TERMINAL_COLOR_RESET ); \
fprintf( stderr, "\t%sResult Type:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_CYAN, type_str, \
TERMINAL_COLOR_RESET ); \
fprintf( stderr, "\t%sEvaluated:\t%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED ); \
print_stmt; \
fprintf( stderr, "%s\n", TERMINAL_COLOR_RESET ); \
fprintf( stderr, "\t%sIn function:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_MAGENTA, \
GREJC_TEST_FUNCTION, TERMINAL_COLOR_RESET ); \
} while ( 0 )
/// @brief Counts the number of variadic macro arguments (up to 8).
///
/// Internal helper used by the overload-dispatch mechanism of @ref test and
/// @ref test_op.
///
/// @param ... Variadic arguments (1 to 8).
/// @return The argument count as an integer preprocessor token.
#define GREJC_VA_NUM_ARGS( ... ) GREJC_VA_NUM_ARGS_IMPL( __VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0 )
#define GREJC_VA_NUM_ARGS_IMPL( _1, _2, _3, _4, _5, _6, _7, _8, N, ... ) N
/// @brief 1-argument overload: assert @p expr with no message. @see test
#define test_1( expr ) \
do { \
if ( !( expr ) ) { \
GREJC_FAIL_REPORT_BASE( #expr, GREJC_TYPE_STR( expr ), GREJC_PRINT_VAL( expr ) ); \
exit( 1 ); \
} \
} while ( 0 )
/// @brief 2-argument overload: assert @p expr with a literal format string
/// (no variadic args). @see test
#define test_2( expr, fmt ) \
do { \
if ( !( expr ) ) { \
GREJC_FAIL_REPORT_BASE( #expr, GREJC_TYPE_STR( expr ), GREJC_PRINT_VAL( expr ) ); \
fprintf( stderr, "\t%sMessage:\t" TERMINAL_COLOR_RESET fmt "\n", TERMINAL_COLOR_YELLOW ); \
exit( 1 ); \
} \
} while ( 0 )
/// @brief 3+ argument overload: assert @p expr with a printf-style message.
/// `test_4`/`test_5`/`test_6` delegate here. @see test
#define test_3( expr, fmt, ... ) \
do { \
if ( !( expr ) ) { \
GREJC_FAIL_REPORT_BASE( #expr, GREJC_TYPE_STR( expr ), GREJC_PRINT_VAL( expr ) ); \
fprintf( stderr, "\t%sMessage:\t" TERMINAL_COLOR_RESET fmt "\n", TERMINAL_COLOR_YELLOW, __VA_ARGS__ ); \
exit( 1 ); \
} \
} while ( 0 )
/// @brief Alias for @ref test_3 (4-argument form). @see test
#define test_4( expr, fmt, ... ) test_3( expr, fmt, __VA_ARGS__ )
/// @brief Alias for @ref test_3 (5-argument form). @see test
#define test_5( expr, fmt, ... ) test_3( expr, fmt, __VA_ARGS__ )
/// @brief Alias for @ref test_3 (6-argument form). @see test
#define test_6( expr, fmt, ... ) test_3( expr, fmt, __VA_ARGS__ )
/// @brief Alias for @ref test_3 (7-argument form). @see test
#define test_7( expr, fmt, ... ) test_3( expr, fmt, __VA_ARGS__ )
/// @brief Alias for @ref test_3 (8-argument form). @see test
#define test_8( expr, fmt, ... ) test_3( expr, fmt, __VA_ARGS__ )
/// @brief Token-pastes `GREJC_VA_NUM_ARGS` onto `test_` to select the right
/// overload (e.g. `test_1`, `test_2`, …).
#define GREJC_DISPATCH( N ) GREJC_DISPATCH_IMPL( N )
#define GREJC_DISPATCH_IMPL( N ) test_##N
/// @brief Assert that an expression evaluates to true (non-zero).
///
/// A drop-in enhancement over standard `assert()` that provides colourful,
/// detailed failure diagnostics on stderr. When `NDEBUG` is defined the macro
/// expands to `((void)0)` and produces no code, matching `assert.h` semantics.
///
/// On failure the report includes:
/// - Source file and line number.
/// - The expression text as written.
/// - The expression's resolved type (via `_Generic` introspection —
/// see @ref GREJC_TYPE_STR).
/// - The expression's actual evaluated value (formatted appropriately for its
/// type via @ref GREJC_PRINT_VAL).
/// - The enclosing function name.
/// - An optional user-supplied message (printf-style).
///
/// The program then terminates with `exit(1)`.
///
/// @par Overloads (based on argument count):
/// | Args | Signature | Behaviour |
/// |------|----------------------------------|-------------------------------------|
/// | 1 | `test(expr)` | Assert @p expr, no message. | |
/// 2 | `test(expr, fmt)` | Assert @p expr, literal `fmt` (no
/// variadic args). | | 3+ | `test(expr, fmt, ...)` | Assert @p
/// expr, printf-style message with args. |
///
/// @param expr Boolean expression to assert (must be true).
/// @param fmt Optional `printf`-style format string (included in the failure
/// report only on assertion failure).
/// @param ... Optional variadic arguments for @p fmt.
///
/// @par Example — basic assertion:
/// @code{.c}
/// int x = 42;
/// test(x == 42);
/// @endcode
///
/// @par Example — with custom message:
/// @code{.c}
/// int result = compute();
/// test(result >= 0, "Expected non-negative result, got %d", result);
/// @endcode
///
/// @par Example — disabled via NDEBUG:
/// @code{.c}
/// #define NDEBUG
/// #include "utils.h"
/// test(0); // expands to ((void)0) — no-op
/// @endcode
///
/// @note The @ref GREJC_PRINT_VAL macro handles `char*` and `const char*`
/// specially (printing them as quoted strings) and pointers via `%p`.
///
/// @see test_op For asserting binary-relation expressions with explicit
/// left-hand-side, operator, and right-hand-side breakdown.
#define test( ... ) GREJC_DISPATCH( GREJC_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
/// @brief Prints the standard failure-report header for @ref test_op (without
/// exit or user message).
///
/// Emits the coloured diagnostic block (file, line, full expression text,
/// "Anatomy" line with run-time values of both operands, enclosing function)
/// to stderr. The caller is responsible for printing any optional message
/// and calling `exit(1)`.
///
/// @param lhs_str Stringified left-hand operand text.
/// @param op_str Stringified operator token.
/// @param rhs_str Stringified right-hand operand text.
/// @param lhs_val Evaluated left-hand operand (any type).
/// @param rhs_val Evaluated right-hand operand (any type).
#define GREJC_FAIL_REPORT_OP_BASE( lhs_str, op_str, rhs_str, lhs_val, rhs_val ) \
do { \
fprintf( stderr, "[%sTEST FAILED%s]{%s%s:%d%s}:\n", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET, \
TERMINAL_COLOR_BLUE, __FILE__, __LINE__, TERMINAL_COLOR_RESET ); \
fprintf( stderr, "\t%sExpression:\t%s%s %s %s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED, lhs_str, \
op_str, rhs_str, TERMINAL_COLOR_RESET ); \
fprintf( stderr, "\t%sAnatomy:\t%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET ); \
GREJC_PRINT_VAL( lhs_val ); \
fprintf( stderr, " %s ", op_str ); \
GREJC_PRINT_VAL( rhs_val ); \
fprintf( stderr, "\n\t%sIn function:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_MAGENTA, \
GREJC_TEST_FUNCTION, TERMINAL_COLOR_RESET ); \
} while ( 0 )
/// @brief 3-argument overload: assert `lhs op rhs` with no message. @see
/// test_op
#define test_op_3( lhs, op, rhs ) \
do { \
if ( !( (lhs)op( rhs ) ) ) { \
GREJC_FAIL_REPORT_OP_BASE( #lhs, #op, #rhs, lhs, rhs ); \
exit( 1 ); \
} \
} while ( 0 )
/// @brief 4-argument overload: assert `lhs op rhs` with a literal format
/// string (no variadic args). @see test_op
#define test_op_4( lhs, op, rhs, fmt ) \
do { \
if ( !( (lhs)op( rhs ) ) ) { \
GREJC_FAIL_REPORT_OP_BASE( #lhs, #op, #rhs, lhs, rhs ); \
fprintf( stderr, "\t%sMessage:\t" TERMINAL_COLOR_RESET fmt "\n", TERMINAL_COLOR_YELLOW ); \
exit( 1 ); \
} \
} while ( 0 )
/// @brief 5+ argument overload: assert `lhs op rhs` with a printf-style
/// message. @see test_op
#define test_op_5( lhs, op, rhs, fmt, ... ) \
do { \
if ( !( (lhs)op( rhs ) ) ) { \
GREJC_FAIL_REPORT_OP_BASE( #lhs, #op, #rhs, lhs, rhs ); \
fprintf( stderr, "\t%sMessage:\t" TERMINAL_COLOR_RESET fmt "\n", TERMINAL_COLOR_YELLOW, __VA_ARGS__ ); \
exit( 1 ); \
} \
} while ( 0 )
/// @brief Alias for @ref test_op_5 (6-argument form). @see test_op
#define test_op_6( lhs, op, rhs, fmt, ... ) test_op_5( lhs, op, rhs, fmt, __VA_ARGS__ )
/// @brief Alias for @ref test_op_5 (7-argument form). @see test_op
#define test_op_7( lhs, op, rhs, fmt, ... ) test_op_5( lhs, op, rhs, fmt, __VA_ARGS__ )
/// @brief Alias for @ref test_op_5 (8-argument form). @see test_op
#define test_op_8( lhs, op, rhs, fmt, ... ) test_op_5( lhs, op, rhs, fmt, __VA_ARGS__ )
/// @brief Token-pastes `GREJC_VA_NUM_ARGS` onto `test_op_` to select the
/// right overload (e.g. `test_op_3`, `test_op_4`, …).
///
/// Separate from @ref GREJC_DISPATCH because `test_op`'s overloads start at
/// 3 arguments rather than 1.
#define GREJC_DISPATCH_OP( N ) GREJC_DISPATCH_OP_IMPL( N )
#define GREJC_DISPATCH_OP_IMPL( N ) test_op_##N
/// @brief Assert that a binary relational/equality operation holds true.
///
/// Like @ref test but specialised for binary-operator expressions such as
/// `a == b`, `x > 0`, or `strcmp(s1, s2) == 0`. The macro takes the left-hand
/// side, the operator, and the right-hand side as three separate arguments so
/// that the failure report can show both the **source text** and the
/// **evaluated values** of each operand individually ("Anatomy" line).
///
/// When `NDEBUG` is defined the macro expands to `((void)0)`.
///
/// On failure the report includes:
/// - Source file and line number.
/// - The full expression text (`lhs op rhs`).
/// - An "Anatomy" line showing the actual run-time values of @p lhs and @p rhs
/// separated by the operator (formatted via @ref GREJC_PRINT_VAL).
/// - The enclosing function name.
/// - An optional user-supplied message (printf-style).
///
/// The program then terminates with `exit(1)`.
///
/// @par Overloads (based on argument count):
/// | Args | Signature | Behaviour |
/// |------|--------------------------------------|------------------------------------------|
/// | 3 | `test_op(lhs, op, rhs)` | Assert `lhs op rhs`, no
/// message. | | 4 | `test_op(lhs, op, rhs, fmt)` | Assert
/// with literal `fmt` (no variadic). | | 5+ | `test_op(lhs, op, rhs, fmt,
/// ...)` | Assert with printf-style message. |
///
/// @param lhs Left-hand operand of the binary expression.
/// @param op Relational or equality operator (`==`, `!=`, `<`, `>`, `<=`,
/// `>=` — passed as a token, not a string).
/// @param rhs Right-hand operand of the binary expression.
/// @param fmt Optional `printf`-style format string (printed only on failure).
/// @param ... Optional variadic arguments for @p fmt.
///
/// @par Example — basic numeric comparison:
/// @code{.c}
/// int a = 5, b = 10;
/// test_op(a, <, b);
/// @endcode
///
/// @par Example — with custom failure message:
/// @code{.c}
/// int value = get_value();
/// test_op(value, >=, 0, "Value out of range: %d", value);
/// @endcode
///
/// @par Example — string comparison:
/// @code{.c}
/// const char *s = get_name();
/// test_op(strcmp(s, "admin"), ==, 0, "Unexpected user: %s", s);
/// @endcode
///
/// @note The operator must be written as a C token, not a string. For example,
/// `test_op(x, ==, y)` is correct; `test_op(x, "==", y)` will **not**
/// compile and would produce a misleading report even if it did.
///
/// @see test For unary truthiness assertions.
#define test_op( ... ) GREJC_DISPATCH_OP( GREJC_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
// ============================================================================
// 2.4.A SEMANTIC STRING & FLOAT ASSERTIONS WITH OFFSET CURSOR DIAGNOSTICS
// ============================================================================
/// @brief Internal diagnostic reporter for @ref test_str_eq failures.
///
/// Emits full actual/expected values, file, line, enclosing function, and a
/// visual cursor '^' highlighting the exact character index where the two strings diverge.
static inline void _grejc_fail_report_str_eq( const char *actual, const char *expected,
const char *expr_str, const char *file, int line,
const char *func, const char *msg_fmt, va_list args ) {
fprintf( stderr, "[%sTEST FAILED%s]{%s%s:%d%s}:\n", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_BLUE, file, line, TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sExpression:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED, expr_str,
TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sExpected:\t%s\"%s\"\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET,
expected ? expected : "NULL" );
fprintf( stderr, "\t%sActual: \t%s\"%s\"\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET,
actual ? actual : "NULL" );
/* Offset diagnosis */
if ( !actual || !expected ) {
fprintf( stderr, "\t%sOffset: \t%s^ (divergence: NULL pointer mismatch)\n",
TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED );
} else {
size_t idx = 0;
while ( actual[idx] && expected[idx] && actual[idx] == expected[idx] )
idx++;
unsigned char ca = (unsigned char)actual[idx];
unsigned char ce = (unsigned char)expected[idx];
/* Format cursor '^' with 1 space for opening quote plus idx spaces */
fprintf( stderr, "\t%sOffset: \t%s ", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
for ( size_t s = 0; s < idx && s < 60; s++ )
fputc( ' ', stderr );
fprintf( stderr, "%s^%s (divergence at index %zu: ", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET, idx );
if ( ca == '\0' )
fprintf( stderr, "'\\0' [0x00]" );
else if ( isprint( ca ) )
fprintf( stderr, "'%c' [0x%02X]", ca, ca );
else
fprintf( stderr, "0x%02X", ca );
fprintf( stderr, " != " );
if ( ce == '\0' )
fprintf( stderr, "'\\0' [0x00]" );
else if ( isprint( ce ) )
fprintf( stderr, "'%c' [0x%02X]", ce, ce );
else
fprintf( stderr, "0x%02X", ce );
fprintf( stderr, ")\n" );
}
fprintf( stderr, "\t%sIn function:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_MAGENTA,
func, TERMINAL_COLOR_RESET );
if ( msg_fmt && msg_fmt[0] ) {
fprintf( stderr, "\t%sMessage:\t%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
vfprintf( stderr, msg_fmt, args );
fprintf( stderr, "\n" );
}
exit( 1 );
}
/// @brief Internal implementation of string equality check.
static inline void _grejc_test_str_eq( const char *actual, const char *expected,
const char *expr_str, const char *file, int line,
const char *func, const char *msg_fmt, ... ) {
if ( actual == expected )
return;
if ( actual && expected && strcmp( actual, expected ) == 0 )
return;
va_list args;
va_start( args, msg_fmt );
_grejc_fail_report_str_eq( actual, expected, expr_str, file, line, func, msg_fmt, args );
va_end( args );
}
/// @brief Internal implementation of string inequality check.
static inline void _grejc_test_str_ne( const char *actual, const char *expected,
const char *expr_str, const char *file, int line,
const char *func, const char *msg_fmt, ... ) {
int equal = 0;
if ( actual == expected )
equal = 1;
else if ( actual && expected && strcmp( actual, expected ) == 0 )
equal = 1;
if ( !equal )
return;
fprintf( stderr, "[%sTEST FAILED%s]{%s%s:%d%s}:\n", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_BLUE, file, line, TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sExpression:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED, expr_str,
TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sValue: \t%s\"%s\" (both strings are identical)\n", TERMINAL_COLOR_YELLOW,
TERMINAL_COLOR_RESET, actual ? actual : "NULL" );
fprintf( stderr, "\t%sIn function:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_MAGENTA,
func, TERMINAL_COLOR_RESET );
if ( msg_fmt && msg_fmt[0] ) {
va_list args;
va_start( args, msg_fmt );
fprintf( stderr, "\t%sMessage:\t%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
vfprintf( stderr, msg_fmt, args );
fprintf( stderr, "\n" );
va_end( args );
}
exit( 1 );
}
/// @brief Internal implementation of string substring check.
static inline void _grejc_test_str_contains( const char *haystack, const char *needle,
const char *expr_str, const char *file, int line,
const char *func, const char *msg_fmt, ... ) {
if ( haystack && needle && strstr( haystack, needle ) != NULL )
return;
fprintf( stderr, "[%sTEST FAILED%s]{%s%s:%d%s}:\n", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_BLUE, file, line, TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sExpression:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED, expr_str,
TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sHaystack: \t%s\"%s\"\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET,
haystack ? haystack : "NULL" );
fprintf( stderr, "\t%sNeedle: \t%s\"%s\" (not found)\n", TERMINAL_COLOR_YELLOW,
TERMINAL_COLOR_RESET, needle ? needle : "NULL" );
fprintf( stderr, "\t%sIn function:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_MAGENTA,
func, TERMINAL_COLOR_RESET );
if ( msg_fmt && msg_fmt[0] ) {
va_list args;
va_start( args, msg_fmt );
fprintf( stderr, "\t%sMessage:\t%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
vfprintf( stderr, msg_fmt, args );
fprintf( stderr, "\n" );
va_end( args );
}
exit( 1 );
}
/// @brief Internal implementation of float equality check.
static inline void _grejc_test_float_eq( float actual, float expected, float epsilon,
const char *expr_str, const char *file, int line,
const char *func, const char *msg_fmt, ... ) {
float diff = fabsf( actual - expected );
float tol = fabsf( epsilon );
if ( diff <= tol )
return;
fprintf( stderr, "[%sTEST FAILED%s]{%s%s:%d%s}:\n", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_BLUE, file, line, TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sExpression:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED, expr_str,
TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sActual: \t%s%f\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET, (double)actual );
fprintf( stderr, "\t%sExpected: \t%s%f\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET, (double)expected );
fprintf( stderr, "\t%sDelta: \t%s%f (allowed tolerance: %f)\n", TERMINAL_COLOR_YELLOW,
TERMINAL_COLOR_RED, (double)diff, (double)tol );
fprintf( stderr, "\t%sIn function:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_MAGENTA,
func, TERMINAL_COLOR_RESET );
if ( msg_fmt && msg_fmt[0] ) {
va_list args;
va_start( args, msg_fmt );
fprintf( stderr, "\t%sMessage:\t%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
vfprintf( stderr, msg_fmt, args );
fprintf( stderr, "\n" );
va_end( args );
}
exit( 1 );
}
/// @brief Internal implementation of double equality check.
static inline void _grejc_test_double_eq( double actual, double expected, double epsilon,
const char *expr_str, const char *file, int line,
const char *func, const char *msg_fmt, ... ) {
double diff = fabs( actual - expected );
double tol = fabs( epsilon );
if ( diff <= tol )
return;
fprintf( stderr, "[%sTEST FAILED%s]{%s%s:%d%s}:\n", TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_BLUE, file, line, TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sExpression:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RED, expr_str,
TERMINAL_COLOR_RESET );
fprintf( stderr, "\t%sActual: \t%s%.9g\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET, actual );
fprintf( stderr, "\t%sExpected: \t%s%.9g\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET, expected );
fprintf( stderr, "\t%sDelta: \t%s%.9g (allowed tolerance: %.9g)\n", TERMINAL_COLOR_YELLOW,
TERMINAL_COLOR_RED, diff, tol );
fprintf( stderr, "\t%sIn function:\t%s%s%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_MAGENTA,
func, TERMINAL_COLOR_RESET );
if ( msg_fmt && msg_fmt[0] ) {
va_list args;
va_start( args, msg_fmt );
fprintf( stderr, "\t%sMessage:\t%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
vfprintf( stderr, msg_fmt, args );
fprintf( stderr, "\n" );
va_end( args );
}
exit( 1 );
}
/* Macros for test_str_eq overloads */
#define test_str_eq_2( lhs, rhs ) \
_grejc_test_str_eq( (lhs), (rhs), "test_str_eq( " #lhs ", " #rhs " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, NULL )
#define test_str_eq_3( lhs, rhs, fmt ) \
_grejc_test_str_eq( (lhs), (rhs), "test_str_eq( " #lhs ", " #rhs " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt )
#define test_str_eq_4( lhs, rhs, fmt, ... ) \
_grejc_test_str_eq( (lhs), (rhs), "test_str_eq( " #lhs ", " #rhs " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt, __VA_ARGS__ )
#define test_str_eq_5( lhs, rhs, fmt, ... ) test_str_eq_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_eq_6( lhs, rhs, fmt, ... ) test_str_eq_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_eq_7( lhs, rhs, fmt, ... ) test_str_eq_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_eq_8( lhs, rhs, fmt, ... ) test_str_eq_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_eq( ... ) GREJC_DISPATCH_STR_EQ( GREJC_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
#define GREJC_DISPATCH_STR_EQ( N ) GREJC_DISPATCH_STR_EQ_IMPL( N )
#define GREJC_DISPATCH_STR_EQ_IMPL( N ) test_str_eq_##N
/* Macros for test_str_ne overloads */
#define test_str_ne_2( lhs, rhs ) \
_grejc_test_str_ne( (lhs), (rhs), "test_str_ne( " #lhs ", " #rhs " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, NULL )
#define test_str_ne_3( lhs, rhs, fmt ) \
_grejc_test_str_ne( (lhs), (rhs), "test_str_ne( " #lhs ", " #rhs " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt )
#define test_str_ne_4( lhs, rhs, fmt, ... ) \
_grejc_test_str_ne( (lhs), (rhs), "test_str_ne( " #lhs ", " #rhs " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt, __VA_ARGS__ )
#define test_str_ne_5( lhs, rhs, fmt, ... ) test_str_ne_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_ne_6( lhs, rhs, fmt, ... ) test_str_ne_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_ne_7( lhs, rhs, fmt, ... ) test_str_ne_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_ne_8( lhs, rhs, fmt, ... ) test_str_ne_4( lhs, rhs, fmt, __VA_ARGS__ )
#define test_str_ne( ... ) GREJC_DISPATCH_STR_NE( GREJC_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
#define GREJC_DISPATCH_STR_NE( N ) GREJC_DISPATCH_STR_NE_IMPL( N )
#define GREJC_DISPATCH_STR_NE_IMPL( N ) test_str_ne_##N
/* Macros for test_str_contains overloads */
#define test_str_contains_2( h, n ) \
_grejc_test_str_contains( (h), (n), "test_str_contains( " #h ", " #n " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, NULL )
#define test_str_contains_3( h, n, fmt ) \
_grejc_test_str_contains( (h), (n), "test_str_contains( " #h ", " #n " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt )
#define test_str_contains_4( h, n, fmt, ... ) \
_grejc_test_str_contains( (h), (n), "test_str_contains( " #h ", " #n " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt, __VA_ARGS__ )
#define test_str_contains_5( h, n, fmt, ... ) test_str_contains_4( h, n, fmt, __VA_ARGS__ )
#define test_str_contains_6( h, n, fmt, ... ) test_str_contains_4( h, n, fmt, __VA_ARGS__ )
#define test_str_contains_7( h, n, fmt, ... ) test_str_contains_4( h, n, fmt, __VA_ARGS__ )
#define test_str_contains_8( h, n, fmt, ... ) test_str_contains_4( h, n, fmt, __VA_ARGS__ )
#define test_str_contains( ... ) GREJC_DISPATCH_STR_CONTAINS( GREJC_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
#define GREJC_DISPATCH_STR_CONTAINS( N ) GREJC_DISPATCH_STR_CONTAINS_IMPL( N )
#define GREJC_DISPATCH_STR_CONTAINS_IMPL( N ) test_str_contains_##N
/* Macros for test_float_eq overloads */
#define test_float_eq_3( lhs, rhs, eps ) \
_grejc_test_float_eq( (float)(lhs), (float)(rhs), (float)(eps), "test_float_eq( " #lhs ", " #rhs ", " #eps " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, NULL )
#define test_float_eq_4( lhs, rhs, eps, fmt ) \
_grejc_test_float_eq( (float)(lhs), (float)(rhs), (float)(eps), "test_float_eq( " #lhs ", " #rhs ", " #eps " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt )
#define test_float_eq_5( lhs, rhs, eps, fmt, ... ) \
_grejc_test_float_eq( (float)(lhs), (float)(rhs), (float)(eps), "test_float_eq( " #lhs ", " #rhs ", " #eps " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt, __VA_ARGS__ )
#define test_float_eq_6( lhs, rhs, eps, fmt, ... ) test_float_eq_5( lhs, rhs, eps, fmt, __VA_ARGS__ )
#define test_float_eq_7( lhs, rhs, eps, fmt, ... ) test_float_eq_5( lhs, rhs, eps, fmt, __VA_ARGS__ )
#define test_float_eq_8( lhs, rhs, eps, fmt, ... ) test_float_eq_5( lhs, rhs, eps, fmt, __VA_ARGS__ )
#define test_float_eq( ... ) GREJC_DISPATCH_FLOAT_EQ( GREJC_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
#define GREJC_DISPATCH_FLOAT_EQ( N ) GREJC_DISPATCH_FLOAT_EQ_IMPL( N )
#define GREJC_DISPATCH_FLOAT_EQ_IMPL( N ) test_float_eq_##N
/* Macros for test_double_eq overloads */
#define test_double_eq_3( lhs, rhs, eps ) \
_grejc_test_double_eq( (double)(lhs), (double)(rhs), (double)(eps), "test_double_eq( " #lhs ", " #rhs ", " #eps " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, NULL )
#define test_double_eq_4( lhs, rhs, eps, fmt ) \
_grejc_test_double_eq( (double)(lhs), (double)(rhs), (double)(eps), "test_double_eq( " #lhs ", " #rhs ", " #eps " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt )
#define test_double_eq_5( lhs, rhs, eps, fmt, ... ) \
_grejc_test_double_eq( (double)(lhs), (double)(rhs), (double)(eps), "test_double_eq( " #lhs ", " #rhs ", " #eps " )", __FILE__, __LINE__, GREJC_TEST_FUNCTION, fmt, __VA_ARGS__ )
#define test_double_eq_6( lhs, rhs, eps, fmt, ... ) test_double_eq_5( lhs, rhs, eps, fmt, __VA_ARGS__ )
#define test_double_eq_7( lhs, rhs, eps, fmt, ... ) test_double_eq_5( lhs, rhs, eps, fmt, __VA_ARGS__ )
#define test_double_eq_8( lhs, rhs, eps, fmt, ... ) test_double_eq_5( lhs, rhs, eps, fmt, __VA_ARGS__ )
#define test_double_eq( ... ) GREJC_DISPATCH_DOUBLE_EQ( GREJC_VA_NUM_ARGS( __VA_ARGS__ ) )( __VA_ARGS__ )
#define GREJC_DISPATCH_DOUBLE_EQ( N ) GREJC_DISPATCH_DOUBLE_EQ_IMPL( N )
#define GREJC_DISPATCH_DOUBLE_EQ_IMPL( N ) test_double_eq_##N
#endif /* NDEBUG */
// ============================================================================
// BOX UTILS
// ============================================================================
/// @defgroup box_chars Box Drawing Characters
///
/// Single-line box drawing characters using Unicode box-drawing symbols.
///
/// @{
#define BOX_H "\u2500" ///< Horizontal line.
#define BOX_V "\u2502" ///< Vertical line.
#define BOX_TL "\u250C" ///< Top-left corner.
#define BOX_TR "\u2510" ///< Top-right corner.
#define BOX_BL "\u2514" ///< Bottom-left corner.
#define BOX_BR "\u2518" ///< Bottom-right corner.
#define BOX_ML "\u251C" ///< Middle-left (T-junction pointing right).
#define BOX_MR "\u2524" ///< Middle-right (T-junction pointing left).
#define BOX_MT "\u252C" ///< Middle-top (T-junction pointing down).
#define BOX_MB "\u2534" ///< Middle-bottom (T-junction pointing up).
#define BOX_CROSS "\u253C" ///< Cross intersection.
/// @}
/// @defgroup box2_chars Double-line Box Drawing Characters
///
/// @{
#define BOX2_H "\u2550" ///< Double horizontal line.
#define BOX2_V "\u2551" ///< Double vertical line.
#define BOX2_TL "\u2554" ///< Double top-left corner.
#define BOX2_TR "\u2557" ///< Double top-right corner.
#define BOX2_BL "\u255A" ///< Double bottom-left corner.
#define BOX2_BR "\u255D" ///< Double bottom-right corner.
/// @}
/// @brief Prints a horizontal line composed of repeated characters.
///
/// Internal helper used by the box and table macros. Prints @p left, followed
/// by @p width repetitions of @p ch, followed by @p right and a newline.
///
/// @param left String printed before the line (e.g. a corner character).
/// @param ch Character (or multi-char string) repeated across the line.
/// @param right String printed after the line.
/// @param width Number of repetitions of @p ch.
static inline void _box_hline( const char *left, const char *ch, const char *right, int width ) {
printf( "%s", left );
for ( int i = 0; i < width; i++ )
printf( "%s", ch );
printf( "%s\n", right );
}
/// @brief Prints @p text centered inside a single-line box.
///
/// The box width is determined by the terminal width. The text is padded with
/// one space on each side.
///
/// @param text The string to display.
/// @param color A @ref TERMINAL_COLOR_* constant for the box border, or
/// @ref TERMINAL_COLOR_RESET for default.
///
/// Example:
/// @code{.c}
/// print_box("Hello, World!", TERMINAL_COLOR_CYAN);
/// @endcode
static inline void print_box( const char *text, const char *color ) {
if ( !text )
text = "";
if ( !color )
color = "";
int term_w = _get_terminal_dimensions().width;
if ( term_w < 4 )
term_w = 80;
int t_len = (int)strlen( text );
int pad_left = ( term_w - 2 - t_len ) / 2;
if ( pad_left < 0 )
pad_left = 0;
int pad_right = term_w - 2 - t_len - pad_left;
if ( pad_right < 0 )
pad_right = 0;
printf( "%s", color );
_box_hline( BOX_TL, BOX_H, BOX_TR, term_w - 2 );
printf( "%s%s%*s%s%*s%s\n", BOX_V, TERMINAL_COLOR_RESET, pad_left + t_len, text, color, pad_right, "", BOX_V );
_box_hline( BOX_BL, BOX_H, BOX_BR, term_w - 2 );
printf( "%s", TERMINAL_COLOR_RESET );
}
/// @brief Prints @p text centered inside a double-line box.
///
/// Identical behaviour to @ref print_box but uses double-line border
/// characters (@ref BOX2_*) instead of single-line ones.
///
/// @param text The string to display.
/// @param color A @ref TERMINAL_COLOR_* constant for the border.
///
/// Example:
/// @code{.c}
/// print_box_double("Hello, World!", TERMINAL_COLOR_CYAN);
/// @endcode
static inline void print_box_double( const char *text, const char *color ) {
if ( !text )
text = "";
if ( !color )
color = "";
int term_w = _get_terminal_dimensions().width;
if ( term_w < 4 )
term_w = 80;
int t_len = (int)strlen( text );
int pad_left = ( term_w - 2 - t_len ) / 2;
if ( pad_left < 0 )
pad_left = 0;
int pad_right = term_w - 2 - t_len - pad_left;
if ( pad_right < 0 )
pad_right = 0;
printf( "%s", color );
_box_hline( BOX2_TL, BOX2_H, BOX2_TR, term_w - 2 );
printf( "%s%s%*s%s%*s%s\n", BOX2_V, TERMINAL_COLOR_RESET, pad_left + t_len, text, color, pad_right, "", BOX2_V );
_box_hline( BOX2_BL, BOX2_H, BOX2_BR, term_w - 2 );
printf( "%s", TERMINAL_COLOR_RESET );
}
// ============================================================================
// TABLE UTILS
// ============================================================================
/// @brief Maximum number of columns supported by the table API.
#define TABLE_MAX_COLS 16
/// @brief Horizontal alignment options for table columns.
typedef enum {
TABLE_ALIGN_LEFT = 0, ///< Aligned to the left (legacy default).
TABLE_ALIGN_CENTER, ///< Centered horizontally.
TABLE_ALIGN_RIGHT ///< Aligned to the right (ideal for numbers).
} TableAlign;
/// @brief Configuration style for a table.
///
/// Stores the column layout, the number of columns, and the colours to use for
/// the header, data rows and borders. Initialise with @ref table_init and
/// optionally override individual column widths with @ref table_set_col_width.
typedef struct {
int col_count; ///< Number of columns in use.
int col_widths[TABLE_MAX_COLS]; ///< Inner width per column (no padding).
TableAlign col_align[TABLE_MAX_COLS]; ///< Alignment per column.
const char *header_color; ///< Colour for header text.
const char *row_color; ///< Colour for data row text.
const char *border_color; ///< Colour for border characters.
} TableStyle;
/// @brief Initialises a @ref TableStyle, distributing available terminal width
/// evenly among @p col_count columns.
///
/// The available width is computed as the terminal width minus the space needed
/// for borders (one per column plus one outer) and padding (two spaces per
/// column). If the terminal is very narrow, each column is given at least 1
/// character of width.
///
/// @param style Pointer to an uninitialised @ref TableStyle.
/// @param col_count Number of columns (maximum @ref TABLE_MAX_COLS).
/// @param header_color Colour for header text (e.g. @ref TERMINAL_COLOR_CYAN).
/// @param row_color Colour for data row text.
/// @param border_color Colour for border characters.
///
/// Example:
/// @code{.c}
/// TableStyle ts;
/// table_init(&ts, 3, TERMINAL_COLOR_CYAN, TERMINAL_COLOR_WHITE,
/// TERMINAL_COLOR_BLUE);
/// @endcode
static inline void table_init( TableStyle *style, int col_count, const char *header_color, const char *row_color,
const char *border_color ) {
if ( !style )
return;
if ( col_count <= 0 || col_count > TABLE_MAX_COLS ) {
style->col_count = 0;
return;
}
const int TERMINAL_WIDTH = _get_terminal_dimensions().width;
style->col_count = col_count;
style->header_color = header_color ? header_color : TERMINAL_COLOR_RESET;
style->row_color = row_color ? row_color : TERMINAL_COLOR_RESET;
style->border_color = border_color ? border_color : TERMINAL_COLOR_RESET;
int term_w = TERMINAL_WIDTH > 0 ? TERMINAL_WIDTH : 80;
/* borders: 1(left) + col_count*(1 right) = col_count+1 chars
padding: col_count * 2 (one space each side) */
int avail = term_w - ( col_count + 1 ) - ( col_count * 2 );
if ( avail < col_count )
avail = col_count;
int base = avail / col_count;
int extra = avail % col_count;
for ( int i = 0; i < col_count; i++ ) {
style->col_widths[i] = base + ( i < extra ? 1 : 0 );
style->col_align[i] = TABLE_ALIGN_LEFT;
}
for ( int i = col_count; i < TABLE_MAX_COLS; i++ ) {
style->col_widths[i] = 0;
style->col_align[i] = TABLE_ALIGN_LEFT;
}
}
/// @brief Overrides the display width of a specific column.
///
/// Use after @ref table_init to give a column a fixed width instead of the
/// evenly-distributed default.
///
/// @param style Pointer to an initialised @ref TableStyle.
/// @param col Column index (0-based).
/// @param width Desired inner width in characters.
static inline void table_set_col_width( TableStyle *style, int col, int width ) {
if ( style && col >= 0 && col < style->col_count )
style->col_widths[col] = width;
}
/// @brief Sets horizontal text alignment for a specific table column.
///
/// @param style Pointer to an initialized @ref TableStyle.
/// @param col_idx Column index (0-based, 0 to col_count - 1).
/// @param align Desired alignment (@ref TABLE_ALIGN_LEFT, @ref TABLE_ALIGN_CENTER, @ref TABLE_ALIGN_RIGHT).
static inline void table_set_col_align( TableStyle *style, int col_idx, TableAlign align ) {
if ( !style || col_idx < 0 || col_idx >= style->col_count )
return;
style->col_align[col_idx] = align;
}
/// @brief Internal: prints one horizontal separator row for the table to stream.
static inline void _table_hline_stream( FILE *stream, const TableStyle *s, const char *left, const char *mid,
const char *right ) {
if ( !s || s->col_count <= 0 )
return;
if ( !stream )
stream = stdout;
fprintf( stream, "%s%s", s->border_color ? s->border_color : "", left ? left : "" );
for ( int c = 0; c < s->col_count; c++ ) {
for ( int i = 0; i < s->col_widths[c] + 2; i++ )
fprintf( stream, "%s", BOX_H );
fprintf( stream, "%s", c < s->col_count - 1 ? ( mid ? mid : "" ) : ( right ? right : "" ) );
}
fprintf( stream, "%s\n", TERMINAL_COLOR_RESET );
}
/// @brief Internal: prints one horizontal separator row for the table to stdout.
static inline void _table_hline( const TableStyle *s, const char *left, const char *mid, const char *right ) {
_table_hline_stream( stdout, s, left, mid, right );
}
/// @brief Internal: prints one data row for the table to stream with column alignment.
static inline void _table_row_stream( FILE *stream, const TableStyle *s, const char *const *cells,
const char *text_color ) {
if ( !s || s->col_count <= 0 )
return;
if ( !stream )
stream = stdout;
fprintf( stream, "%s%s%s", s->border_color ? s->border_color : "", BOX_V, TERMINAL_COLOR_RESET );
for ( int c = 0; c < s->col_count; c++ ) {
int w = s->col_widths[c];
const char *cell = ( cells && cells[c] ) ? cells[c] : "";
int len = (int)strlen( cell );
if ( len > w )
len = w;
int p_left = 0;
int p_right = 0;
TableAlign align = s->col_align[c];
if ( align == TABLE_ALIGN_RIGHT ) {
p_left = w - len;
p_right = 0;
} else if ( align == TABLE_ALIGN_CENTER ) {
p_left = ( w - len ) / 2;
p_right = w - len - p_left;
} else {
p_left = 0;
p_right = w - len;
}
fprintf( stream, "%s ", text_color ? text_color : "" );
if ( p_left > 0 )
fprintf( stream, "%*s", p_left, "" );
fprintf( stream, "%.*s", len, cell );
if ( p_right > 0 )
fprintf( stream, "%*s", p_right, "" );
fprintf( stream, " %s%s%s", s->border_color ? s->border_color : "", BOX_V, TERMINAL_COLOR_RESET );
}
fputc( '\n', stream );
}
/// @brief Internal: prints one data row for the table to stdout.
static inline void _table_row( const TableStyle *s, const char *const *cells, const char *text_color ) {
_table_row_stream( stdout, s, cells, text_color );
}
/// @brief Prints the top border and header row of a table.
static inline void table_print_header( const TableStyle *style, const char *const *headers ) {
if ( !style || style->col_count <= 0 || !headers )
return;
_table_hline( style, BOX_TL, BOX_MT, BOX_TR );
_table_row( style, headers, style->header_color );
_table_hline( style, BOX_ML, BOX_CROSS, BOX_MR );
}
/// @brief Prints a single data row.
static inline void table_print_row( const TableStyle *style, const char *const *cells ) {
if ( !style || style->col_count <= 0 || !cells )
return;
_table_row( style, cells, style->row_color );
}
/// @brief Prints the bottom border of a table.
static inline void table_print_footer( const TableStyle *style ) {
if ( !style || style->col_count <= 0 )
return;
_table_hline( style, BOX_BL, BOX_MB, BOX_BR );
}
#ifndef TABLE_MAX_SUBLINES
#define TABLE_MAX_SUBLINES 128
#endif
typedef struct {
const char *ptr;
int len;
} _TableSlice;
/// @brief Internal: renders a data row with word-wrapping across all columns to stream.
static inline void _table_row_wrapped_stream( FILE *stream, const TableStyle *style, const char *const *cells ) {
if ( !style || style->col_count <= 0 )
return;
if ( !stream )
stream = stdout;
_TableSlice slices[TABLE_MAX_COLS][TABLE_MAX_SUBLINES];
int sub_counts[TABLE_MAX_COLS];
int max_lines = 1;
for ( int c = 0; c < style->col_count; c++ ) {
sub_counts[c] = 0;
int w = style->col_widths[c] > 0 ? style->col_widths[c] : 1;
const char *p = ( cells && cells[c] ) ? cells[c] : "";
if ( *p == '\0' ) {
slices[c][0].ptr = "";
slices[c][0].len = 0;
sub_counts[c] = 1;
continue;
}
while ( *p && sub_counts[c] < TABLE_MAX_SUBLINES ) {
int rem_len = (int)strlen( p );
const char *nl = strchr( p, '\n' );
if ( nl && (int)( nl - p ) <= w ) {
int line_len = (int)( nl - p );
slices[c][sub_counts[c]].ptr = p;
slices[c][sub_counts[c]].len = line_len;
sub_counts[c]++;
p = nl + 1;
continue;
}
if ( rem_len <= w ) {
slices[c][sub_counts[c]].ptr = p;
slices[c][sub_counts[c]].len = rem_len;
sub_counts[c]++;
break;
}
int break_idx = -1;
for ( int i = w; i > 0; i-- ) {
if ( p[i] == ' ' ) {
break_idx = i;
break;
}
}
if ( break_idx > 0 ) {
slices[c][sub_counts[c]].ptr = p;
slices[c][sub_counts[c]].len = break_idx;
sub_counts[c]++;
p += break_idx + 1;
} else {
slices[c][sub_counts[c]].ptr = p;
slices[c][sub_counts[c]].len = w;
sub_counts[c]++;
p += w;
}
}
if ( sub_counts[c] == 0 ) {
slices[c][0].ptr = "";
slices[c][0].len = 0;
sub_counts[c] = 1;
}
if ( sub_counts[c] > max_lines )
max_lines = sub_counts[c];
}
for ( int r = 0; r < max_lines; r++ ) {
fprintf( stream, "%s%s%s", style->border_color ? style->border_color : "", BOX_V, TERMINAL_COLOR_RESET );
for ( int c = 0; c < style->col_count; c++ ) {
int w = style->col_widths[c] > 0 ? style->col_widths[c] : 1;
const char *str = "";
int len = 0;
if ( r < sub_counts[c] ) {
str = slices[c][r].ptr;
len = slices[c][r].len;
}
if ( len > w )
len = w;
int p_left = 0;
int p_right = 0;
TableAlign align = style->col_align[c];
if ( align == TABLE_ALIGN_RIGHT ) {
p_left = w - len;
p_right = 0;
} else if ( align == TABLE_ALIGN_CENTER ) {
p_left = ( w - len ) / 2;
p_right = w - len - p_left;
} else {
p_left = 0;
p_right = w - len;
}
fprintf( stream, "%s ", style->row_color ? style->row_color : "" );
if ( p_left > 0 )
fprintf( stream, "%*s", p_left, "" );
fprintf( stream, "%.*s", len, str );
if ( p_right > 0 )
fprintf( stream, "%*s", p_right, "" );
fprintf( stream, " %s%s%s", style->border_color ? style->border_color : "", BOX_V, TERMINAL_COLOR_RESET );
}
fputc( '\n', stream );
}
}
/// @brief Prints a data row with automatic word-wrapping for cells exceeding column width.
///
/// Word breaks occur smoothly on space boundaries or newline characters without disturbing
/// vertical column borders. Single words that exceed column width are sliced cleanly.
///
/// @param style Pointer to an initialized @ref TableStyle.
/// @param cells Array of @p col_count strings.
static inline void table_print_row_wrapped( const TableStyle *style, const char *const *cells ) {
_table_row_wrapped_stream( stdout, style, cells );
}
// ============================================================================
// 2.3.C DYNAMIC AUTO-FIT TABLE (TableAuto)
// ============================================================================
/// @brief Node representing a single row of cells in dynamic @ref TableAuto.
typedef struct TableAutoRow_s {
char **cells; ///< Array of duplicated cell strings.
struct TableAutoRow_s *next; ///< Pointer to next row.
} TableAutoRow;
/// @brief Dynamic table structure with automatic column width inference.
typedef struct TableAuto_s {
int col_count; ///< Number of columns.
char **headers; ///< Optional array of header strings.
TableAutoRow *rows_head; ///< Head of row linked list.
TableAutoRow *rows_tail; ///< Tail of row linked list.
int row_count; ///< Total rows stored.
TableAlign col_align[TABLE_MAX_COLS]; ///< Alignment per column.
char header_color[32]; ///< Header color escape sequence.
char row_color[32]; ///< Row text color escape sequence.
char border_color[32]; ///< Border color escape sequence.
} TableAuto;
/// @brief Creates a dynamic auto-fit table instance.
///
/// @param col_count Number of columns (1 to @ref TABLE_MAX_COLS).
/// @param headers Array of column header strings (or NULL if table has no header).
/// @return Pointer to allocated @ref TableAuto, or NULL on error.
static inline TableAuto *table_auto_create( int col_count, const char *const *headers ) {
if ( col_count <= 0 || col_count > TABLE_MAX_COLS )
return NULL;
TableAuto *ta = (TableAuto *)calloc( 1, sizeof( TableAuto ) );
if ( !ta )
return NULL;
ta->col_count = col_count;
snprintf( ta->header_color, sizeof( ta->header_color ), "%s", TERMINAL_COLOR_CYAN );
snprintf( ta->row_color, sizeof( ta->row_color ), "%s", TERMINAL_COLOR_WHITE );
snprintf( ta->border_color, sizeof( ta->border_color ), "%s", TERMINAL_COLOR_BLUE );
for ( int i = 0; i < TABLE_MAX_COLS; i++ )
ta->col_align[i] = TABLE_ALIGN_LEFT;
if ( headers ) {
ta->headers = (char **)calloc( (size_t)col_count, sizeof( char * ) );
if ( !ta->headers ) {
free( ta );
return NULL;
}
for ( int i = 0; i < col_count; i++ ) {
ta->headers[i] = strdup( headers[i] ? headers[i] : "" );
if ( !ta->headers[i] ) {
for ( int j = 0; j < i; j++ )
free( ta->headers[j] );
free( ta->headers );
free( ta );
return NULL;
}
}
}
return ta;
}
/// @brief Sets horizontal alignment for a column in @ref TableAuto.
///
/// @param ta Pointer to @ref TableAuto.
/// @param col_idx Column index (0 to col_count - 1).
/// @param align Alignment setting (@ref TABLE_ALIGN_LEFT, @ref TABLE_ALIGN_CENTER, @ref TABLE_ALIGN_RIGHT).
static inline void table_auto_set_align( TableAuto *ta, int col_idx, TableAlign align ) {
if ( !ta || col_idx < 0 || col_idx >= ta->col_count )
return;
ta->col_align[col_idx] = align;
}
/// @brief Sets rendering colors for @ref TableAuto.
///
/// @param ta Pointer to @ref TableAuto.
/// @param header_color ANSI color for headers.
/// @param row_color ANSI color for row cells.
/// @param border_color ANSI color for borders.
static inline void table_auto_set_colors( TableAuto *ta, const char *header_color,
const char *row_color, const char *border_color ) {
if ( !ta )
return;
if ( header_color )
snprintf( ta->header_color, sizeof( ta->header_color ), "%s", header_color );
if ( row_color )
snprintf( ta->row_color, sizeof( ta->row_color ), "%s", row_color );
if ( border_color )
snprintf( ta->border_color, sizeof( ta->border_color ), "%s", border_color );
}
/// @brief Appends a data row to @ref TableAuto. All strings are duplicated internally.
///
/// @param ta Pointer to @ref TableAuto.
/// @param cells Array of string pointers with size equal to col_count.
/// @return 1 on success, 0 on invalid parameters or allocation failure.
static inline int table_auto_add_row( TableAuto *ta, const char *const *cells ) {
if ( !ta || !cells )
return 0;
TableAutoRow *row = (TableAutoRow *)calloc( 1, sizeof( TableAutoRow ) );
if ( !row )
return 0;
row->cells = (char **)calloc( (size_t)ta->col_count, sizeof( char * ) );
if ( !row->cells ) {
free( row );
return 0;
}
for ( int i = 0; i < ta->col_count; i++ ) {
row->cells[i] = strdup( cells[i] ? cells[i] : "" );
if ( !row->cells[i] ) {
for ( int j = 0; j < i; j++ )
free( row->cells[j] );
free( row->cells );
free( row );
return 0;
}
}
if ( !ta->rows_head ) {
ta->rows_head = row;
ta->rows_tail = row;
} else {
ta->rows_tail->next = row;
ta->rows_tail = row;
}
ta->row_count++;
return 1;
}
/// @brief Analyzes content widths, computes dynamic layout and renders the table to stream.
///
/// @param ta Pointer to @ref TableAuto.
/// @param stream Output stream (defaults to stdout if NULL).
static inline void table_auto_render( TableAuto *ta, FILE *stream ) {
if ( !ta || ta->col_count <= 0 )
return;
if ( !stream )
stream = stdout;
int max_w[TABLE_MAX_COLS] = { 0 };
for ( int c = 0; c < ta->col_count; c++ ) {
if ( ta->headers && ta->headers[c] )
max_w[c] = (int)strlen( ta->headers[c] );
else
max_w[c] = 0;
}
for ( TableAutoRow *r = ta->rows_head; r != NULL; r = r->next ) {
for ( int c = 0; c < ta->col_count; c++ ) {
int len = r->cells[c] ? (int)strlen( r->cells[c] ) : 0;
if ( len > max_w[c] )
max_w[c] = len;
}
}
for ( int c = 0; c < ta->col_count; c++ ) {
if ( max_w[c] < 4 )
max_w[c] = 4;
}
int term_w = _get_terminal_dimensions().width;
if ( term_w <= 0 )
term_w = 80;
int border_padding = ( ta->col_count + 1 ) + ( ta->col_count * 2 );
int avail = term_w - border_padding;
if ( avail < ta->col_count * 4 )
avail = ta->col_count * 4;
int total_needed = 0;
for ( int c = 0; c < ta->col_count; c++ )
total_needed += max_w[c];
int final_w[TABLE_MAX_COLS];
if ( total_needed <= avail ) {
for ( int c = 0; c < ta->col_count; c++ )
final_w[c] = max_w[c];
} else {
for ( int c = 0; c < ta->col_count; c++ ) {
int w = ( max_w[c] * avail ) / total_needed;
if ( w < 4 )
w = 4;
final_w[c] = w;
}
}
TableStyle ts;
ts.col_count = ta->col_count;
ts.header_color = ta->header_color;
ts.row_color = ta->row_color;
ts.border_color = ta->border_color;
for ( int c = 0; c < ta->col_count; c++ ) {
ts.col_widths[c] = final_w[c];
ts.col_align[c] = ta->col_align[c];
}
if ( ta->headers ) {
_table_hline_stream( stream, &ts, BOX_TL, BOX_MT, BOX_TR );
_table_row_stream( stream, &ts, (const char *const *)ta->headers, ts.header_color );
_table_hline_stream( stream, &ts, BOX_ML, BOX_CROSS, BOX_MR );
} else {
_table_hline_stream( stream, &ts, BOX_TL, BOX_MT, BOX_TR );
}
for ( TableAutoRow *r = ta->rows_head; r != NULL; r = r->next ) {
int needs_wrap = 0;
for ( int c = 0; c < ta->col_count; c++ ) {
if ( r->cells[c] && (int)strlen( r->cells[c] ) > ts.col_widths[c] ) {
needs_wrap = 1;
break;
}
}
if ( needs_wrap )
_table_row_wrapped_stream( stream, &ts, (const char *const *)r->cells );
else
_table_row_stream( stream, &ts, (const char *const *)r->cells, ts.row_color );
}
_table_hline_stream( stream, &ts, BOX_BL, BOX_MB, BOX_BR );
}
/// @brief Releases all allocated memory associated with @ref TableAuto.
///
/// @param ta Pointer to @ref TableAuto (safe no-op if NULL).
static inline void table_auto_free( TableAuto *ta ) {
if ( !ta )
return;
if ( ta->headers ) {
for ( int i = 0; i < ta->col_count; i++ )
free( ta->headers[i] );
free( ta->headers );
}
TableAutoRow *curr = ta->rows_head;
while ( curr ) {
TableAutoRow *next = curr->next;
if ( curr->cells ) {
for ( int i = 0; i < ta->col_count; i++ )
free( curr->cells[i] );
free( curr->cells );
}
free( curr );
curr = next;
}
free( ta );
}
// ============================================================================
// 2.4.B MODERN TEST RUNNER ENGINE WITH ACCUMULATOR
// ============================================================================
#ifndef TEST_RUNNER_MAX_CASES
#define TEST_RUNNER_MAX_CASES 256
#endif
/// @brief Holds the execution result of an individual test case.
typedef struct {
char name[64]; ///< Test case function name.
int assertions_total; ///< Total assertions executed.
int assertions_failed; ///< Total assertion failures.
double duration_ms; ///< Execution duration in milliseconds.
char first_failure[256]; ///< Details of the first failure.
bool passed; ///< True if all assertions passed.
} _TestCaseResult;
/// @brief Global state of the modern test runner accumulator.
typedef struct {
char suite_name[64]; ///< Suite name.
_TestCaseResult cases[TEST_RUNNER_MAX_CASES]; ///< Registered test cases.
int case_count; ///< Total test cases executed.
int total_assertions; ///< Total assertions across all cases.
int failed_assertions; ///< Total failed assertions across all cases.
int current_case_idx; ///< Index of currently executing case (-1 if none).
} _TestRunnerState;
static _TestRunnerState _test_runner = { .current_case_idx = -1 };
/// @brief Obtains monotonic time in milliseconds.
static inline double _test_runner_get_time_ms( void ) {
struct timespec ts;
clock_gettime( CLOCK_MONOTONIC, &ts );
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0;
}
/// @brief Records assertion result in test runner state.
static inline int _test_runner_record_result( bool passed, const char *expr_str,
const char *file, int line,
const char *func, const char *detail ) {
_test_runner.total_assertions++;
_TestCaseResult *tc = ( _test_runner.current_case_idx >= 0 &&
_test_runner.current_case_idx < _test_runner.case_count )
? &_test_runner.cases[_test_runner.current_case_idx]
: NULL;
if ( tc )
tc->assertions_total++;
if ( passed )
return 1;
_test_runner.failed_assertions++;
if ( tc ) {
tc->assertions_failed++;
tc->passed = false;
if ( tc->first_failure[0] == '\0' ) {
if ( detail && detail[0] )
snprintf( tc->first_failure, sizeof( tc->first_failure ), "%s:%d %s", file, line, detail );
else
snprintf( tc->first_failure, sizeof( tc->first_failure ), "%s:%d (%s)", file, line, expr_str );
}
}
fprintf( stderr, "[%sEXPECT FAILED%s]{%s%s:%d%s}: in %s%s%s: %s",
TERMINAL_COLOR_RED, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_BLUE, file, line, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_MAGENTA, func, TERMINAL_COLOR_RESET,
expr_str );
if ( detail && detail[0] )
fprintf( stderr, " (%s)", detail );
fprintf( stderr, "\n" );
return 0;
}
static inline int _test_runner_expect( bool expr, const char *expr_str, const char *file, int line,
const char *func ) {
return _test_runner_record_result( expr, expr_str, file, line, func, NULL );
}
static inline int _test_runner_expect_eq( long long a, long long b, const char *a_str, const char *b_str,
const char *file, int line, const char *func ) {
char detail[128];
snprintf( detail, sizeof( detail ), "%lld != %lld", a, b );
char expr[128];
snprintf( expr, sizeof( expr ), "%s == %s", a_str, b_str );
return _test_runner_record_result( a == b, expr, file, line, func, detail );
}
static inline int _test_runner_expect_ne( long long a, long long b, const char *a_str, const char *b_str,
const char *file, int line, const char *func ) {
char detail[128];
snprintf( detail, sizeof( detail ), "%lld == %lld", a, b );
char expr[128];
snprintf( expr, sizeof( expr ), "%s != %s", a_str, b_str );
return _test_runner_record_result( a != b, expr, file, line, func, detail );
}
static inline int _test_runner_expect_str_eq( const char *a, const char *b, const char *a_str, const char *b_str,
const char *file, int line, const char *func ) {
bool eq = false;
if ( a == b )
eq = true;
else if ( a && b && strcmp( a, b ) == 0 )
eq = true;
char detail[256];
snprintf( detail, sizeof( detail ), "\"%s\" != \"%s\"", a ? a : "NULL", b ? b : "NULL" );
char expr[128];
snprintf( expr, sizeof( expr ), "strcmp(%s, %s) == 0", a_str, b_str );
return _test_runner_record_result( eq, expr, file, line, func, detail );
}
static inline int _test_runner_expect_str_ne( const char *a, const char *b, const char *a_str, const char *b_str,
const char *file, int line, const char *func ) {
bool eq = false;
if ( a == b )
eq = true;
else if ( a && b && strcmp( a, b ) == 0 )
eq = true;
char detail[256];
snprintf( detail, sizeof( detail ), "\"%s\" == \"%s\"", a ? a : "NULL", b ? b : "NULL" );
char expr[128];
snprintf( expr, sizeof( expr ), "strcmp(%s, %s) != 0", a_str, b_str );
return _test_runner_record_result( !eq, expr, file, line, func, detail );
}
static inline int _test_runner_expect_str_contains( const char *h, const char *n, const char *h_str,
const char *n_str, const char *file, int line,
const char *func ) {
bool contains = ( h && n && strstr( h, n ) != NULL );
char detail[256];
snprintf( detail, sizeof( detail ), "\"%s\" not found in \"%s\"", n ? n : "NULL", h ? h : "NULL" );
char expr[128];
snprintf( expr, sizeof( expr ), "strstr(%s, %s) != NULL", h_str, n_str );
return _test_runner_record_result( contains, expr, file, line, func, detail );
}
static inline int _test_runner_expect_float_eq( float a, float b, float eps, const char *a_str, const char *b_str,
const char *file, int line, const char *func ) {
float diff = fabsf( a - b );
float tol = fabsf( eps );
bool pass = ( diff <= tol );
char detail[128];
snprintf( detail, sizeof( detail ), "|%f - %f| = %f > %f", (double)a, (double)b, (double)diff, (double)tol );
char expr[128];
snprintf( expr, sizeof( expr ), "|%s - %s| <= %f", a_str, b_str, (double)tol );
return _test_runner_record_result( pass, expr, file, line, func, detail );
}
static inline int _test_runner_expect_double_eq( double a, double b, double eps, const char *a_str, const char *b_str,
const char *file, int line, const char *func ) {
double diff = fabs( a - b );
double tol = fabs( eps );
bool pass = ( diff <= tol );
char detail[128];
snprintf( detail, sizeof( detail ), "|%.9g - %.9g| = %.9g > %.9g", a, b, diff, tol );
char expr[128];
snprintf( expr, sizeof( expr ), "|%s - %s| <= %.9g", a_str, b_str, tol );
return _test_runner_record_result( pass, expr, file, line, func, detail );
}
/// @brief Registers and runs a test case in the modern test runner.
static inline void _test_runner_run_case( const char *name, void ( *func )( void ) ) {
if ( !name || !func )
return;
if ( _test_runner.case_count >= TEST_RUNNER_MAX_CASES ) {
fprintf( stderr, "%s[TEST RUNNER] Maximum test cases limit (%d) reached!%s\n",
TERMINAL_COLOR_RED, TEST_RUNNER_MAX_CASES, TERMINAL_COLOR_RESET );
return;
}
int idx = _test_runner.case_count++;
_test_runner.current_case_idx = idx;
_TestCaseResult *tc = &_test_runner.cases[idx];
memset( tc, 0, sizeof( _TestCaseResult ) );
strncpy( tc->name, name, sizeof( tc->name ) - 1 );
tc->passed = true;
double t0 = _test_runner_get_time_ms();
func();
double t1 = _test_runner_get_time_ms();
tc->duration_ms = t1 - t0;
if ( tc->duration_ms < 0.0 )
tc->duration_ms = 0.0;
if ( tc->assertions_failed > 0 )
tc->passed = false;
_test_runner.current_case_idx = -1;
}
/// @brief Renders the consolidated graphical test runner summary table.
///
/// Uses @ref TableStyle and @ref BoxUtils to present a summary of test results,
/// execution time per case, total assertions, and failure details.
///
/// @return 0 if all test cases passed, or the total number of failed test cases.
static inline int test_runner_summary( void ) {
TableStyle ts;
table_init( &ts, 4, TERMINAL_COLOR_CYAN, TERMINAL_COLOR_WHITE, TERMINAL_COLOR_BLUE );
table_set_col_width( &ts, 0, 40 );
table_set_col_width( &ts, 1, 8 );
table_set_col_width( &ts, 2, 12 );
table_set_col_width( &ts, 3, 26 );
table_set_col_align( &ts, 0, TABLE_ALIGN_LEFT );
table_set_col_align( &ts, 1, TABLE_ALIGN_CENTER );
table_set_col_align( &ts, 2, TABLE_ALIGN_RIGHT );
table_set_col_align( &ts, 3, TABLE_ALIGN_LEFT );
const char *headers[] = { "Test Case", "Status", "Duration", "Details" };
table_print_header( &ts, headers );
if ( _test_runner.case_count == 0 ) {
const char *empty_row[] = { "(no tests)", "N/A", "0.00 ms", "0 tests run" };
table_print_row( &ts, empty_row );
table_print_footer( &ts );
printf( "\n%sNo tests executed.%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
return 0;
}
int passed_cases = 0;
int failed_cases = 0;
for ( int i = 0; i < _test_runner.case_count; i++ ) {
_TestCaseResult *tc = &_test_runner.cases[i];
char dur_buf[32];
snprintf( dur_buf, sizeof( dur_buf ), "%.2f ms", tc->duration_ms );
const char *status_str = tc->passed ? "PASS" : "FAIL";
const char *detail_str = tc->passed ? "All checks passed" : tc->first_failure;
if ( tc->passed )
passed_cases++;
else
failed_cases++;
const char *row_cells[] = { tc->name, status_str, dur_buf, detail_str };
table_print_row( &ts, row_cells );
}
table_print_footer( &ts );
printf( "\n%sTest Suite Summary:%s %s%s%s\n", TERMINAL_COLOR_CYAN, TERMINAL_COLOR_RESET,
TERMINAL_COLOR_YELLOW, _test_runner.suite_name[0] ? _test_runner.suite_name : "Default",
TERMINAL_COLOR_RESET );
printf( " %sCases: %s%d Total | %s%d Passed%s | %s%d Failed%s\n",
TERMINAL_COLOR_WHITE, TERMINAL_COLOR_CYAN, _test_runner.case_count,
TERMINAL_COLOR_GREEN, passed_cases, TERMINAL_COLOR_WHITE,
failed_cases > 0 ? TERMINAL_COLOR_RED : TERMINAL_COLOR_GREEN, failed_cases, TERMINAL_COLOR_RESET );
printf( " %sAssertions: %s%d Total | %s%d Passed%s | %s%d Failed%s\n\n",
TERMINAL_COLOR_WHITE, TERMINAL_COLOR_CYAN, _test_runner.total_assertions,
TERMINAL_COLOR_GREEN, _test_runner.total_assertions - _test_runner.failed_assertions, TERMINAL_COLOR_WHITE,
_test_runner.failed_assertions > 0 ? TERMINAL_COLOR_RED : TERMINAL_COLOR_GREEN, _test_runner.failed_assertions,
TERMINAL_COLOR_RESET );
return failed_cases;
}
/* Modern Test Runner Suite & Case Definition Macros */
#define TEST_SUITE( name ) \
do { \
snprintf( _test_runner.suite_name, sizeof( _test_runner.suite_name ), "%s", ( name ) ); \
} while ( 0 )
#define TEST_CASE( name ) static void name( void )
/* Modern Test Runner Non-Fatal Assertions (EXPECT_*) */
#define EXPECT_TRUE( expr ) _test_runner_expect( (expr), #expr, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_FALSE( expr ) _test_runner_expect( !(expr), "!(" #expr ")", __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_EQ( a, b ) _test_runner_expect_eq( (long long)(a), (long long)(b), #a, #b, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_NE( a, b ) _test_runner_expect_ne( (long long)(a), (long long)(b), #a, #b, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_STR_EQ( a, b ) _test_runner_expect_str_eq( (a), (b), #a, #b, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_STR_NE( a, b ) _test_runner_expect_str_ne( (a), (b), #a, #b, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_STR_CONTAINS( h, n ) _test_runner_expect_str_contains( (h), (n), #h, #n, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_FLOAT_EQ( a, b, eps ) _test_runner_expect_float_eq( (float)(a), (float)(b), (float)(eps), #a, #b, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
#define EXPECT_DOUBLE_EQ( a, b, eps ) _test_runner_expect_double_eq( (double)(a), (double)(b), (double)(eps), #a, #b, __FILE__, __LINE__, GREJC_TEST_FUNCTION )
/* Modern Test Runner Local Fatal Assertions (TEST_ASSERT_*) and aliases */
#if !defined(GREJC_LEGACY_HARNESS)
#define TEST_ASSERT_TRUE( expr ) do { if (!EXPECT_TRUE(expr)) return; } while(0)
#define TEST_ASSERT_FALSE( expr ) do { if (!EXPECT_FALSE(expr)) return; } while(0)
#define TEST_ASSERT_EQ( a, b ) do { if (!EXPECT_EQ(a, b)) return; } while(0)
#define TEST_ASSERT_NE( a, b ) do { if (!EXPECT_NE(a, b)) return; } while(0)
#define TEST_ASSERT_STR_EQ( a, b ) do { if (!EXPECT_STR_EQ(a, b)) return; } while(0)
#define TEST_ASSERT_STR_NE( a, b ) do { if (!EXPECT_STR_NE(a, b)) return; } while(0)
#define TEST_ASSERT_STR_CONTAINS( h, n ) do { if (!EXPECT_STR_CONTAINS(h, n)) return; } while(0)
#define TEST_ASSERT_FLOAT_EQ( a, b, eps ) do { if (!EXPECT_FLOAT_EQ(a, b, eps)) return; } while(0)
#define TEST_ASSERT_DOUBLE_EQ( a, b, eps ) do { if (!EXPECT_DOUBLE_EQ(a, b, eps)) return; } while(0)
#ifndef ASSERT_TRUE
#define ASSERT_TRUE( expr ) TEST_ASSERT_TRUE( expr )
#endif
#ifndef ASSERT_FALSE
#define ASSERT_FALSE( expr ) TEST_ASSERT_FALSE( expr )
#endif
#ifndef ASSERT_EQ
#define ASSERT_EQ( a, b ) TEST_ASSERT_EQ( a, b )
#endif
#ifndef ASSERT_NE
#define ASSERT_NE( a, b ) TEST_ASSERT_NE( a, b )
#endif
#ifndef ASSERT_STR_EQ
#define ASSERT_STR_EQ( a, b ) TEST_ASSERT_STR_EQ( a, b )
#endif
#ifndef ASSERT_STR_NE
#define ASSERT_STR_NE( a, b ) TEST_ASSERT_STR_NE( a, b )
#endif
#ifndef ASSERT_STR_CONTAINS
#define ASSERT_STR_CONTAINS( h, n ) TEST_ASSERT_STR_CONTAINS( h, n )
#endif
#ifndef ASSERT_FLOAT_EQ
#define ASSERT_FLOAT_EQ( a, b, eps ) TEST_ASSERT_FLOAT_EQ( a, b, eps )
#endif
#ifndef ASSERT_DOUBLE_EQ
#define ASSERT_DOUBLE_EQ( a, b, eps ) TEST_ASSERT_DOUBLE_EQ( a, b, eps )
#endif
#ifndef RUN_TEST
#define RUN_TEST( case_name ) _test_runner_run_case( #case_name, case_name )
#endif
#endif
// ============================================================================
// MENU UTILS
// ============================================================================
/// @brief Reads a single keypress without waiting for Enter (raw mode).
///
/// Puts the terminal into non-canonical, no-echo mode, reads one character,
/// then restores the original terminal settings.
///
/// Arrow keys are detected via the `\033[` escape sequence and mapped to
/// special integer constants.
///
/// @return The ASCII value of the key pressed, or one of:
/// | Value | Meaning |
/// |-------|--------------|
/// | -1 | Error |
/// | 256 | Arrow Up |
/// | 257 | Arrow Down |
/// | 258 | Arrow Left |
/// | 259 | Arrow Right |
static inline int _menu_read_key( void ) {
struct termios oldt, newt;
int is_tty = ( tcgetattr( STDIN_FILENO, &oldt ) == 0 );
if ( is_tty ) {
newt = oldt;
newt.c_lflag &= ~(unsigned)( ICANON | ECHO );
newt.c_cc[VMIN] = 1;
newt.c_cc[VTIME] = 0;
if ( tcsetattr( STDIN_FILENO, TCSANOW, &newt ) != 0 )
return -1;
}
unsigned char ch = 0;
ssize_t n = read( STDIN_FILENO, &ch, 1 );
if ( n <= 0 ) {
if ( is_tty )
tcsetattr( STDIN_FILENO, TCSANOW, &oldt );
return -1;
}
int result = ch;
if ( ch == 27 ) { /* ESC sequence */
struct pollfd pfd;
pfd.fd = STDIN_FILENO;
pfd.events = POLLIN;
pfd.revents = 0;
if ( poll( &pfd, 1, 50 ) > 0 && ( pfd.revents & POLLIN ) ) {
unsigned char seq[2] = { 0, 0 };
if ( read( STDIN_FILENO, &seq[0], 1 ) == 1 ) {
if ( seq[0] == '[' ) {
pfd.revents = 0;
if ( poll( &pfd, 1, 50 ) > 0 && ( pfd.revents & POLLIN ) &&
read( STDIN_FILENO, &seq[1], 1 ) == 1 ) {
switch ( seq[1] ) {
case 'A':
result = 256;
break; /* Up */
case 'B':
result = 257;
break; /* Down */
case 'C':
result = 259;
break; /* Right */
case 'D':
result = 258;
break; /* Left */
default:
result = seq[1];
break;
}
} else {
result = seq[0];
}
} else {
result = seq[0];
}
} else {
result = 27; /* Standalone ESC */
}
} else {
result = 27; /* Standalone ESC */
}
}
if ( is_tty )
tcsetattr( STDIN_FILENO, TCSANOW, &oldt );
return result;
}
/// @brief Displays an interactive arrow-key menu and returns the user's choice.
///
/// Renders a list of @p items on the terminal. The user navigates with the Up
/// and Down arrow keys (or 'k'/'j' Vim-style), confirms with Enter/Space, or
/// cancels with 'q' or Escape.
///
/// The caller is responsible for clearing or redrawing the surrounding UI
/// before and after the menu, as the function does not clear the screen.
///
/// @param title Title string printed above the menu (NULL for no title).
/// @param items Array of @p count option strings.
/// @param count Number of options in @p items.
/// @param title_color @ref TERMINAL_COLOR_* for the title text.
/// @param cursor_color @ref TERMINAL_COLOR_* for the currently highlighted
/// item.
/// @param item_color @ref TERMINAL_COLOR_* for unselected items.
/// @param cursor_char String drawn before the active item (e.g. `"> "` or
/// `"\u25b6 "`).
///
/// @return The 0-based index of the selected item, or -1 if the user cancelled
/// (pressed 'q' or Escape).
///
/// Example:
/// @code{.c}
/// const char *opts[] = {"New Game", "Load Game", "Options", "Quit"};
/// int choice = menu_select("MAIN MENU", opts, 4,
/// TERMINAL_COLOR_CYAN,
/// TERMINAL_COLOR_GREEN,
/// TERMINAL_COLOR_WHITE,
/// "\u25b6 ");
/// @endcode
static inline int menu_select( const char *title, const char *const *items, int count, const char *title_color,
const char *cursor_color, const char *item_color, const char *cursor_char ) {
if ( count <= 0 || !items )
return -1;
if ( !cursor_char )
cursor_char = "> ";
int cursor_len = 0;
for ( const char *p = cursor_char; *p; p++ )
cursor_len++;
int selected = 0;
int total_lines = count + ( title ? 1 : 0 );
int first_draw = 1;
/* hide cursor while navigating */
printf( "\033[?25l" );
while ( 1 ) {
if ( !first_draw ) {
printf( "\033[%dA\r", total_lines );
}
first_draw = 0;
if ( title ) {
printf( "\033[2K%s%s%s\n", title_color ? title_color : "", title, TERMINAL_COLOR_RESET );
}
for ( int i = 0; i < count; i++ ) {
const char *item_text = items[i] ? items[i] : "";
if ( i == selected ) {
printf( "\033[2K%s%s%s%s\n", cursor_color ? cursor_color : "", cursor_char, item_text, TERMINAL_COLOR_RESET );
} else {
/* indent by cursor width */
printf( "\033[2K%s", item_color ? item_color : "" );
for ( int s = 0; s < cursor_len; s++ )
putchar( ' ' );
printf( "%s%s\n", item_text, TERMINAL_COLOR_RESET );
}
}
fflush( stdout );
int key = _menu_read_key();
if ( key == -1 ) {
/* EOF or read error: restore cursor and return -1 */
printf( "\033[?25h" );
fflush( stdout );
return -1;
}
if ( key == 256 /* Up */ || key == 'k' ) {
selected = ( selected - 1 + count ) % count;
} else if ( key == 257 /* Down */ || key == 'j' ) {
selected = ( selected + 1 ) % count;
} else if ( key == '\n' || key == '\r' || key == ' ' ) {
break;
} else if ( key == 'q' || key == 27 /* ESC */ ) {
selected = -1;
break;
}
}
/* show cursor again */
printf( "\033[?25h" );
fflush( stdout );
return selected;
}
/// @brief Convenience wrapper that renders a double-line box title, then the
/// interactive menu.
///
/// Combines @ref print_box_double with @ref menu_select for quick, polished
/// menus. The @p accent_color is used for both the box border and the
/// selection cursor.
///
/// @param title Title for the box header.
/// @param items Array of option strings.
/// @param count Number of options.
/// @param accent_color @ref TERMINAL_COLOR_* used for both the box border and
/// the highlighted item cursor.
///
/// @return The 0-based index of the selected item, or -1 if cancelled.
///
/// Example:
/// @code{.c}
/// const char *opts[] = {"Start", "Settings", "Exit"};
/// int ch = menu_select_boxed("LAUNCHER", opts, 3, TERMINAL_COLOR_CYAN);
/// @endcode
static inline int menu_select_boxed( const char *title, const char *const *items, int count,
const char *accent_color ) {
if ( count <= 0 || !items )
return -1;
print_box_double( title, accent_color );
putchar( '\n' );
return menu_select( NULL, items, count, accent_color, accent_color, TERMINAL_COLOR_WHITE, "> " );
}
/// @brief Versão altamente customizável de @ref menu_multiselect permitindo especificar paletas de cores.
///
/// O usuário navega pela lista com as setas para Cima/Baixo ou teclas Vim (`k`/`j`),
/// alterna o estado do item focado com a tecla Espaço (`[ ]` <-> `[*]`),
/// alterna todos os itens com a tecla 'a'/'A', inverte a seleção com 'i'/'I',
/// confirma as escolhas com Enter, ou cancela com ESC ou 'q'.
///
/// @param title Título do menu (NULL para omitir).
/// @param items Vetor de itens textuais.
/// @param count Total de itens.
/// @param selected Vetor booleano de estados [count].
/// @param title_color Cor ANSI do título.
/// @param cursor_color Cor ANSI do ponteiro de seleção ativa.
/// @param item_color Cor ANSI do texto dos itens não destacados.
/// @param checked_color Cor ANSI do marcador selecionado `[*]`.
/// @param unchecked_color Cor ANSI do marcador desmarcado `[ ]`.
/// @param cursor_char Caractere indicador de foco (padrão: `"> "`).
/// @return 0 se confirmado, -1 se cancelado.
static inline int menu_multiselect_custom( const char *title, const char *const *items, int count, bool selected[],
const char *title_color, const char *cursor_color, const char *item_color,
const char *checked_color, const char *unchecked_color,
const char *cursor_char ) {
if ( count <= 0 || !items || !selected ) {
return -1;
}
if ( !cursor_char ) {
cursor_char = "> ";
}
int cursor_len = 0;
for ( const char *p = cursor_char; *p; p++ ) {
cursor_len++;
}
bool stack_backup[256];
bool *backup = stack_backup;
if ( count > 256 ) {
backup = (bool *)malloc( (size_t)count * sizeof( bool ) );
if ( !backup ) {
return -1;
}
}
memcpy( backup, selected, (size_t)count * sizeof( bool ) );
int current_pos = 0;
int total_lines = count + ( title ? 1 : 0 );
int first_draw = 1;
/* hide cursor while navigating */
printf( "\033[?25l" );
while ( 1 ) {
if ( !first_draw ) {
printf( "\033[%dA\r", total_lines );
}
first_draw = 0;
if ( title ) {
printf( "\033[2K%s%s%s\n", title_color ? title_color : "", title, TERMINAL_COLOR_RESET );
}
for ( int i = 0; i < count; i++ ) {
const char *item_text = items[i] ? items[i] : "";
const char *chk = selected[i] ? "[*] " : "[ ] ";
const char *chk_c = selected[i] ? ( checked_color ? checked_color : "" )
: ( unchecked_color ? unchecked_color : "" );
if ( i == current_pos ) {
printf( "\033[2K%s%s%s%s%s%s%s%s%s\n",
cursor_color ? cursor_color : "", cursor_char, TERMINAL_COLOR_RESET,
chk_c, chk, TERMINAL_COLOR_RESET,
cursor_color ? cursor_color : "", item_text, TERMINAL_COLOR_RESET );
} else {
printf( "\033[2K" );
for ( int s = 0; s < cursor_len; s++ ) {
putchar( ' ' );
}
printf( "%s%s%s%s%s%s\n",
chk_c, chk, TERMINAL_COLOR_RESET,
item_color ? item_color : "", item_text, TERMINAL_COLOR_RESET );
}
}
fflush( stdout );
int key = _menu_read_key();
if ( key == -1 ) {
/* EOF or read error: restore original state, restore cursor and return -1 */
memcpy( selected, backup, (size_t)count * sizeof( bool ) );
if ( backup != stack_backup ) {
free( backup );
}
printf( "\033[?25h" );
fflush( stdout );
return -1;
}
if ( key == 256 /* Up */ || key == 'k' || key == 'K' ) {
current_pos = ( current_pos - 1 + count ) % count;
} else if ( key == 257 /* Down */ || key == 'j' || key == 'J' ) {
current_pos = ( current_pos + 1 ) % count;
} else if ( key == ' ' ) {
selected[current_pos] = !selected[current_pos];
} else if ( key == 'a' || key == 'A' ) {
bool all_sel = true;
for ( int i = 0; i < count; i++ ) {
if ( !selected[i] ) {
all_sel = false;
break;
}
}
bool new_val = !all_sel;
for ( int i = 0; i < count; i++ ) {
selected[i] = new_val;
}
} else if ( key == 'i' || key == 'I' ) {
for ( int i = 0; i < count; i++ ) {
selected[i] = !selected[i];
}
} else if ( key == '\n' || key == '\r' ) {
/* Confirm: leave selected modified, exit with 0 */
if ( backup != stack_backup ) {
free( backup );
}
printf( "\033[?25h" );
fflush( stdout );
return 0;
} else if ( key == 'q' || key == 'Q' || key == 27 /* ESC */ ) {
/* Cancel: restore backup, exit with -1 */
memcpy( selected, backup, (size_t)count * sizeof( bool ) );
if ( backup != stack_backup ) {
free( backup );
}
printf( "\033[?25h" );
fflush( stdout );
return -1;
}
}
}
/// @brief Exibe menu interativo onde o usuário pode alternar múltiplos itens (checkboxes).
///
/// O usuário navega pela lista com as setas para Cima/Baixo ou teclas Vim (`k`/`j`),
/// alterna o estado do item focado com a tecla Espaço (`[ ]` <-> `[*]`),
/// alterna todos os itens com a tecla 'a'/'A', inverte a seleção com 'i'/'I',
/// confirma as escolhas com Enter, ou cancela com ESC ou 'q'.
///
/// @param items Vetor com os nomes das opções (tamanho @p count).
/// @param count Quantidade total de opções disponíveis.
/// @param selected Vetor booleano de entrada e saída (tamanho @p count) refletindo o estado de cada item.
/// @param title Título exibido acima do menu (se NULL, o menu é renderizado sem cabeçalho).
///
/// @return 0 se o usuário confirmou as escolhas com Enter;
/// -1 se a operação foi cancelada (pressionou 'q', ESC, ou fim de arquivo EOF).
/// Em caso de cancelamento (-1), o vetor @p selected é revertido ao seu estado original de entrada.
static inline int menu_multiselect( const char *const *items, int count, bool selected[], const char *title ) {
return menu_multiselect_custom( title, items, count, selected,
TERMINAL_COLOR_CYAN,
TERMINAL_COLOR_GREEN,
TERMINAL_COLOR_WHITE,
TERMINAL_COLOR_GREEN,
TERMINAL_COLOR_RESET,
"> " );
}
/// @brief Exibe o menu com multisseleção envolvido em uma caixa decorativa com bordas duplas (@ref print_box_double).
///
/// @param title Título do cabeçalho da caixa.
/// @param items Vetor de opções.
/// @param count Quantidade de opções.
/// @param selected Vetor booleano de entrada/saída com as escolhas.
/// @param accent_color Cor de destaque aplicada às bordas e cursor.
/// @return 0 se confirmado, -1 se cancelado.
static inline int menu_multiselect_boxed( const char *title, const char *const *items, int count, bool selected[],
const char *accent_color ) {
if ( count <= 0 || !items || !selected ) {
return -1;
}
print_box_double( title, accent_color );
putchar( '\n' );
const char *col = accent_color ? accent_color : TERMINAL_COLOR_CYAN;
return menu_multiselect_custom( NULL, items, count, selected,
col, col, TERMINAL_COLOR_WHITE,
col, TERMINAL_COLOR_RESET, "> " );
}
// ============================================================================
// PROGRESS BAR UTILS
// ============================================================================
/// @defgroup progress_styles Progress Bar Style Flags
///
/// Constants for selecting the visual style of a @ref ProgressBar.
///
/// @{
#define PROGRESS_STYLE_SIMPLE 0 ///< `[████░░░░] 75%` — block fill.
#define PROGRESS_STYLE_BLOCKS 1 ///< `[▏▎▍▌▋▊▉█]` — smooth ⅛-step fill.
#define PROGRESS_STYLE_ARROW 2 ///< `[=====> ]` — classic arrow.
#define PROGRESS_STYLE_DOTS 3 ///< `[•········]` — dot-fill style.
/// @}
/// @brief State for a single progress bar.
///
/// Initialise with @ref progress_bar_init, then set @p value and call
/// @ref progress_bar_print or @ref progress_bar_update.
typedef struct {
float value; ///< Progress value, 0.0 to 1.0.
int width; ///< Total bar width including brackets; 0 = auto.
int style; ///< One of @ref PROGRESS_STYLE_SIMPLE, etc.
const char *bar_color; ///< Colour of the filled portion.
const char *empty_color; ///< Colour of the empty portion.
const char *label; ///< Optional text printed after the percentage.
int show_percent; ///< If 1, display "XX%" after the bar.
} ProgressBar;
/// @brief Initialises a @ref ProgressBar with sensible defaults.
///
/// Sets value to 0.0, width to auto, percent display on, and applies the
/// given @p style and @p color.
///
/// @param pb Pointer to a @ref ProgressBar to initialise.
/// @param style One of @ref PROGRESS_STYLE_SIMPLE, @ref PROGRESS_STYLE_BLOCKS,
/// @ref PROGRESS_STYLE_ARROW, or @ref PROGRESS_STYLE_DOTS.
/// @param color @ref TERMINAL_COLOR_* for the filled portion of the bar
/// (e.g. @ref TERMINAL_COLOR_GREEN).
///
/// Example:
/// @code{.c}
/// ProgressBar pb;
/// progress_bar_init(&pb, PROGRESS_STYLE_SIMPLE, TERMINAL_COLOR_GREEN);
/// @endcode
static inline void progress_bar_init( ProgressBar *pb, int style, const char *color ) {
if ( !pb )
return;
pb->value = 0.0f;
pb->width = 0; /* auto */
pb->style = style;
pb->bar_color = color ? color : TERMINAL_COLOR_GREEN;
pb->empty_color = TERMINAL_COLOR_WHITE;
pb->label = NULL;
pb->show_percent = 1;
}
/// @brief Internal: renders one progress bar line to stdout (no newline).
///
/// Draws the bar according to the selected style. The bar width is derived
/// from the terminal width minus space reserved for brackets, percentage and
/// label.
///
/// @param pb Pointer to the @ref ProgressBar to render.
static inline void _progress_bar_render( const ProgressBar *pb ) {
if ( !pb )
return;
const int TERMINAL_WIDTH = _get_terminal_dimensions().width / 1.5;
int term_w = TERMINAL_WIDTH > 0 ? TERMINAL_WIDTH : 40;
/* reserve room for "[ ]" (2), " 100%" (5), optional label */
int label_len = 0;
if ( pb->label )
for ( const char *p = pb->label; *p; p++ )
label_len++;
int reserved = 2 + ( pb->show_percent ? 5 : 0 ) + ( label_len ? label_len + 1 : 0 );
int bar_w = ( pb->width > 0 ? pb->width : term_w ) - reserved;
if ( bar_w < 4 )
bar_w = 4;
float v = pb->value < 0.0f ? 0.0f : ( pb->value > 1.0f ? 1.0f : pb->value );
printf( "[" );
if ( pb->style == PROGRESS_STYLE_BLOCKS ) {
/* smooth 8-step block fill */
static const char *const eighths[] = { " ", "\u258F", "\u258E", "\u258D", "\u258C",
"\u258B", "\u258A", "\u2589", "\u2588" };
float cells = v * (float)bar_w;
int full = (int)cells;
int frac = (int)( ( cells - (float)full ) * 8.0f );
printf( "%s", pb->bar_color );
for ( int i = 0; i < full; i++ )
printf( "\u2588" );
if ( full < bar_w ) {
printf( "%s", frac > 0 ? pb->bar_color : pb->empty_color );
printf( "%s", eighths[frac] );
printf( "%s", pb->empty_color );
for ( int i = full + 1; i < bar_w; i++ )
printf( " " );
}
} else if ( pb->style == PROGRESS_STYLE_ARROW ) {
int filled = (int)( v * (float)bar_w );
printf( "%s", pb->bar_color );
for ( int i = 0; i < filled - 1; i++ )
printf( "=" );
if ( filled > 0 && filled < bar_w )
printf( ">" );
else if ( filled > 0 )
printf( "=" );
printf( "%s", pb->empty_color );
for ( int i = filled; i < bar_w; i++ )
printf( " " );
} else if ( pb->style == PROGRESS_STYLE_DOTS ) {
int filled = (int)( v * (float)bar_w );
printf( "%s", pb->bar_color );
for ( int i = 0; i < filled; i++ )
printf( "\u2022" );
printf( "%s", pb->empty_color );
for ( int i = filled; i < bar_w; i++ )
printf( "\u00B7" );
} else {
/* PROGRESS_STYLE_SIMPLE — default */
int filled = (int)( v * (float)bar_w );
printf( "%s", pb->bar_color );
for ( int i = 0; i < filled; i++ )
printf( "\u2588" );
printf( "%s", pb->empty_color );
for ( int i = filled; i < bar_w; i++ )
printf( "\u2591" );
}
printf( "%s]", TERMINAL_COLOR_RESET );
if ( pb->show_percent )
printf( " %3d%%", (int)( v * 100.0f ) );
if ( pb->label )
printf( " %s", pb->label );
}
/// @brief Prints the progress bar followed by a newline.
///
/// Use for static / one-shot display when you just want to show the bar once.
///
/// @param pb Pointer to a @ref ProgressBar (value must be set beforehand).
///
/// Example:
/// @code{.c}
/// ProgressBar pb;
/// progress_bar_init(&pb, PROGRESS_STYLE_BLOCKS, TERMINAL_COLOR_GREEN);
/// pb.value = 0.65f;
/// pb.label = "compiling\u2026";
/// progress_bar_print(&pb);
/// @endcode
static inline void progress_bar_print( const ProgressBar *pb ) {
if ( !pb )
return;
_progress_bar_render( pb );
putchar( '\n' );
}
/// @brief Updates the progress bar **in-place** on the current line.
///
/// Uses `\r` to overwrite the current line, producing a smooth animation.
/// Finishes with a newline when @p value reaches 1.0.
///
/// Call this repeatedly in a loop to animate a live progress indicator.
///
/// @param pb Pointer to the @ref ProgressBar.
/// @param value New progress value (clamped to 0.0 – 1.0).
///
/// Example:
/// @code{.c}
/// ProgressBar pb;
/// progress_bar_init(&pb, PROGRESS_STYLE_SIMPLE, TERMINAL_COLOR_CYAN);
/// for (int i = 0; i <= 100; i++) {
/// progress_bar_update(&pb, i / 100.0f);
/// usleep(20000);
/// }
/// @endcode
static inline void progress_bar_update( ProgressBar *pb, float value ) {
if ( !pb )
return;
pb->value = value;
printf( "\r\033[K" );
_progress_bar_render( pb );
if ( value >= 1.0f )
putchar( '\n' );
fflush( stdout );
}
// ----------------------------------------------------------------------------
// INDETERMINATE SPINNER UTILS
// ----------------------------------------------------------------------------
/// @brief Estilos de animação para o spinner indeterminado.
typedef enum {
SPINNER_STYLE_DOTS = 0, ///< Braille dots: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ (10 quadros, padrão moderno).
SPINNER_STYLE_LINE, ///< Linha clássica ASCII: - \ | / (4 quadros, máxima portabilidade).
SPINNER_STYLE_PULSE, ///< Bloco sombreado em pulso: █ ▓ ▒ ░ ▒ ▓ (6 quadros).
SPINNER_STYLE_BOUNCE ///< Barra quicando: [= ] [ = ] [ = ] [ =] [ = ] [ = ] (6 quadros).
} SpinnerStyle;
/// @brief Estrutura de estado de um spinner de progresso indeterminado.
typedef struct {
SpinnerStyle style; ///< Estilo visual ativo.
int frame; ///< Índice do quadro de animação atual.
const char *label; ///< Rótulo textual descritivo da tarefa.
const char *color; ///< Cor ANSI ou TrueColor aplicada ao glifo do spinner.
int active; ///< 1 se o spinner está ativo e desenhando; 0 se finalizado.
int is_tty; ///< 1 se stdout for TTY interativo; 0 se redirecionado.
} Spinner;
static const char *const _SPINNER_FRAMES_DOTS[] = {
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"
};
static const int _SPINNER_FRAMES_DOTS_COUNT = 10;
static const char *const _SPINNER_FRAMES_LINE[] = {
"-", "\\", "|", "/"
};
static const int _SPINNER_FRAMES_LINE_COUNT = 4;
static const char *const _SPINNER_FRAMES_PULSE[] = {
"█", "▓", "▒", "░", "▒", "▓"
};
static const int _SPINNER_FRAMES_PULSE_COUNT = 6;
static const char *const _SPINNER_FRAMES_BOUNCE[] = {
"[= ]", "[ = ]", "[ = ]", "[ =]", "[ = ]", "[ = ]"
};
static const int _SPINNER_FRAMES_BOUNCE_COUNT = 6;
static inline const char *_spinner_get_frame( SpinnerStyle style, int frame_idx ) {
const char *const *frames;
int count;
switch ( style ) {
case SPINNER_STYLE_LINE:
frames = _SPINNER_FRAMES_LINE;
count = _SPINNER_FRAMES_LINE_COUNT;
break;
case SPINNER_STYLE_PULSE:
frames = _SPINNER_FRAMES_PULSE;
count = _SPINNER_FRAMES_PULSE_COUNT;
break;
case SPINNER_STYLE_BOUNCE:
frames = _SPINNER_FRAMES_BOUNCE;
count = _SPINNER_FRAMES_BOUNCE_COUNT;
break;
case SPINNER_STYLE_DOTS:
default:
frames = _SPINNER_FRAMES_DOTS;
count = _SPINNER_FRAMES_DOTS_COUNT;
break;
}
if ( count <= 0 ) {
return "";
}
int idx = frame_idx % count;
if ( idx < 0 ) {
idx += count;
}
return frames[idx];
}
/// @brief Inicializa e desenha o primeiro quadro do spinner.
///
/// Em ambientes interativos (TTY), oculta o cursor (`\033[?25l`) para evitar cintilação.
/// Em ambientes não-interativos (redirecionamento para arquivos ou CI), imprime o rótulo inicial
/// uma única vez, sem emitir animações por retorno de carro (`\r`), evitando poluição de logs.
///
/// @param sp Ponteiro para a estrutura @ref Spinner a inicializar.
/// @param style Estilo de animação desejado (@ref SpinnerStyle).
/// @param label Texto informativo exibido ao lado do spinner (se NULL, assume "").
/// @param color Sequência de cor para o glifo (se NULL, assume @ref TERMINAL_COLOR_CYAN).
static inline void spinner_init( Spinner *sp, SpinnerStyle style, const char *label, const char *color ) {
if ( sp == NULL ) {
return;
}
sp->style = style;
sp->frame = 0;
sp->label = ( label != NULL ) ? label : "";
sp->color = ( color != NULL ) ? color : TERMINAL_COLOR_CYAN;
sp->active = 1;
sp->is_tty = isatty( STDOUT_FILENO );
if ( sp->is_tty ) {
printf( "\033[?25l" );
printf( "%s%s%s %s",
terminal_color( sp->color ),
_spinner_get_frame( sp->style, 0 ),
terminal_color( TERMINAL_COLOR_RESET ),
sp->label );
fflush( stdout );
} else {
if ( sp->label && sp->label[0] != '\0' ) {
printf( "%s...\n", sp->label );
fflush( stdout );
}
}
}
/// @brief Avança o spinner para o próximo quadro de animação e atualiza opcionalmente o rótulo.
///
/// Sob TTY, limpa a linha atual (`\r\033[K`), renderiza o próximo glifo e o texto.
/// Em saídas não-TTY, não executa impressão para preservar logs limpos.
///
/// @param sp Ponteiro para o @ref Spinner.
/// @param new_label Novo texto explicativo, ou NULL para manter o rótulo atual.
static inline void spinner_update( Spinner *sp, const char *new_label ) {
if ( sp == NULL || !sp->active ) {
return;
}
if ( new_label != NULL ) {
sp->label = new_label;
}
sp->frame++;
if ( sp->is_tty ) {
printf( "\r\033[K%s%s%s %s",
terminal_color( sp->color ),
_spinner_get_frame( sp->style, sp->frame ),
terminal_color( TERMINAL_COLOR_RESET ),
sp->label );
fflush( stdout );
}
}
/// @brief Finaliza o spinner, exibindo o resultado definitivo (sucesso ou falha) e restaurando o cursor.
///
/// Reexibe o cursor do terminal (`\033[?25h`).
/// Em caso de sucesso (`success == true`), emite um símbolo de confirmação (✔ em verde).
/// Em caso de falha (`success == false`), emite um símbolo de erro (✖ em vermelho).
/// Finaliza a linha com quebra `\n` e libera o estado ativo.
///
/// @param sp Ponteiro para o @ref Spinner.
/// @param success true para indicar conclusão bem-sucedida, false para erro.
/// @param final_msg Mensagem final impressa (se NULL, utiliza o último @p label do spinner).
static inline void spinner_done( Spinner *sp, bool success, const char *final_msg ) {
if ( sp == NULL || !sp->active ) {
return;
}
const char *msg = ( final_msg != NULL ) ? final_msg : sp->label;
if ( msg == NULL ) {
msg = "";
}
if ( sp->is_tty ) {
printf( "\033[?25h" );
if ( success ) {
printf( "\r\033[K%s\u2714%s %s\n",
terminal_color( TERMINAL_COLOR_GREEN ),
terminal_color( TERMINAL_COLOR_RESET ),
msg );
} else {
printf( "\r\033[K%s\u2716%s %s\n",
terminal_color( TERMINAL_COLOR_RED ),
terminal_color( TERMINAL_COLOR_RESET ),
msg );
}
fflush( stdout );
} else {
if ( success ) {
printf( "%s\u2714%s %s\n",
terminal_color( TERMINAL_COLOR_GREEN ),
terminal_color( TERMINAL_COLOR_RESET ),
msg );
} else {
printf( "%s\u2716%s %s\n",
terminal_color( TERMINAL_COLOR_RED ),
terminal_color( TERMINAL_COLOR_RESET ),
msg );
}
fflush( stdout );
}
sp->active = 0;
}
// ============================================================================
// ARG PARSER
// ============================================================================
//
// Usage pattern (mirrors Python's argparse):
//
// ArgParser ap;
// argp_init(&ap, "mytool", "1.0", "Does something useful.");
//
// argp_add_flag (&ap, 'v', "verbose", "Enable verbose output");
// argp_add_option(&ap, 'o', "output", "FILE", "Output file path", NULL);
// argp_add_option(&ap, 'n', "count", "NUMBER", "Repeat N times", "1");
// argp_add_pos (&ap, "input", "Input file", 1 /* required */);
//
// if (!argp_parse(&ap, argc, argv)) {
// argp_usage(&ap); // prints help and exits
// }
//
// int verbose = argp_flag(&ap, "verbose");
// const char *out = argp_get (&ap, "output");
// int count = atoi(argp_get(&ap, "count"));
// const char *input = argp_pos (&ap, "input");
//
// argp_free(&ap);
//
// Supported syntax (same as standard POSIX / GNU style):
// -v short flag
// -o file short option with value
// --verbose long flag
// --output=file long option, = form
// --output file long option, space form
// -- stop option parsing; rest are positional
/// @brief Maximum number of arguments (flags + options + positional)
/// that can be registered with @ref argp_add_flag, etc.
#define ARGP_MAX_ARGS 64
/// @brief Maximum number of positional arguments that can be registered
/// with @ref argp_add_pos.
#define ARGP_MAX_POS 16
/// @brief Maximum length of a single argument value string (including the
/// null terminator).
#define ARGP_VAL_LEN 256
/// @brief Maximum number of predefined choices for a choice-constrained option.
#ifndef ARGP_MAX_CHOICES
#define ARGP_MAX_CHOICES 16
#endif
/// @brief Sentinel value to disable numeric bounds checking in @ref argp_get_int and @ref argp_get_float.
/// When min_val > max_val, the range verification is deactivated.
#define ARGP_NO_LIMITS 1, 0
/// @brief Sentinel value to disable bounds checking in @ref argp_get_int.
#define ARGP_NO_LIMITS_INT 1, 0
/// @brief Sentinel value to disable bounds checking in @ref argp_get_float.
#define ARGP_NO_LIMITS_FLOAT 1.0, 0.0
/// @brief Discriminator for the type of an argument in the ArgParser system.
typedef enum {
ARGP_KIND_FLAG, ///< Boolean switch, no value (e.g. `--verbose`).
ARGP_KIND_OPTION, ///< Option that takes a value (e.g. `--output=X`).
ARGP_KIND_POS ///< Positional argument (e.g. `<input>`).
} ArgKind;
/// @brief Describes a single registered argument.
///
/// Fields are filled by the registration functions (@ref argp_add_flag,
/// @ref argp_add_option, @ref argp_add_option_choices, @ref argp_add_pos)
/// and populated during parsing by @ref argp_parse.
typedef struct {
ArgKind kind; ///< Type: flag, option or positional.
char shortname; ///< Single-character short name ('\0' if none).
char longname[32]; ///< Long name without the "--" prefix.
char metavar[32]; ///< For OPTION: value placeholder (e.g. "FILE").
char help[300]; ///< Description shown in `--help`.
char value[ARGP_VAL_LEN]; ///< Parsed value string (or default).
int present; ///< 1 if the argument was supplied by the user.
int required; ///< 1 if absence is a parse error.
const char *choices[ARGP_MAX_CHOICES]; ///< Allowed choice strings (if constrained).
int choice_count; ///< Number of choices in choices[] (0 = unconstrained).
} Arg;
/// @brief Maximum number of subcommands supported by an ArgParser.
#ifndef ARGP_MAX_SUBCMDS
#define ARGP_MAX_SUBCMDS 16
#endif
/* Forward declaration for ArgParser */
struct ArgParser_s;
/// @brief Represents a subcommand registered within an @ref ArgParser.
typedef struct {
char name[32]; ///< Subcommand name (e.g. "build", "commit").
char description[256]; ///< Short description shown in help list.
struct ArgParser_s *sub_parser; ///< Pointer to subcommand parser instance.
} ArgSubcommand;
/// @brief Parser state for a Python-style argument parser.
///
/// Initialise with @ref argp_init, register arguments with @ref argp_add_flag
/// etc., then parse with @ref argp_parse.
typedef struct ArgParser_s {
char prog[64]; ///< Program name, shown in usage line.
char version[32]; ///< Version string, shown with `--version`.
char description[256]; ///< Short description printed below usage.
Arg args[ARGP_MAX_ARGS]; ///< Array of registered arguments.
int count; ///< Number of registered arguments so far.
int pos_count; ///< Number of registered positional slots.
char error[256]; ///< Last parse error message (human-readable).
/* Subcommand fields (2.1.B) */
ArgSubcommand subcommands[ARGP_MAX_SUBCMDS]; ///< Registered subcommands.
int subcmd_count; ///< Total registered subcommands.
const char *selected_subcommand; ///< Activated subcommand name (or NULL).
struct ArgParser_s *parent; ///< Pointer to parent parser (or NULL for root).
} ArgParser;
/// @brief Initialises an @ref ArgParser.
///
/// Zeroes out the parser structure, copies @p prog, @p version and
/// @p description, and automatically registers built-in `--help` and
/// `--version` flags.
///
/// @param ap Pointer to an uninitialised @ref ArgParser.
/// @param prog Program name (shown in the usage line).
/// @param version Version string (shown with `--version`).
/// @param description Short description printed below the usage line.
///
/// Example:
/// @code{.c}
/// ArgParser ap;
/// argp_init(&ap, "mytool", "1.0", "Does something useful.");
/// @endcode
static inline void argp_init( ArgParser *ap, const char *prog, const char *version, const char *description ) {
if ( !ap )
return;
memset( ap, 0, sizeof( ArgParser ) );
if ( !prog )
prog = "";
if ( !version )
version = "";
if ( !description )
description = "";
strncpy( ap->prog, prog, sizeof( ap->prog ) - 1 );
strncpy( ap->version, version, sizeof( ap->version ) - 1 );
strncpy( ap->description, description, sizeof( ap->description ) - 1 );
/* built-in --help / --version */
Arg *h = &ap->args[ap->count++];
h->kind = ARGP_KIND_FLAG;
h->shortname = 'h';
strncpy( h->longname, "help", sizeof( h->longname ) - 1 );
strncpy( h->help, "Show this help message and exit", sizeof( h->help ) - 1 );
strncpy( h->value, "0", sizeof( h->value ) - 1 );
Arg *ver = &ap->args[ap->count++];
ver->kind = ARGP_KIND_FLAG;
ver->shortname = '\0';
strncpy( ver->longname, "version", sizeof( ver->longname ) - 1 );
strncpy( ver->help, "Show version and exit", sizeof( ver->help ) - 1 );
strncpy( ver->value, "0", sizeof( ver->value ) - 1 );
}
/// @brief Registers a subcommand under the root parser.
///
/// Associates @p name and description @p desc with @p sub_parser.
/// When the root parser encounters @p name as the first positional argument,
/// remaining arguments are delegated to @p sub_parser.
/// The sub_parser's prog field is prefixed with the root's prog name (e.g. "git commit").
///
/// @param root Pointer to root ArgParser (cannot be NULL).
/// @param name Subcommand name (e.g. "build", "commit").
/// @param desc Short description shown in root help.
/// @param sub_parser Initialized child @ref ArgParser instance.
/// @return 1 on success, 0 if arguments are invalid, capacity exceeded, or name duplicated.
static inline int argp_add_subcommand( ArgParser *root, const char *name, const char *desc,
ArgParser *sub_parser ) {
if ( !root || !name || name[0] == '\0' || !sub_parser )
return 0;
if ( root->subcmd_count >= ARGP_MAX_SUBCMDS ) {
snprintf( root->error, sizeof( root->error ), "subcommand capacity exceeded (max %d)", ARGP_MAX_SUBCMDS );
return 0;
}
for ( int i = 0; i < root->subcmd_count; i++ ) {
if ( strcmp( root->subcommands[i].name, name ) == 0 ) {
snprintf( root->error, sizeof( root->error ), "subcommand '%s' already registered", name );
return 0;
}
}
ArgSubcommand *sc = &root->subcommands[root->subcmd_count++];
memset( sc, 0, sizeof( ArgSubcommand ) );
strncpy( sc->name, name, sizeof( sc->name ) - 1 );
if ( desc )
strncpy( sc->description, desc, sizeof( sc->description ) - 1 );
sc->sub_parser = sub_parser;
if ( root->prog[0] != '\0' ) {
snprintf( sub_parser->prog, sizeof( sub_parser->prog ), "%.31s %.31s", root->prog, name );
} else {
snprintf( sub_parser->prog, sizeof( sub_parser->prog ), "%.63s", name );
}
return 1;
}
/// @brief Returns the name of the subcommand activated during parsing.
///
/// @param root Pointer to root ArgParser.
/// @return Name of active subcommand, or NULL if none was invoked.
static inline const char *argp_get_subcommand( const ArgParser *root ) {
if ( !root )
return NULL;
return root->selected_subcommand;
}
/// @brief Returns the subcommand parser instance that was activated during parsing.
///
/// @param root Pointer to root ArgParser.
/// @return Pointer to active subcommand's @ref ArgParser, or NULL if none.
static inline ArgParser *argp_get_subcommand_parser( const ArgParser *root ) {
if ( !root || !root->selected_subcommand )
return NULL;
for ( int i = 0; i < root->subcmd_count; i++ ) {
if ( strcmp( root->subcommands[i].name, root->selected_subcommand ) == 0 )
return root->subcommands[i].sub_parser;
}
return NULL;
}
/// @brief Internal: finds a registered @ref Arg by its long name.
///
/// @param ap Pointer to the @ref ArgParser.
/// @param name Long name to search for (without "--").
/// @return Pointer to the matching @ref Arg, or NULL if not found.
static inline Arg *_argp_find( ArgParser *ap, const char *name ) {
if ( !ap || !name )
return NULL;
for ( int i = 0; i < ap->count; i++ )
if ( strcmp( ap->args[i].longname, name ) == 0 )
return &ap->args[i];
return NULL;
}
/// @brief Internal: finds a registered @ref Arg by its short name.
///
/// @param ap Pointer to the @ref ArgParser.
/// @param c Single-character short name.
/// @return Pointer to the matching @ref Arg, or NULL if not found.
static inline Arg *_argp_find_short( ArgParser *ap, char c ) {
if ( !ap || c == '\0' )
return NULL;
for ( int i = 0; i < ap->count; i++ )
if ( ap->args[i].shortname == c )
return &ap->args[i];
return NULL;
}
/// @brief Internal: finds a registered @ref Arg by its long name (const version).
///
/// @param ap Pointer to the @ref ArgParser.
/// @param name Long name to search for (without "--").
/// @return Const pointer to the matching @ref Arg, or NULL if not found.
static inline const Arg *_argp_find_const( const ArgParser *ap, const char *name ) {
if ( !ap || !name )
return NULL;
for ( int i = 0; i < ap->count; i++ )
if ( strcmp( ap->args[i].longname, name ) == 0 )
return &ap->args[i];
return NULL;
}
/// @brief Internal: case-insensitive string comparison helper.
static inline int _argp_strcasecmp( const char *s1, const char *s2 ) {
while ( *s1 && *s2 ) {
int c1 = tolower( (unsigned char)*s1 );
int c2 = tolower( (unsigned char)*s2 );
if ( c1 != c2 )
return c1 - c2;
s1++;
s2++;
}
return tolower( (unsigned char)*s1 ) - tolower( (unsigned char)*s2 );
}
/// @brief Internal: case-insensitive prefix comparison helper.
static inline int _argp_strncasecmp( const char *s1, const char *s2, size_t n ) {
for ( size_t i = 0; i < n; i++ ) {
if ( !s1[i] || !s2[i] )
return tolower( (unsigned char)s1[i] ) - tolower( (unsigned char)s2[i] );
int c1 = tolower( (unsigned char)s1[i] );
int c2 = tolower( (unsigned char)s2[i] );
if ( c1 != c2 )
return c1 - c2;
}
return 0;
}
/// @brief Registers a boolean flag (e.g. `--verbose` / `-v`).
///
/// Flags are either present or absent. After parsing, retrieve with
/// @ref argp_flag. The stored value is "1" when present, "0" otherwise.
///
/// @param ap Pointer to an initialised @ref ArgParser.
/// @param shortname Single character (e.g. 'v'), or '\0' for no short form.
/// @param longname Long name without the "--" prefix (e.g. "verbose").
/// @param help Help string displayed in `--help`.
///
/// Example:
/// @code{.c}
/// argp_add_flag(&ap, 'v', "verbose", "Enable verbose output");
/// // Later: if (argp_flag(&ap, "verbose")) { ... }
/// @endcode
static inline void argp_add_flag( ArgParser *ap, char shortname, const char *longname, const char *help ) {
if ( !ap || !longname || ap->count >= ARGP_MAX_ARGS )
return;
Arg *a = &ap->args[ap->count++];
memset( a, 0, sizeof( Arg ) );
a->kind = ARGP_KIND_FLAG;
a->shortname = shortname;
strncpy( a->longname, longname, sizeof( a->longname ) - 1 );
if ( help )
strncpy( a->help, help, sizeof( a->help ) - 1 );
strncpy( a->value, "0", sizeof( a->value ) - 1 );
}
/// @brief Registers an option that accepts a user-supplied value
/// (e.g. `--output=FILE` or `-o FILE`).
///
/// The option can be specified on the command line in three forms:
/// - `--longname=value` (long form with equals sign)
/// - `--longname value` (long form with space)
/// - `-s value` (short form with space)
///
/// If @p def is non-NULL, the option is *optional* and defaults to that
/// string; the `required` field is automatically set to 0. If @p def is NULL,
/// the option is *required* and parsing will fail if the user does not supply
/// it. The caller may override `a->required = 0` after registration to make a
/// NULL-default option optional (value will be an empty string if omitted).
///
/// After parsing, retrieve the value with @ref argp_get.
///
/// @param ap Pointer to an initialised @ref ArgParser.
/// @param shortname Single-character short name (e.g. 'o'), or '\0' for none.
/// @param longname Long option name without the "--" prefix (e.g. "output").
/// @param metavar Placeholder string shown in `--help` (e.g. "FILE", "N").
/// @param help Description of what this option controls.
/// @param def Default value string, or NULL to make the option required.
///
/// Example:
/// @code{.c}
/// argp_add_option(&ap, 'o', "output", "FILE", "Output file path", "./out");
/// argp_add_option(&ap, 'n', "count", "N", "Number of iterations", "10");
/// // required option (no default):
/// argp_add_option(&ap, 'i', "input", "FILE", "Input file (required)", NULL);
/// @endcode
static inline void argp_add_option( ArgParser *ap, char shortname, const char *longname, const char *metavar,
const char *help, const char *def ) {
if ( !ap || !longname || ap->count >= ARGP_MAX_ARGS )
return;
Arg *a = &ap->args[ap->count++];
memset( a, 0, sizeof( Arg ) );
a->kind = ARGP_KIND_OPTION;
a->shortname = shortname;
a->required = ( def == NULL ) ? 1 : 0;
strncpy( a->longname, longname, sizeof( a->longname ) - 1 );
if ( metavar )
strncpy( a->metavar, metavar, sizeof( a->metavar ) - 1 );
if ( help )
strncpy( a->help, help, sizeof( a->help ) - 1 );
if ( def )
strncpy( a->value, def, sizeof( a->value ) - 1 );
}
/// @brief Registers an option whose accepted values are restricted to a predefined set.
///
/// During @ref argp_parse, the provided value will be validated against @p choices.
/// If the provided value does not match any choice, @ref argp_parse will fail
/// with a descriptive error message listing the valid choices.
///
/// @param ap Pointer to the @ref ArgParser.
/// @param shortname Single-character short name (e.g. 'f'), or '\0' for none.
/// @param longname Long option name without "--" prefix (e.g. "format").
/// @param metavar Placeholder string shown in `--help` (e.g. "FMT"); defaults to "CHOICE" if NULL.
/// @param help Description of what this option controls.
/// @param def Default value string (must belong to choices, or NULL if required).
/// @param choices Array of string pointers containing the allowed choices.
/// @param choice_count Number of choices (must be > 0 and <= ARGP_MAX_CHOICES).
/// @return 1 on success, 0 on invalid parameters or if capacity is exceeded.
///
/// Example:
/// @code{.c}
/// const char *formats[] = { "json", "xml", "yaml" };
/// argp_add_option_choices(&ap, 'f', "format", "FMT", "Output format", "json", formats, 3);
/// @endcode
static inline int argp_add_option_choices( ArgParser *ap, char shortname, const char *longname,
const char *metavar, const char *help, const char *def,
const char *const choices[], int choice_count ) {
if ( !ap || !longname || ap->count >= ARGP_MAX_ARGS )
return 0;
if ( !choices || choice_count <= 0 || choice_count > ARGP_MAX_CHOICES )
return 0;
for ( int i = 0; i < choice_count; i++ ) {
if ( !choices[i] )
return 0;
}
if ( def ) {
int def_found = 0;
for ( int i = 0; i < choice_count; i++ ) {
if ( strcmp( choices[i], def ) == 0 ) {
def_found = 1;
break;
}
}
if ( !def_found )
return 0;
}
Arg *a = &ap->args[ap->count++];
memset( a, 0, sizeof( Arg ) );
a->kind = ARGP_KIND_OPTION;
a->shortname = shortname;
a->required = ( def == NULL ) ? 1 : 0;
strncpy( a->longname, longname, sizeof( a->longname ) - 1 );
if ( metavar && metavar[0] != '\0' )
strncpy( a->metavar, metavar, sizeof( a->metavar ) - 1 );
else
strncpy( a->metavar, "CHOICE", sizeof( a->metavar ) - 1 );
if ( help )
strncpy( a->help, help, sizeof( a->help ) - 1 );
if ( def )
strncpy( a->value, def, sizeof( a->value ) - 1 );
a->choice_count = choice_count;
for ( int i = 0; i < choice_count; i++ )
a->choices[i] = choices[i];
return 1;
}
/// @brief Internal: validates an option value against its registered choices list.
///
/// @param a Pointer to the argument descriptor.
/// @param val Supplied argument value to test.
/// @param err_buf Buffer to store formatted error string on validation failure.
/// @param err_sz Capacity of @p err_buf.
/// @return 1 if valid or no choices registered, 0 if invalid.
static inline int _argp_validate_choice( const Arg *a, const char *val, char *err_buf, size_t err_sz ) {
if ( !a || a->choice_count <= 0 )
return 1;
for ( int i = 0; i < a->choice_count; i++ ) {
if ( a->choices[i] && strcmp( a->choices[i], val ) == 0 )
return 1;
}
char list_buf[192] = { 0 };
size_t pos = 0;
for ( int i = 0; i < a->choice_count; i++ ) {
if ( i > 0 && pos < sizeof( list_buf ) - 2 ) {
list_buf[pos++] = ',';
list_buf[pos++] = ' ';
list_buf[pos] = '\0';
}
if ( a->choices[i] ) {
size_t len = strlen( a->choices[i] );
if ( pos + len < sizeof( list_buf ) - 1 ) {
memcpy( list_buf + pos, a->choices[i], len );
pos += len;
list_buf[pos] = '\0';
}
}
}
snprintf( err_buf, err_sz, "invalid choice '%.64s' for '--%.32s' (choose from: %.128s)", val, a->longname, list_buf );
return 0;
}
/// @brief Registers a positional argument (e.g. `<input>`).
///
/// Positional arguments are filled in the order they were registered. Retrieve
/// parsed values with @ref argp_pos.
///
/// @param ap Pointer to an initialised @ref ArgParser.
/// @param name Name used in help and for @ref argp_pos retrieval.
/// @param help Help string displayed in `--help`.
/// @param required 1 = error if absent, 0 = optional.
///
/// Example:
/// @code{.c}
/// argp_add_pos(&ap, "input", "Input file path", 1);
/// @endcode
static inline void argp_add_pos( ArgParser *ap, const char *name, const char *help, int required ) {
if ( !ap || !name || ap->count >= ARGP_MAX_ARGS || ap->pos_count >= ARGP_MAX_POS )
return;
Arg *a = &ap->args[ap->count++];
memset( a, 0, sizeof( Arg ) );
a->kind = ARGP_KIND_POS;
a->required = required;
ap->pos_count++;
strncpy( a->longname, name, sizeof( a->longname ) - 1 );
if ( help )
strncpy( a->help, help, sizeof( a->help ) - 1 );
}
/// @brief Prints a formatted help message to stdout and exits with code 0.
///
/// The output includes:
/// - A usage line showing the program name and all registered arguments.
/// - The program description (if set).
/// - A formatted list of options with their short/long names, metavar, help
/// text and default values (if any).
/// - A list of positional arguments (if any), marked as optional or required.
///
/// After printing, the function calls `exit(0)`.
///
/// @param ap Pointer to an initialised and populated @ref ArgParser.
static inline void argp_usage( ArgParser *ap ) {
if ( !ap )
return;
/* usage line */
printf( "\n%sUsage:%s %s%s%s", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET, TERMINAL_COLOR_GREEN, ap->prog,
TERMINAL_COLOR_RESET );
for ( int i = 0; i < ap->count; i++ ) {
Arg *a = &ap->args[i];
if ( a->kind == ARGP_KIND_FLAG || a->kind == ARGP_KIND_OPTION ) {
printf( " [" );
if ( a->shortname )
printf( "-%c", a->shortname );
else
printf( "--%s", a->longname );
if ( a->kind == ARGP_KIND_OPTION )
printf( " %s", a->metavar );
printf( "]" );
}
}
if ( ap->subcmd_count > 0 )
printf( " <command> [command options]" );
for ( int i = 0; i < ap->count; i++ ) {
Arg *a = &ap->args[i];
if ( a->kind == ARGP_KIND_POS )
printf( a->required ? " <%s>" : " [%s]", a->longname );
}
printf( "\n\n" );
/* description */
if ( ap->description[0] )
printf( "%s\n\n", ap->description );
/* column width for alignment */
int col = 0;
for ( int i = 0; i < ap->count; i++ ) {
Arg *a = &ap->args[i];
int w = 0;
if ( a->kind == ARGP_KIND_POS ) {
w = (int)strlen( a->longname );
} else {
w = (int)strlen( a->longname ) + 2; /* "--" */
if ( a->shortname )
w += 4; /* "-x, " */
if ( a->kind == ARGP_KIND_OPTION )
w += (int)strlen( a->metavar ) + 1;
}
if ( w > col )
col = w;
}
col += 2;
/* options section */
printf( "%sOptions:%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
for ( int i = 0; i < ap->count; i++ ) {
Arg *a = &ap->args[i];
if ( a->kind == ARGP_KIND_POS )
continue;
int written = 0;
printf( " %s", TERMINAL_COLOR_CYAN );
if ( a->shortname ) {
printf( "-%c, ", a->shortname );
written += 4;
}
printf( "--%s", a->longname );
written += 2 + (int)strlen( a->longname );
if ( a->kind == ARGP_KIND_OPTION ) {
printf( " %s", a->metavar );
written += 1 + (int)strlen( a->metavar );
}
printf( "%s", TERMINAL_COLOR_RESET );
for ( int s = written; s < col; s++ )
putchar( ' ' );
printf( "%s", a->help );
/* show choices for options */
if ( a->kind == ARGP_KIND_OPTION && a->choice_count > 0 ) {
printf( "%s [", TERMINAL_COLOR_BLUE );
for ( int c = 0; c < a->choice_count; c++ ) {
if ( c > 0 )
printf( "|" );
printf( "%s", a->choices[c] );
}
printf( "] (choices: " );
for ( int c = 0; c < a->choice_count; c++ ) {
if ( c > 0 )
printf( ", " );
printf( "%s", a->choices[c] );
}
printf( ")%s", TERMINAL_COLOR_RESET );
}
/* show default for options */
if ( a->kind == ARGP_KIND_OPTION && !a->required && a->value[0] )
printf( "%s (default: %s)%s", TERMINAL_COLOR_BLUE, a->value, TERMINAL_COLOR_RESET );
putchar( '\n' );
}
/* positional section */
int has_pos = 0;
for ( int i = 0; i < ap->count; i++ )
if ( ap->args[i].kind == ARGP_KIND_POS ) {
has_pos = 1;
break;
}
if ( has_pos ) {
printf( "\n%sPositional arguments:%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
for ( int i = 0; i < ap->count; i++ ) {
Arg *a = &ap->args[i];
if ( a->kind != ARGP_KIND_POS )
continue;
int written = (int)strlen( a->longname );
printf( " %s%s%s", TERMINAL_COLOR_CYAN, a->longname, TERMINAL_COLOR_RESET );
for ( int s = written; s < col; s++ )
putchar( ' ' );
printf( "%s", a->help );
if ( !a->required )
printf( "%s (optional)%s", TERMINAL_COLOR_BLUE, TERMINAL_COLOR_RESET );
putchar( '\n' );
}
}
/* subcommands section */
if ( ap->subcmd_count > 0 ) {
int max_sub_w = 0;
for ( int i = 0; i < ap->subcmd_count; i++ ) {
int w = (int)strlen( ap->subcommands[i].name );
if ( w > max_sub_w )
max_sub_w = w;
}
max_sub_w += 2;
if ( max_sub_w < 12 )
max_sub_w = 12;
printf( "\n%sAvailable subcommands:%s\n", TERMINAL_COLOR_YELLOW, TERMINAL_COLOR_RESET );
for ( int i = 0; i < ap->subcmd_count; i++ ) {
int written = (int)strlen( ap->subcommands[i].name );
printf( " %s%s%s", TERMINAL_COLOR_CYAN, ap->subcommands[i].name, TERMINAL_COLOR_RESET );
for ( int s = written; s < max_sub_w; s++ )
putchar( ' ' );
printf( "%s\n", ap->subcommands[i].description );
}
printf( "\nRun '%s <command> --help' for details on a specific subcommand.\n", ap->prog );
}
putchar( '\n' );
exit( 0 );
}
/// @brief Parses command-line arguments according to the registered configuration.
static inline int argp_parse( ArgParser *ap, int argc, char *const argv[] ) {
if ( !ap || argc < 1 || !argv )
return 0;
ap->selected_subcommand = NULL;
ap->error[0] = '\0';
int pos_idx = 0; /* which positional slot we're filling */
int only_pos = 0; /* set to 1 after "--" */
/* collect positional Arg pointers in order */
Arg *positionals[ARGP_MAX_POS];
int npos = 0;
for ( int i = 0; i < ap->count; i++ )
if ( ap->args[i].kind == ARGP_KIND_POS )
positionals[npos++] = &ap->args[i];
for ( int i = 1; i < argc; i++ ) {
const char *tok = argv[i];
/* "--" separator */
if ( !only_pos && strcmp( tok, "--" ) == 0 ) {
only_pos = 1;
continue;
}
/* positional or subcommand */
if ( only_pos || tok[0] != '-' || tok[1] == '\0' ) {
/* Check if this positional matches a registered subcommand */
if ( !only_pos && ap->subcmd_count > 0 ) {
int matched = 0;
for ( int s = 0; s < ap->subcmd_count; s++ ) {
if ( strcmp( ap->subcommands[s].name, tok ) == 0 ) {
matched = 1;
ap->selected_subcommand = ap->subcommands[s].name;
ArgParser *sub = ap->subcommands[s].sub_parser;
int ok = argp_parse( sub, argc - i, argv + i );
if ( !ok ) {
snprintf( ap->error, sizeof( ap->error ), "%s", sub->error );
return 0;
}
return 1;
}
}
if ( !matched && npos == 0 ) {
snprintf( ap->error, sizeof( ap->error ),
"unknown subcommand '%s'. See '%s --help' for available subcommands", tok, ap->prog );
return 0;
}
}
if ( pos_idx >= npos ) {
snprintf( ap->error, sizeof( ap->error ), "unexpected positional argument: %s", tok );
return 0;
}
strncpy( positionals[pos_idx]->value, tok, ARGP_VAL_LEN - 1 );
positionals[pos_idx]->present = 1;
pos_idx++;
continue;
}
/* long option: --name or --name=value */
if ( tok[1] == '-' ) {
const char *name = tok + 2;
char namebuf[32] = { 0 };
const char *eq = strchr( name, '=' );
if ( eq ) {
int nlen = (int)( eq - name );
if ( nlen >= (int)sizeof( namebuf ) )
nlen = (int)sizeof( namebuf ) - 1;
strncpy( namebuf, name, (size_t)nlen );
name = namebuf;
}
Arg *a = _argp_find( ap, name );
if ( !a ) {
snprintf( ap->error, sizeof( ap->error ), "unknown option: --%s", name );
return 0;
}
/* handle --help / --version */
if ( strcmp( name, "help" ) == 0 )
argp_usage( ap );
if ( strcmp( name, "version" ) == 0 ) {
printf( "%s %s\n", ap->prog, ap->version );
exit( 0 );
}
if ( a->kind == ARGP_KIND_FLAG ) {
strncpy( a->value, "1", sizeof( a->value ) - 1 );
a->present = 1;
} else {
const char *val = eq ? eq + 1 : ( i + 1 < argc ? argv[++i] : NULL );
if ( !val ) {
snprintf( ap->error, sizeof( ap->error ), "option --%s requires a value", name );
return 0;
}
if ( !_argp_validate_choice( a, val, ap->error, sizeof( ap->error ) ) )
return 0;
strncpy( a->value, val, ARGP_VAL_LEN - 1 );
a->present = 1;
}
continue;
}
/* short options: -v, -o val, -xvf (flag cluster) */
const char *p = tok + 1;
while ( *p ) {
Arg *a = _argp_find_short( ap, *p );
if ( !a ) {
snprintf( ap->error, sizeof( ap->error ), "unknown option: -%c", *p );
return 0;
}
if ( strcmp( a->longname, "help" ) == 0 )
argp_usage( ap );
if ( strcmp( a->longname, "version" ) == 0 ) {
printf( "%s %s\n", ap->prog, ap->version );
exit( 0 );
}
if ( a->kind == ARGP_KIND_FLAG ) {
strncpy( a->value, "1", sizeof( a->value ) - 1 );
a->present = 1;
p++;
} else {
/* value is the rest of the token or next argv */
const char *val = NULL;
if ( p[1] == '=' ) {
val = p + 2;
} else if ( p[1] != '\0' ) {
val = p + 1;
} else if ( i + 1 < argc ) {
val = argv[++i];
}
if ( !val ) {
snprintf( ap->error, sizeof( ap->error ), "option -%c requires a value", *p );
return 0;
}
if ( !_argp_validate_choice( a, val, ap->error, sizeof( ap->error ) ) )
return 0;
strncpy( a->value, val, ARGP_VAL_LEN - 1 );
a->present = 1;
break; /* consumed rest of token */
}
}
}
/* check required arguments */
for ( int i = 0; i < ap->count; i++ ) {
Arg *a = &ap->args[i];
if ( a->required && !a->present ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( ap->error, sizeof( ap->error ), "required positional argument missing: <%s>", a->longname );
else
snprintf( ap->error, sizeof( ap->error ), "required option missing: --%s", a->longname );
return 0;
}
}
return 1;
}
/// @brief Returns the value of a named flag or option as a C string.
///
/// For flags, returns "1" if present or "0" otherwise.
/// For options, returns the user-supplied value or the registered default.
///
/// @param ap Pointer to the @ref ArgParser after a successful parse.
/// @param name Long name of the argument (without "--").
///
/// @return The value string, or NULL if @p name was never registered.
///
/// Example:
/// @code{.c}
/// const char *out = argp_get(&ap, "output");
/// @endcode
static inline const char *argp_get( const ArgParser *ap, const char *name ) {
if ( !ap || !name )
return NULL;
for ( int i = 0; i < ap->count; i++ )
if ( strcmp( ap->args[i].longname, name ) == 0 )
return ap->args[i].value;
return NULL;
}
/// @brief Convenience: returns 1 if a flag was set, 0 otherwise.
///
/// Equivalent to checking whether @ref argp_get returns "1".
///
/// @param ap Pointer to the @ref ArgParser.
/// @param name Long name of the flag.
///
/// @return 1 if the flag was present on the command line, 0 otherwise.
///
/// Example:
/// @code{.c}
/// if (argp_flag(&ap, "verbose")) {
/// printf("Verbose mode enabled.\n");
/// }
/// @endcode
static inline int argp_flag( const ArgParser *ap, const char *name ) {
if ( !ap || !name )
return 0;
const char *v = argp_get( ap, name );
return v && v[0] == '1';
}
/// @brief Returns the value of a positional argument by name.
///
/// @param ap Pointer to the @ref ArgParser after a successful parse.
/// @param name Name of the positional argument (as registered with
/// @ref argp_add_pos).
///
/// @return The parsed value string, or NULL if the slot was never filled or
/// @p name was not found.
///
/// Example:
/// @code{.c}
/// const char *input = argp_pos(&ap, "input");
/// @endcode
static inline const char *argp_pos( const ArgParser *ap, const char *name ) {
if ( !ap || !name )
return NULL;
for ( int i = 0; i < ap->count; i++ ) {
if ( ap->args[i].kind == ARGP_KIND_POS && strcmp( ap->args[i].longname, name ) == 0 )
return ap->args[i].present ? ap->args[i].value : NULL;
}
return NULL;
}
/// @brief Parses and validates an argument as a signed 32-bit integer with range checking.
///
/// Performs strict parsing:
/// - Rejects NULL pointers, empty strings, and strings with trailing non-whitespace characters.
/// - Detects integer overflow/underflow (ERANGE, < INT_MIN, > INT_MAX).
/// - Enforces inclusive bounds [min_val, max_val] when min_val <= max_val.
/// - If min_val > max_val (e.g. @ref ARGP_NO_LIMITS), range checking is skipped.
/// - Records human-readable diagnostic messages in @p ap->error on failure.
///
/// @param ap Pointer to the @ref ArgParser.
/// @param name Name of the registered option or positional argument.
/// @param out Destination pointer for the parsed integer.
/// @param min_val Minimum allowed value (inclusive).
/// @param max_val Maximum allowed value (inclusive).
/// @return true on success, false on parsing failure, range violation, or absent argument.
///
/// Example:
/// @code{.c}
/// int port;
/// if (!argp_get_int(&ap, "port", &port, 1, 65535)) {
/// argp_print_error(&ap);
/// }
/// @endcode
static inline bool argp_get_int( const ArgParser *ap, const char *name, int *out, int min_val, int max_val ) {
if ( !ap || !name )
return false;
ArgParser *mutable_ap = (ArgParser *)(uintptr_t)ap;
if ( !out ) {
snprintf( mutable_ap->error, sizeof( mutable_ap->error ), "null destination pointer for argument '%.32s'", name );
return false;
}
const Arg *a = _argp_find_const( ap, name );
if ( !a ) {
snprintf( mutable_ap->error, sizeof( mutable_ap->error ), "argument '%.32s' not registered", name );
return false;
}
if ( !a->present && a->value[0] == '\0' )
return false;
const char *str = a->value;
while ( *str && isspace( (unsigned char)*str ) )
str++;
if ( *str == '\0' ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': empty value cannot be converted to integer", a->longname );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': empty value cannot be converted to integer", a->longname );
return false;
}
errno = 0;
char *endptr = NULL;
long val = strtol( str, &endptr, 10 );
if ( endptr == str ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': invalid integer value '%.64s'", a->longname, a->value );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': invalid integer value '%.64s'", a->longname, a->value );
return false;
}
while ( *endptr && isspace( (unsigned char)*endptr ) )
endptr++;
if ( *endptr != '\0' ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': invalid integer value '%.64s' (trailing characters '%.32s')",
a->longname, a->value, endptr );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': invalid integer value '%.64s' (trailing characters '%.32s')",
a->longname, a->value, endptr );
return false;
}
if ( errno == ERANGE || val < INT_MIN || val > INT_MAX ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': value '%.64s' out of integer range", a->longname, a->value );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': value '%.64s' out of integer range", a->longname, a->value );
return false;
}
int ival = (int)val;
if ( min_val <= max_val ) {
if ( ival < min_val || ival > max_val ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': value '%d' out of range [%d, %d]",
a->longname, ival, min_val, max_val );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': value '%d' out of range [%d, %d]",
a->longname, ival, min_val, max_val );
return false;
}
}
*out = ival;
return true;
}
/// @brief Parses and validates an argument as a double-precision float with range checking.
///
/// Performs strict parsing:
/// - Rejects NULL pointers, empty strings, and strings with trailing non-whitespace characters.
/// - Rejects NaN, Inf, and -Inf values.
/// - Detects floating-point overflow (ERANGE).
/// - Enforces inclusive bounds [min_val, max_val] when min_val <= max_val.
/// - If min_val > max_val (e.g. @ref ARGP_NO_LIMITS), range checking is skipped.
/// - Records human-readable diagnostic messages in @p ap->error on failure.
///
/// @param ap Pointer to the @ref ArgParser.
/// @param name Name of the registered option or positional argument.
/// @param out Destination pointer for the parsed double.
/// @param min_val Minimum allowed value (inclusive).
/// @param max_val Maximum allowed value (inclusive).
/// @return true on success, false on parsing failure, range violation, or absent argument.
///
/// Example:
/// @code{.c}
/// double rate;
/// if (!argp_get_float(&ap, "rate", &rate, 0.0, 1.0)) {
/// argp_print_error(&ap);
/// }
/// @endcode
static inline bool argp_get_float( const ArgParser *ap, const char *name, double *out, double min_val, double max_val ) {
if ( !ap || !name )
return false;
ArgParser *mutable_ap = (ArgParser *)(uintptr_t)ap;
if ( !out ) {
snprintf( mutable_ap->error, sizeof( mutable_ap->error ), "null destination pointer for argument '%.32s'", name );
return false;
}
const Arg *a = _argp_find_const( ap, name );
if ( !a ) {
snprintf( mutable_ap->error, sizeof( mutable_ap->error ), "argument '%.32s' not registered", name );
return false;
}
if ( !a->present && a->value[0] == '\0' )
return false;
const char *str = a->value;
while ( *str && isspace( (unsigned char)*str ) )
str++;
if ( *str == '\0' ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': empty value cannot be converted to float", a->longname );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': empty value cannot be converted to float", a->longname );
return false;
}
const char *chk = str;
if ( *chk == '+' || *chk == '-' )
chk++;
if ( _argp_strncasecmp( chk, "nan", 3 ) == 0 || _argp_strncasecmp( chk, "inf", 3 ) == 0 ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': invalid floating-point value '%.64s'", a->longname, a->value );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': invalid floating-point value '%.64s'", a->longname, a->value );
return false;
}
errno = 0;
char *endptr = NULL;
double val = strtod( str, &endptr );
if ( endptr == str ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': invalid floating-point value '%.64s'", a->longname, a->value );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': invalid floating-point value '%.64s'", a->longname, a->value );
return false;
}
while ( *endptr && isspace( (unsigned char)*endptr ) )
endptr++;
if ( *endptr != '\0' ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': invalid floating-point value '%.64s' (trailing characters '%.32s')",
a->longname, a->value, endptr );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': invalid floating-point value '%.64s' (trailing characters '%.32s')",
a->longname, a->value, endptr );
return false;
}
if ( errno == ERANGE || isnan( val ) || isinf( val ) ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': value '%.64s' out of floating-point range", a->longname, a->value );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': value '%.64s' out of floating-point range", a->longname, a->value );
return false;
}
if ( min_val <= max_val ) {
if ( val < min_val || val > max_val ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': value '%g' out of range [%g, %g]",
a->longname, val, min_val, max_val );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': value '%g' out of range [%g, %g]",
a->longname, val, min_val, max_val );
return false;
}
}
*out = val;
return true;
}
/// @brief Parses and converts an argument or flag to a boolean value.
///
/// Supports:
/// - Direct flags (@ref ARGP_KIND_FLAG): sets @p *out to true if present, false if omitted.
/// - String options/positionals: case-insensitive conversion:
/// - Truthy: "1", "true", "t", "yes", "y", "on"
/// - Falsy: "0", "false", "f", "no", "n", "off"
/// - Rejects invalid strings with an error message in @p ap->error.
///
/// @param ap Pointer to the @ref ArgParser.
/// @param name Name of the registered argument.
/// @param out Destination pointer for the parsed boolean.
/// @return true on success, false on invalid value, absent argument, or NULL pointer.
///
/// Example:
/// @code{.c}
/// bool debug;
/// if (argp_get_bool(&ap, "debug", &debug)) {
/// // debug is true or false
/// }
/// @endcode
static inline bool argp_get_bool( const ArgParser *ap, const char *name, bool *out ) {
if ( !ap || !name )
return false;
ArgParser *mutable_ap = (ArgParser *)(uintptr_t)ap;
if ( !out ) {
snprintf( mutable_ap->error, sizeof( mutable_ap->error ), "null destination pointer for argument '%.32s'", name );
return false;
}
const Arg *a = _argp_find_const( ap, name );
if ( !a ) {
snprintf( mutable_ap->error, sizeof( mutable_ap->error ), "argument '%.32s' not registered", name );
return false;
}
if ( a->kind == ARGP_KIND_FLAG ) {
*out = ( a->present != 0 );
return true;
}
if ( !a->present && a->value[0] == '\0' )
return false;
const char *str = a->value;
while ( *str && isspace( (unsigned char)*str ) )
str++;
size_t len = strlen( str );
while ( len > 0 && isspace( (unsigned char)str[len - 1] ) )
len--;
char trimmed[32] = { 0 };
if ( len == 0 || len >= sizeof( trimmed ) ) {
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': invalid boolean value '%.64s' (expected true/false, yes/no, on/off, 1/0)",
a->longname, a->value );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': invalid boolean value '%.64s' (expected true/false, yes/no, on/off, 1/0)",
a->longname, a->value );
return false;
}
memcpy( trimmed, str, len );
trimmed[len] = '\0';
if ( _argp_strcasecmp( trimmed, "1" ) == 0 ||
_argp_strcasecmp( trimmed, "true" ) == 0 ||
_argp_strcasecmp( trimmed, "t" ) == 0 ||
_argp_strcasecmp( trimmed, "yes" ) == 0 ||
_argp_strcasecmp( trimmed, "y" ) == 0 ||
_argp_strcasecmp( trimmed, "on" ) == 0 ) {
*out = true;
return true;
}
if ( _argp_strcasecmp( trimmed, "0" ) == 0 ||
_argp_strcasecmp( trimmed, "false" ) == 0 ||
_argp_strcasecmp( trimmed, "f" ) == 0 ||
_argp_strcasecmp( trimmed, "no" ) == 0 ||
_argp_strcasecmp( trimmed, "n" ) == 0 ||
_argp_strcasecmp( trimmed, "off" ) == 0 ) {
*out = false;
return true;
}
if ( a->kind == ARGP_KIND_POS )
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"positional argument '<%.32s>': invalid boolean value '%.64s' (expected true/false, yes/no, on/off, 1/0)",
a->longname, a->value );
else
snprintf( mutable_ap->error, sizeof( mutable_ap->error ),
"option '--%.32s': invalid boolean value '%.64s' (expected true/false, yes/no, on/off, 1/0)",
a->longname, a->value );
return false;
}
/// @brief Prints the parse error and a short usage hint to stderr.
///
/// Use this after @ref argp_parse returns 0 to inform the user what went wrong
/// and how to get help.
///
/// @param ap Pointer to the @ref ArgParser (error message is in @p ap->error).
///
/// Example:
/// @code{.c}
/// if (!argp_parse(&ap, argc, argv)) {
/// argp_print_error(&ap);
/// return 1;
/// }
/// @endcode
static inline void argp_print_error( const ArgParser *ap ) {
if ( !ap )
return;
fprintf( stderr, "%s%s: error:%s %s\n", TERMINAL_COLOR_RED, ap->prog, TERMINAL_COLOR_RESET, ap->error );
fprintf( stderr, "Try '%s --help' for more information.\n", ap->prog );
}
/// @brief Frees any resources held by the ArgParser.
///
/// The current implementation uses only stack-allocated memory, so this
/// function is a no-op. It is kept for API symmetry so that future versions
/// that might use heap allocation remain backward-compatible.
///
/// @param ap Pointer to the @ref ArgParser (unused).
static inline void argp_free( ArgParser *ap ) { (void)ap; }
#endif /* GREJC_UTILS_H */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment