Last active
September 20, 2026 03:09
-
-
Save grejc/3817f0875b9cea61941b48e465e712db to your computer and use it in GitHub Desktop.
Biblioteca de utilidades para C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// @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