#include #include #include #include #include #include #include #include #define todo_program "todo" #define todo_version "0.1.0" #define max_items 4096 #define max_text 256 enum State { STATE_TODO, STATE_DOING, STATE_DONE }; struct Item { enum State state; char text[max_text]; }; struct List { struct Item items[max_items]; int count; }; enum Mode { MODE_NORMAL, MODE_INSERT }; static const char* mode_name(enum Mode mode) { switch (mode) { case MODE_NORMAL: return "NORMAL"; case MODE_INSERT: return "INSERT"; } return "?"; } static void todo_path(char* buffer, size_t size) { const char* env_path = getenv("TODO_FILE"); if (env_path != NULL && env_path[0] != '\0') { (void) snprintf(buffer, size, "%s", env_path); return; } const char* home = getenv("HOME"); (void) snprintf(buffer, size, "%s/.todo.txt", home != NULL ? home : "."); } static void strip_newline(char* string) { size_t length = strlen(string); while (length > 0 && (string[length - 1] == '\n' || string[length - 1] == '\r')) string[--length] = '\0'; } static void load(struct List* list, const char* path) { list->count = 0; FILE* file = fopen(path, "r"); if (file == NULL) return; char line[max_text + 8]; while (list->count < max_items && fgets(line, sizeof line, file) != NULL) { strip_newline(line); enum State state = STATE_TODO; char* text = line; if (line[0] == '[' && line[1] != '\0' && line[2] == ']') { if (line[1] == 'x' || line[1] == 'X') state = STATE_DONE; else if (line[1] == '-') state = STATE_DOING; text = line + 3; while (*text == ' ') text++; } if (*text == '\0') continue; struct Item* item = &list->items[list->count++]; item->state = state; (void) snprintf(item->text, sizeof item->text, "%s", text); } (void) fclose(file); } static void save(const struct List* list, const char* path) { FILE* file = fopen(path, "w"); if (file == NULL) return; for (int i = 0; i < list->count; i++) { 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) { (void) tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios); (void) write(STDOUT_FILENO, "\x1b[?25h\x1b[?1049l", 14); } static void on_signal(int signal_number) { (void) signal_number; exit(EXIT_SUCCESS); } static int enter_raw_mode(void) { if (!isatty(STDIN_FILENO)) { (void) fprintf(stderr, "%s: not a terminal\n", todo_program); return -1; } if (tcgetattr(STDIN_FILENO, &saved_termios) != 0) return -1; struct termios raw = saved_termios; raw.c_lflag &= ~((tcflag_t) (ICANON | ECHO | ISIG)); raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) return -1; (void) atexit(restore_terminal); (void) signal(SIGINT, on_signal); (void) signal(SIGTERM, on_signal); (void) write(STDOUT_FILENO, "\x1b[?1049h\x1b[?25l", 14); return 0; } static int screen_rows(void) { struct winsize window; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) == 0 && window.ws_row > 0) return window.ws_row; return 24; } static void render(const struct List* list, int cursor, int scroll, enum Mode mode) { int rows = screen_rows(); int visible = rows - 1; if (visible < 1) visible = 1; int number_width = 1; 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 index = scroll + row; if (index >= list->count) { length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); continue; } 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" : ""; 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].state == STATE_DONE; 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); 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, enum Mode* mode, char character) { switch (character) { case 'q': return 0; case 'j': (*cursor)++; break; case 'k': (*cursor)--; 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) { 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; } 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; int cursor = 0; int scroll = 0; for (;;) { int visible = screen_rows() - 1; if (visible < 1) visible = 1; if (cursor >= list->count) cursor = list->count - 1; if (cursor < 0) cursor = 0; if (cursor < scroll) scroll = cursor; if (cursor >= scroll + visible) scroll = cursor - visible + 1; render(list, cursor, scroll, mode); char character; if (read(STDIN_FILENO, &character, 1) != 1) break; switch (mode) { case MODE_NORMAL: if (normal_key(list, path, &cursor, &mode, character) == 0) return; break; case MODE_INSERT: insert_key(list, path, &cursor, &mode, character); break; } } } int main(void) { char path[4096]; todo_path(path, sizeof path); struct List* list = malloc(sizeof *list); if (list == NULL) { (void) fprintf(stderr, "%s: out of memory\n", todo_program); return EXIT_FAILURE; } load(list, path); run(list, path); free(list); return EXIT_SUCCESS; }