hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-29 17:43:37 +0200
committerhachem <im@hachem.wtf>2026-09-29 17:43:37 +0200
commit571d9a642b10c77c4ec5b91c0ccc7597ea7f4490 (patch)
tree79f1d03acf9353d7a6909309b077bf856a63fe38
parenta90c210784ec9e3b4115b9f46c1fc3664340f0ae (diff)
feat: add sections
-rw-r--r--todo.c210
1 files changed, 170 insertions, 40 deletions
diff --git a/todo.c b/todo.c
index 12f5d93..7902fa3 100644
--- a/todo.c
+++ b/todo.c
@@ -8,11 +8,14 @@
#include <time.h>
#include <unistd.h>
-#define todo_program "todo"
-#define todo_version "0.1.0"
+#define TODO_PROGRAM "todo"
+#define TODO_VERSION "0.1.0"
-#define max_items 4096
-#define max_text 256
+#define MAX_ITEMS 4096
+#define MAX_TEXT 256
+
+#define LEFT_MARGIN 2
+#define SECTION_INDENT 2
enum State
{
@@ -24,12 +27,12 @@ enum State
struct Item
{
enum State state;
- char text[max_text];
+ char text[MAX_TEXT];
};
struct List
{
- struct Item items[max_items];
+ struct Item items[MAX_ITEMS];
int count;
};
@@ -82,9 +85,9 @@ static void load(struct List* list, const char* path)
if (file == NULL)
return;
- char line[max_text + 8];
+ char line[MAX_TEXT + 8];
- while (list->count < max_items && fgets(line, sizeof line, file) != NULL)
+ while (list->count < MAX_ITEMS && fgets(line, sizeof line, file) != NULL)
{
strip_newline(line);
@@ -115,6 +118,11 @@ static void load(struct List* list, const char* path)
(void) fclose(file);
}
+static int is_section(const struct Item* item)
+{
+ return item->text[0] == '=';
+}
+
static void save(const struct List* list, const char* path)
{
FILE* file = fopen(path, "w");
@@ -124,6 +132,12 @@ static void save(const struct List* list, const char* path)
for (int i = 0; i < list->count; i++)
{
+ if (is_section(&list->items[i]))
+ {
+ (void) fprintf(file, "%s\n", list->items[i].text);
+ continue;
+ }
+
char marker = list->items[i].state == STATE_DONE ? 'x'
: list->items[i].state == STATE_DOING ? '-'
: ' ';
@@ -136,7 +150,7 @@ static void save(const struct List* list, const char* path)
static void insert_item(struct List* list, int index)
{
- if (list->count >= max_items)
+ if (list->count >= MAX_ITEMS)
return;
if (index < 0)
@@ -164,17 +178,15 @@ static void delete_item(struct List* list, int index)
list->count--;
}
-static int is_separator(const struct Item* item)
-{
- return item->text[0] == '\0';
-}
static struct termios saved_termios;
static void restore_terminal(void)
{
+ static const char reset[] = "\x1b[?25h\x1b[0 q\x1b[?1049l";
+
(void) tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios);
- (void) write(STDOUT_FILENO, "\x1b[?25h\x1b[?1049l", 14);
+ (void) write(STDOUT_FILENO, reset, sizeof reset - 1);
}
static void on_signal(int signal_number)
@@ -187,7 +199,7 @@ static int enter_raw_mode(void)
{
if (!isatty(STDIN_FILENO))
{
- (void) fprintf(stderr, "%s: not a terminal\n", todo_program);
+ (void) fprintf(stderr, "%s: not a terminal\n", TODO_PROGRAM);
return -1;
}
@@ -228,20 +240,52 @@ static int content_rows(void)
return rows < 1 ? 1 : rows;
}
+static void to_roman(int number, char* buffer, size_t size)
+{
+ static const int values[] = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
+ static const char* const symbols[] = { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" };
+
+ size_t length = 0;
+ buffer[0] = '\0';
+
+ for (int i = 0; i < 13; i++)
+ while (number >= values[i] && length + 2 < size)
+ {
+ length += (size_t) snprintf(buffer + length, size - length, "%s", symbols[i]);
+ number -= values[i];
+ }
+}
+
static void render(const struct List* list, int cursor, int scroll, enum Mode mode)
{
int rows = screen_rows();
int visible = content_rows();
- int number_width = 1;
+ int total_items = 0;
+ int done = 0;
- for (int total = list->count; total >= 10; total /= 10)
- number_width++;
+ for (int i = 0; i < list->count; i++)
+ if (!is_section(&list->items[i]))
+ {
+ total_items++;
+ done += list->items[i].state == STATE_DONE;
+ }
- int done = 0;
+ int max_run = 0;
+ int run = 0;
for (int i = 0; i < list->count; i++)
- done += list->items[i].state == STATE_DONE;
+ {
+ if (is_section(&list->items[i]))
+ run = 0;
+ else if (++run > max_run)
+ max_run = run;
+ }
+
+ int number_width = 1;
+
+ for (int count = max_run; count >= 10; count /= 10)
+ number_width++;
char date[64];
time_t now = time(NULL);
@@ -254,22 +298,72 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
int length = 0;
length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "\x1b[H\x1b[2m%s\x1b[0m\x1b[K\r\n\x1b[K\r\n", date);
+ "\x1b[H%*s\x1b[2m%s\x1b[0m\x1b[K\r\n\x1b[K\r\n", LEFT_MARGIN, "", date);
- for (int row = 0; row < visible; row++)
+ int emitted = 0;
+ int index = scroll;
+
+ while (emitted < visible && index < list->count)
{
- int index = scroll + row;
+ const struct Item* item = &list->items[index];
- if (index >= list->count)
+ const char* background = index == cursor ? "\x1b[48;5;235m" : "";
+ const char* background_reset = index == cursor ? "\x1b[49m" : "";
+
+ if (is_section(item))
{
- length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n");
+ if (emitted > 0)
+ {
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n");
+ emitted++;
+
+ if (emitted >= visible)
+ break;
+ }
+
+ 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);
+ }
+ else
+ {
+ int section_number = 0;
+
+ for (int k = 0; k <= index; k++)
+ if (is_section(&list->items[k]))
+ section_number++;
+
+ char roman[16];
+ to_roman(section_number, roman, sizeof roman);
+
+ const char* name = item->text + 1;
+
+ 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);
+ }
+
+ emitted++;
+ index++;
continue;
}
- const struct Item* item = &list->items[index];
+ int number = 0;
+ int scan = index;
+
+ while (scan >= 0 && !is_section(&list->items[scan]))
+ {
+ number++;
+ scan--;
+ }
+
+ int item_indent = LEFT_MARGIN + (scan >= 0 ? SECTION_INDENT : 0);
- const char* background = index == cursor ? "\x1b[48;5;235m" : "";
- const char* background_reset = index == cursor ? "\x1b[49m" : "";
const char* box = item->state == STATE_DONE ? "[x]"
: item->state == STATE_DOING ? "[-]"
: "[ ]";
@@ -277,21 +371,57 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
const char* dim_reset = item->state == STATE_DONE ? "\x1b[22m" : "";
length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "%s\x1b[2m%*d.\x1b[22m %s %s%s%s\x1b[K%s\r\n",
- background, number_width, index + 1,
+ "%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++;
+ }
+
+ while (emitted < visible)
+ {
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n");
+ emitted++;
}
length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "\x1b[%d;1H\x1b[2m-- %s -- %d/%d done\x1b[0m\x1b[K", rows, mode_name(mode), done, list->count);
+ "\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_INSERT && cursor >= 0 && cursor < list->count)
+ if (cursor >= 0 && cursor < list->count)
{
- int cursor_row = cursor - scroll + 3;
- int cursor_column = number_width + 7 + (int) strlen(list->items[cursor].text);
+ 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 = 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;
+ }
+ else
+ {
+ int scan = cursor - 1;
+
+ while (scan >= 0 && !is_section(&list->items[scan]))
+ 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;
+ }
+
+ const char* shape = mode == MODE_INSERT ? "\x1b[6 q" : "\x1b[2 q";
length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "\x1b[%d;%dH\x1b[?25h", cursor_row, cursor_column);
+ "\x1b[%d;%dH%s\x1b[?25h", cursor_row, cursor_column, shape);
}
else
{
@@ -317,7 +447,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
break;
case ' ':
- if (*cursor >= 0 && *cursor < list->count)
+ if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor]))
{
struct Item* item = &list->items[*cursor];
item->state = (enum State) ((item->state + 1) % 3);
@@ -327,7 +457,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
break;
case 'x':
- if (*cursor >= 0 && *cursor < list->count)
+ if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor]))
{
struct Item* item = &list->items[*cursor];
item->state = item->state == STATE_DONE ? STATE_TODO : STATE_DONE;
@@ -389,7 +519,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
{
int next = *cursor + 1;
- while (next < list->count && !is_separator(&list->items[next]))
+ while (next < list->count && !is_section(&list->items[next]))
next++;
*cursor = next < list->count ? next : list->count - 1;
@@ -400,7 +530,7 @@ static int normal_key(struct List* list, const char* path, int* cursor, enum Mod
{
int previous = *cursor - 1;
- while (previous >= 0 && !is_separator(&list->items[previous]))
+ while (previous >= 0 && !is_section(&list->items[previous]))
previous--;
*cursor = previous >= 0 ? previous : 0;
@@ -566,7 +696,7 @@ int main(void)
if (list == NULL)
{
- (void) fprintf(stderr, "%s: out of memory\n", todo_program);
+ (void) fprintf(stderr, "%s: out of memory\n", TODO_PROGRAM);
return EXIT_FAILURE;
}