#include "engine/todo.h" #include #include #include #include #include #include #include #include #define MAX_TODOS UINT_MAX #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" #define KEY_BINDINGS " [a]dd [m]ark [d]elete [w]rite [l]oad [q]uit" char SAVE_FILE[MAX_PATH_LENGTH] = ""; TodoItem *todos = NULL; enum Command { ADD = 'a', MARK = 'm', REMOVE = 'd', QUIT = 'q', WRITE_TO_FILE = 'w', LOAD_FROM_FILE = 'l', UP = 'k', DOWN = 'j', UP_ARROW = KEY_UP, DOWN_ARROW = KEY_DOWN, SPACE = ' ' }; 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(); keypad(stdscr, TRUE); } void draw_header(int width) { attron(COLOR_PAIR(BORDERS_PAIR)); move(0, 0); addstr(" Todd - "); addstr(SAVE_FILE); for (int i = strlen(" Todd - ") + strlen(SAVE_FILE); i < width - 1; i++) { addch(' '); } } void draw_footer(int width, int height) { move(height - 1, 0); addch(' '); int todos_count = arrlen(todos); int marked_count = 0; for (int i = 0; i < todos_count; i++) { if (todos[i].completed) { marked_count++; } } // get the length of the string int length = snprintf(NULL, 0, "%d/%d", marked_count, todos_count); // print the string printw("%d/%d", marked_count, todos_count); // print the rest of the line but leave space for the key bindings for (unsigned long i = length; i < width - strlen(KEY_BINDINGS) - 1; i++) { addch(' '); } // print the key bindings printw(KEY_BINDINGS); 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]; input_string(width, height, title, TODO_MAX_TITLE_LENGTH); TodoItem item = todo_create_item(title); arrput(todos, item); } void mark_command_handler(int index) { TodoItem item = todos[index]; todo_mark_item(&item, !item.completed); todos[index] = item; } void remove_command_handler(int index) { arrdel(todos, index); } void write_to_file_handler(void) { FILE *file = fopen(SAVE_FILE, "wb"); int bs = 0; int todos_count = arrlen(todos); // write a magic value "TODD" to the file fwrite("TODD", sizeof(char), 4, file); // write the number of todos to the file fwrite(&todos_count, sizeof(int), 1, file); // write each todo to the file for (int i = 0; i < todos_count; i++) { char *buffer = todo_item_serialize(&todos[i], &bs); fwrite(buffer, sizeof(char), bs, file); free(buffer); } fclose(file); } 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; // check the "TODD" (without the \0) magic value fread(magic, sizeof(char), 4, file); if (strcmp(magic, "TODD") != 0) { printf("Invalid file format\nExpected TODD, got %s\n", magic); return; } // read the number of todos fread(&todos_count, sizeof(int), 1, file); // read each todo for (int i = 0; i < todos_count; i++) { // read the title length unsigned char title_length = 0; fread(&title_length, sizeof(unsigned char), 1, file); // read the title char *title = malloc(title_length + 1); fread(title, sizeof(char), title_length, file); title[title_length] = '\0'; // read the completed flag bool_t completed = false; fread(&completed, sizeof(bool_t), 1, file); // create the todo item TodoItem item = todo_create_item(title); // mark the item as completed if needed if (completed) { todo_mark_item(&item, true); } // add the item to the list arrput(todos, item); } } int main(int argc, char **argv) { int terminal_width = 0; int terminal_height = 0; int key = 0; int running = 1; int current_line = 0; // set the save file path if provided if (argc > 1 && strlen(argv[1]) < MAX_PATH_LENGTH) { strncpy(SAVE_FILE, argv[1], MAX_PATH_LENGTH); } else { // load the default save file which is "~/.todos.todd" struct passwd *pw = getpwuid(getuid()); const char *homedir = pw->pw_dir; // check if we have enough space to store the path assert(strlen(homedir) + strlen("/.todos.todd") < MAX_PATH_LENGTH); // create the path snprintf(SAVE_FILE, MAX_PATH_LENGTH, "%s/.todos.todd", homedir); } 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: case SPACE: 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: case UP_ARROW: current_line--; if (current_line < 0) { current_line = 0; } break; case DOWN: case DOWN_ARROW: current_line++; if (current_line >= arrlen(todos)) { current_line = arrlen(todos) - 1; } break; } } endwin(); return 0; }