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