diff options
| author | hachem <im@hachem.wtf> | 2026-09-30 07:55:30 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-30 07:55:30 +0200 |
| commit | 036ab6838fd7cc4bd95ca3253d96c721fa23fd54 (patch) | |
| tree | 39c833dd285ba6d3e47c6002da4b0986d6cdc381 | |
| parent | e75e6312b3d75238fdbe24787f4fcce0f17f8574 (diff) | |
feat: add optinal tasks + proper replace
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | todo.1 | 16 | ||||
| -rw-r--r-- | todo.c | 138 |
3 files changed, 126 insertions, 33 deletions
@@ -16,7 +16,7 @@ a little modal todo list that lives in my terminal. o add a new item below i insert at the cursor A append to the item's text - R replace the item's text + R replace mode (overtype from the cursor) x toggle done space cycle todo -> doing -> done dd cut the item @@ -38,9 +38,12 @@ a little modal todo list that lives in my terminal. = project a section -> I. project == phase one a subsection -> i. phase one - write the docs a subtask +* maybe later an optional task -> ( ) buy milk a plain task ``` +within a section, items group as required, then optional, then done, with a blank line between the groups. + ## build ```sh @@ -40,6 +40,14 @@ for deeper levels .BI \- " name" A subtask of the task above it, indented further. A task is finished automatically once all of its subtasks are done. +.TP +.BI * " name" +An optional task, shown with parentheses +.RB ( "( )" ) +instead of a checkbox. +.PP +Within each section, items are grouped \(em required, then optional, then done +\(em with a blank line between the groups. .PP Each task carries one of three states, written in its checkbox: .B [\ ] @@ -79,7 +87,7 @@ Enter insert mode at the cursor. Append to the item's text. .TP .B R -Replace the item's text. +Enter replace mode, overtyping characters from the cursor. .TP .B x Toggle the item between done and not done. @@ -147,8 +155,10 @@ The default list. Each line is a task .RB ( "[ ] text" ", " "[-] text" ", " "[x] text" ), a section .RB ( "= name" ), -or a subtask -.RB ( "- name" ). +a subtask +.RB ( "- name" ), +or an optional task +.RB ( "* name" ). Override the location with .BR $TODO_FILE . .SH ENVIRONMENT @@ -43,6 +43,7 @@ enum Mode { MODE_NORMAL, MODE_INSERT, + MODE_REPLACE, MODE_COMMAND, MODE_SEARCH, MODE_CONFIRM @@ -69,6 +70,7 @@ static const char* mode_name(enum Mode mode) { case MODE_NORMAL: return "NORMAL"; case MODE_INSERT: return "INSERT"; + case MODE_REPLACE: return "REPLACE"; case MODE_COMMAND: return "COMMAND"; case MODE_SEARCH: return "SEARCH"; case MODE_CONFIRM: return "CONFIRM"; @@ -77,6 +79,11 @@ static const char* mode_name(enum Mode mode) return "?"; } +static int is_editing(enum Mode mode) +{ + return mode == MODE_INSERT || mode == MODE_REPLACE; +} + static void todo_path(char* buffer, size_t size) { const char* env_path = getenv("TODO_FILE"); @@ -152,6 +159,22 @@ static int is_subtask(const struct Item* item) return item->text[0] == '-'; } +static int is_optional(const struct Item* item) +{ + return item->text[0] == '*'; +} + +static int task_group(const struct Item* item) +{ + if (item->state == STATE_DONE) + return 2; + + if (is_optional(item)) + return 1; + + return 0; +} + static int section_level(const struct Item* item) { int level = 0; @@ -219,7 +242,7 @@ static void sort_list(struct List* list) while (segment < list->count && !is_section(&list->items[segment])) segment++; - for (int pass = 0; pass < 2; pass++) + for (int group = 0; group < 3; group++) { int k = i; @@ -230,7 +253,7 @@ static void sort_list(struct List* list) while (block < segment && is_subtask(&list->items[block])) block++; - if ((list->items[k].state == STATE_DONE) == pass) + if (task_group(&list->items[k]) == group) { sorted[out++] = list->items[k]; @@ -399,6 +422,9 @@ static int yank_full; static enum Pending pending_action; +static char replace_original[MAX_TEXT]; +static int replace_start; + static void paste_item(struct List* list, int index, int* cursor) { if (list->count >= MAX_ITEMS || !yank_full) @@ -617,6 +643,8 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo int emitted = 0; int index = scroll; + int prev_group = -1; + int cursor_row = -1; while (emitted < visible && index < list->count) { @@ -636,10 +664,15 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo break; } + prev_group = -1; + + if (index == cursor) + cursor_row = 4 + emitted; + int level = section_level(item); int section_indent = LEFT_MARGIN + (level - 1) * SECTION_INDENT; - if (mode == MODE_INSERT && index == cursor) + if (is_editing(mode) && index == cursor) { length += snprintf(buffer + length, sizeof buffer - (size_t) length, "%s%*s\x1b[1m%s\x1b[22m\x1b[K%s\r\n", @@ -705,7 +738,7 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo int indent = base_indent + number_width + SUBTASK_INDENT; const char* name = item->text; - if (!(mode == MODE_INSERT && index == cursor)) + if (!(is_editing(mode) && index == cursor)) { name = item->text + 1; @@ -713,22 +746,54 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo name++; } + if (index == cursor) + cursor_row = 4 + emitted; + 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 group = task_group(item); + + if (prev_group >= 0 && group != prev_group) + { + length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); + emitted++; + + if (emitted >= visible) + break; + } + + prev_group = group; + int number = 0; for (int k = index; k >= 0 && !is_section(&list->items[k]); k--) if (!is_subtask(&list->items[k])) number++; + const char* task_box = is_optional(item) + ? (item->state == STATE_DONE ? "(x)" : item->state == STATE_DOING ? "(-)" : "( )") + : box; + const char* name = item->text; + + if (is_optional(item) && !(is_editing(mode) && index == cursor)) + { + name = item->text + 1; + + while (*name == ' ') + name++; + } + + if (index == cursor) + cursor_row = 4 + emitted; + 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, base_indent, "", number_width, number, - box, dim, item->text, dim_reset, background_reset); + task_box, dim, name, dim_reset, background_reset); } emitted++; @@ -762,24 +827,15 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[%d;%dH\x1b[6 q\x1b[?25h", rows, cursor_column); } - else if (mode != MODE_CONFIRM && cursor >= 0 && cursor < list->count) + else if (mode != MODE_CONFIRM && cursor >= 0 && cursor < list->count && cursor_row >= 0) { - int rows_before = 0; - - for (int i = scroll; i < cursor; i++) - rows_before += is_section(&list->items[i]) && i > scroll ? 2 : 1; - - if (is_section(&list->items[cursor]) && cursor > scroll) - rows_before++; - - int cursor_row = 4 + rows_before; int cursor_column; if (is_section(&list->items[cursor])) { int level = section_level(&list->items[cursor]); int section_indent = LEFT_MARGIN + (level - 1) * SECTION_INDENT; - cursor_column = mode == MODE_INSERT ? section_indent + 1 + edit_pos : section_indent + 1; + cursor_column = is_editing(mode) ? section_indent + 1 + edit_pos : section_indent + 1; } else { @@ -803,7 +859,9 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo cursor_column = text_start + edit_pos; } - const char* shape = mode == MODE_INSERT ? "\x1b[6 q" : "\x1b[2 q"; + const char* shape = mode == MODE_REPLACE ? "\x1b[4 q" + : mode == MODE_INSERT ? "\x1b[6 q" + : "\x1b[2 q"; length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[%d;%dH%s\x1b[?25h", cursor_row, cursor_column, shape); @@ -1075,13 +1133,14 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit { insert_item(list, 0); *cursor = 0; + *edit_pos = 0; } if (*cursor >= 0 && *cursor < list->count) { - list->items[*cursor].text[0] = '\0'; - *edit_pos = 0; - *mode = MODE_INSERT; + (void) snprintf(replace_original, sizeof replace_original, "%s", list->items[*cursor].text); + replace_start = *edit_pos; + *mode = MODE_REPLACE; } break; @@ -1277,7 +1336,19 @@ static void insert_key(struct List* list, int* cursor, enum Mode* mode, int* edi if (key == 127 || key == 8) { - if (*edit_pos > 0) + if (*mode == MODE_REPLACE) + { + if (*edit_pos > replace_start) + { + (*edit_pos)--; + + if (*edit_pos < (int) strlen(replace_original)) + item->text[*edit_pos] = replace_original[*edit_pos]; + else + item->text[*edit_pos] = '\0'; + } + } + else if (*edit_pos > 0) { memmove(&item->text[*edit_pos - 1], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); (*edit_pos)--; @@ -1286,10 +1357,18 @@ static void insert_key(struct List* list, int* cursor, enum Mode* mode, int* edi return; } - if (key >= 32 && key <= 126 && length + 1 < sizeof item->text) + if (key >= 32 && key != 127 && key < 256 && 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; + if (*mode == MODE_REPLACE && *edit_pos < (int) length) + { + item->text[*edit_pos] = (char) key; + } + else + { + memmove(&item->text[*edit_pos + 1], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); + item->text[*edit_pos] = (char) key; + } + (*edit_pos)++; } } @@ -1341,7 +1420,7 @@ static int command_key(struct List* list, const char* path, enum Mode* mode, cha return 1; } - if (key >= 32 && key <= 126 && length + 1 < MAX_COMMAND) + if (key >= 32 && key != 127 && key < 256 && length + 1 < MAX_COMMAND) { command[length] = (char) key; command[length + 1] = '\0'; @@ -1385,7 +1464,7 @@ static void search_key(const struct List* list, int* cursor, enum Mode* mode, ch return; } - if (key >= 32 && key <= 126 && length + 1 < MAX_COMMAND) + if (key >= 32 && key != 127 && key < 256 && length + 1 < MAX_COMMAND) { command[length] = (char) key; command[length + 1] = '\0'; @@ -1451,7 +1530,7 @@ static void run(struct List* list, const char* path) 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); + int caret_max = is_editing(mode) ? text_length : (text_length > 0 ? text_length - 1 : 0); if (edit_pos > caret_max) edit_pos = caret_max; @@ -1485,6 +1564,7 @@ static void run(struct List* list, const char* path) break; case MODE_INSERT: + case MODE_REPLACE: insert_key(list, &cursor, &mode, &edit_pos, key); break; @@ -1522,13 +1602,13 @@ static void print_help(void) "keys:\n" " h j k l / arrows move w / b words { / } sections\n" " gg / G top/bottom o new item i insert at cursor\n" - " A append text R replace text 3j 2dd counts\n" + " A append text R overtype (replace) 3j 2dd counts\n" " x toggle done space cycle state\n" " dd cut item D clear done yy p P yank/paste\n" " J / K move item or section / n N search u ctrl-r undo/redo\n" " :w :q :wq :x write / quit :reset all back to todo :clear empty\n" "\n" - "items: '= name' section, '== name' subsection, '- name' subtask, else a task.\n" + "items: '=' section, '==' subsection, '-' subtask, '*' optional, else a task.\n" "\n" "by hachem <im@hachem.wtf>\n", TODO_PROGRAM, TODO_VERSION, TODO_PROGRAM, TODO_PROGRAM, TODO_PROGRAM); |
