hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-10-03 19:38:22 +0200
committerhachem <im@hachem.wtf>2026-10-03 19:38:22 +0200
commit419700c1bab1e37993260bd86ffe6fddc7d5df49 (patch)
tree820c9eeff98827c2e4f8206d5bf026b6d011c92e
parent41005a0d4102843cefeed911a0fa8bd5774d998c (diff)
feat: archive + important
-rw-r--r--README.md17
-rw-r--r--config.h1
-rw-r--r--todo.188
-rw-r--r--todo.c868
4 files changed, 762 insertions, 212 deletions
diff --git a/README.md b/README.md
index 1500af8..3b11970 100644
--- a/README.md
+++ b/README.md
@@ -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
```
diff --git a/config.h b/config.h
index 1b2d90c..20cc1b7 100644
--- a/config.h
+++ b/config.h
@@ -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]).
diff --git a/todo.1 b/todo.1
index ce630c6..107bd50 100644
--- a/todo.1
+++ b/todo.1
@@ -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"
diff --git a/todo.c b/todo.c
index 8560cfd..6d35169 100644
--- a/todo.c
+++ b/todo.c
@@ -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, &section_done, &section_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;
}