hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/todo.c
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-29 18:23:48 +0200
committerhachem <im@hachem.wtf>2026-09-29 18:23:48 +0200
commit7a61791c5e87796b1984d13d70d5bafc1953bbb3 (patch)
treedf304814e12e6ebcc72ef92e16a4f1c3b2084392 /todo.c
parent571d9a642b10c77c4ec5b91c0ccc7597ea7f4490 (diff)
feat: qol
Diffstat (limited to 'todo.c')
-rw-r--r--todo.c323
1 files changed, 285 insertions, 38 deletions
diff --git a/todo.c b/todo.c
index 7902fa3..128a370 100644
--- a/todo.c
+++ b/todo.c
@@ -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, &current);
+
+ 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, &current);
+
+ 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;
}
}