hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-29 16:28:44 +0200
committerhachem <im@hachem.wtf>2026-09-29 16:28:44 +0200
commit50b8d6f791c91d2881f1aa16c95237c980303712 (patch)
tree371e172df8cdac110d3889b6c1c4b968489117a1
parent992f807b36d4f7cae5187110980d50c593967e82 (diff)
feat: modal todo list
-rw-r--r--todo.c280
1 files changed, 221 insertions, 59 deletions
diff --git a/todo.c b/todo.c
index 802c705..4c87bcf 100644
--- a/todo.c
+++ b/todo.c
@@ -1,10 +1,4 @@
-/*
- * todo - a little modal todo list.
- *
- * Author: hachem <im@hachem.wtf>
- * Version: 0.1.0
- */
-
+#include <ctype.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
@@ -19,9 +13,16 @@
#define max_items 4096
#define max_text 256
+enum State
+{
+ STATE_TODO,
+ STATE_DOING,
+ STATE_DONE
+};
+
struct Item
{
- int done;
+ enum State state;
char text[max_text];
};
@@ -33,44 +34,44 @@ struct List
enum Mode
{
- mode_normal
+ MODE_NORMAL,
+ MODE_INSERT
};
static const char* mode_name(enum Mode mode)
{
switch (mode)
{
- case mode_normal: return "NORMAL";
+ case MODE_NORMAL: return "NORMAL";
+ case MODE_INSERT: return "INSERT";
}
return "?";
}
-static void todo_path(char* buf, size_t size)
+static void todo_path(char* buffer, size_t size)
{
- const char* env = getenv("TODO_FILE");
+ const char* env_path = getenv("TODO_FILE");
- if (env != NULL && env[0] != '\0')
+ if (env_path != NULL && env_path[0] != '\0')
{
- (void) snprintf(buf, size, "%s", env);
+ (void) snprintf(buffer, size, "%s", env_path);
return;
}
const char* home = getenv("HOME");
- (void) snprintf(buf, size, "%s/.todo.txt", home != NULL ? home : ".");
+ (void) snprintf(buffer, size, "%s/.todo.txt", home != NULL ? home : ".");
}
-static void strip_newline(char* s)
+static void strip_newline(char* string)
{
- size_t len = strlen(s);
+ size_t length = strlen(string);
- while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r'))
- s[--len] = '\0';
+ while (length > 0 && (string[length - 1] == '\n' || string[length - 1] == '\r'))
+ string[--length] = '\0';
}
-/* read the list file. each line is "[ ] text" or "[x] text"; anything else is
- * treated as an undone item so hand-edited files still load. */
static void load(struct List* list, const char* path)
{
list->count = 0;
@@ -86,12 +87,16 @@ static void load(struct List* list, const char* path)
{
strip_newline(line);
- int done = 0;
+ enum State state = STATE_TODO;
char* text = line;
if (line[0] == '[' && line[1] != '\0' && line[2] == ']')
{
- done = line[1] == 'x' || line[1] == 'X';
+ if (line[1] == 'x' || line[1] == 'X')
+ state = STATE_DONE;
+ else if (line[1] == '-')
+ state = STATE_DOING;
+
text = line + 3;
while (*text == ' ')
@@ -102,7 +107,7 @@ static void load(struct List* list, const char* path)
continue;
struct Item* item = &list->items[list->count++];
- item->done = done;
+ item->state = state;
(void) snprintf(item->text, sizeof item->text, "%s", text);
}
@@ -117,11 +122,47 @@ static void save(const struct List* list, const char* path)
return;
for (int i = 0; i < list->count; i++)
- (void) fprintf(file, "[%c] %s\n", list->items[i].done ? 'x' : ' ', list->items[i].text);
+ {
+ char marker = list->items[i].state == STATE_DONE ? 'x'
+ : list->items[i].state == STATE_DOING ? '-'
+ : ' ';
+
+ (void) fprintf(file, "[%c] %s\n", marker, list->items[i].text);
+ }
(void) fclose(file);
}
+static void insert_item(struct List* list, int index)
+{
+ if (list->count >= max_items)
+ return;
+
+ if (index < 0)
+ index = 0;
+
+ if (index > list->count)
+ index = list->count;
+
+ for (int i = list->count; i > index; i--)
+ list->items[i] = list->items[i - 1];
+
+ list->items[index].state = STATE_TODO;
+ list->items[index].text[0] = '\0';
+ list->count++;
+}
+
+static void delete_item(struct List* list, int index)
+{
+ if (index < 0 || index >= list->count)
+ return;
+
+ for (int i = index; i < list->count - 1; i++)
+ list->items[i] = list->items[i + 1];
+
+ list->count--;
+}
+
static struct termios saved_termios;
static void restore_terminal(void)
@@ -130,9 +171,9 @@ static void restore_terminal(void)
(void) write(STDOUT_FILENO, "\x1b[?25h\x1b[?1049l", 14);
}
-static void on_signal(int signum)
+static void on_signal(int signal_number)
{
- (void) signum;
+ (void) signal_number;
exit(EXIT_SUCCESS);
}
@@ -166,10 +207,10 @@ static int enter_raw_mode(void)
static int screen_rows(void)
{
- struct winsize ws;
+ struct winsize window;
- if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0)
- return ws.ws_row;
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) == 0 && window.ws_row > 0)
+ return window.ws_row;
return 24;
}
@@ -182,44 +223,73 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
if (visible < 1)
visible = 1;
- char buf[64 * 1024];
- int n = 0;
+ int number_width = 1;
- n += snprintf(buf + n, sizeof buf - (size_t) n, "\x1b[H");
+ for (int total = list->count; total >= 10; total /= 10)
+ number_width++;
+
+ char buffer[64 * 1024];
+ int length = 0;
+
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[H");
for (int row = 0; row < visible; row++)
{
- int i = scroll + row;
+ int index = scroll + row;
- if (i >= list->count)
+ if (index >= list->count)
{
- n += snprintf(buf + n, sizeof buf - (size_t) n, "\x1b[K\r\n");
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n");
continue;
}
- const struct Item* item = &list->items[i];
- const char* highlight = i == cursor ? "\x1b[7m" : "";
- const char* box = item->done ? "\x1b[32m[x]\x1b[0m" : "[ ]";
- const char* dim = item->done ? "\x1b[2m" : "";
+ const struct Item* item = &list->items[index];
+
+ const char* background = index == cursor ? "\x1b[48;5;235m" : "";
+ const char* background_reset = index == cursor ? "\x1b[49m" : "";
+ const char* box_color = item->state == STATE_DONE ? "\x1b[32m"
+ : item->state == STATE_DOING ? "\x1b[33m"
+ : "";
+ const char* box_reset = item->state == STATE_TODO ? "" : "\x1b[39m";
+ const char* box = item->state == STATE_DONE ? "⌜x⌟"
+ : item->state == STATE_DOING ? "⌜-⌟"
+ : "⌜ ⌟";
+ const char* dim = item->state == STATE_DONE ? "\x1b[2m" : "";
+ const char* dim_reset = item->state == STATE_DONE ? "\x1b[22m" : "";
- n += snprintf(buf + n, sizeof buf - (size_t) n,
- "%s %s %s%s\x1b[0m\x1b[K\r\n", highlight, box, dim, item->text);
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length,
+ "%s\x1b[2m%*d.\x1b[22m %s%s%s %s%s%s\x1b[K%s\r\n",
+ background, number_width, index + 1,
+ box_color, box, box_reset, dim, item->text, dim_reset, background_reset);
}
int done = 0;
for (int i = 0; i < list->count; i++)
- done += list->items[i].done;
+ done += list->items[i].state == STATE_DONE;
- n += snprintf(buf + n, sizeof buf - (size_t) n,
- "\x1b[2m-- %s -- %d/%d done\x1b[0m\x1b[K\x1b[J", mode_name(mode), done, list->count);
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length,
+ "\x1b[2m-- %s -- %d/%d done\x1b[0m\x1b[K\x1b[J", mode_name(mode), done, list->count);
- (void) write(STDOUT_FILENO, buf, (size_t) n);
+ if (mode == MODE_INSERT && cursor >= 0 && cursor < list->count)
+ {
+ int cursor_row = cursor - scroll + 1;
+ int cursor_column = number_width + 7 + (int) strlen(list->items[cursor].text);
+
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length,
+ "\x1b[%d;%dH\x1b[?25h", cursor_row, cursor_column);
+ }
+ else
+ {
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[?25l");
+ }
+
+ (void) write(STDOUT_FILENO, buffer, (size_t) length);
}
-static int normal_key(struct List* list, const char* path, int* cursor, char c)
+static int normal_key(struct List* list, const char* path, int* cursor, enum Mode* mode, char character)
{
- switch (c)
+ switch (character)
{
case 'q':
return 0;
@@ -233,15 +303,60 @@ static int normal_key(struct List* list, const char* path, int* cursor, char c)
break;
case ' ':
+ if (*cursor >= 0 && *cursor < list->count)
+ {
+ struct Item* item = &list->items[*cursor];
+ item->state = (enum State) ((item->state + 1) % 3);
+ save(list, path);
+ }
+
+ break;
+
case 'x':
if (*cursor >= 0 && *cursor < list->count)
{
- list->items[*cursor].done = !list->items[*cursor].done;
+ struct Item* item = &list->items[*cursor];
+ item->state = item->state == STATE_DONE ? STATE_TODO : STATE_DONE;
save(list, path);
}
break;
+ case 'a':
+ case 'o':
+ case 'i':
+ insert_item(list, *cursor + 1);
+ *cursor = *cursor + 1 < list->count ? *cursor + 1 : list->count - 1;
+ *mode = MODE_INSERT;
+ break;
+
+ case 'A':
+ if (list->count == 0)
+ {
+ insert_item(list, 0);
+ *cursor = 0;
+ }
+
+ if (*cursor >= 0 && *cursor < list->count)
+ *mode = MODE_INSERT;
+
+ break;
+
+ case 'R':
+ if (list->count == 0)
+ {
+ insert_item(list, 0);
+ *cursor = 0;
+ }
+
+ if (*cursor >= 0 && *cursor < list->count)
+ {
+ list->items[*cursor].text[0] = '\0';
+ *mode = MODE_INSERT;
+ }
+
+ break;
+
default:
break;
}
@@ -249,12 +364,58 @@ static int normal_key(struct List* list, const char* path, int* cursor, char c)
return 1;
}
+static void insert_key(struct List* list, const char* path, int* cursor, enum Mode* mode, char character)
+{
+ struct Item* item = &list->items[*cursor];
+ size_t length = strlen(item->text);
+
+ if (character == 27 || ((character == '\r' || character == '\n') && item->text[0] == '\0'))
+ {
+ if (item->text[0] == '\0')
+ {
+ delete_item(list, *cursor);
+
+ if (*cursor >= list->count)
+ *cursor = list->count - 1;
+
+ if (*cursor < 0)
+ *cursor = 0;
+ }
+
+ save(list, path);
+ *mode = MODE_NORMAL;
+ return;
+ }
+
+ if (character == '\r' || character == '\n')
+ {
+ save(list, path);
+ insert_item(list, *cursor + 1);
+ (*cursor)++;
+ return;
+ }
+
+ if (character == 127 || character == 8)
+ {
+ if (length > 0)
+ item->text[length - 1] = '\0';
+
+ return;
+ }
+
+ if (isprint((unsigned char) character) && length + 1 < sizeof item->text)
+ {
+ item->text[length] = character;
+ item->text[length + 1] = '\0';
+ }
+}
+
static void run(struct List* list, const char* path)
{
if (enter_raw_mode() != 0)
exit(EXIT_FAILURE);
- enum Mode mode = mode_normal;
+ enum Mode mode = MODE_NORMAL;
int cursor = 0;
int scroll = 0;
@@ -279,22 +440,23 @@ static void run(struct List* list, const char* path)
render(list, cursor, scroll, mode);
- char c;
+ char character;
- if (read(STDIN_FILENO, &c, 1) != 1)
+ if (read(STDIN_FILENO, &character, 1) != 1)
break;
- int keep = 1;
-
switch (mode)
{
- case mode_normal:
- keep = normal_key(list, path, &cursor, c);
+ case MODE_NORMAL:
+ if (normal_key(list, path, &cursor, &mode, character) == 0)
+ return;
+
break;
- }
- if (!keep)
- break;
+ case MODE_INSERT:
+ insert_key(list, path, &cursor, &mode, character);
+ break;
+ }
}
}