diff options
| author | hachem <im@hachem.wtf> | 2026-09-29 18:23:48 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-29 18:23:48 +0200 |
| commit | 7a61791c5e87796b1984d13d70d5bafc1953bbb3 (patch) | |
| tree | df304814e12e6ebcc72ef92e16a4f1c3b2084392 | |
| parent | 571d9a642b10c77c4ec5b91c0ccc7597ea7f4490 (diff) | |
feat: qol
| -rw-r--r-- | todo.c | 323 |
1 files changed, 285 insertions, 38 deletions
@@ -1,4 +1,3 @@ -#include <ctype.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> @@ -16,6 +15,7 @@ #define LEFT_MARGIN 2 #define SECTION_INDENT 2 +#define SUBTASK_INDENT 4 enum State { @@ -42,6 +42,14 @@ enum Mode MODE_INSERT }; +enum Key +{ + KEY_UP = 256, + KEY_DOWN, + KEY_LEFT, + KEY_RIGHT +}; + static const char* mode_name(enum Mode mode) { switch (mode) @@ -123,8 +131,70 @@ static int is_section(const struct Item* item) return item->text[0] == '='; } -static void save(const struct List* list, const char* path) +static int is_subtask(const struct Item* item) +{ + return item->text[0] == '-'; +} + +static void normalize_tasks(struct List* list) +{ + for (int i = 0; i < list->count; i++) + { + if (is_section(&list->items[i]) || is_subtask(&list->items[i])) + continue; + + int total = 0; + int done = 0; + int active = 0; + + for (int j = i + 1; j < list->count && is_subtask(&list->items[j]); j++) + { + total++; + + if (list->items[j].state == STATE_DONE) + done++; + else if (list->items[j].state == STATE_DOING) + active++; + } + + if (total == 0) + continue; + + if (done == total) + list->items[i].state = STATE_DONE; + else if (done > 0 || active > 0) + list->items[i].state = STATE_DOING; + else + list->items[i].state = STATE_TODO; + } +} + +static int block_end(const struct List* list, int start) { + int i = start + 1; + + while (i < list->count && !is_section(&list->items[i])) + i++; + + return i; +} + +static void reverse_items(struct List* list, int low, int high) +{ + while (low < high - 1) + { + struct Item moved = list->items[low]; + list->items[low] = list->items[high - 1]; + list->items[high - 1] = moved; + low++; + high--; + } +} + +static void save(struct List* list, const char* path) +{ + normalize_tasks(list); + FILE* file = fopen(path, "w"); if (file == NULL) @@ -256,7 +326,7 @@ static void to_roman(int number, char* buffer, size_t size) } } -static void render(const struct List* list, int cursor, int scroll, enum Mode mode) +static void render(const struct List* list, int cursor, int scroll, enum Mode mode, int edit_pos) { int rows = screen_rows(); int visible = content_rows(); @@ -265,7 +335,7 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo int done = 0; for (int i = 0; i < list->count; i++) - if (!is_section(&list->items[i])) + if (!is_section(&list->items[i]) && !is_subtask(&list->items[i])) { total_items++; done += list->items[i].state == STATE_DONE; @@ -278,7 +348,7 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo { if (is_section(&list->items[i])) run = 0; - else if (++run > max_run) + else if (!is_subtask(&list->items[i]) && ++run > max_run) max_run = run; } @@ -353,16 +423,12 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo continue; } - int number = 0; - int scan = index; + int scan = index - 1; while (scan >= 0 && !is_section(&list->items[scan])) - { - number++; scan--; - } - int item_indent = LEFT_MARGIN + (scan >= 0 ? SECTION_INDENT : 0); + int in_section = scan >= 0; const char* box = item->state == STATE_DONE ? "[x]" : item->state == STATE_DOING ? "[-]" @@ -370,10 +436,39 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo const char* dim = item->state == STATE_DONE ? "\x1b[2m" : ""; const char* dim_reset = item->state == STATE_DONE ? "\x1b[22m" : ""; - length += snprintf(buffer + length, sizeof buffer - (size_t) length, - "%s%*s\x1b[2m%*d.\x1b[22m %s %s%s%s\x1b[K%s\r\n", - background, item_indent, "", number_width, number, - box, dim, item->text, dim_reset, background_reset); + if (is_subtask(item)) + { + int indent = LEFT_MARGIN + (in_section ? SECTION_INDENT : 0) + SUBTASK_INDENT; + const char* name = item->text; + + if (!(mode == MODE_INSERT && index == cursor)) + { + name = item->text + 1; + + while (*name == ' ') + name++; + } + + length += snprintf(buffer + length, sizeof buffer - (size_t) length, + "%s%*s%s %s%s%s\x1b[K%s\r\n", + background, indent, "", box, dim, name, dim_reset, background_reset); + } + else + { + int number = 0; + + for (int k = index; k >= 0 && !is_section(&list->items[k]); k--) + if (!is_subtask(&list->items[k])) + number++; + + int item_indent = LEFT_MARGIN + (in_section ? SECTION_INDENT : 0); + + length += snprintf(buffer + length, sizeof buffer - (size_t) length, + "%s%*s\x1b[2m%*d.\x1b[22m %s %s%s%s\x1b[K%s\r\n", + background, item_indent, "", number_width, number, + box, dim, item->text, dim_reset, background_reset); + } + emitted++; index++; } @@ -399,12 +494,11 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo rows_before++; int cursor_row = 3 + rows_before; - int text_length = (int) strlen(list->items[cursor].text); int cursor_column; if (is_section(&list->items[cursor])) { - cursor_column = mode == MODE_INSERT ? LEFT_MARGIN + text_length + 1 : LEFT_MARGIN + 1; + cursor_column = mode == MODE_INSERT ? LEFT_MARGIN + 1 + edit_pos : LEFT_MARGIN + 1; } else { @@ -414,8 +508,14 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo scan--; int item_indent = LEFT_MARGIN + (scan >= 0 ? SECTION_INDENT : 0); - int text_start = item_indent + number_width + 7; - cursor_column = mode == MODE_INSERT ? text_start + text_length : text_start; + int text_start; + + if (is_subtask(&list->items[cursor])) + text_start = item_indent + SUBTASK_INDENT + 5; + else + text_start = item_indent + number_width + 7; + + cursor_column = text_start + edit_pos; } const char* shape = mode == MODE_INSERT ? "\x1b[6 q" : "\x1b[2 q"; @@ -431,21 +531,69 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo (void) write(STDOUT_FILENO, buffer, (size_t) length); } -static int normal_key(struct List* list, const char* path, int* cursor, enum Mode* mode, char character) +static int read_key(void) { - switch (character) + char character; + + if (read(STDIN_FILENO, &character, 1) != 1) + return -1; + + if (character != 27) + return (unsigned char) character; + + struct termios current; + tcgetattr(STDIN_FILENO, ¤t); + + struct termios timed = current; + timed.c_cc[VMIN] = 0; + timed.c_cc[VTIME] = 1; + (void) tcsetattr(STDIN_FILENO, TCSANOW, &timed); + + char sequence[2]; + ssize_t first = read(STDIN_FILENO, &sequence[0], 1); + ssize_t second = first == 1 ? read(STDIN_FILENO, &sequence[1], 1) : 0; + + (void) tcsetattr(STDIN_FILENO, TCSANOW, ¤t); + + if (first == 1 && second == 1 && sequence[0] == '[') + switch (sequence[1]) + { + case 'A': return KEY_UP; + case 'B': return KEY_DOWN; + case 'C': return KEY_RIGHT; + case 'D': return KEY_LEFT; + } + + return 27; +} + +static int normal_key(struct List* list, const char* path, int* cursor, enum Mode* mode, int* edit_pos, int key) +{ + switch (key) { case 'q': return 0; case 'j': + case KEY_DOWN: (*cursor)++; break; case 'k': + case KEY_UP: (*cursor)--; break; + case 'h': + case KEY_LEFT: + (*edit_pos)--; + break; + + case 'l': + case KEY_RIGHT: + (*edit_pos)++; + break; + case ' ': if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor])) { @@ -468,9 +616,9 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod case 'a': case 'o': - case 'i': insert_item(list, *cursor + 1); *cursor = *cursor + 1 < list->count ? *cursor + 1 : list->count - 1; + *edit_pos = 0; *mode = MODE_INSERT; break; @@ -482,7 +630,28 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod } if (*cursor >= 0 && *cursor < list->count) + { + *edit_pos = (int) strlen(list->items[*cursor].text); + *mode = MODE_INSERT; + } + + break; + + case 'i': + if (*cursor >= 0 && *cursor < list->count) + { + if (is_section(&list->items[*cursor])) + { + int position = 1; + + while (list->items[*cursor].text[position] == ' ') + position++; + + *edit_pos = position; + } + *mode = MODE_INSERT; + } break; @@ -496,6 +665,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod if (*cursor >= 0 && *cursor < list->count) { list->items[*cursor].text[0] = '\0'; + *edit_pos = 0; *mode = MODE_INSERT; } @@ -551,7 +721,25 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod } case 'J': - if (*cursor >= 0 && *cursor + 1 < list->count) + if (*cursor < 0 || *cursor >= list->count) + break; + + if (is_section(&list->items[*cursor])) + { + int start = *cursor; + int end = block_end(list, start); + + if (end < list->count) + { + int next_end = block_end(list, end); + reverse_items(list, start, end); + reverse_items(list, end, next_end); + reverse_items(list, start, next_end); + *cursor = start + (next_end - end); + save(list, path); + } + } + else if (*cursor + 1 < list->count && !is_section(&list->items[*cursor + 1])) { struct Item moved = list->items[*cursor]; list->items[*cursor] = list->items[*cursor + 1]; @@ -563,7 +751,30 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod break; case 'K': - if (*cursor > 0 && *cursor < list->count) + if (*cursor < 0 || *cursor >= list->count) + break; + + if (is_section(&list->items[*cursor])) + { + int start = *cursor; + + if (start > 0) + { + int previous = start - 1; + + while (previous >= 0 && !is_section(&list->items[previous])) + previous--; + + int previous_start = previous >= 0 ? previous : 0; + int end = block_end(list, start); + reverse_items(list, previous_start, start); + reverse_items(list, start, end); + reverse_items(list, previous_start, end); + *cursor = previous_start; + save(list, path); + } + } + else if (*cursor > 0 && !is_section(&list->items[*cursor - 1])) { struct Item moved = list->items[*cursor]; list->items[*cursor] = list->items[*cursor - 1]; @@ -594,12 +805,12 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod return 1; } -static void insert_key(struct List* list, const char* path, int* cursor, enum Mode* mode, char character) +static void insert_key(struct List* list, const char* path, int* cursor, enum Mode* mode, int* edit_pos, int key) { struct Item* item = &list->items[*cursor]; size_t length = strlen(item->text); - if (character == 27 || ((character == '\r' || character == '\n') && item->text[0] == '\0')) + if (key == 27 || ((key == '\r' || key == '\n') && item->text[0] == '\0')) { if (item->text[0] == '\0') { @@ -617,26 +828,47 @@ static void insert_key(struct List* list, const char* path, int* cursor, enum Mo return; } - if (character == '\r' || character == '\n') + if (key == '\r' || key == '\n') { save(list, path); insert_item(list, *cursor + 1); (*cursor)++; + *edit_pos = 0; return; } - if (character == 127 || character == 8) + if (key == KEY_LEFT) { - if (length > 0) - item->text[length - 1] = '\0'; + if (*edit_pos > 0) + (*edit_pos)--; return; } - if (isprint((unsigned char) character) && length + 1 < sizeof item->text) + if (key == KEY_RIGHT) { - item->text[length] = character; - item->text[length + 1] = '\0'; + if (*edit_pos < (int) length) + (*edit_pos)++; + + return; + } + + if (key == 127 || key == 8) + { + if (*edit_pos > 0) + { + memmove(&item->text[*edit_pos - 1], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); + (*edit_pos)--; + } + + return; + } + + if (key >= 32 && key <= 126 && length + 1 < sizeof item->text) + { + memmove(&item->text[*edit_pos + 1], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); + item->text[*edit_pos] = (char) key; + (*edit_pos)++; } } @@ -645,9 +877,12 @@ static void run(struct List* list, const char* path) if (enter_raw_mode() != 0) exit(EXIT_FAILURE); + normalize_tasks(list); + enum Mode mode = MODE_NORMAL; int cursor = 0; int scroll = 0; + int edit_pos = 0; for (;;) { @@ -665,23 +900,35 @@ static void run(struct List* list, const char* path) if (cursor >= scroll + visible) scroll = cursor - visible + 1; - render(list, cursor, scroll, mode); + if (list->count > 0) + { + int text_length = (int) strlen(list->items[cursor].text); + int caret_max = mode == MODE_INSERT ? text_length : (text_length > 0 ? text_length - 1 : 0); + + if (edit_pos > caret_max) + edit_pos = caret_max; + + if (edit_pos < 0) + edit_pos = 0; + } + + render(list, cursor, scroll, mode, edit_pos); - char character; + int key = read_key(); - if (read(STDIN_FILENO, &character, 1) != 1) + if (key == -1) break; switch (mode) { case MODE_NORMAL: - if (normal_key(list, path, &cursor, &mode, character) == 0) + if (normal_key(list, path, &cursor, &mode, &edit_pos, key) == 0) return; break; case MODE_INSERT: - insert_key(list, path, &cursor, &mode, character); + insert_key(list, path, &cursor, &mode, &edit_pos, key); break; } } |
