clix
Single-header file CLI libraries for C
Loading...
Searching...
No Matches
print.h
1// SPDX-License-Identifier: MIT
2/**
3 * @page print.h
4 * @author Jakov Dragičević (github:jakovdev)
5 * @copyright MIT License.
6 * @attention This library is work-in-progress. Do not use if not meaning to contribute.
7 *
8 * Source code:
9 * @include print.h
10 */
11
12/* REQUIRED: C99 or later */
13/* BASIC USAGE:
14 *
15 * pheader("Header text");
16 * ╔══════════════════════════════════════════════════════════════════════════════╗
17 * ║ Header text ║
18 * ╚══════════════════════════════════════════════════════════════════════════════╝
19 *
20 * psection("Setup");
21 * ┌─────────────────────────────────── Setup ────────────────────────────────────┐
22 *
23 * Automatically formats the string like printf
24 * const char *input_file = "input.csv";
25 * pinfo("Reading input file: %s", input_file);
26 * │ • Reading input file: input.csv │
27 *
28 * Only prints if verbose mode is enabled
29 * pverb("Batch size: %zu tasks per batch", batch_size);
30 * │ · Batch size: 6163 tasks per batch │
31 *
32 * Specify location for simple hierarchy
33 * pinfo("Input: %s", input_file);
34 * pinfom("Output: %s", output_file);
35 * pinfol("Compression: %d", compression_level);
36 * │ • Input: in.csv │
37 * │ ├ Output: out.h5 │
38 * │ └ Compression: 0 │
39 *
40 * Progress bar display, has quick return for repeats, draws over empty boxes
41 * int numbers = 1000;
42 * for (int i = 0; i < numbers; i++)
43 * pproport(i / numbers, "Storing numbers");
44 * │ ▶ Storing numbers [■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■········] 86% │
45 *
46 * Interactive prompt with choices
47 * char *choices[] = {"hello", "second", NULL};
48 * int selected = pchoice_s(choices, "Enter column number");
49 * │ 1: hello │
50 * │ 2: second │
51 * │ • Enter column number (1-2): 4 │
52 * │ ! Invalid input! Please enter a number between 1 and 2. │
53 * If valid returns zero-based index of selected choice, invalid prints choices again
54 *
55 * pwarn("Warning text");
56 * │ ! Warning text │
57 *
58 * perr("File not found");
59 * │ ✗ File not found │
60 *
61 * For getting user input
62 * char result[16] = { 0 };
63 * pinput_s(result, "Enter a character: ");
64 * │ • Enter a character: hello │
65 * result will now contain "hello"
66 *
67 * Quick y/N prompt (also has Y/n and y/n variants)
68 * bool answer = print_yN("Do you want to continue?");
69 * │ • Do you want to continue? [y/N]: y │
70 * answer will be true (yes) or false (no)
71 *
72 * pdev("Error %d", value);
73 * │ ✗ _TO_DEV_: Error 42 │
74 * Is perr() only in Debug builds (NDEBUG not defined), adds "_TO_DEV_ :" to string
75 *
76 * Close section (automatic on new section/header or non-abort exit)
77 * psection_end();
78 * └──────────────────────────────────────────────────────────────────────────────┘
79 *
80 * For starting section with no text
81 * psection();
82 * ┌──────────────────────────────────────────────────────────────────────────────┐
83 *
84 * Also available:
85 * print_stream_in(stdin);
86 * print_stream_out(stdout);
87 * print_stream_err(stderr);
88 */
89
90#ifndef PRINT_H_
91#define PRINT_H_
92
93#include <stddef.h>
94#include <stdio.h>
95#include <stdbool.h>
96#include <stdlib.h>
97
98typedef const union _P_INPUT {
99 const char **choices;
100#define P_INPUT_C(c, n) (P_INPUT){ .choices = c }, n, P_CHOICE
101#define P_INPUT_CS(choices) P_INPUT_C(choices, sizeof(choices))
102 char *output;
103#define P_INPUT_P(out, size) (P_INPUT){ .output = (out) }, size, P_PROMPT
104#define P_INPUT_PS(out) P_INPUT_P(out, sizeof(out))
105} P_INPUT;
106
107/* clang-format off */
108#define P_INFO "\x01"
109#define P_VERBOSE "\x02"
110#define P_WARNING "\x03"
111#define P_ERROR "\x04"
112#define P_HEADER "\x05"
113#define P_SECTION "\x06"
114#define P_CHOICE "\x07"
115#define P_PROMPT "\x08"
116#define P_MIDDLE "\x11"
117#define P_LAST "\x12"
118/* clang-format on */
119
120/* Useful for debugging */
121enum p_return {
122 /* All fields are customizable for easier debugging or if checks */
123 PRINT_SUCCESS = 0,
124 PRINT_SKIPPED_BECAUSE_QUIET_OR_VERBOSE_NOT_ENABLED__SUCCESS = 0,
125 PRINT_REPEAT_PROGRESS_PERCENT__SUCCESS = 0,
126 PRINT_FIRST_CHOICE_INDEX__SUCCESS = 0, /* Editable first choice index */
127 PRINT_INVALID_FORMAT_ARGS__ERROR = -1,
128 PRINT_CHOICE_COLLECTION_SHOULD_CONTAIN_2_OR_MORE_CHOICES__ERROR = -2,
129 PRINT_PROMPT_BUFFER_SIZE_SHOULD_BE_2_OR_MORE__ERROR = -2,
130 PRINT_INVALID_INPUT_TYPE__ERROR = -3,
131};
132
133void print_stream_in(FILE *);
134void print_stream_out(FILE *);
135void print_stream_err(FILE *);
136
137#ifdef __cplusplus
138#if defined(_MSC_VER) && !defined(__clang__)
139#define P_RESTRICT __restrict
140#else /* G++, Clang++ */
141#define P_RESTRICT __restrict__
142#endif /* MSVC C++ */
143#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
144#define P_RESTRICT restrict
145#else
146/* While restrict is optional, snprintf and vsnprintf are required */
147#error "Please compile with C99 or later standard"
148#endif
149
150enum p_return print(const char *P_RESTRICT, ...);
151enum p_return progress_bar(int percent, const char *P_RESTRICT, ...);
152enum p_return input(P_INPUT, size_t, const char *P_RESTRICT, ...);
153
154/* "prompt [y/N]: " */
155bool print_yN(const char *P_RESTRICT);
156/* "prompt [Y/n]: " */
157bool print_Yn(const char *P_RESTRICT);
158/* "prompt [y/n]: " */
159bool print_yn(const char *P_RESTRICT);
160
161#define pinfo(...) print(P_INFO __VA_ARGS__)
162#define pinfom(...) print(P_INFO P_MIDDLE __VA_ARGS__)
163#define pinfol(...) print(P_INFO P_LAST __VA_ARGS__)
164
165#define pwarning(...) print(P_WARNING __VA_ARGS__)
166#define pwarningm(...) print(P_WARNING P_MIDDLE __VA_ARGS__)
167#define pwarningl(...) print(P_WARNING P_LAST __VA_ARGS__)
168
169#define pwarn(...) pwarning(__VA_ARGS__)
170#define pwarnm(...) pwarningm(__VA_ARGS__)
171#define pwarnl(...) pwarningl(__VA_ARGS__)
172
173#define pverbose(...) print(P_VERBOSE __VA_ARGS__)
174#define pverbosem(...) print(P_VERBOSE P_MIDDLE __VA_ARGS__)
175#define pverbosel(...) print(P_VERBOSE P_LAST __VA_ARGS__)
176
177#define pverb(...) pverbose(__VA_ARGS__)
178#define pverbm(...) pverbosem(__VA_ARGS__)
179#define pverbl(...) pverbosel(__VA_ARGS__)
180
181#define perr(...) print(P_ERROR __VA_ARGS__)
182#define perrm(...) print(P_ERROR P_MIDDLE __VA_ARGS__)
183#define perrl(...) print(P_ERROR P_LAST __VA_ARGS__)
184
185#define pchoice(choices, n, ...) input(P_INPUT_C(choices, n) __VA_ARGS__)
186#define pchoice_s(choices, ...) input(P_INPUT_CS(choices) __VA_ARGS__)
187#define pinput(out, size, ...) input(P_INPUT_P(out, size) __VA_ARGS__)
188#define pinput_s(out, ...) input(P_INPUT_PS(out) __VA_ARGS__)
189
190#define ppercent(pct, ...) progress_bar(pct, __VA_ARGS__)
191#define pproport(prp, ...) progress_bar((100 * prp), __VA_ARGS__)
192#define pproportc(prp, ...) progress_bar((int)(100 * prp), __VA_ARGS__)
193
194#define pheader(...) print(P_HEADER __VA_ARGS__)
195
196#define psection(...) print(P_SECTION __VA_ARGS__)
197#define psection_end() print(NULL)
198
199/* abort() by itself doesn't automatically call psection_end() unlike exit() */
200#define pabort() \
201 do { \
202 psection_end(); \
203 abort(); \
204 } while (0)
205
206#ifndef PRINT_TO_DEV_STR
207#define PRINT_TO_DEV_STR "_TO_DEV_: "
208#endif
209
210#ifndef NDEBUG
211#define pdev(...) perr(PRINT_TO_DEV_STR __VA_ARGS__)
212#else
213#define pdev(...)
214#endif
215
216#ifdef PRINT_EXTERN_FORCE
217extern bool print_force;
218#endif
219#ifdef PRINT_EXTERN_VERBOSE
220extern bool print_verbose;
221#endif
222#ifdef PRINT_EXTERN_QUIET
223extern bool print_quiet;
224#endif
225#ifdef PRINT_EXTERN_NODETAIL
226extern bool print_nodetail;
227#endif
228
229#undef P_RESTRICT
230#endif /* PRINT_H_ */
231#if defined(PRINT_IMPLEMENTATION) && !defined(_PRINT_IMPLEMENTED)
232#define _PRINT_IMPLEMENTED
233
234#ifdef __cplusplus
235#if defined(_MSC_VER) && !defined(__clang__)
236#define P_RESTRICT __restrict
237#else /* G++, Clang++ */
238#define P_RESTRICT __restrict__
239#endif /* MSVC C++ */
240#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
241#define P_RESTRICT restrict
242#else
243/* While restrict is optional, snprintf and vsnprintf are required */
244#error "Please compile with C99 or later standard"
245#endif
246
247#include <errno.h>
248#include <limits.h>
249#include <stdarg.h>
250#include <stdlib.h>
251#include <string.h>
252
253#ifdef _WIN32
254#ifndef WIN32_LEAN_AND_MEAN
255#define WIN32_LEAN_AND_MEAN
256#define PRINT_UNDEF_WIN32_LEAN_AND_MEAN
257#endif
258#include <windows.h>
259#ifdef PRINT_UNDEF_WIN32_LEAN_AND_MEAN
260#undef WIN32_LEAN_AND_MEAN
261#undef PRINT_UNDEF_WIN32_LEAN_AND_MEAN
262#endif
263#define fputc_unlocked _fputc_nolock
264#define fwrite_unlocked _fwrite_nolock
265#define flockfile _lock_file
266#define funlockfile _unlock_file
267#else
268#include <termios.h>
269#include <unistd.h>
270#include <sys/param.h>
271#ifndef max
272#define max MAX
273#endif
274#ifndef min
275#define min MIN
276#endif
277#endif
278
279#ifndef PRINT_TERMINAL_WIDTH
280#define PRINT_TERMINAL_WIDTH 80
281#endif
282
283static int terminal_environment(void)
284{
285 static int is_terminal = -1;
286 if (is_terminal == -1) {
287#ifdef _WIN32
288 HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
289 DWORD dwMode = 0;
290 is_terminal = (hStdout != INVALID_HANDLE_VALUE &&
291 GetConsoleMode(hStdout, &dwMode));
292#else
293 is_terminal = isatty(STDOUT_FILENO);
294#endif
295 }
296
297 return is_terminal;
298}
299
300static void terminal_init(void)
301{
302#ifdef _WIN32
303 HWND consoleWnd = GetConsoleWindow();
304 DWORD consoleProcessId = 0;
305
306 if (consoleWnd) {
307 GetWindowThreadProcessId(consoleWnd, &consoleProcessId);
308
309 if (consoleProcessId == GetCurrentProcessId()) {
310 char path[MAX_PATH] = { 0 };
311 char dir[MAX_PATH] = { 0 };
312 char name[MAX_PATH] = { 0 };
313
314 if (GetModuleFileNameA(NULL, path, MAX_PATH)) {
315 char *slash = strrchr(path, '\\');
316 if (slash) {
317 size_t dirLen = (size_t)(slash - path);
318 memcpy(dir, path, dirLen);
319 dir[dirLen] = '\0';
320 strcpy(name, slash + 1);
321 } else {
322 strcpy(name, path);
323 dir[0] = '\0';
324 }
325
326 char cmd[2048];
327 if (dir[0]) {
328 snprintf(
329 cmd, sizeof(cmd),
330 "cmd.exe /k \"cd /d \"%s\" && \"%s\"\"",
331 dir, name);
332 } else {
333 snprintf(cmd, sizeof(cmd),
334 "cmd.exe /k \"%s\"", name);
335 }
336
337 STARTUPINFOA si = { 0 };
338 PROCESS_INFORMATION pi = { 0 };
339 si.cb = sizeof(si);
340
341 if (CreateProcessA(NULL, cmd, NULL, NULL, FALSE,
342 CREATE_NEW_CONSOLE, NULL,
343 dir[0] ? dir : NULL, &si,
344 &pi)) {
345 CloseHandle(pi.hProcess);
346 CloseHandle(pi.hThread);
347 ExitProcess(0);
348 }
349 }
350 }
351 }
352
353 SetConsoleOutputCP(CP_UTF8);
354 SetConsoleCP(CP_UTF8);
355
356 HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
357 if (hOut != INVALID_HANDLE_VALUE) {
358 DWORD dwMode = 0;
359 if (GetConsoleMode(hOut, &dwMode)) {
360 dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
361 SetConsoleMode(hOut, dwMode);
362 }
363 }
364
365#endif
366}
367
368static void terminal_mode_raw(void)
369{
370#ifdef _WIN32
371 HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
372 DWORD mode, mask = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT;
373 if (GetConsoleMode(hStdin, &mode))
374 SetConsoleMode(hStdin, mode & ~mask);
375
376#else
377 struct termios term;
378 tcgetattr(STDIN_FILENO, &term);
379 term.c_lflag &= ~((tcflag_t)(ICANON | ECHO));
380 tcsetattr(STDIN_FILENO, TCSANOW, &term);
381#endif
382}
383
384static void terminal_mode_restore(void)
385{
386#ifdef _WIN32
387 HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
388 DWORD mode;
389 GetConsoleMode(hStdin, &mode);
390 SetConsoleMode(hStdin, mode | (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT));
391#else
392 struct termios term;
393 tcgetattr(STDIN_FILENO, &term);
394 term.c_lflag |= (ICANON | ECHO);
395 tcsetattr(STDIN_FILENO, TCSANOW, &term);
396#endif
397}
398
399enum p_location {
400 LOC_FIRST,
401 LOC_MIDDLE,
402 LOC_LAST,
403};
404
405enum p_type {
406 T_NONE,
407 T_INFO,
408 T_VERBOSE,
409 T_WARNING,
410 T_ERROR,
411 T_HEADER,
412 T_SECTION,
413 T_CHOICE,
414 T_PROMPT,
415 T_PROGRESS,
416 T_TYPES
417};
418
419enum color {
420 COLOR_RESET,
421 COLOR_UNDER,
422 COLOR_RED,
423 COLOR_GREEN,
424 COLOR_YELLOW,
425 COLOR_BLUE,
426 COLOR_CYAN,
427 COLOR_GRAY,
428 COLOR_BRIGHT_BLUE,
429 COLOR_BRIGHT_CYAN,
430 COLOR_TYPE_COUNT
431};
432
433enum icon {
434 ICON_NONE,
435 ICON_WARNING,
436 ICON_DOT,
437 ICON_INFO,
438 ICON_ERROR,
439 ICON_ARROW,
440 ICON_TYPE_COUNT
441};
442
443enum box_pos {
444 BOX_TOP_LEFT,
445 BOX_LEFT_TEE,
446 BOX_BOTTOM_LEFT,
447 BOX_TOP_RIGHT,
448 BOX_HORIZONTAL,
449 BOX_VERTICAL,
450 BOX_RIGHT_TEE,
451 BOX_BOTTOM_RIGHT,
452 BOX_CHAR_COUNT
453};
454
455enum box_type { BOX_NORMAL, BOX_FANCY, BOX_TYPE_COUNT };
456
457static struct {
458 const char *codes[COLOR_TYPE_COUNT];
459 const char *icons[ICON_TYPE_COUNT];
460 FILE *in;
461 FILE *out;
462 FILE *err;
463 size_t width;
464 const struct {
465 enum color color;
466 enum icon icon;
467 bool required;
468 } map[T_TYPES];
469 const char boxes[BOX_TYPE_COUNT][BOX_CHAR_COUNT][sizeof("╔")];
470 const char progress_filled_char[sizeof("■")];
471 const char progress_empty_char[sizeof("·")];
472 const char ansi_escape_start[sizeof("\x1b")];
473 char ansi_carriage_return[sizeof("\r")];
474} p = {
475 .map = {
476 [T_NONE] = { COLOR_RESET, ICON_NONE, false },
477 [T_INFO] = { COLOR_BLUE, ICON_INFO, false },
478 [T_VERBOSE] = { COLOR_GRAY, ICON_DOT, true },
479 [T_WARNING] = { COLOR_YELLOW, ICON_WARNING, true },
480 [T_ERROR] = { COLOR_RED, ICON_ERROR, true },
481 [T_HEADER] = { COLOR_BRIGHT_CYAN, ICON_NONE, false },
482 [T_SECTION] = { COLOR_CYAN, ICON_NONE, false },
483 [T_CHOICE] = { COLOR_BLUE, ICON_INFO, true },
484 [T_PROMPT] = { COLOR_BLUE, ICON_INFO, true },
485 [T_PROGRESS] = { COLOR_BLUE, ICON_ARROW, false },
486 },
487#define PCOL(t) p.map[(t)].color
488#define PCOLSIZ(t) (PCOL(t) <= COLOR_UNDER ? 4 : 5)
489 .codes = {
490 [COLOR_RESET] = "\x1b[0m",
491 [COLOR_UNDER] = "\x1b[4m",
492 [COLOR_RED] = "\x1b[31m",
493 [COLOR_GREEN] = "\x1b[32m",
494 [COLOR_YELLOW] = "\x1b[33m",
495 [COLOR_BLUE] = "\x1b[34m",
496 [COLOR_CYAN] = "\x1b[36m",
497 [COLOR_GRAY] = "\x1b[90m",
498 [COLOR_BRIGHT_BLUE] = "\x1b[94m",
499 [COLOR_BRIGHT_CYAN] = "\x1b[96m",
500 },
501#define PICO(t) p.map[(t)].icon
502#define PICOSIZ(t) (PICO(t) >= ICON_ERROR ? 3 : PICO(t))
503 .icons = {
504 [ICON_NONE] = "",
505 [ICON_WARNING] = "!",
506 [ICON_DOT] = "·",
507 [ICON_INFO] = "•",
508 [ICON_ERROR] = "✗",
509 [ICON_ARROW] = "▶",
510 },
511#define BOXSIZ (sizeof(p.boxes[BOX_NORMAL][0]) - 1)
512 .boxes = {
513 { "┌", "├", "└", "┐", "─", "│", "┤", "┘" },
514 { "╔", "╠", "╚", "╗", "═", "║", "╣", "╝" },
515 },
516 .progress_filled_char = "■",
517 .progress_empty_char = "·",
518 .ansi_escape_start = "\x1b",
519 .ansi_carriage_return = "\r",
520 .width = PRINT_TERMINAL_WIDTH,
521};
522
523static bool print_force;
524static bool print_verbose;
525static bool print_quiet;
526static bool print_nodetail;
527
528static bool in_section;
529static bool content_printed;
530static bool is_init;
531
532void print_stream_in(FILE *in)
533{
534 p.in = in;
535}
536
537void print_stream_out(FILE *out)
538{
539 p.out = out;
540}
541
542void print_stream_err(FILE *err)
543{
544 p.err = err;
545}
546
547static void print_section_end(void)
548{
549 if (in_section)
550 psection_end();
551}
552
553static void print_init(void)
554{
555 terminal_init();
556 if (!terminal_environment()) {
557 p.ansi_carriage_return[0] = '\n';
558 print_nodetail = true;
559 }
560
561 print_stream_in(stdin);
562 print_stream_out(stdout);
563 print_stream_err(stderr);
564 atexit(print_section_end);
565 is_init = true;
566}
567
568static void terminal_read_input(char *buf, size_t buf_sz)
569{
570 if (!buf || !buf_sz)
571 return;
572
573 fflush(p.out);
574 terminal_mode_raw();
575
576 size_t i = 0;
577 for (int c; (c = fgetc(p.in)) != EOF && c != '\n' && c != '\r';) {
578 if (c == '\b' || c == 0x7F) {
579 if (i) {
580 while (i && ((buf[i - 1] & 0xC0) == 0x80))
581 i--;
582 buf[--i] = '\0';
583 fwrite("\b \b", 1, 3, p.out);
584 fflush(p.out);
585 }
586 continue;
587 }
588
589 if (i < buf_sz - 1 && (c >= 0x20 && c <= 0x7E)) {
590 buf[i++] = (char)c;
591 buf[i] = '\0';
592 fputc(c, p.out);
593 fflush(p.out);
594 }
595 }
596
597 buf[i] = '\0';
598 terminal_mode_restore();
599}
600
601bool print_yN(const char *P_RESTRICT prompt)
602{
603 if (print_force)
604 return true;
605
606 char result[2] = { 0 };
607 pinput_s(result, "%s [y/N]", prompt);
608 return result[0] == 'y' || result[0] == 'Y';
609}
610
611bool print_Yn(const char *P_RESTRICT prompt)
612{
613 if (print_force)
614 return true;
615
616 char result[2] = { 0 };
617 pinput_s(result, "%s [Y/n]", prompt);
618 return !(result[0] == 'n' || result[0] == 'N');
619}
620
621bool print_yn(const char *P_RESTRICT prompt)
622{
623 if (print_force)
624 return true;
625
626 char result[2] = { 0 };
627repeat:
628 pinput_s(result, "%s [y/n]", prompt);
629 if (result[0] == 'y' || result[0] == 'Y')
630 return true;
631 else if (result[0] == 'n' || result[0] == 'N')
632 return false;
633
634 goto repeat;
635}
636
637#define ouputc(c) fputc_unlocked((c), out)
638#define oprintf(...) fprintf(out, __VA_ARGS__)
639#define ouwrite(buf, size) fwrite_unlocked((buf), 1, (size), out)
640#define ouwico(t) ouwrite(p.icons[PICO(t)], PICOSIZ(t))
641#define ouwcol(t) ouwrite(p.codes[PCOL(t)], PCOLSIZ(t))
642#define ouwbox(bt, bpart) ouwrite(p.boxes[(bt)][bpart], BOXSIZ)
643
644static int last_percentage = -1;
645
646enum p_return print(const char *P_RESTRICT fmt, ...)
647{
648 if (!is_init)
649 print_init();
650
651 FILE *out = p.out;
652
653 char p_buf[BUFSIZ] = { 0 };
654 int p_bufsiz = 0;
655
656 enum p_location loc = LOC_FIRST;
657 enum p_type type = T_NONE;
658 bool simple = false;
659
660 if (!fmt) { /* Section end only */
661 type = T_SECTION;
662 goto skip_fmt;
663 }
664
665 while (*fmt) {
666 unsigned char c = (unsigned char)*fmt;
667 if (c >= T_INFO && c <= T_SECTION) {
668 type = (enum p_type)c;
669 fmt++;
670 } else if (c == 0x11) {
671 loc = LOC_MIDDLE;
672 fmt++;
673 } else if (c == 0x12) {
674 loc = LOC_LAST;
675 fmt++;
676 } else {
677 break;
678 }
679 }
680
681 if (type == T_ERROR)
682 out = p.err;
683
684 if ((print_quiet && !p.map[type].required) ||
685 (type == T_VERBOSE && !print_verbose))
686 return PRINT_SKIPPED_BECAUSE_QUIET_OR_VERBOSE_NOT_ENABLED__SUCCESS;
687
688 if (!in_section && type != T_HEADER && type != T_SECTION)
689 psection();
690
691 if (last_percentage != -1 && content_printed) {
692 fputc('\n', out);
693 last_percentage = -1;
694 }
695
696 {
697 va_list v_args;
698 va_start(v_args, fmt);
699 p_bufsiz = vsnprintf(p_buf, sizeof(p_buf), fmt, v_args);
700 va_end(v_args);
701
702 if (p_bufsiz < 0) {
703 pdev("Failed to format print string");
704 return PRINT_INVALID_FORMAT_ARGS__ERROR;
705 }
706 }
707
708skip_fmt:
709 simple = print_nodetail || (print_quiet && p.map[type].required);
710 const size_t available = p.width - 3 - (!PICO(type) ? 0 : 2);
711 size_t p_buflen = (size_t)p_bufsiz;
712 if (p_buflen > available) { /* Overflow, no box/icon/color then */
713 pdev("Print string too long, doing a simple print");
714 simple = true;
715 }
716
717 flockfile(out);
718
719 if (type == T_HEADER) {
720 if (!fmt)
721 goto cleanup;
722
723 if (simple) {
724 ouputc('\n');
725 ouwrite(p_buf, p_buflen);
726 ouwrite("\n\n", 2);
727 in_section = false;
728 goto cleanup;
729 }
730
731 if (in_section) {
732 funlockfile(out);
733 psection_end();
734 flockfile(out);
735 }
736
737 ouwcol(type);
738 ouwbox(BOX_FANCY, BOX_TOP_LEFT);
739
740 size_t iwlrp;
741 for (iwlrp = 0; iwlrp < p.width - 2; iwlrp++)
742 ouwbox(BOX_FANCY, BOX_HORIZONTAL);
743
744 ouwbox(BOX_FANCY, BOX_TOP_RIGHT);
745 ouwcol(T_NONE);
746 ouputc('\n');
747
748 const size_t l_pad = (p.width - 2 - p_buflen) / 2;
749 const size_t r_pad = p.width - 2 - p_buflen - l_pad;
750
751 ouwcol(type);
752 ouwbox(BOX_FANCY, BOX_VERTICAL);
753 for (iwlrp = 0; iwlrp < l_pad; iwlrp++)
754 ouputc(' ');
755 ouwrite(p_buf, p_buflen);
756 for (iwlrp = 0; iwlrp < r_pad; iwlrp++)
757 ouputc(' ');
758 ouwbox(BOX_FANCY, BOX_VERTICAL);
759 ouwcol(T_NONE);
760 ouputc('\n');
761
762 ouwcol(type);
763 ouwbox(BOX_FANCY, BOX_BOTTOM_LEFT);
764 for (iwlrp = 0; iwlrp < p.width - 2; iwlrp++)
765 ouwbox(BOX_FANCY, BOX_HORIZONTAL);
766
767 ouwbox(BOX_FANCY, BOX_BOTTOM_RIGHT);
768 ouwcol(T_NONE);
769 ouputc('\n');
770 in_section = false;
771 } else if (type == T_SECTION) {
772 if (in_section && (!fmt || content_printed)) {
773 if (!simple) {
774 ouwcol(T_SECTION);
775 ouwbox(BOX_NORMAL, BOX_BOTTOM_LEFT);
776
777 size_t iw;
778 for (iw = 0; iw < p.width - 2; iw++)
779 ouwbox(BOX_NORMAL, BOX_HORIZONTAL);
780
781 ouwbox(BOX_NORMAL, BOX_BOTTOM_RIGHT);
782 ouwcol(T_NONE);
783 }
784
785 ouputc('\n');
786 in_section = false;
787 content_printed = false;
788 }
789
790 if (!fmt)
791 goto cleanup;
792
793 if (simple) {
794 ouwrite(p_buf, p_buflen);
795 ouputc('\n');
796
797 in_section = true;
798 content_printed = false;
799 goto cleanup;
800 }
801
802 size_t l_dashes = 2;
803 size_t r_dashes = p.width - 2 - l_dashes - p_buflen - 2;
804
805 ouwcol(T_SECTION);
806 ouwbox(BOX_NORMAL, BOX_TOP_LEFT);
807
808 if (!p_buf[0])
809 l_dashes += 2;
810
811 while (l_dashes--)
812 ouwbox(BOX_NORMAL, BOX_HORIZONTAL);
813
814 if (p_buf[0]) {
815 ouputc(' ');
816 ouwrite(p.codes[COLOR_UNDER], PCOLSIZ(COLOR_UNDER));
817 ouwrite(p_buf, p_buflen);
818 ouwcol(T_NONE);
819 ouwcol(type);
820 ouputc(' ');
821 }
822
823 while (r_dashes--)
824 ouwbox(BOX_NORMAL, BOX_HORIZONTAL);
825
826 ouwbox(BOX_NORMAL, BOX_TOP_RIGHT);
827 ouwcol(T_NONE);
828 ouputc('\n');
829
830 in_section = true;
831 content_printed = false;
832 } else {
833 if (simple) {
834 ouwrite(p_buf, p_buflen);
835 ouputc('\n');
836 content_printed = true;
837 goto cleanup;
838 }
839
840 ouwcol(T_SECTION);
841 ouwbox(BOX_NORMAL, BOX_VERTICAL);
842 ouwcol(type);
843 ouputc(' ');
844
845 if (PICO(type)) {
846 if (loc != LOC_FIRST)
847 ouwbox(BOX_NORMAL, loc);
848 else
849 ouwico(type);
850
851 ouputc(' ');
852 }
853
854 ouwrite(p_buf, p_buflen);
855 size_t padding = available - p_buflen;
856 while (padding--)
857 ouputc(' ');
858 ouwcol(T_SECTION);
859 ouwbox(BOX_NORMAL, BOX_VERTICAL);
860 ouwcol(T_NONE);
861 ouputc('\n');
862 content_printed = true;
863 }
864
865cleanup:
866 funlockfile(out);
867 return PRINT_SUCCESS;
868}
869
870enum p_return progress_bar(int percent, const char *P_RESTRICT fmt, ...)
871{
872 if (!is_init)
873 print_init();
874
875 FILE *out = p.out;
876
877 if (print_quiet)
878 return PRINT_SKIPPED_BECAUSE_QUIET_OR_VERBOSE_NOT_ENABLED__SUCCESS;
879
880 if (!in_section)
881 psection();
882
883#define CLAMP(val, min_val, max_val) (min(max((val), (min_val)), (max_val)))
884 int p_percent = CLAMP(percent, 0, 100);
885 if (p_percent == last_percentage ||
886 (p_percent == 100 && last_percentage == -1))
887 return PRINT_REPEAT_PROGRESS_PERCENT__SUCCESS;
888
889 last_percentage = p_percent;
890 if (last_percentage == 100)
891 last_percentage = -1;
892
893 char p_buf[BUFSIZ] = { 0 };
894 int p_bufsiz = 0;
895
896 if (fmt) {
897 va_list v_args;
898 va_start(v_args, fmt);
899 p_bufsiz = vsnprintf(p_buf, sizeof(p_buf), fmt, v_args);
900 va_end(v_args);
901
902 if (p_bufsiz < 0) {
903 pdev("Failed to format progress bar string");
904 return PRINT_INVALID_FORMAT_ARGS__ERROR;
905 }
906 }
907
908 bool simple = print_nodetail;
909 const size_t available = p.width - 3 - (!PICO(T_PROGRESS) ? 0 : 2);
910 size_t p_buflen = (size_t)p_bufsiz;
911 if (p_buflen > available) {
912 pdev("Progress bar string too long, doing a simple print");
913 simple = true;
914 }
915
916 flockfile(out);
917
918 const int digits = p_percent < 10 ? 1 : (p_percent < 100 ? 2 : 3);
919
920 if (simple) {
921 if (in_section)
922 ouwrite(p.ansi_carriage_return, 1);
923
924 ouwrite(p_buf, p_buflen);
925 ouputc(' ');
926 if (p_percent == 100) {
927 ouwrite("100%\n", 5);
928 } else {
929 if (p_percent >= 10)
930 ouputc('0' + p_percent / 10);
931 ouputc('0' + p_percent % 10);
932 ouputc('%');
933 }
934
935 fflush(out);
936 content_printed = true;
937 goto cleanup;
938 }
939
940 const size_t meta_width = (size_t)digits + 2 + 1 + 1 + 1;
941 const size_t bar_width = available - p_buflen - meta_width - 1;
942 const size_t filled_width = bar_width * (size_t)p_percent / 100;
943 const size_t empty_width = bar_width - filled_width;
944
945 if (in_section)
946 ouwrite(p.ansi_carriage_return, 1);
947
948 ouwcol(T_SECTION);
949 ouwbox(BOX_NORMAL, BOX_VERTICAL);
950 static bool repeat;
951 if (!repeat || p_percent == 100)
952 ouwcol(T_PROGRESS);
953 else
954 ouwrite(p.codes[COLOR_BRIGHT_BLUE], PCOLSIZ(COLOR_BRIGHT_BLUE));
955 repeat = !repeat;
956
957 ouputc(' ');
958 ouwico(T_PROGRESS);
959 ouputc(' ');
960 ouwrite(p_buf, p_buflen);
961 ouwrite(" [", 2);
962
963 size_t ifew;
964 for (ifew = 0; ifew < filled_width; ifew++)
965 ouwrite(p.progress_filled_char, 3);
966
967 for (ifew = 0; ifew < empty_width; ifew++)
968 ouwrite(p.progress_empty_char, 2);
969
970 ouwrite("] ", 2);
971 if (p_percent == 100) {
972 ouwrite("100% ", 5);
973 } else {
974 if (p_percent >= 10)
975 ouputc('0' + p_percent / 10);
976 ouputc('0' + p_percent % 10);
977 ouwrite("% ", 2);
978 }
979
980 ouwcol(T_SECTION);
981 ouwbox(BOX_NORMAL, BOX_VERTICAL);
982 ouwcol(T_NONE);
983 if (p_percent == 100)
984 ouputc('\n');
985
986 fflush(out);
987 content_printed = true;
988
989cleanup:
990 funlockfile(out);
991 return PRINT_SUCCESS;
992}
993
994enum p_return input(P_INPUT in, size_t size, const char *P_RESTRICT fmt, ...)
995{
996 if (!is_init)
997 print_init();
998
999 if (!fmt) {
1000 pdev("Input format string is NULL");
1001 return PRINT_INVALID_FORMAT_ARGS__ERROR;
1002 }
1003
1004 enum p_type type = T_NONE;
1005 while (*fmt) {
1006 unsigned char c = (unsigned char)*fmt;
1007 if (c >= 0x07 && c <= 0x08) {
1008 type = (enum p_type)c;
1009 fmt++;
1010 } else {
1011 break;
1012 }
1013 }
1014 if (type != T_CHOICE && type != T_PROMPT) {
1015 pdev("Invalid input type");
1016 return PRINT_INVALID_INPUT_TYPE__ERROR;
1017 }
1018
1019 FILE *out = p.out;
1020
1021 if (!in_section)
1022 psection();
1023
1024 if (last_percentage != -1 && content_printed) {
1025 fputc('\n', out);
1026 last_percentage = -1;
1027 }
1028
1029 char p_buf[BUFSIZ] = { 0 };
1030 int p_bufsiz = 0;
1031
1032 {
1033 va_list v_args;
1034 va_start(v_args, fmt);
1035 p_bufsiz = vsnprintf(p_buf, sizeof(p_buf), fmt, v_args);
1036 va_end(v_args);
1037
1038 if (p_bufsiz < 0) {
1039 pdev("Failed to format input string");
1040 return PRINT_INVALID_FORMAT_ARGS__ERROR;
1041 }
1042 }
1043
1044 bool simple = print_nodetail;
1045 const size_t available = p.width - 3 - (!PICO(type) ? 0 : 2);
1046 size_t p_buflen = (size_t)p_bufsiz;
1047 if (p_buflen > available) {
1048 pdev("Input string too long, doing a simple print");
1049 simple = true;
1050 }
1051
1052 flockfile(out);
1053
1054 if (type == T_CHOICE) {
1055 const char **choices = in.choices;
1056 size_t c_count = size;
1057
1058 if (c_count < 2) {
1059 funlockfile(out);
1060 pdev("Not enough choices (<2)");
1061 return PRINT_CHOICE_COLLECTION_SHOULD_CONTAIN_2_OR_MORE_CHOICES__ERROR;
1062 }
1063
1064 choices[c_count] = NULL;
1065
1066 size_t c;
1067 for (c = 0; c < c_count; c++) {
1068 if (simple) {
1069 oprintf("%zu: %s\n", c + 1, choices[c]);
1070 } else {
1071 const int label_chars = snprintf(
1072 NULL, 0, "%zu: %s", c + 1, choices[c]);
1073 const size_t label_len =
1074 (label_chars < 0) ? 0 :
1075 (size_t)label_chars;
1076 const size_t padding =
1077 label_len < available ?
1078 available - label_len + 2 :
1079 0;
1080
1081 ouwcol(T_SECTION);
1082 ouwbox(BOX_NORMAL, BOX_VERTICAL);
1083 ouwcol(type);
1084 oprintf(" %zu: %s%*s", c + 1, choices[c],
1085 (int)padding, "");
1086 ouwcol(T_SECTION);
1087 ouwbox(BOX_NORMAL, BOX_VERTICAL);
1088 ouwcol(T_NONE);
1089 ouputc('\n');
1090 }
1091 }
1092
1093 char i_buffer[PRINT_TERMINAL_WIDTH] = { 0 };
1094
1095 do {
1096 if (!simple) {
1097 ouwcol(T_SECTION);
1098 ouwbox(BOX_NORMAL, BOX_VERTICAL);
1099 ouwcol(type);
1100 ouputc(' ');
1101 ouwico(type);
1102 ouputc(' ');
1103 }
1104
1105 ouwrite(p_buf, p_buflen);
1106 ouwrite(" (1-", 4);
1107 oprintf("%zu", c_count);
1108 ouwrite("): ", 3);
1109
1110 funlockfile(out);
1111 terminal_read_input(i_buffer, sizeof(i_buffer));
1112 flockfile(out);
1113 errno = 0;
1114 char *endptr = NULL;
1115 unsigned long selected = strtoul(i_buffer, &endptr, 10);
1116 if (endptr == i_buffer || *endptr != '\0' ||
1117 errno == ERANGE || selected > INT_MAX)
1118 selected = 0;
1119
1120 if (!simple) {
1121 const int p_chars =
1122 snprintf(NULL, 0, "%s (1-%zu): %s",
1123 p_buf, c_count, i_buffer);
1124 const size_t p_len =
1125 (p_chars < 0) ? 0 : (size_t)p_chars;
1126 size_t p_padding = p_len < available ?
1127 available - p_len :
1128 0;
1129
1130 while (p_padding--)
1131 ouputc(' ');
1132 ouwcol(T_SECTION);
1133 ouwbox(BOX_NORMAL, BOX_VERTICAL);
1134 ouwcol(T_NONE);
1135 }
1136
1137 ouputc('\n');
1138
1139 if (selected >= 1 && selected <= c_count) {
1140 content_printed = true;
1141 funlockfile(out);
1142 return (int)selected - 1 +
1143 PRINT_FIRST_CHOICE_INDEX__SUCCESS;
1144 }
1145
1146 funlockfile(out);
1147 pwarn("Please enter a number between 1 and %zu",
1148 c_count);
1149 flockfile(out);
1150 } while (1);
1151 } else if (type == T_PROMPT) {
1152 char *result = in.output;
1153 const size_t rsz = size;
1154 if (rsz < 2) {
1155 funlockfile(out);
1156 pdev("Output buffer size is too small");
1157 return PRINT_PROMPT_BUFFER_SIZE_SHOULD_BE_2_OR_MORE__ERROR;
1158 }
1159
1160 if (!simple) {
1161 ouwcol(T_SECTION);
1162 ouwbox(BOX_NORMAL, BOX_VERTICAL);
1163 ouwcol(type);
1164 ouputc(' ');
1165 ouwico(type);
1166 ouputc(' ');
1167 }
1168
1169 ouwrite(p_buf, p_buflen);
1170 ouwrite(": ", 2);
1171
1172 funlockfile(out);
1173 terminal_read_input(result, rsz);
1174 flockfile(out);
1175
1176 if (!simple) {
1177 const size_t p_len = p_buflen + 2 + strlen(result);
1178 size_t p_padding =
1179 p_len < available ? available - p_len : 0;
1180
1181 while (p_padding--)
1182 ouputc(' ');
1183 ouwcol(T_SECTION);
1184 ouwbox(BOX_NORMAL, BOX_VERTICAL);
1185 ouwcol(T_NONE);
1186 }
1187
1188 ouputc('\n');
1189 content_printed = true;
1190 }
1191
1192 funlockfile(out);
1193 return PRINT_SUCCESS;
1194}
1195
1196#undef P_RESTRICT
1197
1198#endif /* PRINT_IMPLEMENTATION */
1199
1200/*
1201print.h
1202https://github.com/jakovdev/clix/
1203Copyright (c) 2026 Jakov Dragičević
1204Permission is hereby granted, free of charge, to any person obtaining a copy
1205of this software and associated documentation files (the "Software"), to deal
1206in the Software without restriction, including without limitation the rights
1207to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1208copies of the Software, and to permit persons to whom the Software is
1209furnished to do so, subject to the following conditions:
1210The above copyright notice and this permission notice shall be included in all
1211copies or substantial portions of the Software.
1212THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1213IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1214FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1215AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1216LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1217OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1218SOFTWARE.
1219*/
Definition print.h:98