diff options
author | A404M <ahmadmahmoudiprogrammer@gmail.com> | 2024-08-28 00:18:53 +0330 |
---|---|---|
committer | A404M <ahmadmahmoudiprogrammer@gmail.com> | 2024-08-28 00:18:53 +0330 |
commit | e9d373a154f538ef316940a142f652dba1a9bea6 (patch) | |
tree | 1330afde6a7e695e6e334f12483c732eca90314e /src |
initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/main.c | 32 | ||||
-rw-r--r-- | src/ui/tui.c | 496 | ||||
-rw-r--r-- | src/ui/tui.h | 135 |
3 files changed, 663 insertions, 0 deletions
diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..b7a97d1 --- /dev/null +++ b/src/main.c @@ -0,0 +1,32 @@ +#include "ui/tui.h" +#include <unistd.h> +#include <stdio.h> + +void on_button_click(MOUSE_ACTION mouse_action) { + printf("hello"); + sleep(1); +} + +WIDGET *ui_build(TUI *tui) { + return tui_make_box( + -1, -1, + tui_make_row(tui_make_widget_array( + 2, + tui_make_box( + 8, 2, + tui_make_button(tui_make_text("Hello, World!", COLOR_BLUE), + on_button_click), + COLOR_YELLOW), + tui_make_text("Hello, World!", COLOR_RED), 1)), + COLOR_MAGENTA); +} + +int main() { + TUI *tui = tui_init(); + + tui_start_app(tui, ui_build); + + tui_delete(tui); + + return 0; +} diff --git a/src/ui/tui.c b/src/ui/tui.c new file mode 100644 index 0000000..8ed59ce --- /dev/null +++ b/src/ui/tui.c @@ -0,0 +1,496 @@ +#include "tui.h" +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <time.h> +#include <unistd.h> + +void _tui_init_callbacks(TUI *tui) { + const int width = tui_get_width(tui); + const int height = tui_get_height(tui); + + tui->on_click_callbacks = malloc(width * sizeof(ON_CLICK_CALLBACK *)); + for (int i = 0; i < width; ++i) { + tui->on_click_callbacks[i] = malloc(height * sizeof(ON_CLICK_CALLBACK)); + + for (int j = 0; j < height; ++j) { + tui->on_click_callbacks[i][j] = NULL; + } + } +} + +void _tui_delete_callbacks(TUI *tui, int width) { + for (int i = 0; i < width; ++i) { + free(tui->on_click_callbacks[i]); + } + + free(tui->on_click_callbacks); +} + +TUI *tui_init() { + setbuf(stdout, NULL); + + TUI *tui = malloc(sizeof(TUI)); + tui->buffer = malloc(0); + + tui_get_cursor_pos(tui, &tui->init_cursor_x, &tui->init_cursor_y); + + // Save original serial communication configuration for stdin + tcgetattr(STDIN_FILENO, &tui->original); + + // Put stdin in raw mode so keys get through directly without + // requiring pressing enter. + cfmakeraw(&tui->raw); + tcsetattr(STDIN_FILENO, TCSANOW, &tui->raw); + + // Switch to the alternate buffer screen + write(STDOUT_FILENO, "\e[?47h", 6); + + // Enable mouse tracking + write(STDOUT_FILENO, "\e[?9h", 5); + + _tui_init_callbacks(tui); + + tui_refresh(tui); + return tui; +} + +void tui_delete(TUI *tui) { + const int width = tui_get_width(tui); + + // Revert the terminal back to its original state + write(STDOUT_FILENO, "\e[?9l", 5); + write(STDOUT_FILENO, "\e[?47l", 6); + tcsetattr(STDIN_FILENO, TCSANOW, &tui->original); + + tui_move_to(tui->init_cursor_x, tui->init_cursor_y); + + _tui_delete_callbacks(tui, width); + free(tui); +} + +void tui_refresh(TUI *tui) { + const int width = tui_get_width(tui); + const int height = tui_get_height(tui); + + ioctl(STDOUT_FILENO, TIOCGWINSZ, &tui->size); + + if (width == tui_get_width(tui) && height == tui_get_height(tui)) { + return; + } + _tui_delete_callbacks(tui, width); + _tui_init_callbacks(tui); +} + +int tui_get_width(TUI *tui) { return tui->size.ws_col; } + +int tui_get_height(TUI *tui) { return tui->size.ws_row; } + +int tui_clear_screen() { return printf("\033[2J\r"); } + +int tui_move_top(int n) { return printf("\033[%dA", n); } + +int tui_move_up(int n) { return printf("\033[%dB", n); } + +int tui_move_right(int n) { return printf("\033[%dC", n); } + +int tui_move_left(int n) { return printf("\033[%dD", n); } + +int tui_save_cursor() { return printf("\0337"); } + +int tui_restore_cursor() { return printf("\0338"); } + +void tui_get_cursor_pos(TUI *tui, int *x, int *y) { + char buf[8]; + char cmd[] = "\033[6n"; + tcgetattr(0, &tui->raw); + cfmakeraw(&tui->helper); + tcsetattr(0, TCSANOW, &tui->helper); + if (isatty(fileno(stdin))) { + write(STDOUT_FILENO, cmd, sizeof(cmd)); + read(STDIN_FILENO, buf, sizeof(buf)); + + sscanf(buf, "\033[%d;%dR", y, x); + --x; + --y; + } + tcsetattr(0, TCSANOW, &tui->raw); +} + +int tui_move_to(int x, int y) { return printf("\033[%d;%dH", y + 1, x + 1); } + +int tui_delete_before() { return printf("\b \b"); } + +int tui_delete_under_cursor() { return printf(" \b"); } + +void tui_set_on_click_callback(TUI *tui, int x, int y, + ON_CLICK_CALLBACK callback) { + tui->on_click_callbacks[x][y] = callback; +} + +void tui_handle_mouse_action(TUI *tui, MOUSE_ACTION mouse_action) { + const ON_CLICK_CALLBACK callback = + tui->on_click_callbacks[mouse_action.x][mouse_action.y]; + if (callback != NULL) { + callback(mouse_action); + } +} + +int tui_change_terminal_text_color(COLOR color) { + if (color == COLOR_NO_COLOR) { + return 0; + } else if (color == COLOR_RESET) { + return printf("\033[%dm", COLOR_RESET); + } + return printf("\033[%dm", color + 30); +} + +int tui_change_terminal_background_color(COLOR color) { + if (color == COLOR_NO_COLOR) { + return 0; + } else if (color == COLOR_RESET) { + return printf("\033[%dm", COLOR_RESET); + } + return printf("\033[%dm", color + 40); +} + +int handle_input(TUI *tui) { + unsigned char buff[6]; + read(STDIN_FILENO, &buff, 1); + if (buff[0] == 3) { // User pressd Ctr+C + return 1; + } else if (buff[0] == '\x1B') { // [ESCAPE] + // TODO: fix for inputting actual <ESC> + read(STDIN_FILENO, &buff, 5); + switch (buff[1]) { + case 77: { + MOUSE_ACTION mouse_action = { + .button = buff[2], + .x = buff[3] - 32 - 1, // starts at 0 + .y = buff[4] - 32 - 1, // starts at 0 + }; + tui_handle_mouse_action(tui, mouse_action); + /*printf("button:%u\n\rx:%u\n\ry:%u\n\n\r", mouse_action.button,*/ + /* mouse_action.x, mouse_action.y);*/ + } break; + } + } else { + switch (buff[0]) { + case 'h': + tui_move_left(1); + break; + case 'j': + tui_move_up(1); + break; + case 'k': + tui_move_top(1); + break; + case 'l': + tui_move_right(1); + break; + case 'c': + tui_clear_screen(); + break; + case 's': + tui_save_cursor(); + break; + case 'r': + tui_restore_cursor(); + break; + case '\b': + case 127: // back space + tui_delete_before(); + break; + default: + /*printf("unknown:%c,%d\n\r", buff[0], buff[0]);*/ + break; + } + } + return 0; +} + +void tui_start_app(TUI *tui, WIDGET_BUILDER widget_builder) { + tui_main_loop(tui, widget_builder); +} + +void _tui_draw_widget(TUI *tui, const WIDGET *widget, int width_begin, + int width_end, int height_begin, int height_end, + COLOR parent_color, int *child_width, int *child_height) { + switch (widget->type) { + case WIDGET_TYPE_TEXT: { + const TEXT_METADATA *metadata = widget->metadata; + const int widthDiff = width_end - width_begin; + const int textLen = strlen(metadata->text); + int height = height_begin; + for (; height < height_end; ++height) { + tui_move_to(width_begin, height); + const int begin = (height - height_begin) * widthDiff; + int end = begin + widthDiff; + int shouldExit = 0; + if (end > textLen) { + end = textLen; + shouldExit = 1; + } + + tui_change_terminal_text_color(metadata->color); + tui_change_terminal_background_color(parent_color); + write(STDOUT_FILENO, metadata->text + begin, end - begin); + tui_change_terminal_background_color(COLOR_RESET); + tui_change_terminal_text_color(COLOR_RESET); + if (shouldExit) { + break; + } + } + *child_height = height + 1; + *child_width = + (textLen > widthDiff ? width_end : textLen + width_begin) + 1; + } break; + case WIDGET_TYPE_BUTTON: { + const BUTTON_METADATA *metadata = widget->metadata; + tui_move_to(width_begin, height_begin); + if (metadata->child != NULL) { + _tui_draw_widget(tui, metadata->child, width_begin, width_end, + height_begin, height_end, parent_color, child_width, + child_height); + /*printf("%d,%d\n\r",*child_width,*child_height);*/ + /*sleep(1);*/ + for (int i = width_begin; i < *child_width; ++i) { + for (int j = height_begin; j < *child_height; ++j) { + tui_set_on_click_callback(tui, i, j, metadata->callback); + } + } + } + } break; + case WIDGET_TYPE_COLUMN: { + const COLUMN_METADATA *metadata = widget->metadata; + *child_width = width_begin; + *child_height = height_begin; + for (int i = 0; i < metadata->children->size; ++i) { + const WIDGET *child = metadata->children->widgets[i]; + int width_temp; + _tui_draw_widget(tui, child, width_begin, width_end, *child_height, + height_end, parent_color, &width_temp, child_height); + if (width_temp > *child_width) { + *child_width = width_temp; + } + } + } break; + case WIDGET_TYPE_ROW: { + const ROW_METADATA *metadata = widget->metadata; + *child_width = width_begin; + *child_height = height_begin; + for (int i = 0; i < metadata->children->size; ++i) { + const WIDGET *child = metadata->children->widgets[i]; + int height_temp; + _tui_draw_widget(tui, child, *child_width, width_end, height_begin, + height_end, parent_color, child_width, &height_temp); + if (height_temp > *child_height) { + *child_height = height_temp; + } + } + } break; + case WIDGET_TYPE_BOX: { + const BOX_METADATA *metadata = widget->metadata; + + if (metadata->width != -1) { + width_end = metadata->width + width_begin >= width_end + ? width_end + : metadata->width + width_begin; + } + if (metadata->height != -1) { + height_end = metadata->height + height_begin >= height_end + ? height_end + : metadata->height + height_begin; + } + + for (int y = height_begin; y < height_end; ++y) { + tui_move_to(width_begin, y); + for (int x = width_begin; x < width_end; ++x) { + tui_change_terminal_background_color(metadata->color); + write(STDOUT_FILENO, " ", 1); + tui_change_terminal_background_color(COLOR_RESET); + } + } + + if (metadata->child != NULL) { + int t0, t1; + _tui_draw_widget(tui, metadata->child, width_begin, width_end, + height_begin, height_end, metadata->color, &t0, &t1); + } + *child_width = width_end; + *child_height = height_end; + } break; + default: + fprintf(stderr, "widget type '%d' went wrong in _tui_draw_widget", + widget->type); + exit(1); + } +} + +void tui_main_loop(TUI *tui, WIDGET_BUILDER widget_builder) { + while (1) { + clock_t start = clock(); + tui_refresh(tui); + tui_save_cursor(); + tui_clear_screen(); + WIDGET *root_widget = widget_builder(tui); + + int width, height; + _tui_draw_widget(tui, root_widget, 0, tui_get_width(tui), 0, + tui_get_height(tui), COLOR_NO_COLOR, &width, &height); + + tui_delete_widget(root_widget); + tui_move_to(30, 30); + printf("time:%ld",clock()-start); + tui_restore_cursor(); + if (handle_input(tui)) { + return; + } + } +} + +WIDGET *tui_new_widget(WIDGET_TYPE type, void *metadata) { + WIDGET *widget = malloc(sizeof(WIDGET)); + widget->type = type; + widget->metadata = metadata; + return widget; +} + +void tui_delete_widget(WIDGET *widget) { + switch (widget->type) { + case WIDGET_TYPE_TEXT: + _tui_delete_text(widget); + break; + case WIDGET_TYPE_BUTTON: + _tui_delete_button(widget); + break; + case WIDGET_TYPE_COLUMN: + _tui_delete_column(widget); + break; + case WIDGET_TYPE_ROW: + _tui_delete_row(widget); + break; + case WIDGET_TYPE_BOX: + _tui_delete_box(widget); + break; + default: + fprintf(stderr, "Type error '%d' in tui_delete_widget\n", widget->type); + exit(1); + } + free(widget); +} + +WIDGET *tui_make_text(char *text, COLOR color) { + return tui_new_widget(WIDGET_TYPE_TEXT, _tui_make_text_metadata(text, color)); +} + +TEXT_METADATA *_tui_make_text_metadata(char *text, COLOR color) { + TEXT_METADATA *metadata = malloc(sizeof(TEXT_METADATA)); + metadata->text = malloc(strlen(text) + 1); + strcpy(metadata->text, text); + metadata->color = color; + return metadata; +} + +void _tui_delete_text(WIDGET *text) { + free(((TEXT_METADATA *)text->metadata)->text); + free(text->metadata); +} + +WIDGET *tui_make_button(WIDGET *child, ON_CLICK_CALLBACK callback) { + return tui_new_widget(WIDGET_TYPE_BUTTON, + _tui_make_button_metadata(child, callback)); +} + +BUTTON_METADATA *_tui_make_button_metadata(WIDGET *child, + ON_CLICK_CALLBACK callback) { + BUTTON_METADATA *metadata = malloc(sizeof(BUTTON_METADATA)); + metadata->child = child; + metadata->callback = callback; + return metadata; +} + +void _tui_delete_button(WIDGET *button) { + tui_delete_widget(((BUTTON_METADATA *)button->metadata)->child); + free(button->metadata); +} + +WIDGET *tui_make_column(WIDGET_ARRAY *children) { + return tui_new_widget(WIDGET_TYPE_COLUMN, + _tui_make_column_metadata(children)); +} + +COLUMN_METADATA *_tui_make_column_metadata(WIDGET_ARRAY *children) { + COLUMN_METADATA *metadata = malloc(sizeof(COLUMN_METADATA)); + metadata->children = children; + return metadata; +} + +void _tui_delete_column(WIDGET *column) { + COLUMN_METADATA *metadata = column->metadata; + for (size_t i = 0; i < metadata->children->size; ++i) { + tui_delete_widget(metadata->children->widgets[i]); + } + free(metadata->children); + free(column->metadata); +} + +WIDGET *tui_make_row(WIDGET_ARRAY *children) { + return tui_new_widget(WIDGET_TYPE_ROW, + _tui_make_row_metadata(children)); +} + +ROW_METADATA *_tui_make_row_metadata(WIDGET_ARRAY *children) { + ROW_METADATA *metadata = malloc(sizeof(ROW_METADATA)); + metadata->children = children; + return metadata; +} + +void _tui_delete_row(WIDGET *row) { + ROW_METADATA *metadata = row->metadata; + for (size_t i = 0; i < metadata->children->size; ++i) { + tui_delete_widget(metadata->children->widgets[i]); + } + free(metadata->children); + free(row->metadata); +} + +WIDGET *tui_make_box(int width, int height, WIDGET *child, COLOR color) { + return tui_new_widget(WIDGET_TYPE_BOX, + _tui_make_box_metadata(child, width, height, color)); +} + +BOX_METADATA *_tui_make_box_metadata(WIDGET *child, int width, int height, + COLOR color) { + BOX_METADATA *metadata = malloc(sizeof(BOX_METADATA)); + metadata->width = width; + metadata->height = height; + metadata->child = child; + metadata->color = color; + return metadata; +} + +void _tui_delete_box(WIDGET *box) { + tui_delete_widget(((BOX_METADATA *)box->metadata)->child); + free(box->metadata); +} + +WIDGET_ARRAY *tui_make_widget_array(int size,...){ + va_list arg_pointer; + va_start(arg_pointer, size); + + WIDGET **widgets = malloc(size*sizeof(WIDGET**)); + + for(int i = 0;i < size;++i){ + widgets[i] = va_arg(arg_pointer,WIDGET*); + } + va_end(arg_pointer); + + WIDGET_ARRAY *widget_array = malloc(sizeof(WIDGET_ARRAY)); + widget_array->widgets = widgets; + widget_array->size = size; + + return widget_array; +} diff --git a/src/ui/tui.h b/src/ui/tui.h new file mode 100644 index 0000000..a3e64f3 --- /dev/null +++ b/src/ui/tui.h @@ -0,0 +1,135 @@ +#ifndef A404M_UI_TUI +#define A404M_UI_TUI 1 + +#include <stdlib.h> +#include <sys/ioctl.h> +#include <termios.h> + +typedef enum MOUSE_BUTTON { + MOUSE_BUTTON_LEFT_CLICK = 32, + MOUSE_BUTTON_MIDDLE_CLICK = 33, + MOUSE_BUTTON_RIGHT_CLICK = 34, + MOUSE_BUTTON_SCROLL_UP = 96, + MOUSE_BUTTON_SCROLL_DOWN = 97 +} MOUSE_BUTTON; + +typedef struct MOUSE_ACTION { + MOUSE_BUTTON button; + unsigned int x; + unsigned int y; +} MOUSE_ACTION; + +typedef void (*ON_CLICK_CALLBACK)(MOUSE_ACTION mouse_action); + +typedef struct TUI { + struct winsize size; + struct termios original, raw, helper; + ON_CLICK_CALLBACK **on_click_callbacks; + int init_cursor_x, init_cursor_y; + char *buffer; +} TUI; + +typedef enum WIDGET_TYPE { + WIDGET_TYPE_TEXT, + WIDGET_TYPE_BUTTON, + WIDGET_TYPE_COLUMN, + WIDGET_TYPE_ROW, + WIDGET_TYPE_BOX, +} WIDGET_TYPE; + +typedef struct WIDGET { + WIDGET_TYPE type; + void *metadata; +} WIDGET; + +typedef enum COLOR { + COLOR_NO_COLOR = -1, + COLOR_RESET = 0, + COLOR_RED = 1, + COLOR_GREEN = 2, + COLOR_YELLOW = 3, + COLOR_BLUE = 4, + COLOR_MAGENTA = 5, + COLOR_CYAN = 6, + COLOR_WHITE = 7 +} COLOR; + +typedef struct WIDGET_ARRAY { + int size; + WIDGET **widgets; +} WIDGET_ARRAY; + +typedef struct TEXT_METADATA { + char *text; + COLOR color; +} TEXT_METADATA; + +typedef struct BUTTON_METADATA { + WIDGET *child; + ON_CLICK_CALLBACK callback; +} BUTTON_METADATA; + +typedef struct COLUMN_METADATA { + WIDGET_ARRAY *children; +} COLUMN_METADATA; + +typedef struct ROW_METADATA { + WIDGET_ARRAY *children; +} ROW_METADATA; + +typedef struct BOX_METADATA { + int width; + int height; + WIDGET *child; + COLOR color; +} BOX_METADATA; + +typedef WIDGET *(*WIDGET_BUILDER)(TUI *tui); + +extern TUI *tui_init(); +extern void tui_delete(TUI *tui); +extern void tui_refresh(TUI *tui); + +extern int tui_get_width(TUI *tui); +extern int tui_get_height(TUI *tui); + +extern void tui_get_cursor_pos(TUI *tui, int *x, int *y); +extern int tui_move_to(int x, int y); + +extern int tui_clear_screen(); + +extern void tui_start_app(TUI *tui, WIDGET_BUILDER widget_builder); +extern void _tui_draw_widget(TUI *tui, const WIDGET *widget, int widthBegin, + int widthEnd, int heightBegin, int heightEnd, + COLOR parent_color, int *childWidth, + int *childHeight); +extern void tui_main_loop(TUI *tui, WIDGET_BUILDER widget_builder); + +extern WIDGET *tui_new_widget(WIDGET_TYPE type, void *metadata); +extern void tui_delete_widget(WIDGET *widget); + +extern WIDGET *tui_make_text(char *text, COLOR color); +extern TEXT_METADATA *_tui_make_text_metadata(char *text, COLOR color); +extern void _tui_delete_text(WIDGET *text); + +extern WIDGET *tui_make_button(WIDGET *child, ON_CLICK_CALLBACK callback); +extern BUTTON_METADATA *_tui_make_button_metadata(WIDGET *child, + ON_CLICK_CALLBACK callback); +extern void _tui_delete_button(WIDGET *button); + +extern WIDGET *tui_make_column(WIDGET_ARRAY *children); +extern COLUMN_METADATA *_tui_make_column_metadata(WIDGET_ARRAY *children); +extern void _tui_delete_column(WIDGET *column); + +extern WIDGET *tui_make_row(WIDGET_ARRAY *children); +extern ROW_METADATA *_tui_make_row_metadata(WIDGET_ARRAY *children); +extern void _tui_delete_row(WIDGET *row); + +extern WIDGET *tui_make_box(int width, int height, WIDGET *child, COLOR color); +extern BOX_METADATA *_tui_make_box_metadata(WIDGET *child, int width, + int height, COLOR color); +extern void _tui_delete_box(WIDGET *box); + +extern WIDGET_ARRAY *tui_make_widget_array(int size, ...); + +#endif |