diff options
| author | hachem <im@hachem.wtf> | 2026-10-03 13:44:11 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-10-03 13:44:11 +0200 |
| commit | 2981b6b3bbd9d40aee6c0d23d450dcd58a00adac (patch) | |
| tree | e38002ae53310d7aaaa5416a41e0bbc026c87671 | |
| parent | 908e6d88f32c6eb1855d13156fad301c402a1798 (diff) | |
fix: code that i wrote when i was piss drunk
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | todo.1 | 7 | ||||
| -rw-r--r-- | todo.c | 1115 |
3 files changed, 632 insertions, 491 deletions
@@ -23,6 +23,7 @@ a little modal todo list that lives in my terminal. c0 c$ cc change to start / end of line, or the whole line ct{c} cf{c} change up to (cf: onto) the next {c} ci{d} ca{d} change inside / around a pair: () [] {} <> " ' + dw de db d0 d$ delete over the same motions (also dt df di da) space cycle todo -> doing -> done dd cut the item (with its subtasks and notes) D clear every done item @@ -148,6 +148,13 @@ Cycle the item through todo, doing and done. Cut the item (also copies it for pasting). A task takes its subtasks and notes with it. .TP +.BR dw ", " de ", " db ", " d0 ", " d$ "\fR, etc.\fP" +Delete over a motion, leaving normal mode. The same motions and text objects as +.B c +apply +.RB ( dw ", " de ", " db ", " d0 ", " d$ ", " "dt\fIc\fP" ", " "df\fIc\fP" ", " "di\fId\fP" ", " "da\fId\fP" ), +with counts. +.TP .B D Remove every done item. .TP @@ -58,15 +58,27 @@ enum Key KEY_RIGHT }; +struct Editor +{ + int cursor; + int scroll; + int edit_pos; + int count; + enum Mode mode; + char command[MAX_COMMAND]; + char search[MAX_COMMAND]; + char message[MAX_COMMAND]; +}; + static const char* mode_name(enum Mode mode) { switch (mode) { - case MODE_NORMAL: return "NORMAL"; - case MODE_INSERT: return "INSERT"; + 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_SEARCH: return "SEARCH"; case MODE_CONFIRM: return "CONFIRM"; } @@ -75,7 +87,8 @@ static const char* mode_name(enum Mode mode) static int is_editing(enum Mode mode) { - return mode == MODE_INSERT || mode == MODE_REPLACE; + return mode == MODE_INSERT || + mode == MODE_REPLACE; } static int utf8_width(const char* text, int bytes) @@ -89,32 +102,39 @@ static int utf8_width(const char* text, int bytes) return width; } -static int utf8_left(const char* text, int pos) +static int utf8_left(const char* text, int position) { - if (pos <= 0) + if (position <= 0) return 0; - pos--; + position--; - while (pos > 0 && ((unsigned char) text[pos] & 0xC0) == 0x80) - pos--; + while (position > 0 && ((unsigned char) text[position] & 0xC0) == 0x80) + position--; - return pos; + return position; } -static int utf8_right(const char* text, int pos) +static int utf8_right(const char* text, int position) { int length = (int) strlen(text); - if (pos >= length) + if (position >= length) return length; - pos++; + position++; - while (pos < length && ((unsigned char) text[pos] & 0xC0) == 0x80) - pos++; + while (position < length && ((unsigned char) text[position] & 0xC0) == 0x80) + position++; - return pos; + return position; +} + +static 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; + return utf8_width(name, byte); } static void todo_path(char* buffer, size_t size) @@ -151,7 +171,8 @@ static void load(struct List* list, const char* path) char line[MAX_TEXT + 8]; - while (list->count < MAX_ITEMS && fgets(line, sizeof line, file) != NULL) + while (list->count < MAX_ITEMS && + fgets(line, sizeof line, file) != NULL) { strip_newline(line); @@ -202,24 +223,30 @@ static int is_note(const struct Item* item) return item->text[0] == '>'; } +static int is_child(const struct Item* item) +{ + return is_subtask(item) || is_note(item); +} + +static int is_task(const struct Item* item) +{ + return !is_section(item) && !is_child(item); +} + 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; } @@ -234,7 +261,6 @@ static int text_start(const struct Item* item) while (item->text[i] == ' ') i++; - return i; } @@ -242,7 +268,7 @@ 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])) + if (!is_task(&list->items[i])) continue; int total = 0; @@ -250,7 +276,7 @@ static void normalize_tasks(struct List* list) int active = 0; for (int j = i + 1; - j < list->count && (is_subtask(&list->items[j]) || is_note(&list->items[j])); + j < list->count && is_child(&list->items[j]); j++) { if (!is_subtask(&list->items[j])) @@ -308,8 +334,7 @@ static void sort_list(struct List* list) { int block = k + 1; - while (block < segment - && (is_subtask(&list->items[block]) || is_note(&list->items[block]))) + while (block < segment && is_child(&list->items[block])) block++; if (task_group(&list->items[k]) == group) @@ -357,31 +382,23 @@ static void sort_list(struct List* list) 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]))) + while (i < list->count && is_child(&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])) + if (start >= 0 && start < list->count && is_task(&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; } @@ -414,7 +431,7 @@ static void save(struct List* list, const char* path) continue; } - char marker = list->items[i].state == STATE_DONE ? 'x' + char marker = list->items[i].state == STATE_DONE ? 'x' : list->items[i].state == STATE_DOING ? '-' : ' '; @@ -522,7 +539,7 @@ 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) +static void paste_item(struct List* list, struct Editor* editor, int index) { if (yank_count <= 0 || list->count + yank_count > MAX_ITEMS) return; @@ -541,12 +558,12 @@ static void paste_item(struct List* list, int index, int* cursor) for (int i = 0; i < yank_count; i++) list->items[index + i] = yank_register[i]; - *cursor = index; + editor->cursor = index; } -static char lower(char c) +static char lower(char character) { - return c >= 'A' && c <= 'Z' ? (char) (c + 32) : c; + return character >= 'A' && character <= 'Z' ? (char) (character + 32) : character; } static int contains_fold(const char* haystack, const char* needle) @@ -572,13 +589,13 @@ static int contains_fold(const char* haystack, const char* needle) return 0; } -static int match_at(const char* text, int pos, const char* needle) +static int match_at(const char* text, int position, const char* needle) { int k = 0; - - while (needle[k] != '\0' && text[pos + k] != '\0' && lower(text[pos + k]) == lower(needle[k])) + while (needle[k] != '\0' && + text[position + k] != '\0' && + lower(text[position + k]) == lower(needle[k])) k++; - return needle[k] == '\0' ? k : 0; } @@ -586,11 +603,9 @@ 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; } @@ -610,6 +625,20 @@ static int find_match(const struct List* list, int from, int direction, const ch return -1; } +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; +} + static void insert_item(struct List* list, int index) { if (list->count >= MAX_ITEMS) @@ -640,7 +669,6 @@ static void delete_item(struct List* list, int index) list->count--; } - static struct termios saved_termios; static void restore_terminal(void) @@ -688,37 +716,57 @@ static int enter_raw_mode(void) 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) +struct Line { + 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 is_cursor; + int caret; +}; + +static int emit_wrapped(char* buffer, int length, 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; + 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 total = (int) strlen(text); int byte = 0; int line = 0; @@ -830,7 +878,8 @@ static int emit_wrapped(char* buffer, int length, int cols, 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" }; + 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'; @@ -859,7 +908,7 @@ static int compute_number_width(const struct List* list) { if (is_section(&list->items[i])) run = 0; - else if (!is_subtask(&list->items[i]) && !is_note(&list->items[i]) && ++run > max_run) + else if (!is_child(&list->items[i]) && ++run > max_run) max_run = run; } @@ -871,19 +920,28 @@ static int compute_number_width(const struct List* list) 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) +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(const struct List* list, const struct Editor* editor, int 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; + int rows = screen_rows(); int cols = wrap_column(); int visible = content_rows(); - const char* matches = mode == MODE_SEARCH ? command : search; + const char* matches = mode == MODE_SEARCH ? command : editor->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])) + if (is_task(&list->items[i])) { total_items++; done += list->items[i].state == STATE_DONE; @@ -985,73 +1043,7 @@ static int render(const struct List* list, int cursor, int scroll, enum Mode mod 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 + if (is_task(item)) { int group = task_group(item); @@ -1065,45 +1057,21 @@ static int render(const struct List* list, int cursor, int scroll, enum Mode mod } 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); + char prefix[48]; + struct Line line = { 0 }; + build_line(list, index, number_width, is_editing(mode) && index == cursor, + prefix, sizeof prefix, &line); - 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); - } + line.cols = cols; + line.background = background; + line.background_reset = background_reset; + line.search = matches; + 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); index++; } @@ -1122,6 +1090,10 @@ static int render(const struct List* list, int cursor, int scroll, enum Mode mod "\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 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); else length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[%d;1H%*s\x1b[2m[%s] %d/%d done\x1b[0m\x1b[K", @@ -1221,53 +1193,84 @@ static int read_key(void) return 27; } -static const char* item_display(const struct List* list, int index, int number_width, - int* first_col, int* cont_col) +static const char* strip_marker(const char* text) { - const struct Item* item = &list->items[index]; - int heading_level = 0; + const char* name = text + 1; + + while (*name == ' ') + name++; + + return name; +} +static int base_indent_of(const struct List* list, int index) +{ for (int k = index - 1; k >= 0; k--) if (is_section(&list->items[k])) - { - heading_level = section_level(&list->items[k]); - break; - } + return LEFT_MARGIN + section_level(&list->items[k]) * SECTION_INDENT; - int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT; + 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) +{ + const struct Item* item = &list->items[index]; + int base = base_indent_of(list, index); + + line->style = ""; + line->style_reset = ""; + line->prefix = prefix; if (is_note(item)) { - int indent = base_indent + number_width + 6; - *first_col = indent; - *cont_col = indent + 2; - return item->text; + int indent = base + number_width + 6; + line->text = item->text; + line->first_col = indent; + line->prefix_cols = indent + 2; + line->style = "\x1b[2;3m"; + line->style_reset = "\x1b[22;23m"; + (void) snprintf(prefix, prefix_size, "%*s", indent, ""); + return; } - if (is_subtask(item)) - { - int indent = base_indent + number_width + SUBTASK_INDENT; - *first_col = *cont_col = indent + 4; - const char* name = item->text + 1; + const char* box = item->state == STATE_DONE ? "[x]" + : item->state == STATE_DOING ? "[-]" + : "[ ]"; - while (*name == ' ') - name++; + if (item->state == STATE_DONE) + { + line->style = "\x1b[2m"; + line->style_reset = "\x1b[22m"; + } - return name; + if (is_subtask(item)) + { + int indent = base + number_width + SUBTASK_INDENT; + line->text = raw ? item->text : strip_marker(item->text); + line->first_col = line->prefix_cols = indent + 4; + (void) snprintf(prefix, prefix_size, "%*s%s ", indent, "", box); + return; } - *first_col = *cont_col = base_indent + number_width + 6; - const char* name = item->text; + int number = 0; - if (is_optional(item)) - { - name = item->text + 1; + for (int k = index; k >= 0 && !is_section(&list->items[k]); k--) + if (!is_child(&list->items[k])) + number++; - while (*name == ' ') - name++; - } + const char* task_box = is_optional(item) ? (item->state == STATE_DONE ? "(x)" + : item->state == STATE_DOING ? "(-)" + : "( )") + : box; - return name; + 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); } static int wrap_lines(const char* name, int cols, int first_col, int cont_col, @@ -1374,9 +1377,12 @@ static void place_on_line(const struct List* list, int index, int number_width, return; } - int first_col; - int cont_col; - const char* name = item_display(list, index, number_width, &first_col, &cont_col); + char prefix[48]; + struct Line descriptor; + build_line(list, index, number_width, 0, prefix, sizeof prefix, &descriptor); + const char* name = descriptor.text; + int first_col = descriptor.first_col; + int cont_col = descriptor.prefix_cols; int name_offset = (int) (name - list->items[index].text); int starts[MAX_TEXT]; @@ -1392,8 +1398,11 @@ static void place_on_line(const struct List* list, int index, int number_width, *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) +static void move_visual(struct List* list, struct Editor* editor, int number_width, int cols, int down) { + int* cursor = &editor->cursor; + int* edit_pos = &editor->edit_pos; + if (*cursor < 0 || *cursor >= list->count) return; @@ -1404,9 +1413,12 @@ static void move_visual(struct List* list, int* cursor, int* edit_pos, int numbe if (!is_section(item)) { - int first_col; - int cont_col; - const char* name = item_display(list, *cursor, number_width, &first_col, &cont_col); + char prefix[48]; + struct Line descriptor; + build_line(list, *cursor, number_width, 0, prefix, sizeof prefix, &descriptor); + const char* name = descriptor.text; + int first_col = descriptor.first_col; + int cont_col = descriptor.prefix_cols; int name_offset = (int) (name - item->text); int name_length = (int) strlen(name); @@ -1414,20 +1426,20 @@ static void move_visual(struct List* list, int* cursor, int* edit_pos, int numbe int ends[MAX_TEXT]; lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT); - int pos = *edit_pos - name_offset; + int position = *edit_pos - name_offset; - if (pos < 0) - pos = 0; + if (position < 0) + position = 0; - if (pos > name_length) - pos = name_length; + if (position > name_length) + position = name_length; for (int i = 0; i < lines; i++) - if (starts[i] <= pos) + if (starts[i] <= position) line = i; int base = line == 0 ? first_col : cont_col; - goal = base + display_width(name, starts[line], pos); + goal = base + display_width(name, starts[line], position); if (down && line < lines - 1) { @@ -1492,9 +1504,9 @@ static int matching_open(int close) return 0; } -static void apply_change(struct List* list, int cursor, enum Mode* mode, int* edit_pos, int from, int to) +static void apply_change(struct List* list, struct Editor* editor, int from, int to) { - struct Item* item = &list->items[cursor]; + struct Item* item = &list->items[editor->cursor]; int length = (int) strlen(item->text); if (to > length) @@ -1505,58 +1517,263 @@ static void apply_change(struct List* list, int cursor, enum Mode* mode, int* ed history_record(list); memmove(&item->text[from], &item->text[to], (size_t) (length - to) + 1); - *edit_pos = from; - *mode = MODE_INSERT; + editor->edit_pos = from; + editor->mode = MODE_INSERT; +} + +static void delete_text(struct List* list, struct Editor* editor, int from, int to) +{ + struct Item* item = &list->items[editor->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); + editor->edit_pos = from; +} + +static int motion_range(struct List* list, const struct Editor* editor, int op, int* from, int* to) +{ + int cursor = editor->cursor; + int edit_pos = editor->edit_pos; + int count = editor->count > 0 ? editor->count : 1; + const char* text = list->items[cursor].text; + int length = (int) strlen(text); + int low = text_start(&list->items[cursor]); + + *from = edit_pos; + *to = edit_pos; + + if (op == 'w') + { + int position = edit_pos; + + for (int r = 0; r < count; r++) + { + while (position < length && text[position] != ' ') + position++; + + while (position < length && text[position] == ' ') + position++; + } + + *to = position; + return 1; + } + + if (op == 'e') + { + int position = edit_pos; + + for (int r = 0; r < count; r++) + { + while (position < length && text[position] == ' ') + position++; + + while (position < length && text[position] != ' ') + position++; + } + + *to = position; + return 1; + } + + if (op == 'b') + { + int position = edit_pos; + + for (int r = 0; r < count; r++) + { + if (position > low) + position--; + + while (position > low && text[position] == ' ') + position--; + + while (position > low && text[position - 1] != ' ') + position--; + } + + *from = position; + return 1; + } + + if (op == '$') + { + *to = length; + return 1; + } + + if (op == '0') + { + *from = low; + return 1; + } + + if (op == 'l') + { + for (int r = 0; r < count; r++) + *to = utf8_right(text, *to); + + return 1; + } + + if (op == 'h') + { + for (int r = 0; r < count; r++) + *from = utf8_left(text, *from); + + return 1; + } + + if (op != 't' && op != 'f' && op != 'i' && op != 'a') + { + unget_key(op); + return 0; + } + + int target = read_key(); + + if (target < 32 || target >= 256) + return 0; + + 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) + return 0; + + *from = edit_pos; + *to = op == 'f' ? found + 1 : found; + return 1; + } + + 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; + } + + 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) + return 0; + + if (op == 'i') + { + *from = open_at + 1; + *to = close_at; + } + else + { + *from = open_at; + *to = close_at + 1; + } + + return 1; } -static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit_pos, - char* command, char* search, int count, int key) +static int normal_key(struct List* list, struct Editor* editor, int key) { + int* cursor = &editor->cursor; + int* edit_pos = &editor->edit_pos; + enum Mode* mode = &editor->mode; + char* command = editor->command; + char* search = editor->search; + int count = editor->count > 0 ? editor->count : 1; + 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; - } - + goto_match(list, editor, 1); 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; - } - + goto_match(list, editor, -1); break; } @@ -1585,12 +1802,16 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } case 'p': - paste_item(list, item_block_end(list, *cursor), cursor); + { + paste_item(list, editor, item_block_end(list, *cursor)); break; + } case 'P': - paste_item(list, *cursor, cursor); + { + paste_item(list, editor, *cursor); break; + } case 'j': case KEY_DOWN: @@ -1599,7 +1820,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit int cols = wrap_column(); for (int repeat = 0; repeat < count; repeat++) - move_visual(list, cursor, edit_pos, number_width, cols, 1); + move_visual(list, editor, number_width, cols, 1); break; } @@ -1611,40 +1832,49 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit int cols = wrap_column(); for (int repeat = 0; repeat < count; repeat++) - move_visual(list, cursor, edit_pos, number_width, cols, 0); + move_visual(list, editor, 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; @@ -1664,8 +1894,10 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } break; + } case 'b': + { if (*cursor >= 0 && *cursor < list->count) { const char* text = list->items[*cursor].text; @@ -1687,17 +1919,26 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } 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])) + { + 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]; @@ -1706,8 +1947,10 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } break; + } case 'x': + { if (*cursor >= 0 && *cursor < list->count && *edit_pos < (int) strlen(list->items[*cursor].text)) { @@ -1724,6 +1967,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } break; + } case 'o': { @@ -1738,6 +1982,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } case 'A': + { history_record(list); if (list->count == 0) @@ -1753,8 +1998,10 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } break; + } case 'i': + { if (*cursor >= 0 && *cursor < list->count) { history_record(list); @@ -1768,8 +2015,10 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } break; + } case 'R': + { history_record(list); if (list->count == 0) @@ -1787,6 +2036,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } break; + } case 'c': { @@ -1795,174 +2045,22 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit 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') + if (op == 'c') { - 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); + const struct Item* item = &list->items[*cursor]; + apply_change(list, editor, + text_start(item), (int) strlen(item->text)); 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; + if (op == 'w') + op = 'e'; - 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; - } + int from; + int to; - 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); + if (motion_range(list, editor, op, &from, &to)) + apply_change(list, editor, from, to); break; } @@ -1980,8 +2078,10 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } case 'G': + { *cursor = list->count - 1; break; + } case '}': { @@ -2007,35 +2107,46 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit case 'd': { - int next = read_key(); + int op = read_key(); - if (next != 'd') + if (op == 'd') { - unget_key(next); - break; - } + if (*cursor >= 0 && *cursor < list->count) + { + history_record(list); - if (*cursor >= 0 && *cursor < list->count) - { - history_record(list); + int start = *cursor; + int end = start; - int start = *cursor; - int end = start; + for (int repeat = 0; repeat < count && end < list->count; repeat++) + end = item_block_end(list, end); - for (int repeat = 0; repeat < count && end < list->count; repeat++) - end = item_block_end(list, end); + yank_count = 0; - 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 && yank_count < MAX_ITEMS; i++) - yank_register[yank_count++] = list->items[i]; + for (int i = start; i < end; i++) + delete_item(list, start); - for (int i = start; i < end; i++) - delete_item(list, start); + commit(list); + } - commit(list); + break; } + if (*cursor < 0 || *cursor >= list->count) + { + unget_key(op); + break; + } + + int from; + int to; + + if (motion_range(list, editor, op, &from, &to)) + delete_text(list, editor, from, to); + break; } @@ -2058,7 +2169,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit *cursor = start + (next_end - end); } } - else if (!is_subtask(&list->items[*cursor]) && !is_note(&list->items[*cursor])) + else if (!is_child(&list->items[*cursor])) { int start = *cursor; int end = task_block_end(list, start); @@ -2073,8 +2184,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit *cursor = start + (next_end - end); } } - else if (*cursor + 1 < list->count - && (is_subtask(&list->items[*cursor + 1]) || is_note(&list->items[*cursor + 1]))) + else if (*cursor + 1 < list->count && is_child(&list->items[*cursor + 1])) { history_record(list); struct Item moved = list->items[*cursor]; @@ -2109,13 +2219,12 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit *cursor = previous_start; } } - else if (!is_subtask(&list->items[*cursor]) && !is_note(&list->items[*cursor])) + else if (!is_child(&list->items[*cursor])) { int start = *cursor; int previous = start - 1; - while (previous >= 0 - && (is_subtask(&list->items[previous]) || is_note(&list->items[previous]))) + while (previous >= 0 && is_child(&list->items[previous])) previous--; if (previous >= 0 && !is_section(&list->items[previous])) @@ -2128,8 +2237,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit *cursor = previous; } } - else if (*cursor > 0 - && (is_subtask(&list->items[*cursor - 1]) || is_note(&list->items[*cursor - 1]))) + else if (*cursor > 0 && is_child(&list->items[*cursor - 1])) { history_record(list); struct Item moved = list->items[*cursor]; @@ -2150,9 +2258,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit { int end = item_block_end(list, i); - int drop = !is_section(&list->items[i]) - && !is_subtask(&list->items[i]) - && !is_note(&list->items[i]) + int drop = is_task(&list->items[i]) && list->items[i].state == STATE_DONE; if (!drop) @@ -2168,14 +2274,19 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit } default: + { break; + } } return 1; } -static void insert_key(struct List* list, int* cursor, enum Mode* mode, int* edit_pos, int key) +static void insert_key(struct List* list, struct Editor* editor, int key) { + int* cursor = &editor->cursor; + int* edit_pos = &editor->edit_pos; + enum Mode* mode = &editor->mode; struct Item* item = &list->items[*cursor]; size_t length = strlen(item->text); @@ -2263,8 +2374,10 @@ static void insert_key(struct List* list, int* cursor, enum Mode* mode, int* edi } } -static int command_key(struct List* list, const char* path, enum Mode* mode, char* command, int key) +static int command_key(struct List* list, const char* path, struct Editor* editor, int key) { + enum Mode* mode = &editor->mode; + char* command = editor->command; size_t length = strlen(command); if (key == 27) @@ -2276,7 +2389,14 @@ static int command_key(struct List* list, const char* path, enum Mode* mode, cha if (key == '\r' || key == '\n') { - if (strcmp(command, "reset") == 0 || strcmp(command, "clear") == 0) + if (command[0] == '\0') + { + *mode = MODE_NORMAL; + return 1; + } + + if (strcmp(command, "reset") == 0 || + strcmp(command, "clear") == 0) { pending_action = strcmp(command, "clear") == 0 ? PENDING_CLEAR : PENDING_RESET; command[0] = '\0'; @@ -2293,8 +2413,20 @@ static int command_key(struct List* list, const char* path, enum Mode* mode, cha return 1; } - int write = strchr(command, 'w') != NULL || strcmp(command, "x") == 0; - int quit = strchr(command, 'q') != NULL || strcmp(command, "x") == 0; + int write = strcmp(command, "w") == 0 || + strcmp(command, "wq") == 0 || + strcmp(command, "x") == 0; + int quit = strcmp(command, "q") == 0 || + strcmp(command, "wq") == 0 || + strcmp(command, "x") == 0; + + if (!write && !quit) + { + (void) snprintf(editor->message, sizeof editor->message, "not a command: %s", command); + command[0] = '\0'; + *mode = MODE_NORMAL; + return 1; + } *mode = MODE_NORMAL; @@ -2328,8 +2460,11 @@ static int command_key(struct List* list, const char* path, enum Mode* mode, cha return 1; } -static void search_key(const struct List* list, int* cursor, int* edit_pos, enum Mode* mode, char* command, char* search, int key) +static void search_key(const struct List* list, struct Editor* editor, int key) { + enum Mode* mode = &editor->mode; + char* command = editor->command; + char* search = editor->search; size_t length = strlen(command); if (key == 27) @@ -2344,18 +2479,7 @@ static void search_key(const struct List* list, int* cursor, int* edit_pos, enum (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; - } - + goto_match(list, editor, 1); return; } @@ -2376,8 +2500,10 @@ static void search_key(const struct List* list, int* cursor, int* edit_pos, enum } } -static void confirm_key(struct List* list, enum Mode* mode, int key) +static void confirm_key(struct List* list, struct Editor* editor, int key) { + enum Mode* mode = &editor->mode; + if (key == 'y' || key == 'Y') { history_record(list); @@ -2408,91 +2534,97 @@ static void run(struct List* list, const char* path) 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] = ""; + struct Editor editor = { 0 }; + editor.mode = MODE_NORMAL; for (;;) { - if (cursor >= list->count) - cursor = list->count - 1; + if (editor.cursor >= list->count) + editor.cursor = list->count - 1; - if (cursor < 0) - cursor = 0; + if (editor.cursor < 0) + editor.cursor = 0; if (list->count > 0) { - const char* text = list->items[cursor].text; + const char* text = list->items[editor.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]); + 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 (edit_pos > caret_max) - edit_pos = caret_max; + if (editor.edit_pos > caret_max) + editor.edit_pos = caret_max; - if (edit_pos < caret_min) - edit_pos = caret_min; + if (editor.edit_pos < caret_min) + editor.edit_pos = caret_min; - while (edit_pos > caret_min && ((unsigned char) text[edit_pos] & 0xC0) == 0x80) - edit_pos--; + while (editor.edit_pos > caret_min && ((unsigned char) text[editor.edit_pos] & 0xC0) == 0x80) + editor.edit_pos--; } - if (cursor < scroll) - scroll = cursor; + if (editor.cursor < editor.scroll) + editor.scroll = editor.cursor; - while (scroll < cursor - && render(list, cursor, scroll, mode, edit_pos, command, search, 1) < 0) - scroll++; + while (editor.scroll < editor.cursor && render(list, &editor, 1) < 0) + editor.scroll++; - render(list, cursor, scroll, mode, edit_pos, command, search, 0); + render(list, &editor, 0); int key = read_key(); if (key == -1) break; - switch (mode) + editor.message[0] = '\0'; + + switch (editor.mode) { case MODE_NORMAL: - if ((key >= '1' && key <= '9') || (key == '0' && count > 0)) + { + if ((key >= '1' && key <= '9') || (key == '0' && editor.count > 0)) { - count = count * 10 + (key - '0'); + editor.count = editor.count * 10 + (key - '0'); - if (count > 9999) - count = 9999; + if (editor.count > 9999) + editor.count = 9999; break; } - normal_key(list, &cursor, &mode, &edit_pos, command, search, count > 0 ? count : 1, key); - count = 0; + normal_key(list, &editor, key); + editor.count = 0; break; + } case MODE_INSERT: case MODE_REPLACE: - insert_key(list, &cursor, &mode, &edit_pos, key); + { + insert_key(list, &editor, key); break; + } case MODE_COMMAND: - if (command_key(list, path, &mode, command, key) == 0) + { + if (command_key(list, path, &editor, key) == 0) return; break; + } case MODE_SEARCH: - search_key(list, &cursor, &edit_pos, &mode, command, search, key); + { + search_key(list, &editor, key); break; + } case MODE_CONFIRM: - confirm_key(list, &mode, key); + { + confirm_key(list, &editor, key); break; + } } } } @@ -2518,6 +2650,7 @@ static void print_help(void) " 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" + " dw de db d0 d$ delete over a motion (also c/d t f i a)\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" |
