clix
Single-header file CLI libraries for C
Loading...
Searching...
No Matches
print.h
Author
Jakov Dragičević (github:jakovdev)
Attention
This library is work-in-progress. Do not use if not meaning to contribute.

Source code:

// SPDX-License-Identifier: MIT
/**
* @page print.h
* @author Jakov Dragičević (github:jakovdev)
* @copyright MIT License.
* @attention This library is work-in-progress. Do not use if not meaning to contribute.
*
* Source code:
* @include print.h
*/
/* REQUIRED: C99 or later */
/* BASIC USAGE:
*
* pheader("Header text");
* ╔══════════════════════════════════════════════════════════════════════════════╗
* ║ Header text ║
* ╚══════════════════════════════════════════════════════════════════════════════╝
*
* psection("Setup");
* ┌─────────────────────────────────── Setup ────────────────────────────────────┐
*
* Automatically formats the string like printf
* const char *input_file = "input.csv";
* pinfo("Reading input file: %s", input_file);
* │ • Reading input file: input.csv │
*
* Only prints if verbose mode is enabled
* pverb("Batch size: %zu tasks per batch", batch_size);
* │ · Batch size: 6163 tasks per batch │
*
* Specify location for simple hierarchy
* pinfo("Input: %s", input_file);
* pinfom("Output: %s", output_file);
* pinfol("Compression: %d", compression_level);
* │ • Input: in.csv │
* │ ├ Output: out.h5 │
* │ └ Compression: 0 │
*
* Progress bar display, has quick return for repeats, draws over empty boxes
* int numbers = 1000;
* for (int i = 0; i < numbers; i++)
* pproport(i / numbers, "Storing numbers");
* │ ▶ Storing numbers [■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■········] 86% │
*
* Interactive prompt with choices
* char *choices[] = {"hello", "second", NULL};
* int selected = pchoice_s(choices, "Enter column number");
* │ 1: hello │
* │ 2: second │
* │ • Enter column number (1-2): 4 │
* │ ! Invalid input! Please enter a number between 1 and 2. │
* If valid returns zero-based index of selected choice, invalid prints choices again
*
* pwarn("Warning text");
* │ ! Warning text │
*
* perr("File not found");
* │ ✗ File not found │
*
* For getting user input
* char result[16] = { 0 };
* pinput_s(result, "Enter a character: ");
* │ • Enter a character: hello │
* result will now contain "hello"
*
* Quick y/N prompt (also has Y/n and y/n variants)
* bool answer = print_yN("Do you want to continue?");
* │ • Do you want to continue? [y/N]: y │
* answer will be true (yes) or false (no)
*
* pdev("Error %d", value);
* │ ✗ _TO_DEV_: Error 42 │
* Is perr() only in Debug builds (NDEBUG not defined), adds "_TO_DEV_ :" to string
*
* Close section (automatic on new section/header or non-abort exit)
* psection_end();
* └──────────────────────────────────────────────────────────────────────────────┘
*
* For starting section with no text
* psection();
* ┌──────────────────────────────────────────────────────────────────────────────┐
*
* Also available:
* print_stream_in(stdin);
* print_stream_out(stdout);
* print_stream_err(stderr);
*/
#ifndef PRINT_H_
#define PRINT_H_
#include <stddef.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
typedef const union _P_INPUT {
const char **choices;
#define P_INPUT_C(c, n) (P_INPUT){ .choices = c }, n, P_CHOICE
#define P_INPUT_CS(choices) P_INPUT_C(choices, sizeof(choices))
char *output;
#define P_INPUT_P(out, size) (P_INPUT){ .output = (out) }, size, P_PROMPT
#define P_INPUT_PS(out) P_INPUT_P(out, sizeof(out))
} P_INPUT;
/* clang-format off */
#define P_INFO "\x01"
#define P_VERBOSE "\x02"
#define P_WARNING "\x03"
#define P_ERROR "\x04"
#define P_HEADER "\x05"
#define P_SECTION "\x06"
#define P_CHOICE "\x07"
#define P_PROMPT "\x08"
#define P_MIDDLE "\x11"
#define P_LAST "\x12"
/* clang-format on */
/* Useful for debugging */
enum p_return {
/* All fields are customizable for easier debugging or if checks */
PRINT_SUCCESS = 0,
PRINT_SKIPPED_BECAUSE_QUIET_OR_VERBOSE_NOT_ENABLED__SUCCESS = 0,
PRINT_REPEAT_PROGRESS_PERCENT__SUCCESS = 0,
PRINT_FIRST_CHOICE_INDEX__SUCCESS = 0, /* Editable first choice index */
PRINT_INVALID_FORMAT_ARGS__ERROR = -1,
PRINT_CHOICE_COLLECTION_SHOULD_CONTAIN_2_OR_MORE_CHOICES__ERROR = -2,
PRINT_PROMPT_BUFFER_SIZE_SHOULD_BE_2_OR_MORE__ERROR = -2,
PRINT_INVALID_INPUT_TYPE__ERROR = -3,
};
void print_stream_in(FILE *);
void print_stream_out(FILE *);
void print_stream_err(FILE *);
#ifdef __cplusplus
#if defined(_MSC_VER) && !defined(__clang__)
#define P_RESTRICT __restrict
#else /* G++, Clang++ */
#define P_RESTRICT __restrict__
#endif /* MSVC C++ */
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
#define P_RESTRICT restrict
#else
/* While restrict is optional, snprintf and vsnprintf are required */
#error "Please compile with C99 or later standard"
#endif
enum p_return print(const char *P_RESTRICT, ...);
enum p_return progress_bar(int percent, const char *P_RESTRICT, ...);
enum p_return input(P_INPUT, size_t, const char *P_RESTRICT, ...);
/* "prompt [y/N]: " */
bool print_yN(const char *P_RESTRICT);
/* "prompt [Y/n]: " */
bool print_Yn(const char *P_RESTRICT);
/* "prompt [y/n]: " */
bool print_yn(const char *P_RESTRICT);
#define pinfo(...) print(P_INFO __VA_ARGS__)
#define pinfom(...) print(P_INFO P_MIDDLE __VA_ARGS__)
#define pinfol(...) print(P_INFO P_LAST __VA_ARGS__)
#define pwarning(...) print(P_WARNING __VA_ARGS__)
#define pwarningm(...) print(P_WARNING P_MIDDLE __VA_ARGS__)
#define pwarningl(...) print(P_WARNING P_LAST __VA_ARGS__)
#define pwarn(...) pwarning(__VA_ARGS__)
#define pwarnm(...) pwarningm(__VA_ARGS__)
#define pwarnl(...) pwarningl(__VA_ARGS__)
#define pverbose(...) print(P_VERBOSE __VA_ARGS__)
#define pverbosem(...) print(P_VERBOSE P_MIDDLE __VA_ARGS__)
#define pverbosel(...) print(P_VERBOSE P_LAST __VA_ARGS__)
#define pverb(...) pverbose(__VA_ARGS__)
#define pverbm(...) pverbosem(__VA_ARGS__)
#define pverbl(...) pverbosel(__VA_ARGS__)
#define perr(...) print(P_ERROR __VA_ARGS__)
#define perrm(...) print(P_ERROR P_MIDDLE __VA_ARGS__)
#define perrl(...) print(P_ERROR P_LAST __VA_ARGS__)
#define pchoice(choices, n, ...) input(P_INPUT_C(choices, n) __VA_ARGS__)
#define pchoice_s(choices, ...) input(P_INPUT_CS(choices) __VA_ARGS__)
#define pinput(out, size, ...) input(P_INPUT_P(out, size) __VA_ARGS__)
#define pinput_s(out, ...) input(P_INPUT_PS(out) __VA_ARGS__)
#define ppercent(pct, ...) progress_bar(pct, __VA_ARGS__)
#define pproport(prp, ...) progress_bar((100 * prp), __VA_ARGS__)
#define pproportc(prp, ...) progress_bar((int)(100 * prp), __VA_ARGS__)
#define pheader(...) print(P_HEADER __VA_ARGS__)
#define psection(...) print(P_SECTION __VA_ARGS__)
#define psection_end() print(NULL)
/* abort() by itself doesn't automatically call psection_end() unlike exit() */
#define pabort() \
do { \
psection_end(); \
abort(); \
} while (0)
#ifndef PRINT_TO_DEV_STR
#define PRINT_TO_DEV_STR "_TO_DEV_: "
#endif
#ifndef NDEBUG
#define pdev(...) perr(PRINT_TO_DEV_STR __VA_ARGS__)
#else
#define pdev(...)
#endif
#ifdef PRINT_EXTERN_FORCE
extern bool print_force;
#endif
#ifdef PRINT_EXTERN_VERBOSE
extern bool print_verbose;
#endif
#ifdef PRINT_EXTERN_QUIET
extern bool print_quiet;
#endif
#ifdef PRINT_EXTERN_NODETAIL
extern bool print_nodetail;
#endif
#undef P_RESTRICT
#endif /* PRINT_H_ */
#if defined(PRINT_IMPLEMENTATION) && !defined(_PRINT_IMPLEMENTED)
#define _PRINT_IMPLEMENTED
#ifdef __cplusplus
#if defined(_MSC_VER) && !defined(__clang__)
#define P_RESTRICT __restrict
#else /* G++, Clang++ */
#define P_RESTRICT __restrict__
#endif /* MSVC C++ */
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
#define P_RESTRICT restrict
#else
/* While restrict is optional, snprintf and vsnprintf are required */
#error "Please compile with C99 or later standard"
#endif
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#define PRINT_UNDEF_WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#ifdef PRINT_UNDEF_WIN32_LEAN_AND_MEAN
#undef WIN32_LEAN_AND_MEAN
#undef PRINT_UNDEF_WIN32_LEAN_AND_MEAN
#endif
#define fputc_unlocked _fputc_nolock
#define fwrite_unlocked _fwrite_nolock
#define flockfile _lock_file
#define funlockfile _unlock_file
#else
#include <termios.h>
#include <unistd.h>
#include <sys/param.h>
#ifndef max
#define max MAX
#endif
#ifndef min
#define min MIN
#endif
#endif
#ifndef PRINT_TERMINAL_WIDTH
#define PRINT_TERMINAL_WIDTH 80
#endif
static int terminal_environment(void)
{
static int is_terminal = -1;
if (is_terminal == -1) {
#ifdef _WIN32
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwMode = 0;
is_terminal = (hStdout != INVALID_HANDLE_VALUE &&
GetConsoleMode(hStdout, &dwMode));
#else
is_terminal = isatty(STDOUT_FILENO);
#endif
}
return is_terminal;
}
static void terminal_init(void)
{
#ifdef _WIN32
HWND consoleWnd = GetConsoleWindow();
DWORD consoleProcessId = 0;
if (consoleWnd) {
GetWindowThreadProcessId(consoleWnd, &consoleProcessId);
if (consoleProcessId == GetCurrentProcessId()) {
char path[MAX_PATH] = { 0 };
char dir[MAX_PATH] = { 0 };
char name[MAX_PATH] = { 0 };
if (GetModuleFileNameA(NULL, path, MAX_PATH)) {
char *slash = strrchr(path, '\\');
if (slash) {
size_t dirLen = (size_t)(slash - path);
memcpy(dir, path, dirLen);
dir[dirLen] = '\0';
strcpy(name, slash + 1);
} else {
strcpy(name, path);
dir[0] = '\0';
}
char cmd[2048];
if (dir[0]) {
snprintf(
cmd, sizeof(cmd),
"cmd.exe /k \"cd /d \"%s\" && \"%s\"\"",
dir, name);
} else {
snprintf(cmd, sizeof(cmd),
"cmd.exe /k \"%s\"", name);
}
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
if (CreateProcessA(NULL, cmd, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL,
dir[0] ? dir : NULL, &si,
&pi)) {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
ExitProcess(0);
}
}
}
}
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut != INVALID_HANDLE_VALUE) {
DWORD dwMode = 0;
if (GetConsoleMode(hOut, &dwMode)) {
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
}
}
#endif
}
static void terminal_mode_raw(void)
{
#ifdef _WIN32
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode, mask = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT;
if (GetConsoleMode(hStdin, &mode))
SetConsoleMode(hStdin, mode & ~mask);
#else
struct termios term;
tcgetattr(STDIN_FILENO, &term);
term.c_lflag &= ~((tcflag_t)(ICANON | ECHO));
tcsetattr(STDIN_FILENO, TCSANOW, &term);
#endif
}
static void terminal_mode_restore(void)
{
#ifdef _WIN32
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hStdin, &mode);
SetConsoleMode(hStdin, mode | (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT));
#else
struct termios term;
tcgetattr(STDIN_FILENO, &term);
term.c_lflag |= (ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &term);
#endif
}
enum p_location {
LOC_FIRST,
LOC_MIDDLE,
LOC_LAST,
};
enum p_type {
T_NONE,
T_INFO,
T_VERBOSE,
T_WARNING,
T_ERROR,
T_HEADER,
T_SECTION,
T_CHOICE,
T_PROMPT,
T_PROGRESS,
T_TYPES
};
enum color {
COLOR_RESET,
COLOR_UNDER,
COLOR_RED,
COLOR_GREEN,
COLOR_YELLOW,
COLOR_BLUE,
COLOR_CYAN,
COLOR_GRAY,
COLOR_BRIGHT_BLUE,
COLOR_BRIGHT_CYAN,
COLOR_TYPE_COUNT
};
enum icon {
ICON_NONE,
ICON_WARNING,
ICON_DOT,
ICON_INFO,
ICON_ERROR,
ICON_ARROW,
ICON_TYPE_COUNT
};
enum box_pos {
BOX_TOP_LEFT,
BOX_LEFT_TEE,
BOX_BOTTOM_LEFT,
BOX_TOP_RIGHT,
BOX_HORIZONTAL,
BOX_VERTICAL,
BOX_RIGHT_TEE,
BOX_BOTTOM_RIGHT,
BOX_CHAR_COUNT
};
enum box_type { BOX_NORMAL, BOX_FANCY, BOX_TYPE_COUNT };
static struct {
const char *codes[COLOR_TYPE_COUNT];
const char *icons[ICON_TYPE_COUNT];
FILE *in;
FILE *out;
FILE *err;
size_t width;
const struct {
enum color color;
enum icon icon;
bool required;
} map[T_TYPES];
const char boxes[BOX_TYPE_COUNT][BOX_CHAR_COUNT][sizeof("╔")];
const char progress_filled_char[sizeof("■")];
const char progress_empty_char[sizeof("·")];
const char ansi_escape_start[sizeof("\x1b")];
char ansi_carriage_return[sizeof("\r")];
} p = {
.map = {
[T_NONE] = { COLOR_RESET, ICON_NONE, false },
[T_INFO] = { COLOR_BLUE, ICON_INFO, false },
[T_VERBOSE] = { COLOR_GRAY, ICON_DOT, true },
[T_WARNING] = { COLOR_YELLOW, ICON_WARNING, true },
[T_ERROR] = { COLOR_RED, ICON_ERROR, true },
[T_HEADER] = { COLOR_BRIGHT_CYAN, ICON_NONE, false },
[T_SECTION] = { COLOR_CYAN, ICON_NONE, false },
[T_CHOICE] = { COLOR_BLUE, ICON_INFO, true },
[T_PROMPT] = { COLOR_BLUE, ICON_INFO, true },
[T_PROGRESS] = { COLOR_BLUE, ICON_ARROW, false },
},
#define PCOL(t) p.map[(t)].color
#define PCOLSIZ(t) (PCOL(t) <= COLOR_UNDER ? 4 : 5)
.codes = {
[COLOR_RESET] = "\x1b[0m",
[COLOR_UNDER] = "\x1b[4m",
[COLOR_RED] = "\x1b[31m",
[COLOR_GREEN] = "\x1b[32m",
[COLOR_YELLOW] = "\x1b[33m",
[COLOR_BLUE] = "\x1b[34m",
[COLOR_CYAN] = "\x1b[36m",
[COLOR_GRAY] = "\x1b[90m",
[COLOR_BRIGHT_BLUE] = "\x1b[94m",
[COLOR_BRIGHT_CYAN] = "\x1b[96m",
},
#define PICO(t) p.map[(t)].icon
#define PICOSIZ(t) (PICO(t) >= ICON_ERROR ? 3 : PICO(t))
.icons = {
[ICON_NONE] = "",
[ICON_WARNING] = "!",
[ICON_DOT] = "·",
[ICON_INFO] = "•",
[ICON_ERROR] = "✗",
[ICON_ARROW] = "▶",
},
#define BOXSIZ (sizeof(p.boxes[BOX_NORMAL][0]) - 1)
.boxes = {
{ "┌", "├", "└", "┐", "─", "│", "┤", "┘" },
{ "╔", "╠", "╚", "╗", "═", "║", "╣", "╝" },
},
.progress_filled_char = "■",
.progress_empty_char = "·",
.ansi_escape_start = "\x1b",
.ansi_carriage_return = "\r",
.width = PRINT_TERMINAL_WIDTH,
};
static bool print_force;
static bool print_verbose;
static bool print_quiet;
static bool print_nodetail;
static bool in_section;
static bool content_printed;
static bool is_init;
void print_stream_in(FILE *in)
{
p.in = in;
}
void print_stream_out(FILE *out)
{
p.out = out;
}
void print_stream_err(FILE *err)
{
p.err = err;
}
static void print_section_end(void)
{
if (in_section)
psection_end();
}
static void print_init(void)
{
terminal_init();
if (!terminal_environment()) {
p.ansi_carriage_return[0] = '\n';
print_nodetail = true;
}
print_stream_in(stdin);
print_stream_out(stdout);
print_stream_err(stderr);
atexit(print_section_end);
is_init = true;
}
static void terminal_read_input(char *buf, size_t buf_sz)
{
if (!buf || !buf_sz)
return;
fflush(p.out);
terminal_mode_raw();
size_t i = 0;
for (int c; (c = fgetc(p.in)) != EOF && c != '\n' && c != '\r';) {
if (c == '\b' || c == 0x7F) {
if (i) {
while (i && ((buf[i - 1] & 0xC0) == 0x80))
i--;
buf[--i] = '\0';
fwrite("\b \b", 1, 3, p.out);
fflush(p.out);
}
continue;
}
if (i < buf_sz - 1 && (c >= 0x20 && c <= 0x7E)) {
buf[i++] = (char)c;
buf[i] = '\0';
fputc(c, p.out);
fflush(p.out);
}
}
buf[i] = '\0';
terminal_mode_restore();
}
bool print_yN(const char *P_RESTRICT prompt)
{
if (print_force)
return true;
char result[2] = { 0 };
pinput_s(result, "%s [y/N]", prompt);
return result[0] == 'y' || result[0] == 'Y';
}
bool print_Yn(const char *P_RESTRICT prompt)
{
if (print_force)
return true;
char result[2] = { 0 };
pinput_s(result, "%s [Y/n]", prompt);
return !(result[0] == 'n' || result[0] == 'N');
}
bool print_yn(const char *P_RESTRICT prompt)
{
if (print_force)
return true;
char result[2] = { 0 };
repeat:
pinput_s(result, "%s [y/n]", prompt);
if (result[0] == 'y' || result[0] == 'Y')
return true;
else if (result[0] == 'n' || result[0] == 'N')
return false;
goto repeat;
}
#define ouputc(c) fputc_unlocked((c), out)
#define oprintf(...) fprintf(out, __VA_ARGS__)
#define ouwrite(buf, size) fwrite_unlocked((buf), 1, (size), out)
#define ouwico(t) ouwrite(p.icons[PICO(t)], PICOSIZ(t))
#define ouwcol(t) ouwrite(p.codes[PCOL(t)], PCOLSIZ(t))
#define ouwbox(bt, bpart) ouwrite(p.boxes[(bt)][bpart], BOXSIZ)
static int last_percentage = -1;
enum p_return print(const char *P_RESTRICT fmt, ...)
{
if (!is_init)
print_init();
FILE *out = p.out;
char p_buf[BUFSIZ] = { 0 };
int p_bufsiz = 0;
enum p_location loc = LOC_FIRST;
enum p_type type = T_NONE;
bool simple = false;
if (!fmt) { /* Section end only */
type = T_SECTION;
goto skip_fmt;
}
while (*fmt) {
unsigned char c = (unsigned char)*fmt;
if (c >= T_INFO && c <= T_SECTION) {
type = (enum p_type)c;
fmt++;
} else if (c == 0x11) {
loc = LOC_MIDDLE;
fmt++;
} else if (c == 0x12) {
loc = LOC_LAST;
fmt++;
} else {
break;
}
}
if (type == T_ERROR)
out = p.err;
if ((print_quiet && !p.map[type].required) ||
(type == T_VERBOSE && !print_verbose))
return PRINT_SKIPPED_BECAUSE_QUIET_OR_VERBOSE_NOT_ENABLED__SUCCESS;
if (!in_section && type != T_HEADER && type != T_SECTION)
psection();
if (last_percentage != -1 && content_printed) {
fputc('\n', out);
last_percentage = -1;
}
{
va_list v_args;
va_start(v_args, fmt);
p_bufsiz = vsnprintf(p_buf, sizeof(p_buf), fmt, v_args);
va_end(v_args);
if (p_bufsiz < 0) {
pdev("Failed to format print string");
return PRINT_INVALID_FORMAT_ARGS__ERROR;
}
}
skip_fmt:
simple = print_nodetail || (print_quiet && p.map[type].required);
const size_t available = p.width - 3 - (!PICO(type) ? 0 : 2);
size_t p_buflen = (size_t)p_bufsiz;
if (p_buflen > available) { /* Overflow, no box/icon/color then */
pdev("Print string too long, doing a simple print");
simple = true;
}
flockfile(out);
if (type == T_HEADER) {
if (!fmt)
goto cleanup;
if (simple) {
ouputc('\n');
ouwrite(p_buf, p_buflen);
ouwrite("\n\n", 2);
in_section = false;
goto cleanup;
}
if (in_section) {
funlockfile(out);
psection_end();
flockfile(out);
}
ouwcol(type);
ouwbox(BOX_FANCY, BOX_TOP_LEFT);
size_t iwlrp;
for (iwlrp = 0; iwlrp < p.width - 2; iwlrp++)
ouwbox(BOX_FANCY, BOX_HORIZONTAL);
ouwbox(BOX_FANCY, BOX_TOP_RIGHT);
ouwcol(T_NONE);
ouputc('\n');
const size_t l_pad = (p.width - 2 - p_buflen) / 2;
const size_t r_pad = p.width - 2 - p_buflen - l_pad;
ouwcol(type);
ouwbox(BOX_FANCY, BOX_VERTICAL);
for (iwlrp = 0; iwlrp < l_pad; iwlrp++)
ouputc(' ');
ouwrite(p_buf, p_buflen);
for (iwlrp = 0; iwlrp < r_pad; iwlrp++)
ouputc(' ');
ouwbox(BOX_FANCY, BOX_VERTICAL);
ouwcol(T_NONE);
ouputc('\n');
ouwcol(type);
ouwbox(BOX_FANCY, BOX_BOTTOM_LEFT);
for (iwlrp = 0; iwlrp < p.width - 2; iwlrp++)
ouwbox(BOX_FANCY, BOX_HORIZONTAL);
ouwbox(BOX_FANCY, BOX_BOTTOM_RIGHT);
ouwcol(T_NONE);
ouputc('\n');
in_section = false;
} else if (type == T_SECTION) {
if (in_section && (!fmt || content_printed)) {
if (!simple) {
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_BOTTOM_LEFT);
size_t iw;
for (iw = 0; iw < p.width - 2; iw++)
ouwbox(BOX_NORMAL, BOX_HORIZONTAL);
ouwbox(BOX_NORMAL, BOX_BOTTOM_RIGHT);
ouwcol(T_NONE);
}
ouputc('\n');
in_section = false;
content_printed = false;
}
if (!fmt)
goto cleanup;
if (simple) {
ouwrite(p_buf, p_buflen);
ouputc('\n');
in_section = true;
content_printed = false;
goto cleanup;
}
size_t l_dashes = 2;
size_t r_dashes = p.width - 2 - l_dashes - p_buflen - 2;
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_TOP_LEFT);
if (!p_buf[0])
l_dashes += 2;
while (l_dashes--)
ouwbox(BOX_NORMAL, BOX_HORIZONTAL);
if (p_buf[0]) {
ouputc(' ');
ouwrite(p.codes[COLOR_UNDER], PCOLSIZ(COLOR_UNDER));
ouwrite(p_buf, p_buflen);
ouwcol(T_NONE);
ouwcol(type);
ouputc(' ');
}
while (r_dashes--)
ouwbox(BOX_NORMAL, BOX_HORIZONTAL);
ouwbox(BOX_NORMAL, BOX_TOP_RIGHT);
ouwcol(T_NONE);
ouputc('\n');
in_section = true;
content_printed = false;
} else {
if (simple) {
ouwrite(p_buf, p_buflen);
ouputc('\n');
content_printed = true;
goto cleanup;
}
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(type);
ouputc(' ');
if (PICO(type)) {
if (loc != LOC_FIRST)
ouwbox(BOX_NORMAL, loc);
else
ouwico(type);
ouputc(' ');
}
ouwrite(p_buf, p_buflen);
size_t padding = available - p_buflen;
while (padding--)
ouputc(' ');
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(T_NONE);
ouputc('\n');
content_printed = true;
}
cleanup:
funlockfile(out);
return PRINT_SUCCESS;
}
enum p_return progress_bar(int percent, const char *P_RESTRICT fmt, ...)
{
if (!is_init)
print_init();
FILE *out = p.out;
if (print_quiet)
return PRINT_SKIPPED_BECAUSE_QUIET_OR_VERBOSE_NOT_ENABLED__SUCCESS;
if (!in_section)
psection();
#define CLAMP(val, min_val, max_val) (min(max((val), (min_val)), (max_val)))
int p_percent = CLAMP(percent, 0, 100);
if (p_percent == last_percentage ||
(p_percent == 100 && last_percentage == -1))
return PRINT_REPEAT_PROGRESS_PERCENT__SUCCESS;
last_percentage = p_percent;
if (last_percentage == 100)
last_percentage = -1;
char p_buf[BUFSIZ] = { 0 };
int p_bufsiz = 0;
if (fmt) {
va_list v_args;
va_start(v_args, fmt);
p_bufsiz = vsnprintf(p_buf, sizeof(p_buf), fmt, v_args);
va_end(v_args);
if (p_bufsiz < 0) {
pdev("Failed to format progress bar string");
return PRINT_INVALID_FORMAT_ARGS__ERROR;
}
}
bool simple = print_nodetail;
const size_t available = p.width - 3 - (!PICO(T_PROGRESS) ? 0 : 2);
size_t p_buflen = (size_t)p_bufsiz;
if (p_buflen > available) {
pdev("Progress bar string too long, doing a simple print");
simple = true;
}
flockfile(out);
const int digits = p_percent < 10 ? 1 : (p_percent < 100 ? 2 : 3);
if (simple) {
if (in_section)
ouwrite(p.ansi_carriage_return, 1);
ouwrite(p_buf, p_buflen);
ouputc(' ');
if (p_percent == 100) {
ouwrite("100%\n", 5);
} else {
if (p_percent >= 10)
ouputc('0' + p_percent / 10);
ouputc('0' + p_percent % 10);
ouputc('%');
}
fflush(out);
content_printed = true;
goto cleanup;
}
const size_t meta_width = (size_t)digits + 2 + 1 + 1 + 1;
const size_t bar_width = available - p_buflen - meta_width - 1;
const size_t filled_width = bar_width * (size_t)p_percent / 100;
const size_t empty_width = bar_width - filled_width;
if (in_section)
ouwrite(p.ansi_carriage_return, 1);
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
static bool repeat;
if (!repeat || p_percent == 100)
ouwcol(T_PROGRESS);
else
ouwrite(p.codes[COLOR_BRIGHT_BLUE], PCOLSIZ(COLOR_BRIGHT_BLUE));
repeat = !repeat;
ouputc(' ');
ouwico(T_PROGRESS);
ouputc(' ');
ouwrite(p_buf, p_buflen);
ouwrite(" [", 2);
size_t ifew;
for (ifew = 0; ifew < filled_width; ifew++)
ouwrite(p.progress_filled_char, 3);
for (ifew = 0; ifew < empty_width; ifew++)
ouwrite(p.progress_empty_char, 2);
ouwrite("] ", 2);
if (p_percent == 100) {
ouwrite("100% ", 5);
} else {
if (p_percent >= 10)
ouputc('0' + p_percent / 10);
ouputc('0' + p_percent % 10);
ouwrite("% ", 2);
}
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(T_NONE);
if (p_percent == 100)
ouputc('\n');
fflush(out);
content_printed = true;
cleanup:
funlockfile(out);
return PRINT_SUCCESS;
}
enum p_return input(P_INPUT in, size_t size, const char *P_RESTRICT fmt, ...)
{
if (!is_init)
print_init();
if (!fmt) {
pdev("Input format string is NULL");
return PRINT_INVALID_FORMAT_ARGS__ERROR;
}
enum p_type type = T_NONE;
while (*fmt) {
unsigned char c = (unsigned char)*fmt;
if (c >= 0x07 && c <= 0x08) {
type = (enum p_type)c;
fmt++;
} else {
break;
}
}
if (type != T_CHOICE && type != T_PROMPT) {
pdev("Invalid input type");
return PRINT_INVALID_INPUT_TYPE__ERROR;
}
FILE *out = p.out;
if (!in_section)
psection();
if (last_percentage != -1 && content_printed) {
fputc('\n', out);
last_percentage = -1;
}
char p_buf[BUFSIZ] = { 0 };
int p_bufsiz = 0;
{
va_list v_args;
va_start(v_args, fmt);
p_bufsiz = vsnprintf(p_buf, sizeof(p_buf), fmt, v_args);
va_end(v_args);
if (p_bufsiz < 0) {
pdev("Failed to format input string");
return PRINT_INVALID_FORMAT_ARGS__ERROR;
}
}
bool simple = print_nodetail;
const size_t available = p.width - 3 - (!PICO(type) ? 0 : 2);
size_t p_buflen = (size_t)p_bufsiz;
if (p_buflen > available) {
pdev("Input string too long, doing a simple print");
simple = true;
}
flockfile(out);
if (type == T_CHOICE) {
const char **choices = in.choices;
size_t c_count = size;
if (c_count < 2) {
funlockfile(out);
pdev("Not enough choices (<2)");
return PRINT_CHOICE_COLLECTION_SHOULD_CONTAIN_2_OR_MORE_CHOICES__ERROR;
}
choices[c_count] = NULL;
size_t c;
for (c = 0; c < c_count; c++) {
if (simple) {
oprintf("%zu: %s\n", c + 1, choices[c]);
} else {
const int label_chars = snprintf(
NULL, 0, "%zu: %s", c + 1, choices[c]);
const size_t label_len =
(label_chars < 0) ? 0 :
(size_t)label_chars;
const size_t padding =
label_len < available ?
available - label_len + 2 :
0;
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(type);
oprintf(" %zu: %s%*s", c + 1, choices[c],
(int)padding, "");
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(T_NONE);
ouputc('\n');
}
}
char i_buffer[PRINT_TERMINAL_WIDTH] = { 0 };
do {
if (!simple) {
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(type);
ouputc(' ');
ouwico(type);
ouputc(' ');
}
ouwrite(p_buf, p_buflen);
ouwrite(" (1-", 4);
oprintf("%zu", c_count);
ouwrite("): ", 3);
funlockfile(out);
terminal_read_input(i_buffer, sizeof(i_buffer));
flockfile(out);
errno = 0;
char *endptr = NULL;
unsigned long selected = strtoul(i_buffer, &endptr, 10);
if (endptr == i_buffer || *endptr != '\0' ||
errno == ERANGE || selected > INT_MAX)
selected = 0;
if (!simple) {
const int p_chars =
snprintf(NULL, 0, "%s (1-%zu): %s",
p_buf, c_count, i_buffer);
const size_t p_len =
(p_chars < 0) ? 0 : (size_t)p_chars;
size_t p_padding = p_len < available ?
available - p_len :
0;
while (p_padding--)
ouputc(' ');
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(T_NONE);
}
ouputc('\n');
if (selected >= 1 && selected <= c_count) {
content_printed = true;
funlockfile(out);
return (int)selected - 1 +
PRINT_FIRST_CHOICE_INDEX__SUCCESS;
}
funlockfile(out);
pwarn("Please enter a number between 1 and %zu",
c_count);
flockfile(out);
} while (1);
} else if (type == T_PROMPT) {
char *result = in.output;
const size_t rsz = size;
if (rsz < 2) {
funlockfile(out);
pdev("Output buffer size is too small");
return PRINT_PROMPT_BUFFER_SIZE_SHOULD_BE_2_OR_MORE__ERROR;
}
if (!simple) {
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(type);
ouputc(' ');
ouwico(type);
ouputc(' ');
}
ouwrite(p_buf, p_buflen);
ouwrite(": ", 2);
funlockfile(out);
terminal_read_input(result, rsz);
flockfile(out);
if (!simple) {
const size_t p_len = p_buflen + 2 + strlen(result);
size_t p_padding =
p_len < available ? available - p_len : 0;
while (p_padding--)
ouputc(' ');
ouwcol(T_SECTION);
ouwbox(BOX_NORMAL, BOX_VERTICAL);
ouwcol(T_NONE);
}
ouputc('\n');
content_printed = true;
}
funlockfile(out);
return PRINT_SUCCESS;
}
#undef P_RESTRICT
#endif /* PRINT_IMPLEMENTATION */
/*
print.h
https://github.com/jakovdev/clix/
Copyright (c) 2026 Jakov Dragičević
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
Definition print.h:98