diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ea57b2..1abc72f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25) project(bluelib C) -set(b_modules core object) +set(b_modules core object term) set(b_system_name ${CMAKE_SYSTEM_NAME}) string(TOLOWER ${b_system_name} b_system_name) diff --git a/term-test/printing.c b/term-test/printing.c new file mode 100644 index 0000000..86e3066 --- /dev/null +++ b/term-test/printing.c @@ -0,0 +1,65 @@ +#include + +#define F_GREEN "\033[92m" +#define F_YELLOW "\033[93m" +#define F_RESET "\033[0m" + +static const char *text = F_YELLOW + "But I must " F_GREEN "explain " F_YELLOW "to you " F_GREEN + "how all this " F_YELLOW "mistaken idea of" F_RESET + " denouncing pleasure " F_GREEN "and praising pain was born " F_YELLOW + "and I will give you " F_RESET "a complete account of " F_YELLOW + "the system, and " F_RESET + "expound the actual teachings of the great explorer of the truth, the " + "master-builder of human happiness.\n" + "No one rejects, dislikes, or avoids pleasure itself, because it is " + "pleasure, but because those who do not know how to pursue pleasure " + "rationally encounter consequences that are extremely painful. Nor " + "again is there anyone who loves or pursues or desires to obtain pain " + "of itself, because it is pain, but because occasionally circumstances " + "occur in which toil and pain can procure him some great pleasure.\n" + "To take a trivial example, which of us ever undertakes laborious " + "physical exercise, except to obtain some advantage from it? But who " + "has any right to find fault with a man who chooses to enjoy a " + "pleasure that has no annoying consequences, or one who avoids a pain " + "that produces no resultant pleasure? On the other hand, we denounce " + "with righteous indignation and dislike men who are so beguiled and " + "demoralized by the charms of pleasure of the moment, so blinded by " + "desire, that they cannot foresee."; + +static const char *text2 + = "But I must explain to you how all this mistaken idea of denouncing " + "pleasure and praising pain was born and I will give you a complete " + "account of the system, and expound the actual teachings of the " + "great explorer of the truth, the master-builder of human " + "happiness.\n" + "No one rejects, dislikes, or avoids pleasure itself, because it is " + "pleasure, but because those who do not know how to pursue pleasure " + "rationally encounter consequences that are extremely painful. Nor " + "again is there anyone who loves or pursues or desires to obtain " + "pain of itself, because it is pain, but because occasionally " + "circumstances occur in which toil and pain can procure him some " + "great pleasure.\n" + "To take a trivial example, which of us ever undertakes laborious " + "physical exercise, except to obtain some advantage from it? But who " + "has any right to find fault with a man who chooses to enjoy a " + "pleasure that has no annoying consequences, or one who avoids a " + "pain that produces no resultant pleasure? On the other hand, we " + "denounce with righteous indignation and dislike men who are so " + "beguiled and demoralized by the charms of pleasure of the moment, " + "so blinded by desire, that they cannot foresee."; + +int main(void) +{ + b_paragraph_format format = {}; + format.p_left_margin = 5; + format.p_right_margin = 5; + format.p_flags = B_PARAGRAPH_DOUBLE_LINE_BREAK; + + b_print_paragraph(text, stdout, &format); + + b_i("An informational message\n\nWith multiple lines"); + b_warn("A warning message\nWith multiple lines"); + b_err("An error message\nWith multiple lines"); + return 0; +} diff --git a/term/CMakeLists.txt b/term/CMakeLists.txt new file mode 100644 index 0000000..fb097cd --- /dev/null +++ b/term/CMakeLists.txt @@ -0,0 +1,3 @@ +include(../cmake/Templates.cmake) + +add_bluelib_module(NAME term DEPENDENCIES core object) diff --git a/term/include/blue/term.h b/term/include/blue/term.h new file mode 100644 index 0000000..d8b2198 --- /dev/null +++ b/term/include/blue/term.h @@ -0,0 +1,41 @@ +#ifndef BLUELIB_PRINT_H_ +#define BLUELIB_PRINT_H_ + +#include +#include + +#define b_i(...) b_print(B_PRINT_I, __VA_ARGS__) +#define b_warn(...) b_print(B_PRINT_WARN, __VA_ARGS__) +#define b_err(...) b_print(B_PRINT_ERR, __VA_ARGS__) + +typedef enum b_paragraph_format_flags { + B_PARAGRAPH_DONT_INDENT_FIRST_LINE = 0x01u, + B_PARAGRAPH_HYPHENATE = 0x02u, + B_PARAGRAPH_DOUBLE_LINE_BREAK = 0x04u, + B_PARAGRAPH_MULTI_LINE_BREAK = 0x08u, +} b_paragraph_format_flags; + +typedef struct b_paragraph_format { + b_paragraph_format_flags p_flags; + + unsigned int p_left_margin; + unsigned int p_right_margin; + unsigned int p_line_length; +} b_paragraph_format; + +typedef enum b_print_format { + B_PRINT_NORMAL = 0, + B_PRINT_I, + B_PRINT_WARN, + B_PRINT_ERR, +} b_print_format; + +extern b_status b_print(b_print_format format, const char *str, ...); +extern b_status b_print_paragraph( + const char *str, FILE *fp, b_paragraph_format *format); + +extern int b_fputs(const char *str, FILE *fp); +extern int b_printf(const char *format, ...); +extern int b_fprintf(FILE *fp, const char *format, ...); + +#endif diff --git a/term/paragraph.c b/term/paragraph.c new file mode 100644 index 0000000..c1a7de2 --- /dev/null +++ b/term/paragraph.c @@ -0,0 +1,163 @@ +#include "print.h" + +#include +#include +#include +#include +#include +#include + +#define DEFAULT_PARAGRAPH_WIDTH 160 + +static void indent( + struct b_paragraph_format *format, FILE *fp, unsigned int margin, bool tty) +{ + unsigned int x = 0; + while (x < margin) { + fputs(" ", fp); + x++; + } +} + +static unsigned int extract_line( + const char **sp, unsigned int line_length, b_string *out, + struct b_paragraph_format *format) +{ + const char *start = *sp; + while (isspace(*start) || *start == '\n') { + start++; + } + + if (!*start) { + return 0; + } + + const char *end = NULL; + + unsigned int delta = 0; + unsigned int i; + for (i = 0; start[i]; i++) { + if (start[i] == '\033') { + while (!isalpha(start[i++])) { + delta++; + } + + delta++; + } + + if (!isspace(start[i]) && start[i] != '\n') { + continue; + } + + if ((i - delta) >= line_length) { + break; + } + + end = &start[i]; + + if (start[i] == '\n') { + break; + } + } + + if (start[i] == '\0') { + end = NULL; + } + + unsigned int len; + if (end) { + len = end - start; + } else { + len = strlen(start); + } + + b_string_insert_cstrn(out, start, len, B_NPOS); + + *sp = end; + + return len; +} + +static b_status print_paragraph_tty( + const char *str, FILE *fp, struct b_paragraph_format *format) +{ + unsigned int w = 0, h = 0; + z__b_stream_dimensions(fp, &w, &h); + + if (!w) { + w = DEFAULT_PARAGRAPH_WIDTH; + } + + unsigned int left_margin = format->p_left_margin; + unsigned int line_length = format->p_line_length; + + if (format->p_left_margin + format->p_right_margin >= w) { + return B_SUCCESS; + } + + unsigned int page_width + = w - format->p_left_margin - format->p_right_margin; + if (page_width < line_length || line_length == 0) { + line_length = page_width; + } + + if (!(format->p_flags & B_PARAGRAPH_DONT_INDENT_FIRST_LINE)) { + indent(format, fp, left_margin, true); + } + + b_string *line = b_string_create(); + bool need_indent = false; + + while (str) { + if (*str == '\n') { + if (format->p_flags & B_PARAGRAPH_DOUBLE_LINE_BREAK) { + fputc('\n', fp); + } + + str++; + need_indent = true; + + while (*str == '\n') { + if (format->p_flags & B_PARAGRAPH_MULTI_LINE_BREAK) { + fputc('\n', fp); + } + + str++; + } + } + + b_string_clear(line); + unsigned int this_line + = extract_line(&str, line_length, line, format); + if (this_line == 0) { + break; + } + + if (need_indent) { + indent(format, fp, left_margin, false); + } + + b_fprintf(fp, "%s\n", b_string_ptr(line)); + need_indent = true; + } + + b_string_release(line); + + return B_SUCCESS; +} + +static b_status print_paragraph_file( + const char *str, FILE *fp, struct b_paragraph_format *format) +{ + return B_SUCCESS; +} + +b_status b_print_paragraph( + const char *str, FILE *fp, struct b_paragraph_format *format) +{ + if (z__b_stream_is_tty(fp)) { + return print_paragraph_tty(str, fp, format); + } + + return print_paragraph_tty(str, fp, format); +} diff --git a/term/print.c b/term/print.c new file mode 100644 index 0000000..40d016a --- /dev/null +++ b/term/print.c @@ -0,0 +1,171 @@ +#include "print.h" + +#include +#include +#include +#include + +#define F_BLACK "\033[30m" +#define F_RED "\033[91m" +#define F_YEL "\033[93m" +#define F_CYN "\033[96m" + +#define F_BLACK_BOLD "\033[1;30m" +#define F_RED_BOLD "\033[1;91m" +#define F_YEL_BOLD "\033[1;93m" +#define F_CYN_BOLD "\033[1;96m" + +#define F_BLACK_BG "\033[40m" +#define F_RED_BG "\033[101m" +#define F_YEL_BG "\033[103m" +#define F_CYN_BG "\033[106m" + +#define F_RESET "\033[0m" + +typedef b_status (*print_function)(FILE *fp, const char *s); + +static int print(FILE *fp, const char *str) +{ + return fputs(str, fp); +} + +static int print_no_ansi(FILE *fp, const char *str) +{ + int out = 0; + + for (size_t i = 0; str[i]; i++) { + if (str[i] != '\033') { + fputc(str[i], fp); + out++; + continue; + } + + while (str[i] && !isalpha(str[i])) { + i++; + } + } + + return out; +} + +static b_status print_normal(FILE *fp, const char *s) +{ + return B_SUCCESS; +} + +static b_status print_i(FILE *fp, const char *s) +{ + b_paragraph_format format = {}; + format.p_left_margin = 5; + format.p_right_margin = 5; + format.p_flags = B_PARAGRAPH_DONT_INDENT_FIRST_LINE + | B_PARAGRAPH_MULTI_LINE_BREAK; + + // printf(F_CYN_BG F_BLACK " i " F_RESET " "); + b_fprintf(fp, F_CYN_BOLD " i:" F_RESET " "); + b_print_paragraph(s, fp, &format); + + return B_SUCCESS; +} + +static b_status print_warn(FILE *fp, const char *s) +{ + b_paragraph_format format = {}; + format.p_left_margin = 5; + format.p_right_margin = 5; + format.p_flags = B_PARAGRAPH_DONT_INDENT_FIRST_LINE + | B_PARAGRAPH_MULTI_LINE_BREAK; + + // printf(F_YEL_BG F_BLACK " Warn " F_RESET " "); + b_fprintf(fp, F_YEL_BOLD " !!" F_RESET " "); + b_print_paragraph(s, fp, &format); + + return B_SUCCESS; +} + +static b_status print_err(FILE *fp, const char *s) +{ + b_paragraph_format format = {}; + format.p_left_margin = 5; + format.p_right_margin = 5; + format.p_flags = B_PARAGRAPH_DONT_INDENT_FIRST_LINE + | B_PARAGRAPH_MULTI_LINE_BREAK; + + // printf(F_RED_BG F_BLACK " Err " F_RESET " "); + b_fprintf(fp, F_RED_BOLD "Err:" F_RESET " "); + b_print_paragraph(s, fp, &format); + + return B_SUCCESS; +} + +static print_function format_printers[] = { + [B_PRINT_NORMAL] = print_normal, + [B_PRINT_I] = print_i, + [B_PRINT_WARN] = print_warn, + [B_PRINT_ERR] = print_err, +}; +static size_t nr_format_printers + = sizeof format_printers / sizeof format_printers[B_PRINT_NORMAL]; + +b_status b_print(b_print_format format, const char *str, ...) +{ + char buf[1024]; + + va_list arg; + va_start(arg, str); + vsnprintf(buf, sizeof buf, str, arg); + va_end(arg); + + if (format >= nr_format_printers) { + return B_ERR_INVALID_ARGUMENT; + } + + print_function printer = format_printers[format]; + if (!printer) { + return B_ERR_INVALID_ARGUMENT; + } + + return printer(stdout, buf); +} + +static int b_vfprintf(FILE *fp, const char *format, va_list arg) +{ + char str[1024]; + + vsnprintf(str, sizeof str, format, arg); + + if (z__b_stream_is_tty(fp)) { + return print(fp, str); + } + + return print_no_ansi(fp, str); +} + +int b_fputs(const char *str, FILE *fp) +{ + if (z__b_stream_is_tty(fp)) { + return print(fp, str); + } + + return print_no_ansi(fp, str); +} + +int b_printf(const char *format, ...) +{ + va_list arg; + va_start(arg, format); + int len = b_vfprintf(stdout, format, arg); + va_end(arg); + + return len; +} + +int b_fprintf(FILE *fp, const char *format, ...) +{ + va_list arg; + va_start(arg, format); + int len = b_vfprintf(fp, format, arg); + va_end(arg); + + return len; +} diff --git a/term/print.h b/term/print.h new file mode 100644 index 0000000..cdd5058 --- /dev/null +++ b/term/print.h @@ -0,0 +1,12 @@ +#ifndef _BLUELIB_PRINT_H_ +#define _BLUELIB_PRINT_H_ + +#include +#include + +extern int z__b_stream_is_tty(FILE *fp); +extern int z__b_stream_dimensions(FILE *fp, unsigned int *w, unsigned int *h); +extern int z__b_stream_cursorpos( + FILE *in, FILE *out, unsigned int *x, unsigned int *y); + +#endif diff --git a/term/sys/darwin/print.c b/term/sys/darwin/print.c new file mode 100644 index 0000000..e3484a8 --- /dev/null +++ b/term/sys/darwin/print.c @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + +int z__b_stream_is_tty(FILE *fp) +{ + return isatty(fileno(fp)); +} + +int z__b_stream_dimensions(FILE *fp, unsigned int *w, unsigned int *h) +{ + if (!isatty(fileno(fp))) { + return -1; + } + + struct winsize ws; + if (ioctl(fileno(fp), TIOCGWINSZ, &ws) == -1) { + return -1; + } + + if (w) { + *w = ws.ws_col; + } + + if (h) { + *h = ws.ws_row; + } + + return 0; +} + +int z__b_stream_cursorpos(FILE *in, FILE *out, unsigned int *x, unsigned int *y) +{ + if (!isatty(fileno(in)) || !isatty(fileno(out))) { + return -1; + } + + struct termios term, restore; + + tcgetattr(fileno(in), &term); + tcgetattr(fileno(in), &restore); + term.c_lflag &= ~(ICANON | ECHO); + tcsetattr(fileno(in), TCSANOW, &term); + + const char *cmd = "\033[6n"; + write(fileno(out), cmd, strlen(cmd)); + + char buf[64]; + read(fileno(in), buf, sizeof buf); + + tcsetattr(fileno(in), TCSANOW, &restore); + + unsigned int row, col; + int r = sscanf(buf, "\033[%u;%uR", &row, &col); + if (r != 2) { + return -1; + } + + *x = col - 1; + *y = row - 1; + + return 0; +} diff --git a/term/sys/linux/print.c b/term/sys/linux/print.c new file mode 100644 index 0000000..ec82af3 --- /dev/null +++ b/term/sys/linux/print.c @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + +int z__c_stream_is_tty(FILE *fp) +{ + return isatty(fileno(fp)); +} + +int z__c_stream_dimensions(FILE *fp, unsigned int *w, unsigned int *h) +{ + if (!isatty(fileno(fp))) { + return -1; + } + + struct winsize ws; + if (ioctl(fileno(fp), TIOCGWINSZ, &ws) == -1) { + return -1; + } + + if (w) { + *w = ws.ws_col; + } + + if (h) { + *h = ws.ws_row; + } + + return 0; +} + +int z__c_stream_cursorpos(FILE *in, FILE *out, unsigned int *x, unsigned int *y) +{ + if (!isatty(fileno(in)) || !isatty(fileno(out))) { + return -1; + } + + struct termios term, restore; + + tcgetattr(fileno(in), &term); + tcgetattr(fileno(in), &restore); + term.c_lflag &= ~(ICANON|ECHO); + tcsetattr(fileno(in), TCSANOW, &term); + + const char *cmd = "\033[6n"; + write(fileno(out), cmd, strlen(cmd)); + + char buf[64]; + read(fileno(in), buf, sizeof buf); + + tcsetattr(fileno(in), TCSANOW, &restore); + + unsigned int row, col; + int r = sscanf(buf, "\033[%u;%uR", &row, &col); + if (r != 2) { + return -1; + } + + *x = col - 1; + *y = row - 1; + + return 0; +}