QUOTE: Let your heart guide you always.

libterm

An easy-to-use terminal library

commit 5d9e0d1bc57d24826ae87b45abbca59e4f760c72
parent 2b48f436ef0032ef2d395208be5a64fa97bbd5b8
Author: Sophie <info@soophie.de>
Date:   Tue, 13 May 2025 13:03:00 +0000

feat: Abstracted teleterm to backend API (POSIX, WINDOWS)

Diffstat:
Msrc/libterm.h | 574++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
1 file changed, 412 insertions(+), 162 deletions(-)

diff --git a/src/libterm.h b/src/libterm.h @@ -3,7 +3,6 @@ #include <stddef.h> #include <stdbool.h> -#include <termios.h> typedef enum { TERM_CODE_RAWMODE_ENABLE, @@ -29,25 +28,23 @@ typedef enum { KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, -} term_keycode_t; +} term_key_t; + +typedef struct term term_t; -typedef void (*term_dispatch_fn_t)(int fd, term_code_t code, void *data, size_t len); -typedef void *(*term_fetch_fn_t)(int fd, term_code_t code, void *data, size_t len, size_t ret_len); +typedef void (*term_dispatch_fn_t)(term_t *self, term_code_t code, void *data, size_t len); +typedef void *(*term_fetch_fn_t)(term_t *self, term_code_t code, void *data, size_t len, size_t ret_len); -typedef struct { - struct termios termios; +struct term { char *buffer; size_t len; - bool teleterm_mode; term_dispatch_fn_t dispatch; term_fetch_fn_t fetch; int fd; -} term_t; +}; term_t term_init(void); void term_cleanup(term_t *self); -void term_fd_set(term_t *self, int fd); -void term_teleterm_enable(term_t *self, term_dispatch_fn_t dispatch, term_fetch_fn_t fetch); void term_rawmode_enable(term_t *self); void term_rawmode_disable(term_t *self); void term_write(term_t *self, char *str); @@ -64,10 +61,8 @@ int term_poll_key(term_t *self, int timeout_ms); #include <stdlib.h> #include <string.h> #include <unistd.h> -#include <sys/ioctl.h> #include <errno.h> #include <stdarg.h> -#include <poll.h> void _term_panic(term_t *self, const char *str) { write(self->fd, "\x1b[2J", 4); @@ -77,46 +72,19 @@ void _term_panic(term_t *self, const char *str) { exit(EXIT_FAILURE); } -term_t term_init(void) { - return (term_t) { - .buffer = NULL, - .len = 0, - .teleterm_mode = false, - .dispatch = NULL, - .fetch = NULL, - .fd = STDOUT_FILENO, - }; -} +#ifdef LIB_TERM_POSIX -void term_cleanup(term_t *self) { - if (self) { - if (self->buffer) { - free(self->buffer); - self->buffer = NULL; - self->len = 0; - } - } -} +#include <termios.h> +#include <sys/ioctl.h> +#include <poll.h> -void term_fd_set(term_t *self, int fd) { - self->fd = fd; -} +static struct termios _posix_termios; -void term_teleterm_enable(term_t *self, term_dispatch_fn_t dispatch, term_fetch_fn_t fetch) { - self->teleterm_mode = true; - self->dispatch = dispatch; - self->fetch = fetch; -} - -void term_rawmode_enable(term_t *self) { - if (self->teleterm_mode) { - self->dispatch(self->fd, TERM_CODE_RAWMODE_ENABLE, NULL, 0); - return; - } - if (tcgetattr(STDIN_FILENO, &self->termios) == -1) { +static void posix_rawmode_enable(term_t *self) { + if (tcgetattr(STDIN_FILENO, &_posix_termios) == -1) { _term_panic(self, "tcgetattr"); } - struct termios raw = self->termios; + struct termios raw = _posix_termios; raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); raw.c_oflag &= ~(OPOST); raw.c_cflag |= (CS8); @@ -128,71 +96,36 @@ void term_rawmode_enable(term_t *self) { } } -void term_rawmode_disable(term_t *self) { - if (self->teleterm_mode) { - self->dispatch(self->fd, TERM_CODE_RAWMODE_DISABLE, NULL, 0); - return; - } - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &self->termios) == -1) { +static void posix_rawmode_disable(term_t *self) { + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &_posix_termios) == -1) { _term_panic(self, "tcsetattr"); } } -void term_write(term_t *self, char *str) { - if (self->teleterm_mode) { - self->dispatch(self->fd, TERM_CODE_WRITE, NULL, 0); - } - size_t str_len = strlen(str); - char *new_buffer = (char *) realloc(self->buffer, sizeof(char) * (self->len + str_len + 1)); - if (new_buffer == NULL) { - fprintf(stderr, "Cannot realloc memory!\n"); - return; - } - self->buffer = new_buffer; - memcpy(self->buffer + self->len, str, str_len); - self->len += str_len; - self->buffer[self->len] = '\0'; -} - -void term_writef(term_t *self, const char *format, ...) { - va_list args; - va_start(args, format); - char str[1000]; - vsnprintf(str, sizeof(str), format, args); - term_write(self, str); - va_end(args); +static void posix_write(term_t *self, void *data, size_t len) { + (void) self; + (void) data; + (void) len; } -void term_flush(term_t *self) { +static void posix_flush(term_t *self, void *data, size_t len) { + (void) data; + (void) len; if (self->len > 0) { - if (self->teleterm_mode) { - self->dispatch(self->fd, TERM_CODE_FLUSH, self->buffer, self->len); - } - else { - write(self->fd, self->buffer, self->len); - } + write(self->fd, self->buffer, self->len); free(self->buffer); self->buffer = NULL; self->len = 0; } } -int term_read_cursor(term_t *self, size_t *rows, size_t *cols) { - if (self->teleterm_mode) { - void *data = self->fetch(self->fd, TERM_CODE_CURSOR_READ, NULL, 0, 2 * sizeof(size_t)); - if (data) { - size_t *tuple = (size_t *) data; - *rows = tuple[0]; - *cols = tuple[1]; - free(data); - return 0; - } - return -1; - } +static void *posix_read_cursor(term_t *self, void *data, size_t len) { + (void) data; + (void) len; char buf[32]; unsigned int i = 0; if (write(self->fd, "\x1b[6n", 4) != 4) { - return -1; + return NULL; } while (i < sizeof(buf) - 1) { if (read(STDIN_FILENO, &buf[i], 1) != 1) { @@ -205,53 +138,36 @@ int term_read_cursor(term_t *self, size_t *rows, size_t *cols) { } buf[i] = '\0'; if (buf[0] != '\x1b' || buf[1] != '[') { - return -1; + return NULL; } - if (sscanf(&buf[2], "%zu;%zu", rows, cols) != 2) { - return -1; + size_t *tuple = malloc(2 * sizeof(size_t)); + if (sscanf(&buf[2], "%zu;%zu", &tuple[0], &tuple[1]) != 2) { + free(tuple); + return NULL; } - return 0; + return tuple; } -int term_read_window(term_t *self, size_t *rows, size_t *cols) { - if (self->teleterm_mode) { - void *data = self->fetch(self->fd, TERM_CODE_WINDOW_READ, NULL, 0, 2 * sizeof(size_t)); - if (data) { - size_t *tuple = (size_t *) data; - *rows = tuple[0]; - *cols = tuple[1]; - free(data); - return 0; - } - return -1; - } +static void *posix_read_window(term_t *self, void *data, size_t len) { struct winsize ws; if (ioctl(self->fd, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { - if (write(self->fd, "\x1b[999C\x1b[999B", 12) != 12) { - return -1; + if (write(self->fd, "\x1b[999C\x1b[999B", 12) == 12) { + return posix_read_cursor(self, data, len); } - return term_read_cursor(self, rows, cols); } else { - *cols = ws.ws_col; - *rows = ws.ws_row; - return 0; + size_t *tuple = malloc(2 * sizeof(size_t)); + tuple[0] = ws.ws_row; + tuple[1] = ws.ws_col; + return tuple; } + return NULL; } -int term_read_key(term_t *self) { - if (self->teleterm_mode) { - void *data = self->fetch(self->fd, TERM_CODE_KEY_READ, NULL, 0, sizeof(int)); - if (data) { - int *single = (int *) data; - int key = *single; - free(data); - return key; - } - return -1; - } +static void *posix_read_key(term_t *self) { int len; char c; + int key; while ((len = read(STDIN_FILENO, &c, 1)) != 1) { if (len == -1 && errno != EAGAIN) { _term_panic(self, "read"); @@ -260,83 +176,417 @@ int term_read_key(term_t *self) { if (c == '\x1b') { char seq[3]; if (read(STDIN_FILENO, &seq[0], 1) != 1) { - return '\x1b'; + key = '\x1b'; } if (read(STDIN_FILENO, &seq[1], 1) != 1) { - return '\x1b'; + key = '\x1b'; } if (seq[0] == '[') { if (seq[1] >= '0' && seq[1] <= '9') { if (read(STDIN_FILENO, &seq[2], 1) != 1) { - return '\x1b'; + key = '\x1b'; } if (seq[2] == '~') { switch (seq[1]) { case '1': - return KEY_HOME; + key = KEY_HOME; + break; case '3': - return KEY_DEL; + key = KEY_DEL; + break; case '4': - return KEY_END; + key = KEY_END; + break; case '5': - return KEY_PAGE_UP; + key = KEY_PAGE_UP; + break; case '6': - return KEY_PAGE_DOWN; + key = KEY_PAGE_DOWN; + break; case '7': - return KEY_HOME; + key = KEY_HOME; + break; case '8': - return KEY_END; + key = KEY_END; + break; + default: + break; } } } else { switch (seq[1]) { case 'A': - return KEY_ARROW_UP; + key = KEY_ARROW_UP; + break; case 'B': - return KEY_ARROW_DOWN; + key = KEY_ARROW_DOWN; + break; case 'C': - return KEY_ARROW_RIGHT; + key = KEY_ARROW_RIGHT; + break; case 'D': - return KEY_ARROW_LEFT; + key = KEY_ARROW_LEFT; + break; case 'H': - return KEY_HOME; + key = KEY_HOME; + break; case 'F': - return KEY_END; + key = KEY_END; + break; + default: + key = '\x1b'; + break; } - return '\x1b'; } } else if (seq[0] == 'O') { switch (seq[1]) { case 'H': - return KEY_HOME; + key = KEY_HOME; + break; case 'F': - return KEY_END; + key = KEY_END; + break; + default: + break; } } } - return c; + else { + key = c; + } + int *ret = malloc(sizeof(int)); + *ret = key; + return ret; } -int term_poll_key(term_t *self, int timeout_ms) { - if (self->teleterm_mode) { - void *data = self->fetch(self->fd, TERM_CODE_KEY_POLL, &timeout_ms, sizeof(timeout_ms), sizeof(int)); - if (data) { - int *single = (int *) data; - int key = *single; - free(data); - return key; - } - return -1; - } +static void *posix_poll_key(term_t *self, void *data, size_t len) { + (void) len; struct pollfd fds[1]; fds[0].fd = STDIN_FILENO; fds[0].events = POLLIN | POLLPRI; + int timeout_ms = *(int *) data; if (poll(fds, 1, timeout_ms)) { - return term_read_key(self); + return posix_read_key(self); + } + int *ret = malloc(sizeof(int)); + *ret = 0; + return ret; +} + +static void posix_dispatch(term_t *self, term_code_t code, void *data, size_t len) { + switch (code) { + case TERM_CODE_RAWMODE_ENABLE: + posix_rawmode_enable(self); + break; + case TERM_CODE_RAWMODE_DISABLE: + posix_rawmode_disable(self); + break; + case TERM_CODE_WRITE: + posix_write(self, data, len); + break; + case TERM_CODE_FLUSH: + posix_flush(self, data, len); + break; + default: + break; + } +} + +static void *posix_fetch(term_t *self, term_code_t code, void *data, size_t len, size_t ret_len) { + (void) ret_len; + switch (code) { + case TERM_CODE_CURSOR_READ: + return posix_read_cursor(self, data, len); + case TERM_CODE_WINDOW_READ: + return posix_read_window(self, data, len); + case TERM_CODE_KEY_READ: + return posix_read_key(self); + case TERM_CODE_KEY_POLL: + return posix_poll_key(self, data, len); + default: + return NULL; + } +} + +#endif // LIB_TERM_POSIX + +#ifdef LIB_TERM_WINDOWS + +#include <windows.h> +#include <conio.h> + +static DWORD _windows_console_mode; + +static void windows_rawmode_enable(term_t *self) { + HANDLE handle = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode; + if (!GetConsoleMode(handle, &mode)) { + _term_panic(self, "GetConsoleMode"); + } + _windows_console_mode = mode; + mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); + if (!SetConsoleMode(handle, mode)) { + _term_panic(self, "SetConsoleMode"); + } +} + +static void windows_rawmode_disable(term_t *self) { + HANDLE handle = GetStdHandle(STD_INPUT_HANDLE); + if (!SetConsoleMode(handle, _windows_console_mode)) { + _term_panic(self, "SetConsoleMode"); + } +} + +static void windows_write(term_t *self, void *data, size_t len) { + DWORD written; + WriteConsole(GetStdHandle(self->fd), data, (DWORD) len, &written, NULL); +} + +static void windows_flush(term_t *self, void *data, size_t len) { + (void) data; + (void) len; + if (self->len > 0) { + DWORD written; + WriteConsole(GetStdHandle(self->fd), self->buffer, (DWORD) self->len, &written, NULL); + free(self->buffer); + self->buffer = NULL; + self->len = 0; + } +} + +static void *windows_read_cursor(term_t *self, void *data, size_t len) { + (void) data; + (void) len; + CONSOLE_SCREEN_BUFFER_INFO console_info; + if (!GetConsoleScreenBufferInfo(GetStdHandle(self->fd), &console_info)) { + return NULL; + } + size_t *tuple = malloc(2 * sizeof(size_t)); + tuple[0] = console_info.dwCursorPosition.Y + 1; + tuple[1] = console_info.dwCursorPosition.X + 1; + return tuple; +} + +static void *windows_read_window(term_t *self, void *data, size_t len) { + (void) data; + (void) len; + CONSOLE_SCREEN_BUFFER_INFO console_info; + if (!GetConsoleScreenBufferInfo(GetStdHandle(self->fd), &console_info)) { + return NULL; + } + size_t *tuple = malloc(2 * sizeof(size_t)); + tuple[0] = console_info.srWindow.Bottom - console_info.srWindow.Top + 1; + tuple[1] = console_info.srWindow.Right - console_info.srWindow.Left + 1; + return tuple; +} + +static void *windows_read_key(term_t *self) { + (void) self; + int c = _getch(); + if (c == 0 || c == 224) { + int ext = _getch(); + switch (ext) { + case 72: + c = KEY_ARROW_UP; + break; + case 80: + c = KEY_ARROW_DOWN; + break; + case 75: + c = KEY_ARROW_LEFT; + break; + case 77: + c = KEY_ARROW_RIGHT; + break; + case 71: + c = KEY_HOME; + break; + case 79: + c = KEY_END; + break; + case 73: + c = KEY_PAGE_UP; + break; + case 81: + c = KEY_PAGE_DOWN; + break; + case 83: + c = KEY_DEL; + break; + default: + c = 0; + break; + } + } + int *ret = malloc(sizeof(int)); + *ret = c; + return ret; +} + +static void *windows_poll_key(term_t *self, void *data, size_t len) { + (void) len; + int timeout_ms = *(int *) data; + DWORD start = GetTickCount(); + while (GetTickCount() - start < (DWORD) timeout_ms) { + if (_kbhit()) { + return windows_read_key(self); + } + Sleep(1); + } + int *ret = malloc(sizeof(int)); + *ret = 0; + return ret; +} + +static void windows_dispatch(term_t *self, term_code_t code, void *data, size_t len) { + switch (code) { + case TERM_CODE_RAWMODE_ENABLE: + windows_rawmode_enable(self); + break; + case TERM_CODE_RAWMODE_DISABLE: + windows_rawmode_disable(self); + break; + case TERM_CODE_WRITE: + windows_write(self, data, len); + break; + case TERM_CODE_FLUSH: + windows_flush(self, data, len); + break; + default: + break; + } +} + +static void *windows_fetch(term_t *self, term_code_t code, void *data, size_t len, size_t ret_len) { + (void) ret_len; + switch (code) { + case TERM_CODE_CURSOR_READ: + return windows_read_cursor(self, data, len); + case TERM_CODE_WINDOW_READ: + return windows_read_window(self, data, len); + case TERM_CODE_KEY_READ: + return windows_read_key(self); + case TERM_CODE_KEY_POLL: + return windows_poll_key(self, data, len); + default: + return NULL; + } +} + +#endif // LIB_TERM_WINDOWS + +term_t term_init(void) { + term_t self = { + .buffer = NULL, + .len = 0, + .dispatch = NULL, + .fetch = NULL, + .fd = 0, + }; + #ifdef LIB_TERM_POSIX + self.dispatch = posix_dispatch; + self.fetch = posix_fetch; + self.fd = STDOUT_FILENO; + #endif /* LIB_TERM_POSIX */ + #ifdef LIB_TERM_WINDOWS + self.dispatch = windows_dispatch; + self.fetch = windows_fetch; + self.fd = STD_OUTPUT_HANDLE; + #endif /* LIB_TERM_WINDOWS */ + return self; +} + +void term_cleanup(term_t *self) { + if (self) { + if (self->buffer) { + free(self->buffer); + self->buffer = NULL; + self->len = 0; + } + } +} + +void term_rawmode_enable(term_t *self) { + self->dispatch(self, TERM_CODE_RAWMODE_ENABLE, NULL, 0); +} + +void term_rawmode_disable(term_t *self) { + self->dispatch(self, TERM_CODE_RAWMODE_DISABLE, NULL, 0); +} + +void term_write(term_t *self, char *str) { + self->dispatch(self, TERM_CODE_WRITE, NULL, 0); + size_t str_len = strlen(str); + char *new_buffer = (char *) realloc(self->buffer, sizeof(char) * (self->len + str_len + 1)); + if (new_buffer == NULL) { + fprintf(stderr, "Cannot realloc memory!\n"); + return; + } + self->buffer = new_buffer; + memcpy(self->buffer + self->len, str, str_len); + self->len += str_len; + self->buffer[self->len] = '\0'; +} + +void term_writef(term_t *self, const char *format, ...) { + va_list args; + va_start(args, format); + char str[1000]; + vsnprintf(str, sizeof(str), format, args); + term_write(self, str); + va_end(args); +} + +void term_flush(term_t *self) { + self->dispatch(self, TERM_CODE_FLUSH, NULL, 0); +} + +int term_read_cursor(term_t *self, size_t *rows, size_t *cols) { + void *data = self->fetch(self, TERM_CODE_CURSOR_READ, NULL, 0, 2 * sizeof(size_t)); + if (data) { + size_t *tuple = (size_t *) data; + *rows = tuple[0]; + *cols = tuple[1]; + free(data); + return 0; + } + return -1; +} + +int term_read_window(term_t *self, size_t *rows, size_t *cols) { + void *data = self->fetch(self, TERM_CODE_WINDOW_READ, NULL, 0, 2 * sizeof(size_t)); + if (data) { + size_t *tuple = (size_t *) data; + *rows = tuple[0]; + *cols = tuple[1]; + free(data); + return 0; + } + return -1; +} + +int term_read_key(term_t *self) { + void *data = self->fetch(self, TERM_CODE_KEY_READ, NULL, 0, sizeof(int)); + if (data) { + int *single = (int *) data; + int key = *single; + free(data); + return key; + } + return -1; +} + +int term_poll_key(term_t *self, int timeout_ms) { + void *data = self->fetch(self, TERM_CODE_KEY_POLL, &timeout_ms, sizeof(timeout_ms), sizeof(int)); + if (data) { + int *single = (int *) data; + int key = *single; + free(data); + return key; } - return 0; + return -1; } #endif // LIB_TERM_IMPL