hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-29 18:49:30 +0200
committerhachem <im@hachem.wtf>2026-09-29 18:49:30 +0200
commita172a638591614794a39c494daaba35eb78a91c3 (patch)
treea9ba704093984d98d5298e4654d411840cbf5fa5
parent7a61791c5e87796b1984d13d70d5bafc1953bbb3 (diff)
feat: add command mode
-rw-r--r--todo.c252
1 files changed, 211 insertions, 41 deletions
diff --git a/todo.c b/todo.c
index 128a370..1fab14f 100644
--- a/todo.c
+++ b/todo.c
@@ -1,3 +1,4 @@
+#include <locale.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
@@ -12,6 +13,7 @@
#define MAX_ITEMS 4096
#define MAX_TEXT 256
+#define MAX_COMMAND 64
#define LEFT_MARGIN 2
#define SECTION_INDENT 2
@@ -39,7 +41,8 @@ struct List
enum Mode
{
MODE_NORMAL,
- MODE_INSERT
+ MODE_INSERT,
+ MODE_COMMAND
};
enum Key
@@ -56,6 +59,7 @@ static const char* mode_name(enum Mode mode)
{
case MODE_NORMAL: return "NORMAL";
case MODE_INSERT: return "INSERT";
+ case MODE_COMMAND: return "COMMAND";
}
return "?";
@@ -136,6 +140,16 @@ static int is_subtask(const struct Item* item)
return item->text[0] == '-';
}
+static int section_level(const struct Item* item)
+{
+ int level = 0;
+
+ while (item->text[level] == '=')
+ level++;
+
+ return level;
+}
+
static void normalize_tasks(struct List* list)
{
for (int i = 0; i < list->count; i++)
@@ -169,6 +183,64 @@ static void normalize_tasks(struct List* list)
}
}
+static void sort_list(struct List* list)
+{
+ struct Item* sorted = malloc((size_t) list->count * sizeof *sorted);
+
+ if (sorted == NULL)
+ return;
+
+ int out = 0;
+ int i = 0;
+
+ while (i < list->count)
+ {
+ if (is_section(&list->items[i]))
+ {
+ sorted[out++] = list->items[i];
+ i++;
+ continue;
+ }
+
+ int segment = i;
+
+ while (segment < list->count && !is_section(&list->items[segment]))
+ segment++;
+
+ for (int pass = 0; pass < 2; pass++)
+ {
+ int k = i;
+
+ while (k < segment)
+ {
+ int block = k + 1;
+
+ while (block < segment && is_subtask(&list->items[block]))
+ block++;
+
+ if ((list->items[k].state == STATE_DONE) == pass)
+ {
+ sorted[out++] = list->items[k];
+
+ for (int sub_pass = 0; sub_pass < 2; sub_pass++)
+ for (int s = k + 1; s < block; s++)
+ if ((list->items[s].state == STATE_DONE) == sub_pass)
+ sorted[out++] = list->items[s];
+ }
+
+ k = block;
+ }
+ }
+
+ i = segment;
+ }
+
+ for (int x = 0; x < list->count; x++)
+ list->items[x] = sorted[x];
+
+ free(sorted);
+}
+
static int block_end(const struct List* list, int start)
{
int i = start + 1;
@@ -218,6 +290,12 @@ static void save(struct List* list, const char* path)
(void) fclose(file);
}
+static void commit(struct List* list)
+{
+ normalize_tasks(list);
+ sort_list(list);
+}
+
static void insert_item(struct List* list, int index)
{
if (list->count >= MAX_ITEMS)
@@ -326,7 +404,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, int edit_pos)
+static void render(const struct List* list, int cursor, int scroll, enum Mode mode, int edit_pos, const char* command)
{
int rows = screen_rows();
int visible = content_rows();
@@ -391,31 +469,46 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
break;
}
+ int level = section_level(item);
+ int section_indent = LEFT_MARGIN + (level - 1) * SECTION_INDENT;
+
if (mode == MODE_INSERT && index == cursor)
{
length += snprintf(buffer + length, sizeof buffer - (size_t) length,
"%s%*s\x1b[1m%s\x1b[22m\x1b[K%s\r\n",
- background, LEFT_MARGIN, "", item->text, background_reset);
+ background, section_indent, "", item->text, background_reset);
}
else
{
int section_number = 0;
- for (int k = 0; k <= index; k++)
+ for (int k = index; k >= 0; k--)
if (is_section(&list->items[k]))
- section_number++;
+ {
+ int other = section_level(&list->items[k]);
+
+ if (other == level)
+ section_number++;
+ else if (other < level)
+ break;
+ }
char roman[16];
to_roman(section_number, roman, sizeof roman);
- const char* name = item->text + 1;
+ if (level >= 2)
+ for (char* letter = roman; *letter != '\0'; letter++)
+ if (*letter >= 'A' && *letter <= 'Z')
+ *letter = (char) (*letter + 32);
+
+ const char* name = item->text + level;
while (*name == ' ')
name++;
length += snprintf(buffer + length, sizeof buffer - (size_t) length,
"%s%*s\x1b[1m%s. %s\x1b[22m\x1b[K%s\r\n",
- background, LEFT_MARGIN, "", roman, name, background_reset);
+ background, section_indent, "", roman, name, background_reset);
}
emitted++;
@@ -423,12 +516,16 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
continue;
}
- int scan = index - 1;
+ int heading_level = 0;
- while (scan >= 0 && !is_section(&list->items[scan]))
- scan--;
+ for (int k = index - 1; k >= 0; k--)
+ if (is_section(&list->items[k]))
+ {
+ heading_level = section_level(&list->items[k]);
+ break;
+ }
- int in_section = scan >= 0;
+ int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT;
const char* box = item->state == STATE_DONE ? "[x]"
: item->state == STATE_DOING ? "[-]"
@@ -438,7 +535,7 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
if (is_subtask(item))
{
- int indent = LEFT_MARGIN + (in_section ? SECTION_INDENT : 0) + SUBTASK_INDENT;
+ int indent = base_indent + SUBTASK_INDENT;
const char* name = item->text;
if (!(mode == MODE_INSERT && index == cursor))
@@ -461,11 +558,9 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
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,
+ background, base_indent, "", number_width, number,
box, dim, item->text, dim_reset, background_reset);
}
@@ -479,11 +574,22 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
emitted++;
}
- length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "\x1b[%d;1H%*s\x1b[2m-- %s -- %d/%d done\x1b[0m\x1b[K",
- rows, LEFT_MARGIN, "", mode_name(mode), done, total_items);
+ if (mode == MODE_COMMAND)
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length,
+ "\x1b[%d;1H%*s:%s\x1b[K", rows, LEFT_MARGIN, "", command);
+ else
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length,
+ "\x1b[%d;1H%*s\x1b[2m[%s] %d/%d done\x1b[0m\x1b[K",
+ rows, LEFT_MARGIN, "", mode_name(mode), done, total_items);
+
+ if (mode == MODE_COMMAND)
+ {
+ int cursor_column = LEFT_MARGIN + 2 + (int) strlen(command);
- if (cursor >= 0 && cursor < list->count)
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length,
+ "\x1b[%d;%dH\x1b[6 q\x1b[?25h", rows, cursor_column);
+ }
+ else if (cursor >= 0 && cursor < list->count)
{
int rows_before = 0;
@@ -498,22 +604,28 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
if (is_section(&list->items[cursor]))
{
- cursor_column = mode == MODE_INSERT ? LEFT_MARGIN + 1 + edit_pos : LEFT_MARGIN + 1;
+ 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;
}
else
{
- int scan = cursor - 1;
+ int heading_level = 0;
- while (scan >= 0 && !is_section(&list->items[scan]))
- scan--;
+ for (int k = cursor - 1; k >= 0; k--)
+ if (is_section(&list->items[k]))
+ {
+ heading_level = section_level(&list->items[k]);
+ break;
+ }
- int item_indent = LEFT_MARGIN + (scan >= 0 ? SECTION_INDENT : 0);
+ int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT;
int text_start;
if (is_subtask(&list->items[cursor]))
- text_start = item_indent + SUBTASK_INDENT + 5;
+ text_start = base_indent + SUBTASK_INDENT + 5;
else
- text_start = item_indent + number_width + 7;
+ text_start = base_indent + number_width + 7;
cursor_column = text_start + edit_pos;
}
@@ -567,13 +679,18 @@ static int read_key(void)
return 27;
}
-static int normal_key(struct List* list, const char* path, int* cursor, enum Mode* mode, int* edit_pos, int key)
+static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit_pos, char* command, int key)
{
switch (key)
{
case 'q':
return 0;
+ case ':':
+ command[0] = '\0';
+ *mode = MODE_COMMAND;
+ break;
+
case 'j':
case KEY_DOWN:
(*cursor)++;
@@ -599,7 +716,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
{
struct Item* item = &list->items[*cursor];
item->state = (enum State) ((item->state + 1) % 3);
- save(list, path);
+ commit(list);
}
break;
@@ -609,7 +726,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
{
struct Item* item = &list->items[*cursor];
item->state = item->state == STATE_DONE ? STATE_TODO : STATE_DONE;
- save(list, path);
+ commit(list);
}
break;
@@ -714,7 +831,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
if (read(STDIN_FILENO, &next, 1) == 1 && next == 'd')
{
delete_item(list, *cursor);
- save(list, path);
+ commit(list);
}
break;
@@ -736,7 +853,6 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
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]))
@@ -745,7 +861,6 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
list->items[*cursor] = list->items[*cursor + 1];
list->items[*cursor + 1] = moved;
(*cursor)++;
- save(list, path);
}
break;
@@ -771,7 +886,6 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
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]))
@@ -780,7 +894,6 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
list->items[*cursor] = list->items[*cursor - 1];
list->items[*cursor - 1] = moved;
(*cursor)--;
- save(list, path);
}
break;
@@ -794,7 +907,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
list->items[kept++] = list->items[i];
list->count = kept;
- save(list, path);
+ commit(list);
break;
}
@@ -805,7 +918,7 @@ 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, int* edit_pos, int key)
+static void insert_key(struct List* list, int* cursor, enum Mode* mode, int* edit_pos, int key)
{
struct Item* item = &list->items[*cursor];
size_t length = strlen(item->text);
@@ -823,14 +936,13 @@ static void insert_key(struct List* list, const char* path, int* cursor, enum Mo
*cursor = 0;
}
- save(list, path);
+ normalize_tasks(list);
*mode = MODE_NORMAL;
return;
}
if (key == '\r' || key == '\n')
{
- save(list, path);
insert_item(list, *cursor + 1);
(*cursor)++;
*edit_pos = 0;
@@ -872,17 +984,67 @@ static void insert_key(struct List* list, const char* path, int* cursor, enum Mo
}
}
+static int command_key(struct List* list, const char* path, enum Mode* mode, char* command, int key)
+{
+ size_t length = strlen(command);
+
+ if (key == 27)
+ {
+ command[0] = '\0';
+ *mode = MODE_NORMAL;
+ return 1;
+ }
+
+ if (key == '\r' || key == '\n')
+ {
+ int write = strchr(command, 'w') != NULL || strcmp(command, "x") == 0;
+ int quit = strchr(command, 'q') != NULL || strcmp(command, "x") == 0;
+
+ *mode = MODE_NORMAL;
+
+ if (write)
+ save(list, path);
+
+ command[0] = '\0';
+
+ if (quit)
+ return 0;
+
+ return 1;
+ }
+
+ if (key == 127 || key == 8)
+ {
+ if (length > 0)
+ command[length - 1] = '\0';
+ else
+ *mode = MODE_NORMAL;
+
+ return 1;
+ }
+
+ if (key >= 32 && key <= 126 && length + 1 < MAX_COMMAND)
+ {
+ command[length] = (char) key;
+ command[length + 1] = '\0';
+ }
+
+ return 1;
+}
+
static void run(struct List* list, const char* path)
{
if (enter_raw_mode() != 0)
exit(EXIT_FAILURE);
normalize_tasks(list);
+ sort_list(list);
enum Mode mode = MODE_NORMAL;
int cursor = 0;
int scroll = 0;
int edit_pos = 0;
+ char command[MAX_COMMAND] = "";
for (;;)
{
@@ -912,7 +1074,7 @@ static void run(struct List* list, const char* path)
edit_pos = 0;
}
- render(list, cursor, scroll, mode, edit_pos);
+ render(list, cursor, scroll, mode, edit_pos, command);
int key = read_key();
@@ -922,13 +1084,19 @@ static void run(struct List* list, const char* path)
switch (mode)
{
case MODE_NORMAL:
- if (normal_key(list, path, &cursor, &mode, &edit_pos, key) == 0)
+ if (normal_key(list, &cursor, &mode, &edit_pos, command, key) == 0)
return;
break;
case MODE_INSERT:
- insert_key(list, path, &cursor, &mode, &edit_pos, key);
+ insert_key(list, &cursor, &mode, &edit_pos, key);
+ break;
+
+ case MODE_COMMAND:
+ if (command_key(list, path, &mode, command, key) == 0)
+ return;
+
break;
}
}
@@ -936,6 +1104,8 @@ static void run(struct List* list, const char* path)
int main(void)
{
+ (void) setlocale(LC_ALL, "");
+
char path[4096];
todo_path(path, sizeof path);