#include #include #include #include #include #include #include #include #include #include #include "config.h" #define TODO_PROGRAM "todo" #define TODO_VERSION "0.1.0" 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, MODE_REPLACE, MODE_COMMAND, MODE_SEARCH, MODE_CONFIRM }; enum Pending { PENDING_NONE, PENDING_RESET, PENDING_CLEAR }; enum Key { KEY_UP = 256, KEY_DOWN, KEY_LEFT, KEY_RIGHT }; static const char* mode_name(enum Mode mode) { switch (mode) { case MODE_NORMAL: return "NORMAL"; case MODE_INSERT: return "INSERT"; case MODE_REPLACE: return "REPLACE"; case MODE_COMMAND: return "COMMAND"; case MODE_SEARCH: return "SEARCH"; case MODE_CONFIRM: return "CONFIRM"; } return "?"; } static int is_editing(enum Mode mode) { return mode == MODE_INSERT || mode == MODE_REPLACE; } static int utf8_width(const char* text, int bytes) { int width = 0; for (int i = 0; i < bytes && text[i] != '\0'; i++) if (((unsigned char) text[i] & 0xC0) != 0x80) width++; return width; } static int utf8_left(const char* text, int pos) { if (pos <= 0) return 0; pos--; while (pos > 0 && ((unsigned char) text[pos] & 0xC0) == 0x80) pos--; return pos; } static int utf8_right(const char* text, int pos) { int length = (int) strlen(text); if (pos >= length) return length; pos++; while (pos < length && ((unsigned char) text[pos] & 0xC0) == 0x80) pos++; return pos; } 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 int is_section(const struct Item* item) { return item->text[0] == '='; } static int is_subtask(const struct Item* item) { return item->text[0] == '-'; } static int is_optional(const struct Item* item) { return item->text[0] == '*'; } static int is_note(const struct Item* item) { return item->text[0] == '>'; } static int task_group(const struct Item* item) { if (item->state == STATE_DONE) return 2; if (is_optional(item)) return 1; return 0; } static int section_level(const struct Item* item) { int level = 0; while (item->text[level] == '=') level++; return level; } static int text_start(const struct Item* item) { int i = 0; if (is_section(item)) i = section_level(item); else if (is_subtask(item) || is_optional(item) || is_note(item)) i = 1; while (item->text[i] == ' ') i++; return i; } static void normalize_tasks(struct List* list) { for (int i = 0; i < list->count; i++) { if (is_section(&list->items[i]) || is_subtask(&list->items[i]) || is_note(&list->items[i])) continue; int total = 0; int done = 0; int active = 0; for (int j = i + 1; j < list->count && (is_subtask(&list->items[j]) || is_note(&list->items[j])); j++) { if (!is_subtask(&list->items[j])) continue; total++; if (list->items[j].state == STATE_DONE) done++; else if (list->items[j].state == STATE_DOING) active++; } if (total == 0) continue; if (done == total) list->items[i].state = STATE_DONE; else if (done > 0 || active > 0) list->items[i].state = STATE_DOING; else list->items[i].state = STATE_TODO; } } 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 group = 0; group < 3; group++) { int k = i; while (k < segment) { int block = k + 1; while (block < segment && (is_subtask(&list->items[block]) || is_note(&list->items[block]))) block++; if (task_group(&list->items[k]) == group) { sorted[out++] = list->items[k]; int head = k + 1; while (head < block && is_note(&list->items[head])) sorted[out++] = list->items[head++]; for (int sub_pass = 0; sub_pass < 2; sub_pass++) { int unit = head; while (unit < block) { int unit_end = unit + 1; while (unit_end < block && is_note(&list->items[unit_end])) unit_end++; if ((list->items[unit].state == STATE_DONE) == sub_pass) for (int s = unit; s < unit_end; s++) sorted[out++] = list->items[s]; unit = unit_end; } } } k = block; } } i = segment; } for (int x = 0; x < list->count; x++) list->items[x] = sorted[x]; free(sorted); } static int task_block_end(const struct List* list, int start) { int i = start + 1; while (i < list->count && (is_subtask(&list->items[i]) || is_note(&list->items[i]))) i++; return i; } static int item_block_end(const struct List* list, int start) { if (start >= 0 && start < list->count && !is_section(&list->items[start]) && !is_subtask(&list->items[start]) && !is_note(&list->items[start])) return task_block_end(list, start); return start + 1; } static int block_end(const struct List* list, int start) { int i = start + 1; while (i < list->count && !is_section(&list->items[i])) i++; return i; } static void reverse_items(struct List* list, int low, int high) { while (low < high - 1) { struct Item moved = list->items[low]; list->items[low] = list->items[high - 1]; list->items[high - 1] = moved; low++; high--; } } static void save(struct List* list, const char* path) { normalize_tasks(list); FILE* file = fopen(path, "w"); if (file == NULL) return; for (int i = 0; i < list->count; i++) { if (is_section(&list->items[i]) || is_note(&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 ? '-' : ' '; (void) fprintf(file, "[%c] %s\n", marker, list->items[i].text); } (void) fclose(file); } static void commit(struct List* list) { normalize_tasks(list); sort_list(list); } 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); static struct Item yank_register[MAX_ITEMS]; static int yank_count; static enum Pending pending_action; static char replace_original[MAX_TEXT]; static int replace_start; static void paste_item(struct List* list, int index, int* cursor) { if (yank_count <= 0 || list->count + yank_count > MAX_ITEMS) return; if (index < 0) index = 0; if (index > list->count) index = list->count; history_record(list); for (int i = 0; i < yank_count; i++) insert_item(list, index + i); for (int i = 0; i < yank_count; i++) list->items[index + i] = yank_register[i]; *cursor = index; } static char lower(char c) { return c >= 'A' && c <= 'Z' ? (char) (c + 32) : c; } static int contains_fold(const char* haystack, const char* needle) { if (needle[0] == '\0') return 0; for (; *haystack != '\0'; haystack++) { const char* h = haystack; const char* n = needle; while (*h != '\0' && *n != '\0' && lower(*h) == lower(*n)) { h++; n++; } if (*n == '\0') return 1; } return 0; } static int match_at(const char* text, int pos, const char* needle) { int k = 0; while (needle[k] != '\0' && text[pos + k] != '\0' && lower(text[pos + k]) == lower(needle[k])) k++; return needle[k] == '\0' ? k : 0; } static int match_offset(const char* text, const char* needle) { if (needle[0] == '\0') return -1; for (int i = 0; text[i] != '\0'; i++) if (match_at(text, i, needle) > 0) return i; return -1; } static int find_match(const struct List* list, int from, int direction, const char* query) { if (list->count == 0 || query[0] == '\0') return -1; for (int step = 1; step <= list->count; step++) { int index = ((from + direction * step) % list->count + list->count) % list->count; if (contains_fold(list->items[index].text, query)) return index; } return -1; } 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) { static const char reset[] = "\x1b[?25h\x1b[0 q\x1b[?1049l"; (void) tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios); (void) write(STDOUT_FILENO, reset, sizeof reset - 1); } 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 int screen_cols(void) { struct winsize window; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) == 0 && window.ws_col > 0) return window.ws_col; return 80; } static int content_rows(void) { int rows = screen_rows() - 4; return rows < 1 ? 1 : rows; } static int emit_wrapped(char* buffer, int length, int cols, const char* background, const char* background_reset, const char* prefix, int prefix_cols, int first_col, const char* style, const char* style_reset, const char* text, const char* search, int* emitted, int visible, int is_cursor, int caret_cols, int* cursor_row, int* cursor_column) { int total = (int) strlen(text); int byte = 0; int line = 0; int text_col = 0; char mask[MAX_TEXT]; memset(mask, 0, sizeof mask); if (search != NULL && search[0] != '\0') for (int i = 0; i < total && i < MAX_TEXT; i++) { int m = match_at(text, i, search); for (int k = 0; k < m && i + k < MAX_TEXT; k++) mask[i + k] = 1; } do { while (line > 0 && byte < total && text[byte] == ' ') { byte++; text_col++; } if (line > 0 && byte >= total) break; int base = line == 0 ? first_col : prefix_cols; int avail = cols - base; if (avail < 1) avail = 1; int row = 4 + *emitted; int line_start_col = text_col; int end = byte; int width = 0; int space_pos = -1; int space_width = 0; while (end < total && width < avail) { if (text[end] == ' ') { space_pos = end; space_width = width; } end++; while (end < total && ((unsigned char) text[end] & 0xC0) == 0x80) end++; width++; } if (end < total && text[end] != ' ' && space_pos > byte) { end = space_pos; width = space_width; } if (line == 0) length += snprintf(buffer + length, 64 * 1024 - (size_t) length, "%s%s%s", background, prefix, style); else length += snprintf(buffer + length, 64 * 1024 - (size_t) length, "%s%*s%s", background, prefix_cols, "", style); for (int i = byte; i < end;) { int j = i; while (j < end && mask[j] == mask[i]) j++; if (mask[i]) length += snprintf(buffer + length, 64 * 1024 - (size_t) length, "\x1b[7m%.*s\x1b[27m", j - i, text + i); else length += snprintf(buffer + length, 64 * 1024 - (size_t) length, "%.*s", j - i, text + i); i = j; } length += snprintf(buffer + length, 64 * 1024 - (size_t) length, "%s\x1b[K%s\r\n", style_reset, background_reset); if (is_cursor && *cursor_row < 0 && caret_cols >= line_start_col && caret_cols <= line_start_col + width) { *cursor_row = row; *cursor_column = base + 1 + caret_cols - line_start_col; } byte = end; text_col = line_start_col + width; line++; (*emitted)++; } while (byte < total && *emitted < visible); return length; } 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 int wrap_column(void) { int cols = screen_cols(); return cols > WRAP_COLUMN ? WRAP_COLUMN : cols; } static int compute_number_width(const struct List* list) { int max_run = 0; int run = 0; for (int i = 0; i < list->count; i++) { if (is_section(&list->items[i])) run = 0; else if (!is_subtask(&list->items[i]) && !is_note(&list->items[i]) && ++run > max_run) max_run = run; } int number_width = 1; for (int count = max_run; count >= 10; count /= 10) number_width++; return number_width; } static int render(const struct List* list, int cursor, int scroll, enum Mode mode, int edit_pos, const char* command, const char* search, int measure) { int rows = screen_rows(); int cols = wrap_column(); int visible = content_rows(); const char* matches = mode == MODE_SEARCH ? command : search; int total_items = 0; int done = 0; for (int i = 0; i < list->count; i++) if (!is_section(&list->items[i]) && !is_subtask(&list->items[i]) && !is_note(&list->items[i])) { total_items++; done += list->items[i].state == STATE_DONE; } int number_width = compute_number_width(list); 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%*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; int prev_group = -1; int cursor_row = -1; int cursor_column = -1; while (emitted < visible && index < list->count) { const struct Item* item = &list->items[index]; const char* background = index == cursor ? HIGHLIGHT : ""; const char* background_reset = index == cursor ? HIGHLIGHT_RESET : ""; if (is_section(item)) { if (emitted > 0) { length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); emitted++; if (emitted >= visible) break; } prev_group = -1; if (index == cursor) cursor_row = 4 + emitted; int level = section_level(item); int section_indent = LEFT_MARGIN + (level - 1) * SECTION_INDENT; if (is_editing(mode) && index == cursor) { length += snprintf(buffer + length, sizeof buffer - (size_t) length, "%s%*s\x1b[1m%s\x1b[22m\x1b[K%s\r\n", background, section_indent, "", item->text, background_reset); } else { int section_number = 0; for (int k = index; k >= 0; k--) if (is_section(&list->items[k])) { 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); 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, section_indent, "", roman, name, background_reset); } emitted++; index++; continue; } int heading_level = 0; for (int k = index - 1; k >= 0; k--) if (is_section(&list->items[k])) { heading_level = section_level(&list->items[k]); break; } int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT; if (is_note(item)) { int indent = base_indent + number_width + 6; const char* name = item->text; char prefix[16]; (void) snprintf(prefix, sizeof prefix, "%*s", indent, ""); int caret = 0; if (index == cursor) caret = utf8_width(name, edit_pos); length = emit_wrapped(buffer, length, cols, background, background_reset, prefix, indent + 2, indent, "\x1b[2;3m", "\x1b[22;23m", name, matches, &emitted, visible, index == cursor, caret, &cursor_row, &cursor_column); index++; continue; } 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" : ""; if (is_subtask(item)) { int indent = base_indent + number_width + SUBTASK_INDENT; const char* name = item->text; if (!(is_editing(mode) && index == cursor)) { name = item->text + 1; while (*name == ' ') name++; } char prefix[32]; (void) snprintf(prefix, sizeof prefix, "%*s%s ", indent, "", box); int caret = 0; if (index == cursor) { int offset = (int) (name - item->text); int byte = edit_pos - offset < 0 ? 0 : edit_pos - offset; caret = utf8_width(name, byte); } length = emit_wrapped(buffer, length, cols, background, background_reset, prefix, indent + 4, indent + 4, dim, dim_reset, name, matches, &emitted, visible, index == cursor, caret, &cursor_row, &cursor_column); } else { int group = task_group(item); if (prev_group >= 0 && group != prev_group) { length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); emitted++; if (emitted >= visible) break; } prev_group = group; int number = 0; for (int k = index; k >= 0 && !is_section(&list->items[k]); k--) if (!is_subtask(&list->items[k]) && !is_note(&list->items[k])) number++; const char* task_box = is_optional(item) ? (item->state == STATE_DONE ? "(x)" : item->state == STATE_DOING ? "(-)" : "( )") : box; const char* name = item->text; if (is_optional(item) && !(is_editing(mode) && index == cursor)) { name = item->text + 1; while (*name == ' ') name++; } char prefix[48]; (void) snprintf(prefix, sizeof prefix, "%*s\x1b[2m%*d.\x1b[22m %s ", base_indent, "", number_width, number, task_box); int caret = 0; if (index == cursor) { int offset = (int) (name - item->text); int byte = edit_pos - offset < 0 ? 0 : edit_pos - offset; caret = utf8_width(name, byte); } length = emit_wrapped(buffer, length, cols, background, background_reset, prefix, base_indent + number_width + 6, base_indent + number_width + 6, dim, dim_reset, name, matches, &emitted, visible, index == cursor, caret, &cursor_row, &cursor_column); } index++; } while (emitted < visible) { length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); emitted++; } if (mode == MODE_COMMAND || mode == MODE_SEARCH) length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[%d;1H%*s%c%s\x1b[K", rows, LEFT_MARGIN, "", mode == MODE_SEARCH ? '/' : ':', command); else if (mode == MODE_CONFIRM) length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[%d;1H%*s%s [y/n]\x1b[K", rows, LEFT_MARGIN, "", pending_action == PENDING_CLEAR ? "clear the whole list?" : "reset all items to todo?"); 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 || mode == MODE_SEARCH) { cursor_column = LEFT_MARGIN + 2 + (int) strlen(command); length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[%d;%dH\x1b[6 q\x1b[?25h", rows, cursor_column); } else if (mode != MODE_CONFIRM && cursor >= 0 && cursor < list->count && cursor_row >= 0) { if (is_section(&list->items[cursor])) { int level = section_level(&list->items[cursor]); int section_indent = LEFT_MARGIN + (level - 1) * SECTION_INDENT; int caret = utf8_width(list->items[cursor].text, edit_pos); cursor_column = is_editing(mode) ? section_indent + 1 + caret : section_indent + 1; } const char* shape = mode == MODE_REPLACE ? "\x1b[4 q" : mode == MODE_INSERT ? "\x1b[6 q" : "\x1b[2 q"; if (cursor_column >= 0) length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[%d;%dH%s\x1b[?25h", cursor_row, cursor_column, shape); } else { length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[?25l"); } if (!measure) (void) write(STDOUT_FILENO, buffer, (size_t) length); return cursor_row; } static int pending_key = -1; static void unget_key(int key) { pending_key = key; } static int read_key(void) { if (pending_key != -1) { int key = pending_key; pending_key = -1; return key; } char character; if (read(STDIN_FILENO, &character, 1) != 1) return -1; if (character != 27) return (unsigned char) character; int flags = fcntl(STDIN_FILENO, F_GETFL); (void) fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); char first; if (read(STDIN_FILENO, &first, 1) != 1) { (void) fcntl(STDIN_FILENO, F_SETFL, flags); return 27; } if (first == '[') { char second; ssize_t got = read(STDIN_FILENO, &second, 1); (void) fcntl(STDIN_FILENO, F_SETFL, flags); 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_key = (unsigned char) first; return 27; } static const char* item_display(const struct List* list, int index, int number_width, int* first_col, int* cont_col) { const struct Item* item = &list->items[index]; int heading_level = 0; for (int k = index - 1; k >= 0; k--) if (is_section(&list->items[k])) { heading_level = section_level(&list->items[k]); break; } int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT; if (is_note(item)) { int indent = base_indent + number_width + 6; *first_col = indent; *cont_col = indent + 2; return item->text; } if (is_subtask(item)) { int indent = base_indent + number_width + SUBTASK_INDENT; *first_col = *cont_col = indent + 4; const char* name = item->text + 1; while (*name == ' ') name++; return name; } *first_col = *cont_col = base_indent + number_width + 6; const char* name = item->text; if (is_optional(item)) { name = item->text + 1; while (*name == ' ') name++; } return name; } static int wrap_lines(const char* name, int cols, int first_col, int cont_col, int* starts, int* ends, int max_lines) { int total = (int) strlen(name); int byte = 0; int line = 0; do { while (line > 0 && byte < total && name[byte] == ' ') byte++; if (line > 0 && byte >= total) break; int base = line == 0 ? first_col : cont_col; int avail = cols - base; if (avail < 1) avail = 1; int end = byte; int width = 0; int space_pos = -1; while (end < total && width < avail) { if (name[end] == ' ') space_pos = end; end++; while (end < total && ((unsigned char) name[end] & 0xC0) == 0x80) end++; width++; } if (end < total && name[end] != ' ' && space_pos > byte) end = space_pos; if (line < max_lines) { starts[line] = byte; ends[line] = end; } byte = end; line++; } while (byte < total); if (line == 0) { if (max_lines > 0) { starts[0] = 0; ends[0] = 0; } line = 1; } return line; } static int display_width(const char* name, int from, int to) { int width = 0; for (int i = from; i < to; i++) if (((unsigned char) name[i] & 0xC0) != 0x80) width++; return width; } static int offset_at_width(const char* name, int start, int end, int want) { int i = start; int width = 0; while (i < end && width < want) { i++; while (i < end && ((unsigned char) name[i] & 0xC0) == 0x80) i++; width++; } return i; } static void place_on_line(const struct List* list, int index, int number_width, int cols, int last, int goal, int* edit_pos) { if (is_section(&list->items[index])) { *edit_pos = text_start(&list->items[index]); return; } int first_col; int cont_col; const char* name = item_display(list, index, number_width, &first_col, &cont_col); int name_offset = (int) (name - list->items[index].text); int starts[MAX_TEXT]; int ends[MAX_TEXT]; int lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT); int line = last ? lines - 1 : 0; int base = line == 0 ? first_col : cont_col; int want = goal - base; if (want < 0) want = 0; *edit_pos = name_offset + offset_at_width(name, starts[line], ends[line], want); } static void move_visual(struct List* list, int* cursor, int* edit_pos, int number_width, int cols, int down) { if (*cursor < 0 || *cursor >= list->count) return; const struct Item* item = &list->items[*cursor]; int line = 0; int lines = 1; int goal = 0; if (!is_section(item)) { int first_col; int cont_col; const char* name = item_display(list, *cursor, number_width, &first_col, &cont_col); int name_offset = (int) (name - item->text); int name_length = (int) strlen(name); int starts[MAX_TEXT]; int ends[MAX_TEXT]; lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT); int pos = *edit_pos - name_offset; if (pos < 0) pos = 0; if (pos > name_length) pos = name_length; for (int i = 0; i < lines; i++) if (starts[i] <= pos) line = i; int base = line == 0 ? first_col : cont_col; goal = base + display_width(name, starts[line], pos); if (down && line < lines - 1) { int want = goal - cont_col; if (want < 0) want = 0; *edit_pos = name_offset + offset_at_width(name, starts[line + 1], ends[line + 1], want); return; } if (!down && line > 0) { int target = line - 1; int target_base = target == 0 ? first_col : cont_col; int want = goal - target_base; if (want < 0) want = 0; *edit_pos = name_offset + offset_at_width(name, starts[target], ends[target], want); return; } } if (down && *cursor + 1 < list->count) { (*cursor)++; place_on_line(list, *cursor, number_width, cols, 0, goal, edit_pos); } else if (!down && *cursor > 0) { (*cursor)--; place_on_line(list, *cursor, number_width, cols, 1, goal, edit_pos); } } static int matching_close(int open) { switch (open) { case '(': return ')'; case '[': return ']'; case '{': return '}'; case '<': return '>'; } return 0; } static int matching_open(int close) { switch (close) { case ')': return '('; case ']': return '['; case '}': return '{'; case '>': return '<'; } return 0; } static void apply_change(struct List* list, int cursor, enum Mode* mode, int* edit_pos, int from, int to) { struct Item* item = &list->items[cursor]; int length = (int) strlen(item->text); if (to > length) to = length; if (from < 0 || to < from) return; history_record(list); memmove(&item->text[from], &item->text[to], (size_t) (length - to) + 1); *edit_pos = from; *mode = MODE_INSERT; } static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit_pos, char* command, char* search, int count, int key) { switch (key) { case 27: search[0] = '\0'; break; case ':': command[0] = '\0'; *mode = MODE_COMMAND; break; case '/': command[0] = '\0'; *mode = MODE_SEARCH; break; case 'n': { int match = find_match(list, *cursor, 1, search); if (match >= 0) { *cursor = match; int off = match_offset(list->items[match].text, search); if (off >= 0) *edit_pos = off; } break; } case 'N': { int match = find_match(list, *cursor, -1, search); if (match >= 0) { *cursor = match; int off = match_offset(list->items[match].text, search); if (off >= 0) *edit_pos = off; } break; } case 'y': { int next = read_key(); if (next != 'y') { unget_key(next); break; } if (*cursor >= 0 && *cursor < list->count) { int start = *cursor; int end = item_block_end(list, start); yank_count = 0; for (int i = start; i < end && yank_count < MAX_ITEMS; i++) yank_register[yank_count++] = list->items[i]; } break; } case 'p': paste_item(list, item_block_end(list, *cursor), cursor); break; case 'P': paste_item(list, *cursor, cursor); break; case 'j': case KEY_DOWN: { int number_width = compute_number_width(list); int cols = wrap_column(); for (int repeat = 0; repeat < count; repeat++) move_visual(list, cursor, edit_pos, number_width, cols, 1); break; } case 'k': case KEY_UP: { int number_width = compute_number_width(list); int cols = wrap_column(); for (int repeat = 0; repeat < count; repeat++) move_visual(list, cursor, edit_pos, number_width, cols, 0); break; } case 'h': case KEY_LEFT: if (*cursor >= 0 && *cursor < list->count) for (int repeat = 0; repeat < count; repeat++) *edit_pos = utf8_left(list->items[*cursor].text, *edit_pos); break; case 'l': case KEY_RIGHT: if (*cursor >= 0 && *cursor < list->count) for (int repeat = 0; repeat < count; repeat++) *edit_pos = utf8_right(list->items[*cursor].text, *edit_pos); break; case '0': if (*cursor >= 0 && *cursor < list->count) *edit_pos = text_start(&list->items[*cursor]); break; case '$': if (*cursor >= 0 && *cursor < list->count) *edit_pos = (int) strlen(list->items[*cursor].text); 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; for (int repeat = 0; repeat < count; repeat++) { 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; for (int repeat = 0; repeat < count; repeat++) { 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]) && !is_note(&list->items[*cursor])) { history_record(list); struct Item* item = &list->items[*cursor]; item->state = (enum State) ((item->state + 1) % 3); commit(list); } break; case 'x': if (*cursor >= 0 && *cursor < list->count && *edit_pos < (int) strlen(list->items[*cursor].text)) { history_record(list); struct Item* item = &list->items[*cursor]; for (int repeat = 0; repeat < count && *edit_pos < (int) strlen(item->text); repeat++) { int next = utf8_right(item->text, *edit_pos); memmove(&item->text[*edit_pos], &item->text[next], strlen(item->text) - (size_t) next + 1); } } break; case 'o': { history_record(list); int at = item_block_end(list, *cursor); insert_item(list, at); *cursor = at < list->count ? at : list->count - 1; *edit_pos = 0; *mode = MODE_INSERT; break; } case 'A': history_record(list); if (list->count == 0) { insert_item(list, 0); *cursor = 0; } if (*cursor >= 0 && *cursor < list->count) { *edit_pos = (int) strlen(list->items[*cursor].text); *mode = MODE_INSERT; } break; case 'i': if (*cursor >= 0 && *cursor < list->count) { history_record(list); int start = text_start(&list->items[*cursor]); if (*edit_pos < start) *edit_pos = start; *mode = MODE_INSERT; } break; case 'R': history_record(list); if (list->count == 0) { insert_item(list, 0); *cursor = 0; *edit_pos = 0; } if (*cursor >= 0 && *cursor < list->count) { (void) snprintf(replace_original, sizeof replace_original, "%s", list->items[*cursor].text); replace_start = *edit_pos; *mode = MODE_REPLACE; } break; case 'c': { if (*cursor < 0 || *cursor >= list->count) break; int op = read_key(); const char* text = list->items[*cursor].text; int length = (int) strlen(text); int low = text_start(&list->items[*cursor]); if (op == 'w' || op == 'e' || op == 'b' || op == '$' || op == '0' || op == 'c' || op == 'h' || op == 'l') { int from = *edit_pos; int to = *edit_pos; if (op == 'w' || op == 'e') { int pos = *edit_pos; for (int r = 0; r < count; r++) { while (pos < length && text[pos] == ' ') pos++; while (pos < length && text[pos] != ' ') pos++; } to = pos; } else if (op == 'b') { int pos = *edit_pos; for (int r = 0; r < count; r++) { if (pos > low) pos--; while (pos > low && text[pos] == ' ') pos--; while (pos > low && text[pos - 1] != ' ') pos--; } from = pos; } else if (op == '$') { to = length; } else if (op == '0') { from = low; } else if (op == 'c') { from = low; to = length; } else if (op == 'l') { for (int r = 0; r < count; r++) to = utf8_right(text, to); } else { for (int r = 0; r < count; r++) from = utf8_left(text, from); } apply_change(list, *cursor, mode, edit_pos, from, to); break; } if (op != 't' && op != 'f' && op != 'i' && op != 'a') { unget_key(op); break; } int target = read_key(); if (target < 32 || target >= 256) break; if (op == 't' || op == 'f') { int found = -1; for (int i = *edit_pos + 1; i < length; i++) if ((unsigned char) text[i] == target) { found = i; break; } if (found >= 0) apply_change(list, *cursor, mode, edit_pos, *edit_pos, op == 'f' ? found + 1 : found); break; } int open_char = target; int close_char = matching_close(target); if (close_char == 0) { int open = matching_open(target); if (open != 0) { open_char = open; close_char = target; } else { close_char = target; } } int open_at = -1; int close_at = -1; for (int i = *edit_pos; i >= low; i--) if ((unsigned char) text[i] == open_char) { open_at = i; break; } if (open_at >= 0) { for (int i = open_at + 1; i < length; i++) if ((unsigned char) text[i] == close_char) { close_at = i; break; } if (close_at < *edit_pos) { open_at = -1; close_at = -1; } } if (open_at < 0) { for (int i = *edit_pos; i < length; i++) if ((unsigned char) text[i] == open_char) { open_at = i; break; } for (int i = open_at + 1; open_at >= 0 && i < length; i++) if ((unsigned char) text[i] == close_char) { close_at = i; break; } } if (open_at < 0 || close_at < 0) break; if (op == 'i') apply_change(list, *cursor, mode, edit_pos, open_at + 1, close_at); else apply_change(list, *cursor, mode, edit_pos, open_at, close_at + 1); break; } case 'g': { int next = read_key(); if (next == 'g') *cursor = 0; else unget_key(next); break; } case 'G': *cursor = list->count - 1; break; case '}': { int next = *cursor + 1; while (next < list->count && !is_section(&list->items[next])) next++; *cursor = next < list->count ? next : list->count - 1; break; } case '{': { int previous = *cursor - 1; while (previous >= 0 && !is_section(&list->items[previous])) previous--; *cursor = previous >= 0 ? previous : 0; break; } case 'd': { int next = read_key(); if (next != 'd') { unget_key(next); break; } if (*cursor >= 0 && *cursor < list->count) { history_record(list); int start = *cursor; int end = start; for (int repeat = 0; repeat < count && end < list->count; repeat++) end = item_block_end(list, end); yank_count = 0; for (int i = start; i < end && yank_count < MAX_ITEMS; i++) yank_register[yank_count++] = list->items[i]; for (int i = start; i < end; i++) delete_item(list, start); commit(list); } break; } case 'J': if (*cursor < 0 || *cursor >= list->count) break; if (is_section(&list->items[*cursor])) { int start = *cursor; int end = block_end(list, start); if (end < list->count) { history_record(list); int next_end = block_end(list, end); reverse_items(list, start, end); reverse_items(list, end, next_end); reverse_items(list, start, next_end); *cursor = start + (next_end - end); } } else if (!is_subtask(&list->items[*cursor]) && !is_note(&list->items[*cursor])) { int start = *cursor; int end = task_block_end(list, start); if (end < list->count && !is_section(&list->items[end])) { history_record(list); int next_end = task_block_end(list, end); reverse_items(list, start, end); reverse_items(list, end, next_end); reverse_items(list, start, next_end); *cursor = start + (next_end - end); } } else if (*cursor + 1 < list->count && (is_subtask(&list->items[*cursor + 1]) || is_note(&list->items[*cursor + 1]))) { history_record(list); struct Item moved = list->items[*cursor]; list->items[*cursor] = list->items[*cursor + 1]; list->items[*cursor + 1] = moved; (*cursor)++; } break; case 'K': if (*cursor < 0 || *cursor >= list->count) break; if (is_section(&list->items[*cursor])) { int start = *cursor; if (start > 0) { history_record(list); int previous = start - 1; while (previous >= 0 && !is_section(&list->items[previous])) previous--; int previous_start = previous >= 0 ? previous : 0; int end = block_end(list, start); reverse_items(list, previous_start, start); reverse_items(list, start, end); reverse_items(list, previous_start, end); *cursor = previous_start; } } else if (!is_subtask(&list->items[*cursor]) && !is_note(&list->items[*cursor])) { int start = *cursor; int previous = start - 1; while (previous >= 0 && (is_subtask(&list->items[previous]) || is_note(&list->items[previous]))) previous--; if (previous >= 0 && !is_section(&list->items[previous])) { history_record(list); int end = task_block_end(list, start); reverse_items(list, previous, start); reverse_items(list, start, end); reverse_items(list, previous, end); *cursor = previous; } } else if (*cursor > 0 && (is_subtask(&list->items[*cursor - 1]) || is_note(&list->items[*cursor - 1]))) { history_record(list); struct Item moved = list->items[*cursor]; list->items[*cursor] = list->items[*cursor - 1]; list->items[*cursor - 1] = moved; (*cursor)--; } break; case 'D': { history_record(list); int kept = 0; int i = 0; while (i < list->count) { int end = item_block_end(list, i); int drop = !is_section(&list->items[i]) && !is_subtask(&list->items[i]) && !is_note(&list->items[i]) && list->items[i].state == STATE_DONE; if (!drop) for (int j = i; j < end; j++) list->items[kept++] = list->items[j]; i = end; } list->count = kept; commit(list); break; } default: break; } return 1; } 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); if (key == 27 || ((key == '\r' || key == '\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; } normalize_tasks(list); *mode = MODE_NORMAL; return; } if (key == '\r' || key == '\n') { int at = item_block_end(list, *cursor); insert_item(list, at); *cursor = at; *edit_pos = 0; return; } if (key == KEY_LEFT) { *edit_pos = utf8_left(item->text, *edit_pos); return; } if (key == KEY_RIGHT) { *edit_pos = utf8_right(item->text, *edit_pos); return; } if (key == 127 || key == 8) { if (*mode == MODE_REPLACE) { if (*edit_pos > replace_start) { int start = utf8_left(item->text, *edit_pos); int original_length = (int) strlen(replace_original); if (*edit_pos <= original_length) for (int i = start; i < *edit_pos; i++) item->text[i] = replace_original[i]; else memmove(&item->text[start], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); *edit_pos = start; } } else if (*edit_pos > 0) { int start = utf8_left(item->text, *edit_pos); memmove(&item->text[start], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); *edit_pos = start; } return; } if (key >= 32 && key != 127 && key < 256 && length + 1 < sizeof item->text) { if (*mode == MODE_REPLACE && *edit_pos < (int) length) { item->text[*edit_pos] = (char) key; } else { memmove(&item->text[*edit_pos + 1], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); item->text[*edit_pos] = (char) key; } (*edit_pos)++; } } 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') { if (strcmp(command, "reset") == 0 || strcmp(command, "clear") == 0) { pending_action = strcmp(command, "clear") == 0 ? PENDING_CLEAR : PENDING_RESET; command[0] = '\0'; *mode = MODE_CONFIRM; return 1; } if (strcmp(command, "reorder") == 0) { history_record(list); commit(list); command[0] = '\0'; *mode = MODE_NORMAL; return 1; } 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 != 127 && key < 256 && length + 1 < MAX_COMMAND) { command[length] = (char) key; command[length + 1] = '\0'; } return 1; } static void search_key(const struct List* list, int* cursor, int* edit_pos, enum Mode* mode, char* command, char* search, int key) { size_t length = strlen(command); if (key == 27) { command[0] = '\0'; *mode = MODE_NORMAL; return; } if (key == '\r' || key == '\n') { (void) snprintf(search, MAX_COMMAND, "%s", command); command[0] = '\0'; *mode = MODE_NORMAL; int match = find_match(list, *cursor, 1, search); if (match >= 0) { *cursor = match; int off = match_offset(list->items[match].text, search); if (off >= 0) *edit_pos = off; } return; } if (key == 127 || key == 8) { if (length > 0) command[length - 1] = '\0'; else *mode = MODE_NORMAL; return; } if (key >= 32 && key != 127 && key < 256 && length + 1 < MAX_COMMAND) { command[length] = (char) key; command[length + 1] = '\0'; } } static void confirm_key(struct List* list, enum Mode* mode, int key) { if (key == 'y' || key == 'Y') { history_record(list); if (pending_action == PENDING_RESET) { for (int i = 0; i < list->count; i++) if (!is_section(&list->items[i])) list->items[i].state = STATE_TODO; } else if (pending_action == PENDING_CLEAR) { list->count = 0; } commit(list); } pending_action = PENDING_NONE; *mode = MODE_NORMAL; } 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; int count = 0; char command[MAX_COMMAND] = ""; char search[MAX_COMMAND] = ""; for (;;) { if (cursor >= list->count) cursor = list->count - 1; if (cursor < 0) cursor = 0; if (list->count > 0) { const char* text = list->items[cursor].text; int text_length = (int) strlen(text); int caret_max = is_editing(mode) ? text_length : (text_length > 0 ? text_length - 1 : 0); int caret_min = is_editing(mode) ? 0 : text_start(&list->items[cursor]); if (caret_min > caret_max) caret_min = caret_max; if (edit_pos > caret_max) edit_pos = caret_max; if (edit_pos < caret_min) edit_pos = caret_min; while (edit_pos > caret_min && ((unsigned char) text[edit_pos] & 0xC0) == 0x80) edit_pos--; } if (cursor < scroll) scroll = cursor; while (scroll < cursor && render(list, cursor, scroll, mode, edit_pos, command, search, 1) < 0) scroll++; render(list, cursor, scroll, mode, edit_pos, command, search, 0); int key = read_key(); if (key == -1) break; switch (mode) { case MODE_NORMAL: if ((key >= '1' && key <= '9') || (key == '0' && count > 0)) { count = count * 10 + (key - '0'); if (count > 9999) count = 9999; break; } normal_key(list, &cursor, &mode, &edit_pos, command, search, count > 0 ? count : 1, key); count = 0; break; case MODE_INSERT: case MODE_REPLACE: insert_key(list, &cursor, &mode, &edit_pos, key); break; case MODE_COMMAND: if (command_key(list, path, &mode, command, key) == 0) return; break; case MODE_SEARCH: search_key(list, &cursor, &edit_pos, &mode, command, search, key); break; case MODE_CONFIRM: confirm_key(list, &mode, key); break; } } } static void print_version(void) { (void) printf("%s %s\n", TODO_PROGRAM, TODO_VERSION); } static void print_help(void) { (void) printf( "%s %s - a little modal todo list\n" "\n" "usage: %s open the list ($TODO_FILE, or ~/.todo.txt)\n" " %s -h | --help\n" " %s -v | --version\n" "\n" "keys:\n" " h j k l / arrows move w / b words 0 / $ line ends\n" " { / } sections gg / G top/bottom\n" " o new item i insert at cursor A append text\n" " R overtype (replace) 3j 2dd counts\n" " x delete char under cursor space cycle state\n" " cw ce cb c0 c$ cc change word / to start / end / whole\n" " ct{c} cf{c} change till / onto a char\n" " ci{d} ca{d} change inside / around () [] {} <> \" '\n" " dd cut item D clear done yy p P yank/paste\n" " J / K move item or section / n N search (highlights as you type)\n" " esc clear search highlight u ctrl-r undo / redo\n" " :w :q :wq :x write / quit :reset all back to todo :clear empty\n" " :reorder re-sort the list\n" "\n" "items: '=' section, '==' subsection, '-' subtask, '*' optional, '>' note.\n" "\n" "by hachem \n", TODO_PROGRAM, TODO_VERSION, TODO_PROGRAM, TODO_PROGRAM, TODO_PROGRAM); } int main(int argc, char** argv) { if (argc >= 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { print_help(); return EXIT_SUCCESS; } if (argc >= 2 && (strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0)) { print_version(); return EXIT_SUCCESS; } (void) setlocale(LC_ALL, ""); 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; }