From a5734d1323355e777c69ed39dcea4f6bc2d3bc88 Mon Sep 17 00:00:00 2001 From: Zvonimir Rudinski Date: Sat, 6 Jan 2024 17:53:01 +0100 Subject: [PATCH] add a semi-functional TUI --- Makefile | 2 +- README.md | 1 + main.c | 309 +++++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 240 insertions(+), 72 deletions(-) diff --git a/Makefile b/Makefile index cde8e45..cbeb6db 100644 --- a/Makefile +++ b/Makefile @@ -13,4 +13,4 @@ clean: rm -f *.o all: todo.o main.o - $(CC) $(CFLAGS) todo.o main.o -o todd + $(CC) $(CFLAGS) todo.o main.o -lncurses -o todd diff --git a/README.md b/README.md index db24b0d..60f969d 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,4 @@ To build `todd` all you need to do is run `make`. ## 3rd Party Todd uses the following dependencies: - stb_ds - https://github.com/nothings/stb +- ncurses - https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/index.html diff --git a/main.c b/main.c index df67a56..1a2193d 100644 --- a/main.c +++ b/main.c @@ -3,68 +3,206 @@ #include #include #include +#include #define MAX_TODOS UINT_MAX -#define SAVE_FILE "todos.todd" +#define MAX_PATH_LENGTH 1024 #define STB_DS_IMPLEMENTATION +#define BORDERS_PAIR 1 +#define DEFAULT_PAIR 2 +#define COMPLETED_PAIR 3 + #include "3rd-party/stb-ds.h" +char SAVE_FILE[MAX_PATH_LENGTH] = "todos.todd"; TodoItem *todos = NULL; enum Command { ADD = 'a', MARK = 'm', PRINT = 'p', - REMOVE = 'r', + REMOVE = 'd', QUIT = 'q', WRITE_TO_FILE = 'w', LOAD_FROM_FILE = 'l', + + UP = 'k', + DOWN = 'j', }; -void add_command_handler(void) { +void clear_todos(void) { + for (int i = 0; i < arrlen(todos); i++) { + todo_free_item(&todos[i]); + } + + arrfree(todos); +} + +void initialize_curses(int *width, int *height) { + initscr(); + curs_set(0); + int tw, th; + getmaxyx(stdscr, th, tw); + + *width = tw; + *height = th; + + start_color(); + init_pair(BORDERS_PAIR, COLOR_BLACK, COLOR_WHITE); + init_pair(DEFAULT_PAIR, COLOR_WHITE, COLOR_BLACK); + init_pair(COMPLETED_PAIR, COLOR_BLACK, COLOR_GREEN); + // turn off echoing of keys, and enter cbreak mode, + // where no buffering is performed on keyboard input + cbreak(); + noecho(); +} + +void draw_header(int width) { + attron(COLOR_PAIR(BORDERS_PAIR)); + move(0, 0); + addstr(" Todd"); + for (int i = strlen("Todd"); i < width - 1; i++) { + addch(' '); + } +} + +void draw_footer(int width, int height) { + move(height - 1, 0); + for (int i = 0; i < width - 1; i++) { + addch(' '); + } + attroff(COLOR_PAIR(BORDERS_PAIR)); +} + +void alert(const char *message, int terminal_width, int terminal_height) { + int window_height = 5; + int window_width = terminal_width - 4; + int window_y = terminal_height / 2 - 1; + int window_x = 2; + + int message_x = (terminal_width - 4 - strlen(message)) / 2; + int message_y = 2; + + // create a new window + WINDOW *alert_window = newwin(window_height, window_width, window_y, window_x); + // draw a border around the window + box(alert_window, 0, 0); + // print the message in the middle of the window + mvwprintw(alert_window, message_y, message_x, message); + // refresh the window + wrefresh(alert_window); + // wait for a key press + getch(); + + // delete the window + delwin(alert_window); +} + +void input_string(int terminal_width, int terminal_height, char *buffer, int buffer_size) { + int window_height = 5; + int window_width = terminal_width - 4; + int window_y = terminal_height / 2 - 1; + int window_x = 2; + + int message_x = strlen("Enter todo title: ") + 1; + int message_y = 2; + + curs_set(1); + // create a new window + WINDOW *input_window = newwin(window_height, window_width, window_y, window_x); + // draw a border around the window + box(input_window, 0, 0); + // print the message in the middle of the window + mvwprintw(input_window, message_y, message_x, "Enter todo title: "); + // refresh the window + wrefresh(input_window); + // wait for a key press + echo(); + mvwgetnstr(input_window, message_y, message_x + strlen("Enter todo title: "), buffer, buffer_size); + noecho(); + + // delete the window + delwin(input_window); + curs_set(0); +} + +void display_todos(int width, int height, int current_line) { + int current_y = 1; + int current_index = current_line; + const char *padding = "..."; + unsigned long title_length = 0; + unsigned long padding_length = strlen(padding); + + char *title = (char *)malloc(width); + + attron(COLOR_PAIR(DEFAULT_PAIR)); + + // TODO: Use an offset or something + + while(current_y < height - 2) { + // check if we're out of bounds + if (current_index >= arrlen(todos)) { + break; + } + + // copy the title to the buffer + title_length = strlen(todos[current_index].title); + strncpy(title, todos[current_index].title, width - padding_length - 1); + // append the padding if needed + if (title_length > strlen(title)) { + strcat(title, padding); + } + + // move to the correct line + move(current_y, 0); + + // if the current todo is completed, print it as green + if (todos[current_index].completed) { + attroff(COLOR_PAIR(DEFAULT_PAIR)); + attron(COLOR_PAIR(COMPLETED_PAIR)); + } + + // highlight the current line + if (current_index == current_line) { + attron(A_UNDERLINE); + } + + printw("%d. %s", current_index + 1, title); + + // increment the current y and index + current_y++; + current_index++; + + attroff(A_UNDERLINE); + attroff(COLOR_PAIR(COMPLETED_PAIR)); + attron(COLOR_PAIR(DEFAULT_PAIR)); + } + + free(title); +} + +// Command handlers +void add_command_handler(int width, int height) { char title[TODO_MAX_TITLE_LENGTH]; - printf("Enter todo title: "); - fgets(title, TODO_MAX_TITLE_LENGTH - 1, stdin); - // remove the newline character from the buffer - title[strlen(title) - 1] = '\0'; + + input_string(width, height, title, TODO_MAX_TITLE_LENGTH); TodoItem item = todo_create_item(title); arrput(todos, item); } -void mark_command_handler(void) { - int op_index = -1; - do { - printf("Enter todo index: "); - op_index = getc(stdin) - '0'; - getc(stdin); // remove the newline character from the buffer - } while (op_index < 0 || op_index >= arrlen(todos)); - - TodoItem item = todos[op_index]; +void mark_command_handler(int index) { + TodoItem item = todos[index]; todo_mark_item(&item, !item.completed); - todos[op_index] = item; + todos[index] = item; } -void print_command_handler(void) { - for (int i = 0; i < arrlen(todos); i++) { - printf("%d. ", i); - todo_print_item(&todos[i]); - } -} - -void remove_command_handler(void) { - int op_index = -1; - do { - printf("Enter todo index: "); - op_index = getc(stdin) - '0'; - getc(stdin); // remove the newline character from the buffer - } while (op_index < 0 || op_index >= arrlen(todos)); - - arrdel(todos, op_index); +void remove_command_handler(int index) { + arrdel(todos, index); } void write_to_file_handler(void) { @@ -85,6 +223,9 @@ void write_to_file_handler(void) { } void load_from_file_handler(void) { + // clear the current todos + clear_todos(); + FILE *file = fopen(SAVE_FILE, "rb"); char magic[5] = {'\0'}; int todos_count = 0; @@ -126,47 +267,73 @@ void load_from_file_handler(void) { } } -int main(void) { - char cmd; +int main(int argc, char **argv) { + int terminal_width = 0; + int terminal_height = 0; + int key = 0; + int running = 1; + int current_line = 0; - do { - printf("Enter command: "); - cmd = getc(stdin); - getc(stdin); // remove the newline character from the buffer - - switch (cmd) { - case ADD: - add_command_handler(); - break; - case MARK: - mark_command_handler(); - break; - case PRINT: - print_command_handler(); - break; - case REMOVE: - remove_command_handler(); - break; - case WRITE_TO_FILE: - write_to_file_handler(); - break; - case LOAD_FROM_FILE: - load_from_file_handler(); - break; - case QUIT: - break; - default: - printf("Invalid command\n"); - break; - } - - } while (cmd != QUIT); - - for (int i = 0; i < arrlen(todos); i++) { - todo_free_item(&todos[i]); + // set the save file path if provided + if (argc > 1 && strlen(argv[1]) < MAX_PATH_LENGTH) { + strncpy(SAVE_FILE, argv[1], MAX_PATH_LENGTH); } - arrfree(todos); + initialize_curses(&terminal_width, &terminal_height); + + while (running) { + clear(); + draw_header(terminal_width); + draw_footer(terminal_width, terminal_height); + display_todos(terminal_width, terminal_height, current_line); + refresh(); + + // wait for a key press + key = getch(); + + // handle the key press + switch (key) { + case LOAD_FROM_FILE: + load_from_file_handler(); + alert("Loaded!", terminal_width, terminal_height); + break; + case WRITE_TO_FILE: + write_to_file_handler(); + alert("Saved!", terminal_width, terminal_height); + break; + case ADD: + add_command_handler(terminal_width, terminal_height); + break; + case MARK: + mark_command_handler(current_line); + break; + case REMOVE: + remove_command_handler(current_line); + if (current_line >= arrlen(todos)) { + current_line = arrlen(todos) - 1; + } + break; + case QUIT: + running = 0; + break; + case UP: + current_line--; + if (current_line < 0) { + current_line = 0; + } + break; + case DOWN: + current_line++; + if (current_line >= arrlen(todos)) { + current_line = arrlen(todos) - 1; + } + break; + } + } + + endwin(); + + return 0; }