diff options
| author | hachem <im@hachem.wtf> | 2026-10-03 17:27:07 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-10-03 17:27:07 +0200 |
| commit | 41005a0d4102843cefeed911a0fa8bd5774d998c (patch) | |
| tree | 9110455dcdeeab7483516810121bd13c10d9ea24 | |
| parent | 2981b6b3bbd9d40aee6c0d23d450dcd58a00adac (diff) | |
fix: more refactoring holy hell
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | config.h | 56 | ||||
| -rw-r--r-- | todo.1 | 14 | ||||
| -rw-r--r-- | todo.c | 884 |
4 files changed, 512 insertions, 447 deletions
@@ -34,10 +34,13 @@ a little modal todo list that lives in my terminal. / n N search (highlights as you type, jumps to match), next, previous esc clear the search highlight u ctrl-r undo, redo + ? show the keys in-app (any key closes) :w :q :wq :x write, quit, write and quit :reset set every item back to todo :clear remove every item :reorder re-sort / regroup the list + :theme NAME switch theme (default, gruvbox, nord, mono, custom) + tab complete the command (or theme name) in the : line ``` ## items @@ -66,8 +69,6 @@ it's one file, so if you don't want the makefile: $ cc -O2 todo.c -o todo ``` -tweak the layout, wrap column and highlight colour in [config.h](config.h). - ## license `todo` is licensed under the mit license. see [license](LICENSE) for more. @@ -1,12 +1,48 @@ -#define LEFT_MARGIN 2 -#define SECTION_INDENT 2 -#define SUBTASK_INDENT 4 -#define WRAP_COLUMN 80 +/* layout, in columns */ +#define LEFT_MARGIN 2 /* left padding for everything */ +#define SECTION_INDENT 2 /* extra indent per section depth */ +#define SUBTASK_INDENT 4 /* extra indent for subtasks */ +#define WRAP_COLUMN 80 /* wrap item text past this column */ -#define MAX_ITEMS 4096 -#define MAX_TEXT 256 -#define MAX_COMMAND 64 -#define MAX_HISTORY 64 +/* colour theme (see todo.c for each): + * - THEME_DEFAULT + * - THEME_GRUVBOX + * - THEME_NORD + * - THEME_MONO + * - THEME_CUSTOM (the CUSTOM_THEME below) */ +#define THEME THEME_DEFAULT -#define HIGHLIGHT "\x1b[48;5;235m" -#define HIGHLIGHT_RESET "\x1b[49m" +/* used when THEME is THEME_CUSTOM */ +#define CUSTOM_THEME { \ + .highlight = "\x1b[48;5;235m", .highlight_reset = "\x1b[49m", \ + .section = "\x1b[1m", .section_reset = "\x1b[22m", \ + .note = "\x1b[2;3m", .note_reset = "\x1b[22;23m", \ + .done = "\x1b[2m", .done_reset = "\x1b[22m", \ + .number = "\x1b[2m", .number_reset = "\x1b[22m", \ + .muted = "\x1b[2m", .muted_reset = "\x1b[0m", \ + .match = "\x1b[7m", .match_reset = "\x1b[27m", \ +} + +/* checkbox glyphs (display only; the file always stores [ ] [-] [x]). + * OPT_* are for optional (*) tasks; any width is fine. */ +#define MARK_TODO "[ ]" +#define MARK_DOING "[-]" +#define MARK_DONE "[x]" +#define MARK_OPT_TODO "( )" +#define MARK_OPT_DOING "(-)" +#define MARK_OPT_DONE "(x)" + +/* cursor shape per mode (DECSCUSR) */ +#define CURSOR_NORMAL "\x1b[2 q" /* block */ +#define CURSOR_INSERT "\x1b[6 q" /* bar */ +#define CURSOR_REPLACE "\x1b[4 q" /* underline */ + +/* header clock, strftime(3) formats */ +#define DATE_FORMAT "%a %d %b %Y" +#define TIME_FORMAT "%H:%M" + +/* limits (rarely need changing) */ +#define MAX_ITEMS 4096 /* most items in a list */ +#define MAX_TEXT 256 /* longest item text, in bytes */ +#define MAX_COMMAND 64 /* longest : command or / search */ +#define MAX_HISTORY 64 /* undo/redo depth */ @@ -179,6 +179,9 @@ is pressed in normal mode. .TP .BR u ", " "ctrl-r" Undo and redo. +.TP +.B ? +Show the key bindings on screen; any key returns. .SH "INSERT MODE" Type to insert at the cursor. .B enter @@ -191,7 +194,10 @@ deletes, and the arrow keys move the cursor. Press .B : to enter a command, then -.BR enter : +.BR enter . +.B Tab +completes the command name, or the theme name after +.BR ":theme" . .TP .B :w Write the list to its file. @@ -211,6 +217,12 @@ Remove every item, emptying the list. .B :reorder Re-sort the list, regrouping items (required, optional, done) within each section. Useful after reordering or hand-editing. +.TP +.BI :theme " name" +Switch the colour theme to +.I name +.RB ( default ", " gruvbox ", " nord ", " mono ", " custom ); +with no name, cycle to the next. .SH OPTIONS .TP .BR \-h ", " \-\-help @@ -1,5 +1,6 @@ #include <fcntl.h> #include <locale.h> +#include <stdarg.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> @@ -14,6 +15,14 @@ #define TODO_PROGRAM "todo" #define TODO_VERSION "0.1.0" +#define CLEAR_EOL "\x1b[K" +#define CURSOR_HOME "\x1b[H" +#define CURSOR_SHOW "\x1b[?25h" +#define CURSOR_HIDE "\x1b[?25l" +#define CURSOR_RESET "\x1b[0 q" +#define SCREEN_ENTER "\x1b[?1049h" +#define SCREEN_LEAVE "\x1b[?1049l" + enum State { STATE_TODO, @@ -40,7 +49,8 @@ enum Mode MODE_REPLACE, MODE_COMMAND, MODE_SEARCH, - MODE_CONFIRM + MODE_CONFIRM, + MODE_HELP }; enum Pending @@ -64,12 +74,94 @@ struct Editor int scroll; int edit_pos; int count; + enum Mode mode; + char command[MAX_COMMAND]; char search[MAX_COMMAND]; char message[MAX_COMMAND]; }; +enum ThemeId +{ + THEME_DEFAULT, + THEME_GRUVBOX, + THEME_NORD, + THEME_MONO, + THEME_CUSTOM +}; + +struct Theme +{ + const char* highlight; + const char* highlight_reset; + const char* section; + const char* section_reset; + const char* note; + const char* note_reset; + const char* done; + const char* done_reset; + const char* number; + const char* number_reset; + const char* muted; + const char* muted_reset; + const char* match; + const char* match_reset; +}; + +static const struct Theme themes[] = { + [THEME_DEFAULT] = { + .highlight = "\x1b[48;5;235m", .highlight_reset = "\x1b[49m", + .section = "\x1b[1m", .section_reset = "\x1b[22m", + .note = "\x1b[2;3m", .note_reset = "\x1b[22;23m", + .done = "\x1b[2m", .done_reset = "\x1b[22m", + .number = "\x1b[2m", .number_reset = "\x1b[22m", + .muted = "\x1b[2m", .muted_reset = "\x1b[0m", + .match = "\x1b[7m", .match_reset = "\x1b[27m", + }, + [THEME_GRUVBOX] = { + .highlight = "\x1b[48;5;237m", .highlight_reset = "\x1b[49m", + .section = "\x1b[1;38;5;214m", .section_reset = "\x1b[22;39m", + .note = "\x1b[3;38;5;108m", .note_reset = "\x1b[23;39m", + .done = "\x1b[38;5;245m", .done_reset = "\x1b[39m", + .number = "\x1b[38;5;243m", .number_reset = "\x1b[39m", + .muted = "\x1b[38;5;245m", .muted_reset = "\x1b[39m", + .match = "\x1b[30;48;5;214m", .match_reset = "\x1b[39;49m", + }, + [THEME_NORD] = { + .highlight = "\x1b[48;5;238m", .highlight_reset = "\x1b[49m", + .section = "\x1b[1;38;5;110m", .section_reset = "\x1b[22;39m", + .note = "\x1b[3;38;5;103m", .note_reset = "\x1b[23;39m", + .done = "\x1b[38;5;240m", .done_reset = "\x1b[39m", + .number = "\x1b[38;5;240m", .number_reset = "\x1b[39m", + .muted = "\x1b[38;5;109m", .muted_reset = "\x1b[39m", + .match = "\x1b[30;48;5;109m", .match_reset = "\x1b[39;49m", + }, + [THEME_MONO] = { + .highlight = "\x1b[7m", .highlight_reset = "\x1b[27m", + .section = "\x1b[1m", .section_reset = "\x1b[22m", + .note = "\x1b[3m", .note_reset = "\x1b[23m", + .done = "\x1b[9m", .done_reset = "\x1b[29m", + .number = "", .number_reset = "", + .muted = "", .muted_reset = "", + .match = "\x1b[7m", .match_reset = "\x1b[27m", + }, + [THEME_CUSTOM] = CUSTOM_THEME, +}; + +static const char* const theme_names[] = { + [THEME_DEFAULT] = "default", + [THEME_GRUVBOX] = "gruvbox", + [THEME_NORD] = "nord", + [THEME_MONO] = "mono", + [THEME_CUSTOM] = "custom", +}; + +#define THEME_COUNT ((int) (sizeof themes / sizeof themes[0])) + +static int theme_index = THEME; +static const struct Theme* theme = &themes[THEME]; + static const char* mode_name(enum Mode mode) { switch (mode) @@ -80,57 +172,51 @@ static const char* mode_name(enum Mode mode) case MODE_COMMAND: return "COMMAND"; case MODE_SEARCH: return "SEARCH"; case MODE_CONFIRM: return "CONFIRM"; + case MODE_HELP: return "HELP"; } return "?"; } -static int is_editing(enum Mode mode) +static inline int is_editing(enum Mode mode) { return mode == MODE_INSERT || mode == MODE_REPLACE; } -static int utf8_width(const char* text, int bytes) +static inline 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 position) +static inline int utf8_left(const char* text, int position) { if (position <= 0) return 0; - position--; while (position > 0 && ((unsigned char) text[position] & 0xC0) == 0x80) position--; - return position; } -static int utf8_right(const char* text, int position) +static inline int utf8_right(const char* text, int position) { int length = (int) strlen(text); - if (position >= length) return length; - position++; while (position < length && ((unsigned char) text[position] & 0xC0) == 0x80) position++; - return position; } -static int caret_cols(const char* name, const char* text, int edit_pos) +static inline int caret_cols(const char* name, const char* text, int edit_pos) { int offset = (int) (name - text); int byte = edit_pos - offset < 0 ? 0 : edit_pos - offset; @@ -140,7 +226,6 @@ static int caret_cols(const char* name, const char* text, int edit_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); @@ -148,14 +233,12 @@ static void todo_path(char* buffer, size_t size) } 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'; } @@ -165,7 +248,6 @@ static void load(struct List* list, const char* path) list->count = 0; FILE* file = fopen(path, "r"); - if (file == NULL) return; @@ -203,37 +285,63 @@ static void load(struct List* list, const char* path) (void) fclose(file); } -static int is_section(const struct Item* item) +static inline int is_section(const struct Item* item) { return item->text[0] == '='; } -static int is_subtask(const struct Item* item) +static inline int is_subtask(const struct Item* item) { return item->text[0] == '-'; } -static int is_optional(const struct Item* item) +static inline int is_optional(const struct Item* item) { return item->text[0] == '*'; } -static int is_note(const struct Item* item) +static inline int is_note(const struct Item* item) { return item->text[0] == '>'; } -static int is_child(const struct Item* item) +static inline int is_important(const struct Item* item) +{ + return item->text[0] == '!'; +} + +static inline int is_child(const struct Item* item) { return is_subtask(item) || is_note(item); } -static int is_task(const struct Item* item) +static inline int is_task(const struct Item* item) { return !is_section(item) && !is_child(item); } -static int task_group(const struct Item* item) +static inline int valid_cursor(const struct List* list, int index) +{ + return index >= 0 && index < list->count; +} + +static inline char state_char(enum State state) +{ + return state == STATE_DONE ? 'x' : state == STATE_DOING ? '-' : ' '; +} + +static inline const char* checkbox(enum State state, int optional) +{ + if (optional) + return state == STATE_DONE ? MARK_OPT_DONE + : state == STATE_DOING ? MARK_OPT_DOING + : MARK_OPT_TODO; + return state == STATE_DONE ? MARK_DONE + : state == STATE_DOING ? MARK_DOING + : MARK_TODO; +} + +static inline int task_group(const struct Item* item) { if (item->state == STATE_DONE) return 2; @@ -242,7 +350,7 @@ static int task_group(const struct Item* item) return 0; } -static int section_level(const struct Item* item) +static inline int section_level(const struct Item* item) { int level = 0; while (item->text[level] == '=') @@ -250,13 +358,12 @@ static int section_level(const struct Item* item) return level; } -static int text_start(const struct Item* item) +static inline 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)) + else if (is_subtask(item) || is_optional(item) || is_note(item) || is_important(item)) i = 1; while (item->text[i] == ' ') @@ -292,7 +399,6 @@ static void normalize_tasks(struct List* list) if (total == 0) continue; - if (done == total) list->items[i].state = STATE_DONE; else if (done > 0 || active > 0) @@ -305,7 +411,6 @@ static void normalize_tasks(struct List* list) static void sort_list(struct List* list) { struct Item* sorted = malloc((size_t) list->count * sizeof *sorted); - if (sorted == NULL) return; @@ -322,18 +427,15 @@ static void sort_list(struct List* list) } 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_child(&list->items[block])) block++; @@ -342,14 +444,12 @@ static void sort_list(struct List* list) 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; @@ -375,7 +475,6 @@ static void sort_list(struct List* list) for (int x = 0; x < list->count; x++) list->items[x] = sorted[x]; - free(sorted); } @@ -419,7 +518,6 @@ static void save(struct List* list, const char* path) normalize_tasks(list); FILE* file = fopen(path, "w"); - if (file == NULL) return; @@ -431,11 +529,7 @@ static void save(struct List* list, const char* path) 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) fprintf(file, "[%c] %s\n", state_char(list->items[i].state), list->items[i].text); } (void) fclose(file); @@ -463,10 +557,8 @@ 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; } @@ -497,10 +589,8 @@ static void stack_clear(struct Snapshot* stack, int* count) 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); } @@ -509,7 +599,6 @@ 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]; @@ -521,7 +610,6 @@ 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]; @@ -541,12 +629,11 @@ static int replace_start; static void paste_item(struct List* list, struct Editor* editor, int index) { - if (yank_count <= 0 || list->count + yank_count > MAX_ITEMS) + if (yank_count <= 0 || + list->count + yank_count > MAX_ITEMS) return; - if (index < 0) index = 0; - if (index > list->count) index = list->count; @@ -554,14 +641,13 @@ static void paste_item(struct List* list, struct Editor* editor, int index) 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]; editor->cursor = index; } -static char lower(char character) +static inline char lower(char character) { return character >= 'A' && character <= 'Z' ? (char) (character + 32) : character; } @@ -576,7 +662,9 @@ static int contains_fold(const char* haystack, const char* needle) const char* h = haystack; const char* n = needle; - while (*h != '\0' && *n != '\0' && lower(*h) == lower(*n)) + while (*h != '\0' && + *n != '\0' && + lower(*h) == lower(*n)) { h++; n++; @@ -592,7 +680,7 @@ static int contains_fold(const char* haystack, const char* needle) static int match_at(const char* text, int position, const char* needle) { int k = 0; - while (needle[k] != '\0' && + while (needle[k] != '\0' && text[position + k] != '\0' && lower(text[position + k]) == lower(needle[k])) k++; @@ -617,7 +705,6 @@ static int find_match(const struct List* list, int from, int direction, const ch 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; } @@ -628,13 +715,11 @@ static int find_match(const struct List* list, int from, int direction, const ch static void goto_match(const struct List* list, struct Editor* editor, int direction) { int match = find_match(list, editor->cursor, direction, editor->search); - if (match < 0) return; editor->cursor = match; int offset = match_offset(list->items[match].text, editor->search); - if (offset >= 0) editor->edit_pos = offset; } @@ -643,10 +728,8 @@ 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; @@ -665,7 +748,6 @@ static void delete_item(struct List* list, int index) for (int i = index; i < list->count - 1; i++) list->items[i] = list->items[i + 1]; - list->count--; } @@ -673,8 +755,7 @@ static struct termios saved_termios; static void restore_terminal(void) { - static const char reset[] = "\x1b[?25h\x1b[0 q\x1b[?1049l"; - + static const char reset[] = CURSOR_SHOW CURSOR_RESET SCREEN_LEAVE; (void) tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios); (void) write(STDOUT_FILENO, reset, sizeof reset - 1); } @@ -708,8 +789,7 @@ static int enter_raw_mode(void) (void) signal(SIGINT, on_signal); (void) signal(SIGTERM, on_signal); - (void) write(STDOUT_FILENO, "\x1b[?1049h\x1b[?25l", 14); - + (void) write(STDOUT_FILENO, SCREEN_ENTER CURSOR_HIDE, 14); return 0; } @@ -737,40 +817,71 @@ static int content_rows(void) struct Line { - int cols; const char* background; const char* background_reset; const char* prefix; + int prefix_cols; int first_col; + int cols; + const char* style; const char* style_reset; const char* text; const char* search; + int is_cursor; int caret; + int upper; /* uppercase the text (important items) */ }; -static int emit_wrapped(char* buffer, int length, const struct Line* spec, - int* emitted, int visible, int* cursor_row, int* cursor_column) +static int wrap_lines(const char* name, int cols, int first_col, int cont_col, + int* starts, int* ends, int max_lines); +static inline int display_width(const char* name, int from, int to); + +struct Out +{ + char* buffer; + int length; + int cap; +}; + +static void append(struct Out* out, const char* format, ...) +{ + if (out->length >= out->cap) + return; + + va_list args; + va_start(args, format); + int written = vsnprintf(out->buffer + out->length, (size_t) (out->cap - out->length), format, args); + va_end(args); + + if (written > 0) + out->length += written; + if (out->length > out->cap) + out->length = out->cap; +} + +static void emit_wrapped(struct Out* out, const struct Line* spec, + int* emitted, int visible, int* cursor_row, int* cursor_column) { - int cols = spec->cols; const char* background = spec->background; const char* background_reset = spec->background_reset; const char* prefix = spec->prefix; + int prefix_cols = spec->prefix_cols; int first_col = spec->first_col; + int cols = spec->cols; + const char* style = spec->style; const char* style_reset = spec->style_reset; const char* text = spec->text; const char* search = spec->search; + int is_cursor = spec->is_cursor; int caret_cols = spec->caret; - + int upper = spec->upper; int total = (int) strlen(text); - int byte = 0; - int line = 0; - int text_col = 0; char mask[MAX_TEXT]; memset(mask, 0, sizeof mask); @@ -784,95 +895,64 @@ static int emit_wrapped(char* buffer, int length, const struct Line* spec, mask[i + k] = 1; } - do - { - while (line > 0 && byte < total && text[byte] == ' ') - { - byte++; - text_col++; - } - - if (line > 0 && byte >= total) - break; + int starts[MAX_TEXT]; + int ends[MAX_TEXT]; + int lines = wrap_lines(text, cols, first_col, prefix_cols, starts, ends, MAX_TEXT); + for (int line = 0; line < lines && *emitted < visible; line++) + { 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; - } + int start = starts[line]; + int end = ends[line]; if (line == 0) - length += snprintf(buffer + length, 64 * 1024 - (size_t) length, - "%s%s%s", background, prefix, style); + append(out, "%s%s%s", background, prefix, style); else - length += snprintf(buffer + length, 64 * 1024 - (size_t) length, - "%s%*s%s", background, prefix_cols, "", style); + append(out, "%s%*s%s", background, prefix_cols, "", style); - for (int i = byte; i < end;) + for (int i = start; i < end;) { int j = i; while (j < end && mask[j] == mask[i]) j++; + const char* chunk = text + i; + char upper_buf[MAX_TEXT]; + + if (upper) + { + for (int k = 0; k < j - i; k++) + { + char c = text[i + k]; + upper_buf[k] = c >= 'a' && c <= 'z' ? (char) (c - 32) : c; + } + chunk = upper_buf; + } + if (mask[i]) - length += snprintf(buffer + length, 64 * 1024 - (size_t) length, - "\x1b[7m%.*s\x1b[27m", j - i, text + i); + append(out, "%s%.*s%s", theme->match, j - i, chunk, theme->match_reset); else - length += snprintf(buffer + length, 64 * 1024 - (size_t) length, - "%.*s", j - i, text + i); + append(out, "%.*s", j - i, chunk); i = j; } - length += snprintf(buffer + length, 64 * 1024 - (size_t) length, - "%s\x1b[K%s\r\n", style_reset, background_reset); + append(out, "%s" CLEAR_EOL "%s\r\n", style_reset, background_reset); - if (is_cursor && *cursor_row < 0 - && caret_cols >= line_start_col && caret_cols <= line_start_col + width) + if (is_cursor && *cursor_row < 0) { - *cursor_row = row; - *cursor_column = base + 1 + caret_cols - line_start_col; + int line_start_col = display_width(text, 0, start); + int width = display_width(text, start, end); + if (caret_cols >= line_start_col && caret_cols <= line_start_col + width) + { + *cursor_row = 4 + *emitted; + *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) @@ -923,11 +1003,59 @@ static int compute_number_width(const struct List* list) static void build_line(const struct List* list, int index, int number_width, int raw, char* prefix, size_t prefix_size, struct Line* line); +static int render_help(int measure) +{ + static const char* const lines[] = { + "", + " todo \xe2\x80\x94 keys press any key to return", + "", + " move h j k l 0 $ line w b word", + " { } section gg G ends", + " edit i insert a/A append o new R replace", + " x del char dd cut item D clear done", + " cw ce cb c0 c$ cc (also d: dw de db ...)", + " ct cf ci ca <char or ()[]{}<>\"'>", + " state space cycle todo/doing/done", + " yy p P yank / paste", + " J K move item or section", + " find / n N esc clears the highlight", + " undo u ctrl-r", + " cmd :w :q :wq :x :theme NAME", + " :reset :clear :reorder", + "", + " items = section == subsection", + " - subtask * optional > note", + }; + + char buffer[8 * 1024]; + struct Out out = { buffer, 0, (int) sizeof buffer }; + int rows = screen_rows(); + int count = (int) (sizeof lines / sizeof lines[0]); + + append(&out, CURSOR_HOME CURSOR_HIDE); + + for (int row = 0; row < rows; row++) + { + append(&out, "%s" CLEAR_EOL, row < count ? lines[row] : ""); + if (row < rows - 1) + append(&out, "\r\n"); + } + + if (!measure) + (void) write(STDOUT_FILENO, buffer, (size_t) out.length); + + return -1; +} + static int render(const struct List* list, const struct Editor* editor, int measure) { + enum Mode mode = editor->mode; + + if (mode == MODE_HELP) + return render_help(measure); + int cursor = editor->cursor; int scroll = editor->scroll; - enum Mode mode = editor->mode; int edit_pos = editor->edit_pos; const char* command = editor->command; @@ -953,19 +1081,19 @@ static int render(const struct List* list, const struct Editor* editor, int meas 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) + if (local == NULL || strftime(date, sizeof date, DATE_FORMAT, local) == 0) date[0] = '\0'; - if (local == NULL || strftime(clock, sizeof clock, "%H:%M", local) == 0) + if (local == NULL || strftime(clock, sizeof clock, TIME_FORMAT, local) == 0) clock[0] = '\0'; char buffer[64 * 1024]; - int length = 0; + struct Out out = { buffer, 0, (int) sizeof buffer }; - 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); + append(&out, + CURSOR_HOME "%*s%s%s%s" CLEAR_EOL "\r\n%*s%s%s%s" CLEAR_EOL "\r\n" CLEAR_EOL "\r\n", + LEFT_MARGIN, "", theme->muted, date, theme->muted_reset, + LEFT_MARGIN, "", theme->muted, clock, theme->muted_reset); int emitted = 0; int index = scroll; @@ -977,14 +1105,14 @@ static int render(const struct List* list, const struct Editor* editor, int meas { const struct Item* item = &list->items[index]; - const char* background = index == cursor ? HIGHLIGHT : ""; - const char* background_reset = index == cursor ? HIGHLIGHT_RESET : ""; + const char* background = index == cursor ? theme->highlight : ""; + const char* background_reset = index == cursor ? theme->highlight_reset : ""; if (is_section(item)) { if (emitted > 0) { - length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); + append(&out, CLEAR_EOL "\r\n"); emitted++; if (emitted >= visible) @@ -1001,9 +1129,9 @@ static int render(const struct List* list, const struct Editor* editor, int meas 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); + append(&out, + "%s%*s%s%s%s" CLEAR_EOL "%s\r\n", + background, section_indent, "", theme->section, item->text, theme->section_reset, background_reset); } else { @@ -1013,7 +1141,6 @@ static int render(const struct List* list, const struct Editor* editor, int meas if (is_section(&list->items[k])) { int other = section_level(&list->items[k]); - if (other == level) section_number++; else if (other < level) @@ -1033,9 +1160,9 @@ static int render(const struct List* list, const struct Editor* editor, int meas 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); + append(&out, + "%s%*s%s%s. %s%s" CLEAR_EOL "%s\r\n", + background, section_indent, "", theme->section, roman, name, theme->section_reset, background_reset); } emitted++; @@ -1046,10 +1173,9 @@ static int render(const struct List* list, const struct Editor* editor, int meas if (is_task(item)) { 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"); + append(&out, CLEAR_EOL "\r\n"); emitted++; if (emitted >= visible) @@ -1071,42 +1197,42 @@ static int render(const struct List* list, const struct Editor* editor, int meas line.is_cursor = index == cursor; line.caret = index == cursor ? caret_cols(line.text, item->text, edit_pos) : 0; - length = emit_wrapped(buffer, length, &line, &emitted, visible, &cursor_row, &cursor_column); + emit_wrapped(&out, &line, &emitted, visible, &cursor_row, &cursor_column); index++; } while (emitted < visible) { - length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[K\r\n"); + append(&out, CLEAR_EOL "\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); + append(&out, + "\x1b[%d;1H%*s%c%s" CLEAR_EOL, + 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?"); + append(&out, + "\x1b[%d;1H%*s%s [y/n]" CLEAR_EOL, + rows, LEFT_MARGIN, "", + pending_action == PENDING_CLEAR ? "clear the whole list?" : "reset all items to todo?"); else if (editor->message[0] != '\0') - length += snprintf(buffer + length, sizeof buffer - (size_t) length, - "\x1b[%d;1H%*s\x1b[2m%s\x1b[0m\x1b[K", - rows, LEFT_MARGIN, "", editor->message); + append(&out, + "\x1b[%d;1H%*s%s%s%s" CLEAR_EOL, + rows, LEFT_MARGIN, "", theme->muted, editor->message, theme->muted_reset); 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); + append(&out, + "\x1b[%d;1H%*s%s[%s] %d/%d done%s" CLEAR_EOL, + rows, LEFT_MARGIN, "", theme->muted, mode_name(mode), done, total_items, theme->muted_reset); 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); + append(&out, + "\x1b[%d;%dH" CURSOR_INSERT CURSOR_SHOW, rows, cursor_column); } - else if (mode != MODE_CONFIRM && cursor >= 0 && cursor < list->count && cursor_row >= 0) + else if (mode != MODE_CONFIRM && valid_cursor(list, cursor) && cursor_row >= 0) { if (is_section(&list->items[cursor])) { @@ -1116,21 +1242,21 @@ static int render(const struct List* list, const struct Editor* editor, int meas 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"; + const char* shape = mode == MODE_REPLACE ? CURSOR_REPLACE + : mode == MODE_INSERT ? CURSOR_INSERT + : CURSOR_NORMAL; if (cursor_column >= 0) - length += snprintf(buffer + length, sizeof buffer - (size_t) length, - "\x1b[%d;%dH%s\x1b[?25h", cursor_row, cursor_column, shape); + append(&out, + "\x1b[%d;%dH%s" CURSOR_SHOW, cursor_row, cursor_column, shape); } else { - length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[?25l"); + append(&out, CURSOR_HIDE); } if (!measure) - (void) write(STDOUT_FILENO, buffer, (size_t) length); + (void) write(STDOUT_FILENO, buffer, (size_t) out.length); return cursor_row; } @@ -1193,7 +1319,7 @@ static int read_key(void) return 27; } -static const char* strip_marker(const char* text) +static inline const char* strip_marker(const char* text) { const char* name = text + 1; @@ -1212,9 +1338,6 @@ static int base_indent_of(const struct List* list, int index) return LEFT_MARGIN; } -/* fill a Line's geometry, prefix string and style for a non-section item. - raw keeps the item's marker visible (used while editing it). the caller - fills the per-frame fields: cols, background, search, is_cursor, caret. */ static void build_line(const struct List* list, int index, int number_width, int raw, char* prefix, size_t prefix_size, struct Line* line) { @@ -1224,6 +1347,7 @@ static void build_line(const struct List* list, int index, int number_width, int line->style = ""; line->style_reset = ""; line->prefix = prefix; + line->upper = 0; if (is_note(item)) { @@ -1231,27 +1355,24 @@ static void build_line(const struct List* list, int index, int number_width, int line->text = item->text; line->first_col = indent; line->prefix_cols = indent + 2; - line->style = "\x1b[2;3m"; - line->style_reset = "\x1b[22;23m"; + line->style = theme->note; + line->style_reset = theme->note_reset; (void) snprintf(prefix, prefix_size, "%*s", indent, ""); return; } - const char* box = item->state == STATE_DONE ? "[x]" - : item->state == STATE_DOING ? "[-]" - : "[ ]"; - if (item->state == STATE_DONE) { - line->style = "\x1b[2m"; - line->style_reset = "\x1b[22m"; + line->style = theme->done; + line->style_reset = theme->done_reset; } if (is_subtask(item)) { int indent = base + number_width + SUBTASK_INDENT; + const char* box = checkbox(item->state, 0); line->text = raw ? item->text : strip_marker(item->text); - line->first_col = line->prefix_cols = indent + 4; + line->first_col = line->prefix_cols = indent + utf8_width(box, (int) strlen(box)) + 1; (void) snprintf(prefix, prefix_size, "%*s%s ", indent, "", box); return; } @@ -1262,15 +1383,12 @@ static void build_line(const struct List* list, int index, int number_width, int if (!is_child(&list->items[k])) number++; - const char* task_box = is_optional(item) ? (item->state == STATE_DONE ? "(x)" - : item->state == STATE_DOING ? "(-)" - : "( )") - : box; + const char* box = checkbox(item->state, is_optional(item)); line->text = is_optional(item) && !raw ? strip_marker(item->text) : item->text; - line->first_col = line->prefix_cols = base + number_width + 6; - (void) snprintf(prefix, prefix_size, "%*s\x1b[2m%*d.\x1b[22m %s ", - base, "", number_width, number, task_box); + line->first_col = line->prefix_cols = base + number_width + utf8_width(box, (int) strlen(box)) + 3; + (void) snprintf(prefix, prefix_size, "%*s%s%*d.%s %s ", + base, "", theme->number, number_width, number, theme->number_reset, box); } static int wrap_lines(const char* name, int cols, int first_col, int cont_col, @@ -1290,7 +1408,6 @@ static int wrap_lines(const char* name, int cols, int first_col, int cont_col, int base = line == 0 ? first_col : cont_col; int avail = cols - base; - if (avail < 1) avail = 1; @@ -1339,7 +1456,7 @@ static int wrap_lines(const char* name, int cols, int first_col, int cont_col, return line; } -static int display_width(const char* name, int from, int to) +static inline int display_width(const char* name, int from, int to) { int width = 0; @@ -1391,7 +1508,6 @@ static void place_on_line(const struct List* list, int index, int number_width, int line = last ? lines - 1 : 0; int base = line == 0 ? first_col : cont_col; int want = goal - base; - if (want < 0) want = 0; @@ -1403,7 +1519,7 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid int* cursor = &editor->cursor; int* edit_pos = &editor->edit_pos; - if (*cursor < 0 || *cursor >= list->count) + if (!valid_cursor(list, *cursor)) return; const struct Item* item = &list->items[*cursor]; @@ -1427,10 +1543,8 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT); int position = *edit_pos - name_offset; - if (position < 0) position = 0; - if (position > name_length) position = name_length; @@ -1444,7 +1558,6 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid if (down && line < lines - 1) { int want = goal - cont_col; - if (want < 0) want = 0; @@ -1457,7 +1570,6 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid int target = line - 1; int target_base = target == 0 ? first_col : cont_col; int want = goal - target_base; - if (want < 0) want = 0; @@ -1478,7 +1590,7 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid } } -static int matching_close(int open) +static inline int matching_close(int open) { switch (open) { @@ -1491,7 +1603,7 @@ static int matching_close(int open) return 0; } -static int matching_open(int close) +static inline int matching_open(int close) { switch (close) { @@ -1508,10 +1620,8 @@ static void apply_change(struct List* list, struct Editor* editor, int from, int { struct Item* item = &list->items[editor->cursor]; int length = (int) strlen(item->text); - if (to > length) to = length; - if (from < 0 || to < from) return; @@ -1525,10 +1635,8 @@ static void delete_text(struct List* list, struct Editor* editor, int from, int { struct Item* item = &list->items[editor->cursor]; int length = (int) strlen(item->text); - if (to > length) to = length; - if (from < 0 || to <= from) return; @@ -1638,7 +1746,6 @@ static int motion_range(struct List* list, const struct Editor* editor, int op, } int target = read_key(); - if (target < 32 || target >= 256) return 0; @@ -1663,11 +1770,9 @@ static int motion_range(struct List* list, const struct Editor* editor, int op, 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; @@ -1746,48 +1851,37 @@ static int normal_key(struct List* list, struct Editor* editor, int key) switch (key) { case 27: - { search[0] = '\0'; break; - } - case ':': { command[0] = '\0'; *mode = MODE_COMMAND; - break; - } - + } break; case '/': { command[0] = '\0'; *mode = MODE_SEARCH; + } break; + case '?': + *mode = MODE_HELP; break; - } - case 'n': - { goto_match(list, editor, 1); break; - } - case 'N': - { goto_match(list, editor, -1); break; - } - case 'y': { int next = read_key(); - if (next != 'y') { unget_key(next); break; } - if (*cursor >= 0 && *cursor < list->count) + if (valid_cursor(list, *cursor)) { int start = *cursor; int end = item_block_end(list, start); @@ -1797,22 +1891,13 @@ static int normal_key(struct List* list, struct Editor* editor, int key) for (int i = start; i < end && yank_count < MAX_ITEMS; i++) yank_register[yank_count++] = list->items[i]; } - - break; - } - + } break; case 'p': - { paste_item(list, editor, item_block_end(list, *cursor)); break; - } - case 'P': - { paste_item(list, editor, *cursor); break; - } - case 'j': case KEY_DOWN: { @@ -1821,10 +1906,7 @@ static int normal_key(struct List* list, struct Editor* editor, int key) for (int repeat = 0; repeat < count; repeat++) move_visual(list, editor, number_width, cols, 1); - - break; - } - + } break; case 'k': case KEY_UP: { @@ -1833,111 +1915,34 @@ static int normal_key(struct List* list, struct Editor* editor, int key) for (int repeat = 0; repeat < count; repeat++) move_visual(list, editor, number_width, cols, 0); - - break; - } - + } 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) + if (valid_cursor(list, *cursor)) { - 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--; - } + int op = key == KEY_LEFT ? 'h' : key == KEY_RIGHT ? 'l' : key; + int from; + int to; - *edit_pos = position; + motion_range(list, editor, op, &from, &to); + *edit_pos = op == 'h' || op == '0' || op == 'b' ? from : to; } - 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]) && + if (valid_cursor(list, *cursor) && + !is_section(&list->items[*cursor]) && !is_note(&list->items[*cursor])) { history_record(list); @@ -1945,13 +1950,9 @@ static int normal_key(struct List* list, struct Editor* editor, int key) item->state = (enum State) ((item->state + 1) % 3); commit(list); } - break; - } - case 'x': - { - if (*cursor >= 0 && *cursor < list->count + if (valid_cursor(list, *cursor) && *edit_pos < (int) strlen(list->items[*cursor].text)) { history_record(list); @@ -1965,10 +1966,7 @@ static int normal_key(struct List* list, struct Editor* editor, int key) strlen(item->text) - (size_t) next + 1); } } - break; - } - case 'o': { history_record(list); @@ -1978,9 +1976,7 @@ static int normal_key(struct List* list, struct Editor* editor, int key) *cursor = at < list->count ? at : list->count - 1; *edit_pos = 0; *mode = MODE_INSERT; - break; - } - + } break; case 'A': { history_record(list); @@ -1991,32 +1987,24 @@ static int normal_key(struct List* list, struct Editor* editor, int key) *cursor = 0; } - if (*cursor >= 0 && *cursor < list->count) + if (valid_cursor(list, *cursor)) { *edit_pos = (int) strlen(list->items[*cursor].text); *mode = MODE_INSERT; } - - break; - } - + } break; case 'i': - { - if (*cursor >= 0 && *cursor < list->count) + if (valid_cursor(list, *cursor)) { 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); @@ -2028,23 +2016,19 @@ static int normal_key(struct List* list, struct Editor* editor, int key) *edit_pos = 0; } - if (*cursor >= 0 && *cursor < list->count) + if (valid_cursor(list, *cursor)) { (void) snprintf(replace_original, sizeof replace_original, "%s", list->items[*cursor].text); replace_start = *edit_pos; *mode = MODE_REPLACE; } - - break; - } - + } break; case 'c': { - if (*cursor < 0 || *cursor >= list->count) + if (!valid_cursor(list, *cursor)) break; int op = read_key(); - if (op == 'c') { const struct Item* item = &list->items[*cursor]; @@ -2061,28 +2045,18 @@ static int normal_key(struct List* list, struct Editor* editor, int key) if (motion_range(list, editor, op, &from, &to)) apply_change(list, editor, from, to); - - break; - } - + } break; case 'g': { int next = read_key(); - if (next == 'g') *cursor = 0; else unget_key(next); - - break; - } - + } break; case 'G': - { *cursor = list->count - 1; break; - } - case '}': { int next = *cursor + 1; @@ -2091,9 +2065,7 @@ static int normal_key(struct List* list, struct Editor* editor, int key) next++; *cursor = next < list->count ? next : list->count - 1; - break; - } - + } break; case '{': { int previous = *cursor - 1; @@ -2102,16 +2074,13 @@ static int normal_key(struct List* list, struct Editor* editor, int key) previous--; *cursor = previous >= 0 ? previous : 0; - break; - } - + } break; case 'd': { int op = read_key(); - if (op == 'd') { - if (*cursor >= 0 && *cursor < list->count) + if (valid_cursor(list, *cursor)) { history_record(list); @@ -2135,7 +2104,7 @@ static int normal_key(struct List* list, struct Editor* editor, int key) break; } - if (*cursor < 0 || *cursor >= list->count) + if (!valid_cursor(list, *cursor)) { unget_key(op); break; @@ -2146,19 +2115,16 @@ static int normal_key(struct List* list, struct Editor* editor, int key) if (motion_range(list, editor, op, &from, &to)) delete_text(list, editor, from, to); - - break; - } - + } break; case 'J': - if (*cursor < 0 || *cursor >= list->count) + { + if (!valid_cursor(list, *cursor)) break; if (is_section(&list->items[*cursor])) { int start = *cursor; int end = block_end(list, start); - if (end < list->count) { history_record(list); @@ -2173,7 +2139,6 @@ static int normal_key(struct List* list, struct Editor* editor, int key) { int start = *cursor; int end = task_block_end(list, start); - if (end < list->count && !is_section(&list->items[end])) { history_record(list); @@ -2192,17 +2157,16 @@ static int normal_key(struct List* list, struct Editor* editor, int key) list->items[*cursor + 1] = moved; (*cursor)++; } - - break; + } break; case 'K': - if (*cursor < 0 || *cursor >= list->count) + { + if (!valid_cursor(list, *cursor)) break; if (is_section(&list->items[*cursor])) { int start = *cursor; - if (start > 0) { history_record(list); @@ -2245,8 +2209,7 @@ static int normal_key(struct List* list, struct Editor* editor, int key) list->items[*cursor - 1] = moved; (*cursor)--; } - - break; + } break; case 'D': { @@ -2270,13 +2233,9 @@ static int normal_key(struct List* list, struct Editor* editor, int key) list->count = kept; commit(list); - break; - } + } break; - default: - { - break; - } + default: break; } return 1; @@ -2298,7 +2257,6 @@ static void insert_key(struct List* list, struct Editor* editor, int key) if (*cursor >= list->count) *cursor = list->count - 1; - if (*cursor < 0) *cursor = 0; } @@ -2337,7 +2295,6 @@ static void insert_key(struct List* list, struct Editor* editor, int key) { 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]; @@ -2374,12 +2331,57 @@ static void insert_key(struct List* list, struct Editor* editor, int key) } } +static void complete_word(char* buf, size_t size, const char* const* options, int count) +{ + size_t len = strlen(buf); + const char* first = NULL; + size_t common = 0; + + for (int i = 0; i < count; i++) + { + if (strncmp(options[i], buf, len) != 0) + continue; + + if (first == NULL) + { + first = options[i]; + common = strlen(first); + continue; + } + + size_t k = 0; + while (k < common && options[i][k] == first[k]) + k++; + common = k; + } + + if (first == NULL || common >= size) + return; + + memcpy(buf, first, common); + buf[common] = '\0'; +} + static int command_key(struct List* list, const char* path, struct Editor* editor, int key) { + static const char* const names[] = { + "w", "q", "wq", "x", "reset", "clear", "reorder", "theme", + }; + enum Mode* mode = &editor->mode; char* command = editor->command; size_t length = strlen(command); + if (key == '\t') + { + if (strncmp(command, "theme ", 6) == 0) + complete_word(command + 6, MAX_COMMAND - 6, theme_names, THEME_COUNT); + else + complete_word(command, MAX_COMMAND, names, (int) (sizeof names / sizeof names[0])); + + return 1; + } + if (key == 27) { command[0] = '\0'; @@ -2413,7 +2415,37 @@ static int command_key(struct List* list, const char* path, struct Editor* edito return 1; } - int write = strcmp(command, "w") == 0 || + if (strcmp(command, "theme") == 0 || + strncmp(command, "theme ", 6) == 0) + { + const char* name = command + 5; + while (*name == ' ') + name++; + + if (*name == '\0') + { + theme_index = (theme_index + 1) % THEME_COUNT; + } + else + { + int found = -1; + for (int i = 0; i < THEME_COUNT; i++) + if (strcmp(name, theme_names[i]) == 0) + found = i; + + if (found < 0) + (void) snprintf(editor->message, sizeof editor->message, "no theme: %s", name); + else + theme_index = found; + } + + theme = &themes[theme_index]; + command[0] = '\0'; + *mode = MODE_NORMAL; + return 1; + } + + int write = strcmp(command, "w") == 0 || strcmp(command, "wq") == 0 || strcmp(command, "x") == 0; int quit = strcmp(command, "q") == 0 || @@ -2515,9 +2547,7 @@ static void confirm_key(struct List* list, struct Editor* editor, int key) list->items[i].state = STATE_TODO; } else if (pending_action == PENDING_CLEAR) - { list->count = 0; - } commit(list); } @@ -2541,7 +2571,6 @@ static void run(struct List* list, const char* path) { if (editor.cursor >= list->count) editor.cursor = list->count - 1; - if (editor.cursor < 0) editor.cursor = 0; @@ -2551,13 +2580,11 @@ static void run(struct List* list, const char* path) int text_length = (int) strlen(text); int caret_max = is_editing(editor.mode) ? text_length : (text_length > 0 ? text_length - 1 : 0); int caret_min = is_editing(editor.mode) ? 0 : text_start(&list->items[editor.cursor]); - if (caret_min > caret_max) caret_min = caret_max; if (editor.edit_pos > caret_max) editor.edit_pos = caret_max; - if (editor.edit_pos < caret_min) editor.edit_pos = caret_min; @@ -2574,7 +2601,6 @@ static void run(struct List* list, const char* path) render(list, &editor, 0); int key = read_key(); - if (key == -1) break; @@ -2587,7 +2613,6 @@ static void run(struct List* list, const char* path) if ((key >= '1' && key <= '9') || (key == '0' && editor.count > 0)) { editor.count = editor.count * 10 + (key - '0'); - if (editor.count > 9999) editor.count = 9999; @@ -2596,35 +2621,24 @@ static void run(struct List* list, const char* path) normal_key(list, &editor, key); editor.count = 0; - break; - } - + } break; case MODE_INSERT: case MODE_REPLACE: - { insert_key(list, &editor, key); break; - } - case MODE_COMMAND: - { if (command_key(list, path, &editor, key) == 0) return; - break; - } - case MODE_SEARCH: - { search_key(list, &editor, key); break; - } - case MODE_CONFIRM: - { confirm_key(list, &editor, key); break; - } + case MODE_HELP: + editor.mode = MODE_NORMAL; + break; } } } @@ -2656,8 +2670,9 @@ static void print_help(void) " 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" + " ? show keys in-app\n" " :w :q :wq :x write / quit :reset all back to todo :clear empty\n" - " :reorder re-sort the list\n" + " :reorder re-sort the list :theme NAME switch theme\n" "\n" "items: '=' section, '==' subsection, '-' subtask, '*' optional, '>' note.\n" "\n" @@ -2667,13 +2682,15 @@ static void print_help(void) int main(int argc, char** argv) { - if (argc >= 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) + 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)) + if (argc >= 2 && (strcmp(argv[1], "-v") == 0 || + strcmp(argv[1], "--version") == 0)) { print_version(); return EXIT_SUCCESS; @@ -2685,7 +2702,6 @@ int main(int argc, char** argv) todo_path(path, sizeof path); struct List* list = malloc(sizeof *list); - if (list == NULL) { (void) fprintf(stderr, "%s: out of memory\n", TODO_PROGRAM); |
