hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-30 07:55:30 +0200
committerhachem <im@hachem.wtf>2026-09-30 07:55:30 +0200
commit036ab6838fd7cc4bd95ca3253d96c721fa23fd54 (patch)
tree39c833dd285ba6d3e47c6002da4b0986d6cdc381
parente75e6312b3d75238fdbe24787f4fcce0f17f8574 (diff)
feat: add optinal tasks + proper replace
-rw-r--r--README.md5
-rw-r--r--todo.116
-rw-r--r--todo.c138
3 files changed, 126 insertions, 33 deletions
diff --git a/README.md b/README.md
index e487077..dc83cca 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/todo.1 b/todo.1
index 9deb04e..c9eb00f 100644
--- a/todo.1
+++ b/todo.1
@@ -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
diff --git a/todo.c b/todo.c
index a4084a2..738d506 100644
--- a/todo.c
+++ b/todo.c
@@ -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);