hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-29 19:12:57 +0200
committerhachem <im@hachem.wtf>2026-09-29 19:12:57 +0200
commitcdc7348d5fee6a82036ad0b1cb5b3038591cf7ee (patch)
tree197905d5fa7c5402f683b4c636929164836008d4
parenta172a638591614794a39c494daaba35eb78a91c3 (diff)
feat: more qol
-rw-r--r--todo.c213
1 files changed, 192 insertions, 21 deletions
diff --git a/todo.c b/todo.c
index 1fab14f..d4b09ce 100644
--- a/todo.c
+++ b/todo.c
@@ -1,3 +1,4 @@
+#include <fcntl.h>
#include <locale.h>
#include <signal.h>
#include <stdio.h>
@@ -296,6 +297,90 @@ static void commit(struct List* list)
sort_list(list);
}
+#define MAX_HISTORY 64
+
+struct Snapshot
+{
+ struct Item* items;
+ int count;
+};
+
+static struct Snapshot undo_stack[MAX_HISTORY];
+static struct Snapshot redo_stack[MAX_HISTORY];
+static int undo_count;
+static int redo_count;
+
+static struct Snapshot snapshot_take(const struct List* list)
+{
+ struct Snapshot snap;
+ snap.count = list->count;
+ snap.items = malloc((size_t) list->count * sizeof *snap.items);
+
+ if (snap.items != NULL)
+ memcpy(snap.items, list->items, (size_t) list->count * sizeof *snap.items);
+
+ return snap;
+}
+
+static void snapshot_apply(struct List* list, const struct Snapshot* snap)
+{
+ list->count = snap->count;
+ memcpy(list->items, snap->items, (size_t) snap->count * sizeof *snap->items);
+}
+
+static void stack_push(struct Snapshot* stack, int* count, struct Snapshot snap)
+{
+ if (*count == MAX_HISTORY)
+ {
+ free(stack[0].items);
+ memmove(&stack[0], &stack[1], (size_t) (MAX_HISTORY - 1) * sizeof *stack);
+ (*count)--;
+ }
+
+ stack[(*count)++] = snap;
+}
+
+static void stack_clear(struct Snapshot* stack, int* count)
+{
+ while (*count > 0)
+ free(stack[--(*count)].items);
+}
+
+static void history_record(struct List* list)
+{
+ struct Snapshot snap = snapshot_take(list);
+
+ if (snap.items == NULL)
+ return;
+
+ stack_push(undo_stack, &undo_count, snap);
+ stack_clear(redo_stack, &redo_count);
+}
+
+static void history_undo(struct List* list)
+{
+ if (undo_count == 0)
+ return;
+
+ stack_push(redo_stack, &redo_count, snapshot_take(list));
+
+ struct Snapshot snap = undo_stack[--undo_count];
+ snapshot_apply(list, &snap);
+ free(snap.items);
+}
+
+static void history_redo(struct List* list)
+{
+ if (redo_count == 0)
+ return;
+
+ stack_push(undo_stack, &undo_count, snapshot_take(list));
+
+ struct Snapshot snap = redo_stack[--redo_count];
+ snapshot_apply(list, &snap);
+ free(snap.items);
+}
+
static void insert_item(struct List* list, int index)
{
if (list->count >= MAX_ITEMS)
@@ -383,7 +468,7 @@ static int screen_rows(void)
static int content_rows(void)
{
- int rows = screen_rows() - 3;
+ int rows = screen_rows() - 4;
return rows < 1 ? 1 : rows;
}
@@ -436,17 +521,22 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
number_width++;
char date[64];
+ char clock[32];
time_t now = time(NULL);
struct tm* local = localtime(&now);
if (local == NULL || strftime(date, sizeof date, "%a %d %b %Y", local) == 0)
date[0] = '\0';
+ if (local == NULL || strftime(clock, sizeof clock, "%H:%M", local) == 0)
+ clock[0] = '\0';
+
char buffer[64 * 1024];
int length = 0;
length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "\x1b[H%*s\x1b[2m%s\x1b[0m\x1b[K\r\n\x1b[K\r\n", LEFT_MARGIN, "", date);
+ "\x1b[H%*s\x1b[2m%s\x1b[0m\x1b[K\r\n%*s\x1b[2m%s\x1b[0m\x1b[K\r\n\x1b[K\r\n",
+ LEFT_MARGIN, "", date, LEFT_MARGIN, "", clock);
int emitted = 0;
int index = scroll;
@@ -599,7 +689,7 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
if (is_section(&list->items[cursor]) && cursor > scroll)
rows_before++;
- int cursor_row = 3 + rows_before;
+ int cursor_row = 4 + rows_before;
int cursor_column;
if (is_section(&list->items[cursor]))
@@ -645,6 +735,15 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
static int read_key(void)
{
+ static int pending = -1;
+
+ if (pending != -1)
+ {
+ int key = pending;
+ pending = -1;
+ return key;
+ }
+
char character;
if (read(STDIN_FILENO, &character, 1) != 1)
@@ -653,29 +752,40 @@ static int read_key(void)
if (character != 27)
return (unsigned char) character;
- struct termios current;
- tcgetattr(STDIN_FILENO, &current);
+ /* an escape: peek for an arrow sequence without blocking. a byte that is
+ * not part of a sequence is pushed back so a lone escape followed by a
+ * command (e.g. :wq) never swallows the following keys. */
+ int flags = fcntl(STDIN_FILENO, F_GETFL);
+ (void) fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
- struct termios timed = current;
- timed.c_cc[VMIN] = 0;
- timed.c_cc[VTIME] = 1;
- (void) tcsetattr(STDIN_FILENO, TCSANOW, &timed);
+ char first;
- char sequence[2];
- ssize_t first = read(STDIN_FILENO, &sequence[0], 1);
- ssize_t second = first == 1 ? read(STDIN_FILENO, &sequence[1], 1) : 0;
+ if (read(STDIN_FILENO, &first, 1) != 1)
+ {
+ (void) fcntl(STDIN_FILENO, F_SETFL, flags);
+ return 27;
+ }
- (void) tcsetattr(STDIN_FILENO, TCSANOW, &current);
+ if (first == '[')
+ {
+ char second;
+ ssize_t got = read(STDIN_FILENO, &second, 1);
+ (void) fcntl(STDIN_FILENO, F_SETFL, flags);
- 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;
- }
+ if (got == 1)
+ switch (second)
+ {
+ case 'A': return KEY_UP;
+ case 'B': return KEY_DOWN;
+ case 'C': return KEY_RIGHT;
+ case 'D': return KEY_LEFT;
+ }
+ return 27;
+ }
+
+ (void) fcntl(STDIN_FILENO, F_SETFL, flags);
+ pending = (unsigned char) first;
return 27;
}
@@ -711,9 +821,56 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
(*edit_pos)++;
break;
+ case 'w':
+ if (*cursor >= 0 && *cursor < list->count)
+ {
+ const char* text = list->items[*cursor].text;
+ int length = (int) strlen(text);
+ int position = *edit_pos;
+
+ while (position < length && text[position] != ' ')
+ position++;
+
+ while (position < length && text[position] == ' ')
+ position++;
+
+ *edit_pos = position;
+ }
+
+ break;
+
+ case 'b':
+ if (*cursor >= 0 && *cursor < list->count)
+ {
+ const char* text = list->items[*cursor].text;
+ int position = *edit_pos;
+
+ if (position > 0)
+ position--;
+
+ while (position > 0 && text[position] == ' ')
+ position--;
+
+ while (position > 0 && text[position - 1] != ' ')
+ position--;
+
+ *edit_pos = position;
+ }
+
+ break;
+
+ case 'u':
+ history_undo(list);
+ break;
+
+ case 18:
+ history_redo(list);
+ break;
+
case ' ':
if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor]))
{
+ history_record(list);
struct Item* item = &list->items[*cursor];
item->state = (enum State) ((item->state + 1) % 3);
commit(list);
@@ -724,6 +881,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
case 'x':
if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor]))
{
+ history_record(list);
struct Item* item = &list->items[*cursor];
item->state = item->state == STATE_DONE ? STATE_TODO : STATE_DONE;
commit(list);
@@ -733,6 +891,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
case 'a':
case 'o':
+ history_record(list);
insert_item(list, *cursor + 1);
*cursor = *cursor + 1 < list->count ? *cursor + 1 : list->count - 1;
*edit_pos = 0;
@@ -740,6 +899,8 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
break;
case 'A':
+ history_record(list);
+
if (list->count == 0)
{
insert_item(list, 0);
@@ -757,6 +918,8 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
case 'i':
if (*cursor >= 0 && *cursor < list->count)
{
+ history_record(list);
+
if (is_section(&list->items[*cursor]))
{
int position = 1;
@@ -773,6 +936,8 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
break;
case 'R':
+ history_record(list);
+
if (list->count == 0)
{
insert_item(list, 0);
@@ -830,6 +995,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
if (read(STDIN_FILENO, &next, 1) == 1 && next == 'd')
{
+ history_record(list);
delete_item(list, *cursor);
commit(list);
}
@@ -841,6 +1007,8 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
if (*cursor < 0 || *cursor >= list->count)
break;
+ history_record(list);
+
if (is_section(&list->items[*cursor]))
{
int start = *cursor;
@@ -869,6 +1037,8 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
if (*cursor < 0 || *cursor >= list->count)
break;
+ history_record(list);
+
if (is_section(&list->items[*cursor]))
{
int start = *cursor;
@@ -900,6 +1070,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
case 'D':
{
+ history_record(list);
int kept = 0;
for (int i = 0; i < list->count; i++)