diff options
| author | hachem <im@hachem.wtf> | 2026-10-03 19:38:22 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-10-03 19:38:22 +0200 |
| commit | 419700c1bab1e37993260bd86ffe6fddc7d5df49 (patch) | |
| tree | 820c9eeff98827c2e4f8206d5bf026b6d011c92e | |
| parent | 41005a0d4102843cefeed911a0fa8bd5774d998c (diff) | |
feat: archive + important
| -rw-r--r-- | README.md | 17 | ||||
| -rw-r--r-- | config.h | 1 | ||||
| -rw-r--r-- | todo.1 | 88 | ||||
| -rw-r--r-- | todo.c | 868 |
4 files changed, 762 insertions, 212 deletions
@@ -24,21 +24,33 @@ a little modal todo list that lives in my terminal. 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) + . repeat the last change + >> << demote / promote (task <-> subtask, or section depth) space cycle todo -> doing -> done dd cut the item (with its subtasks and notes) D clear every done item yy p P yank, paste below, paste above (a task carries its block) J / K move the item down / up (a task carries its block; a heading, its section) += visual + V select whole items; j / k extend the selection + then d cut, y yank, space cycle; esc cancels + += folding + za fold / unfold the section at the cursor (zo open, zc close) + headings show (done/total) and a ▸ when collapsed + = other / 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 + :help :? show the keys in-app (any key closes) + :w :q :wq :x write, quit (q refuses if unsaved), write and quit + :q! quit, discarding unsaved changes :reset set every item back to todo :clear remove every item :reorder re-sort / regroup the list + :archive move done items out to the archive file (~/.todo.archive.txt) :theme NAME switch theme (default, gruvbox, nord, mono, custom) tab complete the command (or theme name) in the : line ``` @@ -51,6 +63,7 @@ a little modal todo list that lives in my terminal. - write the docs a subtask * maybe later an optional task -> ( ) > extra detail a note on the task above +! pay rent an important task -> bold !PAY RENT! buy milk a plain task ``` @@ -21,6 +21,7 @@ .number = "\x1b[2m", .number_reset = "\x1b[22m", \ .muted = "\x1b[2m", .muted_reset = "\x1b[0m", \ .match = "\x1b[7m", .match_reset = "\x1b[27m", \ + .important = "\x1b[1m", .important_reset = "\x1b[22m", \ } /* checkbox glyphs (display only; the file always stores [ ] [-] [x]). @@ -49,6 +49,12 @@ instead of a checkbox. .BI > " text" A note on the task above it: extra description, shown indented and dimmed with no checkbox. Notes move with their task. +.TP +.BI ! " name" +An important task, shown bold and in capitals with the leading +.B ! +kept and a trailing one added +.RB ( "!NAME!" ). .PP Within each section, items are grouped \(em required, then optional, then done \(em with a blank line between the groups. @@ -165,6 +171,53 @@ and notes as one block. .BR J ", " K Move the item down or up. A task moves with its subtasks and notes; a section heading moves the whole section. +.TP +.BR >> ", " << +Demote or promote the item. +.B >> +turns a task into a subtask and deepens a section +.RB ( = " to " == ); +.B << +turns a subtask back into a task and shallows a section +.RB ( == " to " = ). +.TP +.B . +Repeat the last change, such as a +.BR dd ", " x ", " p ", " >> ", " +a state toggle, or an insert started with +.BR o ", " i ", " A ", or " c +through to the +.B escape +that ended it. The count is not repeated. +.SS "Visual mode" +.TP +.B V +Start a line-wise selection anchored at the cursor. +.B j +and +.B k +extend it. Then +.B d +cuts the selected items (also yanking them), +.B y +yanks them, and +.B space +cycles the state of each selected task. +.B escape +cancels. +.SS Folding +.TP +.BR za ", " zo ", " zc +Fold the section at the cursor: +.B za +toggles it, +.B zo +opens it, and +.B zc +closes it. A folded heading hides its contents and is marked as collapsed. Each heading +also shows a +.RB ( done / total ) +count of the tasks within it. .SS Other .TP .BR / ", " n ", " N @@ -179,9 +232,6 @@ 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 @@ -203,11 +253,17 @@ completes the command name, or the theme name after Write the list to its file. .TP .B :q -Quit, discarding changes since the last write. +Quit. Refuses if there are unsaved changes. +.TP +.B :q! +Quit, discarding any unsaved changes. .TP .BR :wq ", " :x Write and quit. .TP +.BR :help ", " :? +Show the key bindings on screen; any key returns. +.TP .B :reset Set every item back to todo, clearing all done and doing states. .TP @@ -218,6 +274,13 @@ Remove every item, emptying the list. Re-sort the list, regrouping items (required, optional, done) within each section. Useful after reordering or hand-editing. .TP +.B :archive +Move every done item out of the list, appending it to the archive file. Unlike +.BR D , +which discards them, this keeps a record you can revisit. The archive is itself +a valid list, so it can be opened with +.BR todo . +.TP .BI :theme " name" Switch the colour theme to .I name @@ -245,12 +308,29 @@ or a note .RB ( "> text" ). Override the location with .BR $TODO_FILE . +.TP +.B ~/.todo.archive.txt +Where +.B :archive +appends done items. It is the list file's name with +.I .txt +replaced by +.IR .archive.txt , +or the list file's name with +.I .archive +appended. Override it with +.BR $TODO_ARCHIVE . .SH ENVIRONMENT .TP .B TODO_FILE Path to the list file, used instead of .BR ~/.todo.txt . .TP +.B TODO_ARCHIVE +Path to the archive file for +.BR :archive , +used instead of the name derived from the list file. +.TP .B LC_TIME Sets the locale for the date and time in the header. .SH "EXIT STATUS" @@ -1,13 +1,16 @@ -#include <fcntl.h> #include <locale.h> -#include <stdarg.h> #include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <time.h> + +#include <fcntl.h> #include <sys/ioctl.h> #include <termios.h> -#include <time.h> #include <unistd.h> #include "config.h" @@ -33,6 +36,7 @@ enum State struct Item { enum State state; + bool folded; char text[MAX_TEXT]; }; @@ -50,7 +54,8 @@ enum Mode MODE_COMMAND, MODE_SEARCH, MODE_CONFIRM, - MODE_HELP + MODE_HELP, + MODE_VISUAL }; enum Pending @@ -74,6 +79,7 @@ struct Editor int scroll; int edit_pos; int count; + int anchor; enum Mode mode; @@ -93,20 +99,14 @@ enum ThemeId 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; + 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; + const char* important; const char* important_reset; }; static const struct Theme themes[] = { @@ -118,6 +118,7 @@ static const struct Theme themes[] = { .number = "\x1b[2m", .number_reset = "\x1b[22m", .muted = "\x1b[2m", .muted_reset = "\x1b[0m", .match = "\x1b[7m", .match_reset = "\x1b[27m", + .important = "\x1b[1m", .important_reset = "\x1b[22m", }, [THEME_GRUVBOX] = { .highlight = "\x1b[48;5;237m", .highlight_reset = "\x1b[49m", @@ -127,6 +128,7 @@ static const struct Theme themes[] = { .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", + .important = "\x1b[1;38;5;208m", .important_reset = "\x1b[22;39m", }, [THEME_NORD] = { .highlight = "\x1b[48;5;238m", .highlight_reset = "\x1b[49m", @@ -136,6 +138,7 @@ static const struct Theme themes[] = { .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", + .important = "\x1b[1;38;5;110m", .important_reset = "\x1b[22;39m", }, [THEME_MONO] = { .highlight = "\x1b[7m", .highlight_reset = "\x1b[27m", @@ -145,6 +148,7 @@ static const struct Theme themes[] = { .number = "", .number_reset = "", .muted = "", .muted_reset = "", .match = "\x1b[7m", .match_reset = "\x1b[27m", + .important = "\x1b[1m", .important_reset = "\x1b[22m", }, [THEME_CUSTOM] = CUSTOM_THEME, }; @@ -173,14 +177,15 @@ static const char* mode_name(enum Mode mode) case MODE_SEARCH: return "SEARCH"; case MODE_CONFIRM: return "CONFIRM"; case MODE_HELP: return "HELP"; + case MODE_VISUAL: return "VISUAL"; } return "?"; } -static inline int is_editing(enum Mode mode) +static inline bool is_editing(enum Mode mode) { - return mode == MODE_INSERT || + return mode == MODE_INSERT || mode == MODE_REPLACE; } @@ -188,7 +193,7 @@ 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) + if (((uint8_t) text[i] & 0xC0) != 0x80) width++; return width; } @@ -198,8 +203,7 @@ 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) + while (position > 0 && ((uint8_t) text[position] & 0xC0) == 0x80) position--; return position; } @@ -211,7 +215,7 @@ static inline int utf8_right(const char* text, int position) return length; position++; - while (position < length && ((unsigned char) text[position] & 0xC0) == 0x80) + while (position < length && ((uint8_t) text[position] & 0xC0) == 0x80) position++; return position; } @@ -243,6 +247,8 @@ static void strip_newline(char* string) string[--length] = '\0'; } +static bool dirty; + static void load(struct List* list, const char* path) { list->count = 0; @@ -259,7 +265,7 @@ static void load(struct List* list, const char* path) strip_newline(line); enum State state = STATE_TODO; - char* text = line; + const char* text = line; if (line[0] == '[' && line[1] != '\0' && line[2] == ']') { @@ -279,48 +285,23 @@ static void load(struct List* list, const char* path) struct Item* item = &list->items[list->count++]; item->state = state; + item->folded = false; (void) snprintf(item->text, sizeof item->text, "%s", text); } (void) fclose(file); + dirty = false; } -static inline int is_section(const struct Item* item) -{ - return item->text[0] == '='; -} - -static inline int is_subtask(const struct Item* item) -{ - return item->text[0] == '-'; -} - -static inline int is_optional(const struct Item* item) -{ - return item->text[0] == '*'; -} - -static inline int is_note(const struct Item* item) -{ - return item->text[0] == '>'; -} - -static inline int is_important(const struct Item* item) -{ - return item->text[0] == '!'; -} +static inline bool is_section(const struct Item* item) { return item->text[0] == '='; } +static inline bool is_subtask(const struct Item* item) { return item->text[0] == '-'; } +static inline bool is_optional(const struct Item* item) { return item->text[0] == '*'; } +static inline bool is_note(const struct Item* item) { return item->text[0] == '>'; } +static inline bool is_important(const struct Item* item) { return item->text[0] == '!'; } +static inline bool is_child(const struct Item* item) { return is_subtask(item) || is_note(item); } +static inline bool is_task(const struct Item* item) { return !is_section(item) && !is_child(item); } -static inline int is_child(const struct Item* item) -{ - return is_subtask(item) || is_note(item); -} - -static inline int is_task(const struct Item* item) -{ - return !is_section(item) && !is_child(item); -} - -static inline int valid_cursor(const struct List* list, int index) +static inline bool valid_cursor(const struct List* list, int index) { return index >= 0 && index < list->count; } @@ -330,7 +311,7 @@ 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) +static inline const char* checkbox(enum State state, bool optional) { if (optional) return state == STATE_DONE ? MARK_OPT_DONE @@ -363,7 +344,7 @@ 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) || is_important(item)) + else if (is_subtask(item) || is_optional(item) || is_note(item)) i = 1; while (item->text[i] == ' ') @@ -501,6 +482,140 @@ static int block_end(const struct List* list, int start) return i; } +static void section_counts(const struct List* list, int index, int* done, int* total) +{ + int level = section_level(&list->items[index]); + *done = 0; + *total = 0; + + for (int i = index + 1; i < list->count; i++) + { + if (is_section(&list->items[i])) + { + if (section_level(&list->items[i]) <= level) + break; + continue; + } + + if (!is_task(&list->items[i])) + continue; + + (*total)++; + *done += list->items[i].state == STATE_DONE; + } +} + +static int section_of(const struct List* list, int index) +{ + for (int i = index; i >= 0; i--) + if (is_section(&list->items[i])) + return i; + return -1; +} + +static int fold_root(const struct List* list, int index) +{ + for (int k = 0; k < index; k++) + { + if (!is_section(&list->items[k]) || !list->items[k].folded) + continue; + + int level = section_level(&list->items[k]); + int end = k + 1; + + while (end < list->count && + !(is_section(&list->items[end]) && section_level(&list->items[end]) <= level)) + end++; + + if (index < end) + return k; + } + + return -1; +} + +static bool hidden(const struct List* list, int index) +{ + return fold_root(list, index) >= 0; +} + +static int next_visible(const struct List* list, int index) +{ + for (int i = index + 1; i < list->count; i++) + if (!hidden(list, i)) + return i; + return -1; +} + +static int prev_visible(const struct List* list, int index) +{ + for (int i = index - 1; i >= 0; i--) + if (!hidden(list, i)) + return i; + return -1; +} + +static void archive_path(const char* path, char* buf, size_t size) +{ + const char* env = getenv("TODO_ARCHIVE"); + if (env != NULL && env[0] != '\0') + { + (void) snprintf(buf, size, "%s", env); + return; + } + + size_t len = strlen(path); + + if (len > 4 && strcmp(path + len - 4, ".txt") == 0) + (void) snprintf(buf, size, "%.*s.archive.txt", (int) (len - 4), path); + else + (void) snprintf(buf, size, "%s.archive", path); +} + +static int archive_done(struct List* list, const char* path) +{ + normalize_tasks(list); + + char apath[4096]; + archive_path(path, apath, sizeof apath); + + FILE* file = fopen(apath, "a"); + if (file == NULL) + return -1; + + int archived = 0; + int kept = 0; + int i = 0; + + while (i < list->count) + { + int end = item_block_end(list, i); + bool drop = is_task(&list->items[i]) && list->items[i].state == STATE_DONE; + + if (drop) + for (int j = i; j < end; j++) + { + const struct Item* it = &list->items[j]; + + if (is_section(it) || is_note(it)) + (void) fprintf(file, "%s\n", it->text); + else + (void) fprintf(file, "[%c] %s\n", state_char(it->state), it->text); + + archived++; + } + else + for (int j = i; j < end; j++) + list->items[kept++] = list->items[j]; + + i = end; + } + + (void) fclose(file); + list->count = kept; + return archived; +} + static void reverse_items(struct List* list, int low, int high) { while (low < high - 1) @@ -513,26 +628,37 @@ static void reverse_items(struct List* list, int low, int high) } } -static void save(struct List* list, const char* path) +static int save(struct List* list, const char* path) { normalize_tasks(list); FILE* file = fopen(path, "w"); if (file == NULL) - return; + return -1; + + bool ok = true; for (int i = 0; i < list->count; i++) { + int written; + if (is_section(&list->items[i]) || is_note(&list->items[i])) - { - (void) fprintf(file, "%s\n", list->items[i].text); - continue; - } + written = fprintf(file, "%s\n", list->items[i].text); + else + written = fprintf(file, "[%c] %s\n", state_char(list->items[i].state), list->items[i].text); - (void) fprintf(file, "[%c] %s\n", state_char(list->items[i].state), list->items[i].text); + if (written < 0) + ok = false; } - (void) fclose(file); + if (fclose(file) != 0) + ok = false; + + if (!ok) + return -1; + + dirty = false; + return 0; } static void commit(struct List* list) @@ -551,6 +677,7 @@ static struct Snapshot undo_stack[MAX_HISTORY]; static struct Snapshot redo_stack[MAX_HISTORY]; static int undo_count; static int redo_count; +static uint64_t edits; static struct Snapshot snapshot_take(const struct List* list) { @@ -586,8 +713,11 @@ static void stack_clear(struct Snapshot* stack, int* count) free(stack[--(*count)].items); } -static void history_record(struct List* list) +static void history_record(const struct List* list) { + dirty = true; + edits++; + struct Snapshot snap = snapshot_take(list); if (snap.items == NULL) return; @@ -604,6 +734,7 @@ static void history_undo(struct List* list) struct Snapshot snap = undo_stack[--undo_count]; snapshot_apply(list, &snap); free(snap.items); + dirty = true; } static void history_redo(struct List* list) @@ -615,6 +746,7 @@ static void history_redo(struct List* list) struct Snapshot snap = redo_stack[--redo_count]; snapshot_apply(list, &snap); free(snap.items); + dirty = true; } static void insert_item(struct List* list, int index); @@ -652,18 +784,18 @@ static inline char lower(char character) return character >= 'A' && character <= 'Z' ? (char) (character + 32) : character; } -static int contains_fold(const char* haystack, const char* needle) +static bool contains_fold(const char* haystack, const char* needle) { if (needle[0] == '\0') - return 0; + return false; for (; *haystack != '\0'; haystack++) { const char* h = haystack; const char* n = needle; - while (*h != '\0' && - *n != '\0' && + while (*h != '\0' && + *n != '\0' && lower(*h) == lower(*n)) { h++; @@ -671,10 +803,10 @@ static int contains_fold(const char* haystack, const char* needle) } if (*n == '\0') - return 1; + return true; } - return 0; + return false; } static int match_at(const char* text, int position, const char* needle) @@ -737,6 +869,7 @@ static void insert_item(struct List* list, int index) list->items[i] = list->items[i - 1]; list->items[index].state = STATE_TODO; + list->items[index].folded = false; list->items[index].text[0] = '\0'; list->count++; } @@ -768,12 +901,6 @@ static void on_signal(int signal_number) static int enter_raw_mode(void) { - if (!isatty(STDIN_FILENO)) - { - (void) fprintf(stderr, "%s: not a terminal\n", TODO_PROGRAM); - return -1; - } - if (tcgetattr(STDIN_FILENO, &saved_termios) != 0) return -1; @@ -830,9 +957,9 @@ struct Line const char* text; const char* search; - int is_cursor; + bool is_cursor; int caret; - int upper; /* uppercase the text (important items) */ + bool upper; }; static int wrap_lines(const char* name, int cols, int first_col, int cont_col, @@ -878,9 +1005,9 @@ static void emit_wrapped(struct Out* out, const struct Line* spec, 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; + bool is_cursor = spec->is_cursor; + int caret_col = spec->caret; + bool upper = spec->upper; int total = (int) strlen(text); char mask[MAX_TEXT]; @@ -938,16 +1065,19 @@ static void emit_wrapped(struct Out* out, const struct Line* spec, i = j; } + if (upper && line == lines - 1) + append(out, "!"); + append(out, "%s" CLEAR_EOL "%s\r\n", style_reset, background_reset); if (is_cursor && *cursor_row < 0) { 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) + if (caret_col >= line_start_col && caret_col <= line_start_col + width) { *cursor_row = 4 + *emitted; - *cursor_column = base + 1 + caret_cols - line_start_col; + *cursor_column = base + 1 + caret_col - line_start_col; } } @@ -1000,43 +1130,58 @@ static int compute_number_width(const struct List* list) return number_width; } -static void build_line(const struct List* list, int index, int number_width, int raw, +static void build_line(const struct List* list, int index, int number_width, bool editing, 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", + static const struct { const char* label; const char* body; } entries[] = { + { "move", "h j k l 0 $ line w b word { } section gg G" }, + { "edit", "i insert a / A append o new R replace" }, + { "", "x delete char dd cut item D clear done" }, + { "", "cw ce cb c0 c$ cc (also d: dw de db ...)" }, + { "", "ct cf ci ca <char or ()[]{}<>\"'>" }, + { "", ". repeat the last change" }, + { "", ">> << demote / promote (task<->subtask, = depth)" }, + { "state", "space cycle todo/doing/done yy p P yank/paste" }, + { "", "J K move item or section" }, + { "visual","V select lines then d y space" }, + { "fold", "za toggle section zo open zc close" }, + { "find", "/ n N esc clears the highlight" }, + { "undo", "u ctrl-r" }, + { "cmd", ":w :q :q! :wq :x :theme NAME :help" }, + { "", ":reset :clear :reorder :archive" }, + { "items", "= section == subsection - subtask" }, + { "", "* optional > note ! important" }, }; + static const char* const rule = + "\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80" + "\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80" + "\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80" + "\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80"; + char buffer[8 * 1024]; struct Out out = { buffer, 0, (int) sizeof buffer }; int rows = screen_rows(); - int count = (int) (sizeof lines / sizeof lines[0]); + int count = (int) (sizeof entries / sizeof entries[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 == 1) + append(&out, " %stodo%s %skeys%s", + theme->important, theme->important_reset, theme->muted, theme->muted_reset); + else if (row == 2) + append(&out, " %s%s%s", theme->muted, rule, theme->muted_reset); + else if (row >= 4 && row - 4 < count) + append(&out, " %s%-7s%s%s", + theme->section, entries[row - 4].label, theme->section_reset, entries[row - 4].body); + else if (row == 4 + count + 1) + append(&out, " %spress any key to return%s", theme->muted, theme->muted_reset); + + append(&out, CLEAR_EOL); if (row < rows - 1) append(&out, "\r\n"); } @@ -1065,6 +1210,14 @@ static int render(const struct List* list, const struct Editor* editor, int meas const char* matches = mode == MODE_SEARCH ? command : editor->search; + int sel_lo = -1; + int sel_hi = -1; + if (mode == MODE_VISUAL) + { + sel_lo = editor->anchor < cursor ? editor->anchor : cursor; + sel_hi = editor->anchor < cursor ? cursor : editor->anchor; + } + int total_items = 0; int done = 0; @@ -1080,7 +1233,7 @@ static int render(const struct List* list, const struct Editor* editor, int meas char date[64]; char clock[32]; time_t now = time(NULL); - struct tm* local = localtime(&now); + const struct tm* local = localtime(&now); if (local == NULL || strftime(date, sizeof date, DATE_FORMAT, local) == 0) date[0] = '\0'; @@ -1105,8 +1258,15 @@ 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 ? theme->highlight : ""; - const char* background_reset = index == cursor ? theme->highlight_reset : ""; + if (hidden(list, index)) + { + index++; + continue; + } + + bool selected = index == cursor || (sel_lo >= 0 && index >= sel_lo && index <= sel_hi); + const char* background = selected ? theme->highlight : ""; + const char* background_reset = selected ? theme->highlight_reset : ""; if (is_section(item)) { @@ -1160,9 +1320,24 @@ static int render(const struct List* list, const struct Editor* editor, int meas while (*name == ' ') name++; + char progress[64]; + int n = 0; + int section_done, section_total; + section_counts(list, index, §ion_done, §ion_total); + + progress[0] = '\0'; + + if (section_total > 0) + n += snprintf(progress + n, sizeof progress - (size_t) n, " %s(%d/%d)%s", + theme->muted, section_done, section_total, theme->muted_reset); + + if (item->folded) + (void) snprintf(progress + n, sizeof progress - (size_t) n, " %s\xe2\x96\xb8%s", + theme->muted, theme->muted_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); + "%s%*s%s%s. %s%s%s" CLEAR_EOL "%s\r\n", + background, section_indent, "", theme->section, roman, name, theme->section_reset, progress, background_reset); } emitted++; @@ -1261,29 +1436,36 @@ static int render(const struct List* list, const struct Editor* editor, int meas return cursor_row; } +#define DOT_CAP 512 + static int pending_key = -1; +static int dot_record[DOT_CAP]; +static int dot_record_len; +static int rec_buf[DOT_CAP]; +static int rec_len; +static bool recording; + +static int replay_buf[DOT_CAP]; +static int replay_len; +static int replay_pos = -1; + static void unget_key(int key) { pending_key = key; + if (recording && rec_len > 0) + rec_len--; } -static int read_key(void) +static int read_key_raw(void) { - if (pending_key != -1) - { - int key = pending_key; - pending_key = -1; - return key; - } - char character; if (read(STDIN_FILENO, &character, 1) != 1) return -1; if (character != 27) - return (unsigned char) character; + return (uint8_t) character; int flags = fcntl(STDIN_FILENO, F_GETFL); (void) fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); @@ -1315,17 +1497,59 @@ static int read_key(void) } (void) fcntl(STDIN_FILENO, F_SETFL, flags); - pending_key = (unsigned char) first; + pending_key = (uint8_t) first; return 27; } +static int read_key(void) +{ + int key; + + if (pending_key != -1) + { + key = pending_key; + pending_key = -1; + } + else if (replay_pos >= 0 && replay_pos < replay_len) + { + key = replay_buf[replay_pos++]; + } + else + { + replay_pos = -1; + key = read_key_raw(); + } + + if (recording && key != -1) + { + if (rec_len < DOT_CAP) + rec_buf[rec_len++] = key; + else + recording = false; + } + + return key; +} + +static bool is_change_start(int key) +{ + switch (key) + { + case 'x': case 'd': case 'c': case 'o': + case 'i': case 'A': case 'R': case 'p': + case 'P': case 'D': case 'J': case 'K': + case ' ': case '>': case '<': + return true; + } + + return false; +} + static inline const char* strip_marker(const char* text) { const char* name = text + 1; - while (*name == ' ') name++; - return name; } @@ -1334,11 +1558,10 @@ 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])) return LEFT_MARGIN + section_level(&list->items[k]) * SECTION_INDENT; - return LEFT_MARGIN; } -static void build_line(const struct List* list, int index, int number_width, int raw, +static void build_line(const struct List* list, int index, int number_width, bool editing, char* prefix, size_t prefix_size, struct Line* line) { const struct Item* item = &list->items[index]; @@ -1347,7 +1570,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; + line->upper = false; if (is_note(item)) { @@ -1371,21 +1594,27 @@ static void build_line(const struct List* list, int index, int number_width, int { int indent = base + number_width + SUBTASK_INDENT; const char* box = checkbox(item->state, 0); - line->text = raw ? item->text : strip_marker(item->text); + line->text = editing ? item->text : strip_marker(item->text); line->first_col = line->prefix_cols = indent + utf8_width(box, (int) strlen(box)) + 1; (void) snprintf(prefix, prefix_size, "%*s%s ", indent, "", box); return; } int number = 0; - for (int k = index; k >= 0 && !is_section(&list->items[k]); k--) if (!is_child(&list->items[k])) number++; const char* box = checkbox(item->state, is_optional(item)); - line->text = is_optional(item) && !raw ? strip_marker(item->text) : item->text; + line->text = is_optional(item) && !editing ? strip_marker(item->text) : item->text; + line->upper = is_important(item) && !editing; + if (line->upper) + { + line->style = theme->important; + line->style_reset = theme->important_reset; + } + 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); @@ -1422,7 +1651,7 @@ static int wrap_lines(const char* name, int cols, int first_col, int cont_col, end++; - while (end < total && ((unsigned char) name[end] & 0xC0) == 0x80) + while (end < total && ((uint8_t) name[end] & 0xC0) == 0x80) end++; width++; @@ -1459,11 +1688,9 @@ static int wrap_lines(const char* name, int cols, int first_col, int cont_col, static inline int display_width(const char* name, int from, int to) { int width = 0; - for (int i = from; i < to; i++) - if (((unsigned char) name[i] & 0xC0) != 0x80) + if (((uint8_t) name[i] & 0xC0) != 0x80) width++; - return width; } @@ -1471,17 +1698,13 @@ static int offset_at_width(const char* name, int start, int end, int want) { int i = start; int width = 0; - while (i < end && width < want) { i++; - - while (i < end && ((unsigned char) name[i] & 0xC0) == 0x80) + while (i < end && ((uint8_t) name[i] & 0xC0) == 0x80) i++; - width++; } - return i; } @@ -1508,6 +1731,7 @@ 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; @@ -1523,8 +1747,6 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid return; const struct Item* item = &list->items[*cursor]; - int line = 0; - int lines = 1; int goal = 0; if (!is_section(item)) @@ -1540,7 +1762,7 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid int starts[MAX_TEXT]; int ends[MAX_TEXT]; - lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT); + int lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT); int position = *edit_pos - name_offset; if (position < 0) @@ -1548,6 +1770,7 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid if (position > name_length) position = name_length; + int line = 0; for (int i = 0; i < lines; i++) if (starts[i] <= position) line = i; @@ -1578,15 +1801,23 @@ static void move_visual(struct List* list, struct Editor* editor, int number_wid } } - if (down && *cursor + 1 < list->count) + if (down) { - (*cursor)++; - place_on_line(list, *cursor, number_width, cols, 0, goal, edit_pos); + int next = next_visible(list, *cursor); + if (next >= 0) + { + *cursor = next; + place_on_line(list, *cursor, number_width, cols, 0, goal, edit_pos); + } } - else if (!down && *cursor > 0) + else { - (*cursor)--; - place_on_line(list, *cursor, number_width, cols, 1, goal, edit_pos); + int prev = prev_visible(list, *cursor); + if (prev >= 0) + { + *cursor = prev; + place_on_line(list, *cursor, number_width, cols, 1, goal, edit_pos); + } } } @@ -1754,7 +1985,7 @@ static int motion_range(struct List* list, const struct Editor* editor, int op, int found = -1; for (int i = edit_pos + 1; i < length; i++) - if ((unsigned char) text[i] == target) + if ((uint8_t) text[i] == target) { found = i; break; @@ -1783,7 +2014,7 @@ static int motion_range(struct List* list, const struct Editor* editor, int op, int close_at = -1; for (int i = edit_pos; i >= low; i--) - if ((unsigned char) text[i] == open_char) + if ((uint8_t) text[i] == open_char) { open_at = i; break; @@ -1792,7 +2023,7 @@ static int motion_range(struct List* list, const struct Editor* editor, int op, if (open_at >= 0) { for (int i = open_at + 1; i < length; i++) - if ((unsigned char) text[i] == close_char) + if ((uint8_t) text[i] == close_char) { close_at = i; break; @@ -1808,14 +2039,14 @@ static int motion_range(struct List* list, const struct Editor* editor, int op, if (open_at < 0) { for (int i = edit_pos; i < length; i++) - if ((unsigned char) text[i] == open_char) + if ((uint8_t) 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) + if ((uint8_t) text[i] == close_char) { close_at = i; break; @@ -1863,9 +2094,6 @@ static int normal_key(struct List* list, struct Editor* editor, int key) command[0] = '\0'; *mode = MODE_SEARCH; } break; - case '?': - *mode = MODE_HELP; - break; case 'n': goto_match(list, editor, 1); break; @@ -1930,7 +2158,7 @@ static int normal_key(struct List* list, struct Editor* editor, int key) int from; int to; - motion_range(list, editor, op, &from, &to); + (void) motion_range(list, editor, op, &from, &to); *edit_pos = op == 'h' || op == '0' || op == 'b' ? from : to; } break; @@ -2056,7 +2284,86 @@ static int normal_key(struct List* list, struct Editor* editor, int key) } break; case 'G': *cursor = list->count - 1; + if (valid_cursor(list, *cursor) && hidden(list, *cursor)) + { + int prev = prev_visible(list, *cursor); + if (prev >= 0) + *cursor = prev; + } + break; + case 'V': + if (valid_cursor(list, *cursor)) + { + editor->anchor = *cursor; + *mode = MODE_VISUAL; + } break; + case 'z': + { + int next = read_key(); + int section = section_of(list, *cursor); + + if (section < 0) + break; + + if (next == 'a') + list->items[section].folded = !list->items[section].folded; + else if (next == 'o') + list->items[section].folded = false; + else if (next == 'c') + list->items[section].folded = true; + else + { + unget_key(next); + break; + } + } break; + case '>': + { + int op = read_key(); + if (op != '>' || !valid_cursor(list, *cursor)) + { + unget_key(op); + break; + } + + struct Item* item = &list->items[*cursor]; + char buf[MAX_TEXT]; + + if (is_section(item)) + (void) snprintf(buf, sizeof buf, "=%s", item->text); + else if (is_task(item) && !is_optional(item) && !is_important(item)) + (void) snprintf(buf, sizeof buf, "- %s", item->text + text_start(item)); + else + break; + + history_record(list); + (void) snprintf(item->text, sizeof item->text, "%s", buf); + commit(list); + } break; + case '<': + { + int op = read_key(); + if (op != '<' || !valid_cursor(list, *cursor)) + { + unget_key(op); + break; + } + + struct Item* item = &list->items[*cursor]; + char buf[MAX_TEXT]; + + if (is_section(item) && section_level(item) >= 2) + (void) snprintf(buf, sizeof buf, "%s", item->text + 1); + else if (is_subtask(item)) + (void) snprintf(buf, sizeof buf, "%s", item->text + text_start(item)); + else + break; + + history_record(list); + (void) snprintf(item->text, sizeof item->text, "%s", buf); + commit(list); + } break; case '}': { int next = *cursor + 1; @@ -2089,15 +2396,12 @@ static int normal_key(struct List* list, struct Editor* editor, int key) for (int repeat = 0; repeat < count && end < list->count; repeat++) end = item_block_end(list, end); - yank_count = 0; for (int i = start; i < end && yank_count < MAX_ITEMS; i++) yank_register[yank_count++] = list->items[i]; - for (int i = start; i < end; i++) delete_item(list, start); - commit(list); } @@ -2221,13 +2525,11 @@ static int normal_key(struct List* list, struct Editor* editor, int key) { int end = item_block_end(list, i); - int drop = is_task(&list->items[i]) + bool drop = is_task(&list->items[i]) && list->items[i].state == STATE_DONE; - if (!drop) for (int j = i; j < end; j++) list->items[kept++] = list->items[j]; - i = end; } @@ -2243,9 +2545,9 @@ static int normal_key(struct List* list, struct Editor* editor, 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; + 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); @@ -2315,12 +2617,10 @@ static void insert_key(struct List* list, struct Editor* editor, int key) return; } - if (key >= 32 && key != 127 && key < 256 && length + 1 < sizeof item->text) + if (key >= 32 && key < 256 && length + 1 < sizeof item->text) { if (*mode == MODE_REPLACE && *edit_pos < (int) length) - { item->text[*edit_pos] = (char) key; - } else { memmove(&item->text[*edit_pos + 1], &item->text[*edit_pos], length - (size_t) *edit_pos + 1); @@ -2365,7 +2665,8 @@ static void complete_word(char* buf, size_t size, const char* const* options, in 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", + "w", "q", "q!", "wq", "x", "reset", + "clear", "reorder", "archive", "theme", "help", }; enum Mode* mode = &editor->mode; @@ -2378,7 +2679,6 @@ static int command_key(struct List* list, const char* path, struct Editor* edito 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; } @@ -2415,7 +2715,33 @@ static int command_key(struct List* list, const char* path, struct Editor* edito return 1; } - if (strcmp(command, "theme") == 0 || + if (strcmp(command, "archive") == 0) + { + history_record(list); + int moved = archive_done(list, path); + + if (moved < 0) + (void) snprintf(editor->message, sizeof editor->message, "could not write archive"); + else + { + commit(list); + (void) snprintf(editor->message, sizeof editor->message, + "archived %d item%s", moved, moved == 1 ? "" : "s"); + } + + command[0] = '\0'; + *mode = MODE_NORMAL; + return 1; + } + + if (strcmp(command, "help") == 0 || strcmp(command, "?") == 0) + { + command[0] = '\0'; + *mode = MODE_HELP; + return 1; + } + + if (strcmp(command, "theme") == 0 || strncmp(command, "theme ", 6) == 0) { const char* name = command + 5; @@ -2423,9 +2749,7 @@ static int command_key(struct List* list, const char* path, struct Editor* edito name++; if (*name == '\0') - { theme_index = (theme_index + 1) % THEME_COUNT; - } else { int found = -1; @@ -2445,12 +2769,14 @@ static int command_key(struct List* list, const char* path, struct Editor* edito return 1; } - 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; + bool force = strcmp(command, "q!") == 0; + bool write = strcmp(command, "w") == 0 || + strcmp(command, "wq") == 0 || + strcmp(command, "x") == 0; + bool quit = force || + strcmp(command, "q") == 0 || + strcmp(command, "wq") == 0 || + strcmp(command, "x") == 0; if (!write && !quit) { @@ -2460,16 +2786,27 @@ static int command_key(struct List* list, const char* path, struct Editor* edito return 1; } - *mode = MODE_NORMAL; + if (quit && !write && !force && dirty) + { + (void) snprintf(editor->message, sizeof editor->message, "unsaved changes (:q! to discard)"); + command[0] = '\0'; + *mode = MODE_NORMAL; + return 1; + } - if (write) - save(list, path); + if (write && save(list, path) != 0) + { + (void) snprintf(editor->message, sizeof editor->message, "could not write %s", path); + command[0] = '\0'; + *mode = MODE_NORMAL; + return 1; + } + *mode = MODE_NORMAL; command[0] = '\0'; if (quit) return 0; - return 1; } @@ -2479,11 +2816,10 @@ static int command_key(struct List* list, const char* path, struct Editor* edito command[length - 1] = '\0'; else *mode = MODE_NORMAL; - return 1; } - if (key >= 32 && key != 127 && key < 256 && length + 1 < MAX_COMMAND) + if (key >= 32 && key < 256 && length + 1 < MAX_COMMAND) { command[length] = (char) key; command[length + 1] = '\0'; @@ -2521,11 +2857,10 @@ static void search_key(const struct List* list, struct Editor* editor, int key) command[length - 1] = '\0'; else *mode = MODE_NORMAL; - return; } - if (key >= 32 && key != 127 && key < 256 && length + 1 < MAX_COMMAND) + if (key >= 32 && key < 256 && length + 1 < MAX_COMMAND) { command[length] = (char) key; command[length + 1] = '\0'; @@ -2556,8 +2891,90 @@ static void confirm_key(struct List* list, struct Editor* editor, int key) *mode = MODE_NORMAL; } +static void visual_key(struct List* list, struct Editor* editor, int key) +{ + int* cursor = &editor->cursor; + enum Mode* mode = &editor->mode; + + int lo = editor->anchor < *cursor ? editor->anchor : *cursor; + int hi = editor->anchor < *cursor ? *cursor : editor->anchor; + + switch (key) + { + case 27: + case 'V': + *mode = MODE_NORMAL; + break; + case 'j': + case KEY_DOWN: + { + int next = next_visible(list, *cursor); + if (next >= 0) + *cursor = next; + } break; + case 'k': + case KEY_UP: + { + int prev = prev_visible(list, *cursor); + if (prev >= 0) + *cursor = prev; + } break; + case 'G': + *cursor = list->count - 1; + break; + case 'g': + { + int next = read_key(); + if (next == 'g') + *cursor = 0; + else + unget_key(next); + } break; + case 'y': + yank_count = 0; + for (int i = lo; i <= hi && i < list->count && yank_count < MAX_ITEMS; i++) + yank_register[yank_count++] = list->items[i]; + *mode = MODE_NORMAL; + break; + case 'd': + case 'x': + if (valid_cursor(list, lo)) + { + history_record(list); + + yank_count = 0; + for (int i = lo; i <= hi && i < list->count && yank_count < MAX_ITEMS; i++) + yank_register[yank_count++] = list->items[i]; + + for (int i = lo; i <= hi && lo < list->count; i++) + delete_item(list, lo); + + *cursor = lo; + commit(list); + } + *mode = MODE_NORMAL; + break; + case ' ': + if (valid_cursor(list, lo)) + { + history_record(list); + + for (int i = lo; i <= hi && i < list->count; i++) + if (!is_section(&list->items[i]) && !is_note(&list->items[i])) + list->items[i].state = (enum State) ((list->items[i].state + 1) % 3); + + commit(list); + } + *mode = MODE_NORMAL; + break; + default: break; + } +} + static void run(struct List* list, const char* path) { + if (!isatty(STDIN_FILENO)) + return; if (enter_raw_mode() != 0) exit(EXIT_FAILURE); @@ -2567,6 +2984,8 @@ static void run(struct List* list, const char* path) struct Editor editor = { 0 }; editor.mode = MODE_NORMAL; + uint64_t edits_at_start = 0; + for (;;) { if (editor.cursor >= list->count) @@ -2588,7 +3007,7 @@ static void run(struct List* list, const char* path) if (editor.edit_pos < caret_min) editor.edit_pos = caret_min; - while (editor.edit_pos > caret_min && ((unsigned char) text[editor.edit_pos] & 0xC0) == 0x80) + while (editor.edit_pos > caret_min && ((uint8_t) text[editor.edit_pos] & 0xC0) == 0x80) editor.edit_pos--; } @@ -2598,7 +3017,7 @@ static void run(struct List* list, const char* path) while (editor.scroll < editor.cursor && render(list, &editor, 1) < 0) editor.scroll++; - render(list, &editor, 0); + (void) render(list, &editor, 0); int key = read_key(); if (key == -1) @@ -2606,6 +3025,22 @@ static void run(struct List* list, const char* path) editor.message[0] = '\0'; + if (editor.mode == MODE_NORMAL && key == '.' && replay_pos < 0 && dot_record_len > 0) + { + memcpy(replay_buf, dot_record, (size_t) dot_record_len * sizeof *replay_buf); + replay_len = dot_record_len; + replay_pos = 0; + continue; + } + + if (editor.mode == MODE_NORMAL && replay_pos < 0 && !recording && is_change_start(key)) + { + recording = true; + rec_len = 0; + rec_buf[rec_len++] = key; + edits_at_start = edits; + } + switch (editor.mode) { case MODE_NORMAL: @@ -2619,7 +3054,7 @@ static void run(struct List* list, const char* path) break; } - normal_key(list, &editor, key); + (void) normal_key(list, &editor, key); editor.count = 0; } break; case MODE_INSERT: @@ -2639,13 +3074,29 @@ static void run(struct List* list, const char* path) case MODE_HELP: editor.mode = MODE_NORMAL; break; + case MODE_VISUAL: + visual_key(list, &editor, key); + break; } - } -} -static void print_version(void) -{ - (void) printf("%s %s\n", TODO_PROGRAM, TODO_VERSION); + if (recording && editor.mode == MODE_NORMAL) + { + recording = false; + + if (edits != edits_at_start && rec_len > 0) + { + memcpy(dot_record, rec_buf, (size_t) rec_len * sizeof *dot_record); + dot_record_len = rec_len; + } + } + + if (valid_cursor(list, editor.cursor) && hidden(list, editor.cursor)) + { + int root = fold_root(list, editor.cursor); + if (root >= 0) + editor.cursor = root; + } + } } static void print_help(void) @@ -2668,13 +3119,18 @@ static void print_help(void) " 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" + " . repeat the last change V visual select (then d / y / space)\n" + " >> << demote / promote (task<->subtask, section depth)\n" + " za fold section (zo open, zc close)\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" + " :w :q :q! :wq :x write / quit (q! discards) :reset all back to todo\n" + " :clear empty the list\n" " :reorder re-sort the list :theme NAME switch theme\n" + " :archive move done items to the archive file :help :? show keys\n" "\n" - "items: '=' section, '==' subsection, '-' subtask, '*' optional, '>' note.\n" + "items: '=' section, '==' subsection, '-' subtask, '*' optional, '>' note,\n" + " '!' important (shown bold, in caps, with a trailing !).\n" "\n" "by hachem <im@hachem.wtf>\n", TODO_PROGRAM, TODO_VERSION, TODO_PROGRAM, TODO_PROGRAM, TODO_PROGRAM); @@ -2692,7 +3148,7 @@ int main(int argc, char** argv) if (argc >= 2 && (strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0)) { - print_version(); + (void) printf("%s %s\n", TODO_PROGRAM, TODO_VERSION); return EXIT_SUCCESS; } |
