diff options
| author | hachem <im@hachem.wtf> | 2026-09-29 16:28:44 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-29 16:28:44 +0200 |
| commit | 50b8d6f791c91d2881f1aa16c95237c980303712 (patch) | |
| tree | 371e172df8cdac110d3889b6c1c4b968489117a1 | |
| parent | 992f807b36d4f7cae5187110980d50c593967e82 (diff) | |
feat: modal todo list
| -rw-r--r-- | todo.c | 280 |
1 files changed, 221 insertions, 59 deletions
@@ -1,10 +1,4 @@ -/* - * todo - a little modal todo list. - * - * Author: hachem <im@hachem.wtf> - * Version: 0.1.0 - */ - +#include <ctype.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> @@ -19,9 +13,16 @@ #define max_items 4096 #define max_text 256 +enum State +{ + STATE_TODO, + STATE_DOING, + STATE_DONE +}; + struct Item { - int done; + enum State state; char text[max_text]; }; @@ -33,44 +34,44 @@ struct List enum Mode { - mode_normal + MODE_NORMAL, + MODE_INSERT }; static const char* mode_name(enum Mode mode) { switch (mode) { - case mode_normal: return "NORMAL"; + case MODE_NORMAL: return "NORMAL"; + case MODE_INSERT: return "INSERT"; } return "?"; } -static void todo_path(char* buf, size_t size) +static void todo_path(char* buffer, size_t size) { - const char* env = getenv("TODO_FILE"); + const char* env_path = getenv("TODO_FILE"); - if (env != NULL && env[0] != '\0') + if (env_path != NULL && env_path[0] != '\0') { - (void) snprintf(buf, size, "%s", env); + (void) snprintf(buffer, size, "%s", env_path); return; } const char* home = getenv("HOME"); - (void) snprintf(buf, size, "%s/.todo.txt", home != NULL ? home : "."); + (void) snprintf(buffer, size, "%s/.todo.txt", home != NULL ? home : "."); } -static void strip_newline(char* s) +static void strip_newline(char* string) { - size_t len = strlen(s); + size_t length = strlen(string); - while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r')) - s[--len] = '\0'; + while (length > 0 && (string[length - 1] == '\n' || string[length - 1] == '\r')) + string[--length] = '\0'; } -/* read the list file. each line is "[ ] text" or "[x] text"; anything else is - * treated as an undone item so hand-edited files still load. */ static void load(struct List* list, const char* path) { list->count = 0; @@ -86,12 +87,16 @@ static void load(struct List* list, const char* path) { strip_newline(line); - int done = 0; + enum State state = STATE_TODO; char* text = line; if (line[0] == '[' && line[1] != '\0' && line[2] == ']') { - done = line[1] == 'x' || line[1] == 'X'; + if (line[1] == 'x' || line[1] == 'X') + state = STATE_DONE; + else if (line[1] == '-') + state = STATE_DOING; + text = line + 3; while (*text == ' ') @@ -102,7 +107,7 @@ static void load(struct List* list, const char* path) continue; struct Item* item = &list->items[list->count++]; - item->done = done; + item->state = state; (void) snprintf(item->text, sizeof item->text, "%s", text); } @@ -117,11 +122,47 @@ static void save(const struct List* list, const char* path) return; for (int i = 0; i < list->count; i++) - (void) fprintf(file, "[%c] %s\n", list->items[i].done ? 'x' : ' ', list->items[i].text); + { + char marker = list->items[i].state == STATE_DONE ? 'x' + : list->items[i].state == STATE_DOING ? '-' + : ' '; + + (void) fprintf(file, "[%c] %s\n", marker, list->items[i].text); + } (void) fclose(file); } +static void insert_item(struct List* list, int index) +{ + if (list->count >= max_items) + return; + + if (index < 0) + index = 0; + + if (index > list->count) + index = list->count; + + for (int i = list->count; i > index; i--) + list->items[i] = list->items[i - 1]; + + list->items[index].state = STATE_TODO; + list->items[index].text[0] = '\0'; + list->count++; +} + +static void delete_item(struct List* list, int index) +{ + if (index < 0 || index >= list->count) + return; + + for (int i = index; i < list->count - 1; i++) + list->items[i] = list->items[i + 1]; + + list->count--; +} + static struct termios saved_termios; static void restore_terminal(void) @@ -130,9 +171,9 @@ static void restore_terminal(void) (void) write(STDOUT_FILENO, "\x1b[?25h\x1b[?1049l", 14); } -static void on_signal(int signum) +static void on_signal(int signal_number) { - (void) signum; + (void) signal_number; exit(EXIT_SUCCESS); } @@ -166,10 +207,10 @@ static int enter_raw_mode(void) static int screen_rows(void) { - struct winsize ws; + struct winsize window; - if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0) - return ws.ws_row; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) == 0 && window.ws_row > 0) + return window.ws_row; return 24; } @@ -182,44 +223,73 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo if (visible < 1) visible = 1; - char buf[64 * 1024]; - int n = 0; + int number_width = 1; - n += snprintf(buf + n, sizeof buf - (size_t) n, "\x1b[H"); + for (int total = list->count; total >= 10; total /= 10) + number_width++; + + char buffer[64 * 1024]; + int length = 0; + + length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[H"); for (int row = 0; row < visible; row++) { - int i = scroll + row; + int index = scroll + row; - if (i >= list->count) + if (index >= list->count) { - n += snprintf(buf + n, sizeof buf - (size_t) n, "\x1b[K\r\n"); + length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); continue; } - const struct Item* item = &list->items[i]; - const char* highlight = i == cursor ? "\x1b[7m" : ""; - const char* box = item->done ? "\x1b[32m[x]\x1b[0m" : "[ ]"; - const char* dim = item->done ? "\x1b[2m" : ""; + const struct Item* item = &list->items[index]; + + const char* background = index == cursor ? "\x1b[48;5;235m" : ""; + const char* background_reset = index == cursor ? "\x1b[49m" : ""; + const char* box_color = item->state == STATE_DONE ? "\x1b[32m" + : item->state == STATE_DOING ? "\x1b[33m" + : ""; + const char* box_reset = item->state == STATE_TODO ? "" : "\x1b[39m"; + const char* box = item->state == STATE_DONE ? "⌜x⌟" + : item->state == STATE_DOING ? "⌜-⌟" + : "⌜ ⌟"; + const char* dim = item->state == STATE_DONE ? "\x1b[2m" : ""; + const char* dim_reset = item->state == STATE_DONE ? "\x1b[22m" : ""; - n += snprintf(buf + n, sizeof buf - (size_t) n, - "%s %s %s%s\x1b[0m\x1b[K\r\n", highlight, box, dim, item->text); + length += snprintf(buffer + length, sizeof buffer - (size_t) length, + "%s\x1b[2m%*d.\x1b[22m %s%s%s %s%s%s\x1b[K%s\r\n", + background, number_width, index + 1, + box_color, box, box_reset, dim, item->text, dim_reset, background_reset); } int done = 0; for (int i = 0; i < list->count; i++) - done += list->items[i].done; + done += list->items[i].state == STATE_DONE; - n += snprintf(buf + n, sizeof buf - (size_t) n, - "\x1b[2m-- %s -- %d/%d done\x1b[0m\x1b[K\x1b[J", mode_name(mode), done, list->count); + length += snprintf(buffer + length, sizeof buffer - (size_t) length, + "\x1b[2m-- %s -- %d/%d done\x1b[0m\x1b[K\x1b[J", mode_name(mode), done, list->count); - (void) write(STDOUT_FILENO, buf, (size_t) n); + if (mode == MODE_INSERT && cursor >= 0 && cursor < list->count) + { + int cursor_row = cursor - scroll + 1; + int cursor_column = number_width + 7 + (int) strlen(list->items[cursor].text); + + length += snprintf(buffer + length, sizeof buffer - (size_t) length, + "\x1b[%d;%dH\x1b[?25h", cursor_row, cursor_column); + } + else + { + length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[?25l"); + } + + (void) write(STDOUT_FILENO, buffer, (size_t) length); } -static int normal_key(struct List* list, const char* path, int* cursor, char c) +static int normal_key(struct List* list, const char* path, int* cursor, enum Mode* mode, char character) { - switch (c) + switch (character) { case 'q': return 0; @@ -233,15 +303,60 @@ static int normal_key(struct List* list, const char* path, int* cursor, char c) break; case ' ': + if (*cursor >= 0 && *cursor < list->count) + { + struct Item* item = &list->items[*cursor]; + item->state = (enum State) ((item->state + 1) % 3); + save(list, path); + } + + break; + case 'x': if (*cursor >= 0 && *cursor < list->count) { - list->items[*cursor].done = !list->items[*cursor].done; + struct Item* item = &list->items[*cursor]; + item->state = item->state == STATE_DONE ? STATE_TODO : STATE_DONE; save(list, path); } break; + case 'a': + case 'o': + case 'i': + insert_item(list, *cursor + 1); + *cursor = *cursor + 1 < list->count ? *cursor + 1 : list->count - 1; + *mode = MODE_INSERT; + break; + + case 'A': + if (list->count == 0) + { + insert_item(list, 0); + *cursor = 0; + } + + if (*cursor >= 0 && *cursor < list->count) + *mode = MODE_INSERT; + + break; + + case 'R': + if (list->count == 0) + { + insert_item(list, 0); + *cursor = 0; + } + + if (*cursor >= 0 && *cursor < list->count) + { + list->items[*cursor].text[0] = '\0'; + *mode = MODE_INSERT; + } + + break; + default: break; } @@ -249,12 +364,58 @@ static int normal_key(struct List* list, const char* path, int* cursor, char c) return 1; } +static void insert_key(struct List* list, const char* path, int* cursor, enum Mode* mode, char character) +{ + struct Item* item = &list->items[*cursor]; + size_t length = strlen(item->text); + + if (character == 27 || ((character == '\r' || character == '\n') && item->text[0] == '\0')) + { + if (item->text[0] == '\0') + { + delete_item(list, *cursor); + + if (*cursor >= list->count) + *cursor = list->count - 1; + + if (*cursor < 0) + *cursor = 0; + } + + save(list, path); + *mode = MODE_NORMAL; + return; + } + + if (character == '\r' || character == '\n') + { + save(list, path); + insert_item(list, *cursor + 1); + (*cursor)++; + return; + } + + if (character == 127 || character == 8) + { + if (length > 0) + item->text[length - 1] = '\0'; + + return; + } + + if (isprint((unsigned char) character) && length + 1 < sizeof item->text) + { + item->text[length] = character; + item->text[length + 1] = '\0'; + } +} + static void run(struct List* list, const char* path) { if (enter_raw_mode() != 0) exit(EXIT_FAILURE); - enum Mode mode = mode_normal; + enum Mode mode = MODE_NORMAL; int cursor = 0; int scroll = 0; @@ -279,22 +440,23 @@ static void run(struct List* list, const char* path) render(list, cursor, scroll, mode); - char c; + char character; - if (read(STDIN_FILENO, &c, 1) != 1) + if (read(STDIN_FILENO, &character, 1) != 1) break; - int keep = 1; - switch (mode) { - case mode_normal: - keep = normal_key(list, path, &cursor, c); + case MODE_NORMAL: + if (normal_key(list, path, &cursor, &mode, character) == 0) + return; + break; - } - if (!keep) - break; + case MODE_INSERT: + insert_key(list, path, &cursor, &mode, character); + break; + } } } |
