hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-10-03 08:44:46 +0200
committerhachem <im@hachem.wtf>2026-10-03 08:44:46 +0200
commit908e6d88f32c6eb1855d13156fad301c402a1798 (patch)
tree33c6e459c466052cf235a227720dddab86abd749
parent7fe8c5ed9d741a58118f145a5d5ed14e533b9d39 (diff)
feat: more qol after using it for a couple days
-rw-r--r--Makefile5
-rw-r--r--README.md22
-rw-r--r--config.h12
-rw-r--r--todo.183
-rw-r--r--todo.c1172
5 files changed, 1123 insertions, 171 deletions
diff --git a/Makefile b/Makefile
index a4fb984..66c84ca 100644
--- a/Makefile
+++ b/Makefile
@@ -27,6 +27,7 @@ endif
TARGET := todo
SRC := todo.c
+HDR := config.h
PREFIX ?= /usr/local
bindir := $(DESTDIR)$(PREFIX)/bin
@@ -34,8 +35,8 @@ mandir := $(DESTDIR)$(PREFIX)/share/man/man1
all: $(TARGET)
-$(TARGET): $(SRC)
- $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+$(TARGET): $(SRC) $(HDR)
+ $(CC) $(CFLAGS) $(SRC) -o $@ $(LDFLAGS)
clean:
rm -rf $(TARGET) $(TARGET).dSYM
diff --git a/README.md b/README.md
index dc83cca..dced413 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,8 @@ a little modal todo list that lives in my terminal.
```text
= movement
- h j k l move (h/l caret in the item, j/k between items)
+ h j k l move (h/l caret in the item, j/k by display line)
+ 0 / $ start / end of the item
w / b next / previous word
{ / } previous / next section
gg / G top / bottom
@@ -17,19 +18,25 @@ a little modal todo list that lives in my terminal.
i insert at the cursor
A append to the item's text
R replace mode (overtype from the cursor)
- x toggle done
+ x delete the character under the cursor
+ cw ce cb change word (ce to end, cb back)
+ c0 c$ cc change to start / end of line, or the whole line
+ ct{c} cf{c} change up to (cf: onto) the next {c}
+ ci{d} ca{d} change inside / around a pair: () [] {} <> " '
space cycle todo -> doing -> done
- dd cut the item
+ dd cut the item (with its subtasks and notes)
D clear every done item
- yy p P yank, paste below, paste above
- J / K move the item down / up (on a heading, moves the whole section)
+ 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)
= other
- / n N search, next match, previous match
+ / n N search (highlights as you type, jumps to match), next, previous
+ esc clear the search highlight
u ctrl-r undo, redo
: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
```
## items
@@ -39,6 +46,7 @@ a little modal todo list that lives in my terminal.
== phase one a subsection -> i. phase one
- write the docs a subtask
* maybe later an optional task -> ( )
+> extra detail a note on the task above
buy milk a plain task
```
@@ -57,6 +65,8 @@ 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
new file mode 100644
index 0000000..059d178
--- /dev/null
+++ b/config.h
@@ -0,0 +1,12 @@
+#define LEFT_MARGIN 2
+#define SECTION_INDENT 2
+#define SUBTASK_INDENT 4
+#define WRAP_COLUMN 80
+
+#define MAX_ITEMS 4096
+#define MAX_TEXT 256
+#define MAX_COMMAND 64
+#define MAX_HISTORY 64
+
+#define HIGHLIGHT "\x1b[48;5;235m"
+#define HIGHLIGHT_RESET "\x1b[49m"
diff --git a/todo.1 b/todo.1
index c9eb00f..2047f23 100644
--- a/todo.1
+++ b/todo.1
@@ -45,6 +45,10 @@ automatically once all of its subtasks are done.
An optional task, shown with parentheses
.RB ( "( )" )
instead of a checkbox.
+.TP
+.BI > " text"
+A note on the task above it: extra description, shown indented and dimmed with
+no checkbox. Notes move with their task.
.PP
Within each section, items are grouped \(em required, then optional, then done
\(em with a blank line between the groups.
@@ -60,8 +64,12 @@ done.
.SS Movement
.TP
.BR h ", " j ", " k ", " l
-Move: \fBh\fR/\fBl\fR move the cursor within the item, \fBj\fR/\fBk\fR between
-items. Arrow keys work too.
+Move: \fBh\fR/\fBl\fR move the cursor within the item, \fBj\fR/\fBk\fR by display
+line, stepping through a wrapped item before moving to the next. Arrow keys work
+too.
+.TP
+.BR 0 ", " $
+Move the cursor to the start or end of the item.
.TP
.BR w ", " b
Move to the next or previous word.
@@ -90,26 +98,77 @@ Append to the item's text.
Enter replace mode, overtyping characters from the cursor.
.TP
.B x
-Toggle the item between done and not done.
+Delete the character under the cursor.
+.TP
+.BR cw ", " ce ", " cb ", " c0 ", " c$ ", " cc
+Change over a motion: a word
+.RB ( cw ", " ce ),
+back a word
+.RB ( cb ),
+to the start
+.RB ( c0 )
+or end
+.RB ( c$ )
+of the line, or the whole line
+.RB ( cc ).
+A count applies, as in
+.BR 2cw .
+.TP
+.BI ct c "\fR, \fP" cf c
+Change the text up to the next
+.IR c ,
+then enter insert mode.
+.B cf
+deletes the
+.I c
+as well.
+.TP
+.BI ci d "\fR, \fP" ca d
+Change the text inside
+.RB ( ci )
+or around
+.RB ( ca )
+the delimiter pair
+.IR d ,
+one of
+.BR () ,
+.BR [] ,
+.BR {} ,
+.BR <> ,
+or a matching
+.B \(dq
+or
+.BR ' .
+If the cursor is before the pair, the next one on the line is used.
.TP
.B space
Cycle the item through todo, doing and done.
.TP
.B dd
-Cut the item (also copies it for pasting).
+Cut the item (also copies it for pasting). A task takes its subtasks and notes
+with it.
.TP
.B D
Remove every done item.
.TP
.BR yy ", " p ", " P
-Yank the item, paste it below, or paste it above.
+Yank the item, paste it below, or paste it above. A task carries its subtasks
+and notes as one block.
.TP
.BR J ", " K
-Move the item down or up. On a section heading this moves the whole section.
+Move the item down or up. A task moves with its subtasks and notes; a section
+heading moves the whole section.
.SS Other
.TP
.BR / ", " n ", " N
-Search for text, then jump to the next or previous match.
+Search for text and jump to the match, placing the cursor on the word;
+.B n
+and
+.B N
+repeat forward and backward. Matches are highlighted as you type and stay
+highlighted until the next search or until
+.B escape
+is pressed in normal mode.
.TP
.BR u ", " "ctrl-r"
Undo and redo.
@@ -141,6 +200,10 @@ Set every item back to todo, clearing all done and doing states.
.TP
.B :clear
Remove every item, emptying the list.
+.TP
+.B :reorder
+Re-sort the list, regrouping items (required, optional, done) within each
+section. Useful after reordering or hand-editing.
.SH OPTIONS
.TP
.BR \-h ", " \-\-help
@@ -157,8 +220,10 @@ a section
.RB ( "= name" ),
a subtask
.RB ( "- name" ),
-or an optional task
-.RB ( "* name" ).
+an optional task
+.RB ( "* name" ),
+or a note
+.RB ( "> text" ).
Override the location with
.BR $TODO_FILE .
.SH ENVIRONMENT
diff --git a/todo.c b/todo.c
index fd26604..1aca5e9 100644
--- a/todo.c
+++ b/todo.c
@@ -9,17 +9,11 @@
#include <time.h>
#include <unistd.h>
+#include "config.h"
+
#define TODO_PROGRAM "todo"
#define TODO_VERSION "0.1.0"
-#define MAX_ITEMS 4096
-#define MAX_TEXT 256
-#define MAX_COMMAND 64
-
-#define LEFT_MARGIN 2
-#define SECTION_INDENT 2
-#define SUBTASK_INDENT 4
-
enum State
{
STATE_TODO,
@@ -84,8 +78,6 @@ static int is_editing(enum Mode mode)
return mode == MODE_INSERT || mode == MODE_REPLACE;
}
-/* utf-8 helpers: edit_pos is a byte offset, but the cursor and h/l/backspace
- * work in whole characters. a continuation byte has its top two bits as 10. */
static int utf8_width(const char* text, int bytes)
{
int width = 0;
@@ -205,6 +197,11 @@ static int is_optional(const struct Item* item)
return item->text[0] == '*';
}
+static int is_note(const struct Item* item)
+{
+ return item->text[0] == '>';
+}
+
static int task_group(const struct Item* item)
{
if (item->state == STATE_DONE)
@@ -226,19 +223,39 @@ static int section_level(const struct Item* item)
return level;
}
+static 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))
+ i = 1;
+
+ while (item->text[i] == ' ')
+ i++;
+
+ return i;
+}
+
static void normalize_tasks(struct List* list)
{
for (int i = 0; i < list->count; i++)
{
- if (is_section(&list->items[i]) || is_subtask(&list->items[i]))
+ if (is_section(&list->items[i]) || is_subtask(&list->items[i]) || is_note(&list->items[i]))
continue;
int total = 0;
int done = 0;
int active = 0;
- for (int j = i + 1; j < list->count && is_subtask(&list->items[j]); j++)
+ for (int j = i + 1;
+ j < list->count && (is_subtask(&list->items[j]) || is_note(&list->items[j]));
+ j++)
{
+ if (!is_subtask(&list->items[j]))
+ continue;
+
total++;
if (list->items[j].state == STATE_DONE)
@@ -291,17 +308,37 @@ static void sort_list(struct List* list)
{
int block = k + 1;
- while (block < segment && is_subtask(&list->items[block]))
+ while (block < segment
+ && (is_subtask(&list->items[block]) || is_note(&list->items[block])))
block++;
if (task_group(&list->items[k]) == group)
{
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++)
- for (int s = k + 1; s < block; s++)
- if ((list->items[s].state == STATE_DONE) == sub_pass)
- sorted[out++] = list->items[s];
+ {
+ int unit = head;
+
+ while (unit < block)
+ {
+ int unit_end = unit + 1;
+
+ while (unit_end < block && is_note(&list->items[unit_end]))
+ unit_end++;
+
+ if ((list->items[unit].state == STATE_DONE) == sub_pass)
+ for (int s = unit; s < unit_end; s++)
+ sorted[out++] = list->items[s];
+
+ unit = unit_end;
+ }
+ }
}
k = block;
@@ -317,6 +354,27 @@ static void sort_list(struct List* list)
free(sorted);
}
+static int task_block_end(const struct List* list, int start)
+{
+ int i = start + 1;
+
+ while (i < list->count && (is_subtask(&list->items[i]) || is_note(&list->items[i])))
+ i++;
+
+ return i;
+}
+
+static int item_block_end(const struct List* list, int start)
+{
+ if (start >= 0 && start < list->count
+ && !is_section(&list->items[start])
+ && !is_subtask(&list->items[start])
+ && !is_note(&list->items[start]))
+ return task_block_end(list, start);
+
+ return start + 1;
+}
+
static int block_end(const struct List* list, int start)
{
int i = start + 1;
@@ -350,7 +408,7 @@ static void save(struct List* list, const char* path)
for (int i = 0; i < list->count; i++)
{
- if (is_section(&list->items[i]))
+ if (is_section(&list->items[i]) || is_note(&list->items[i]))
{
(void) fprintf(file, "%s\n", list->items[i].text);
continue;
@@ -372,8 +430,6 @@ static void commit(struct List* list)
sort_list(list);
}
-#define MAX_HISTORY 64
-
struct Snapshot
{
struct Item* items;
@@ -458,8 +514,8 @@ static void history_redo(struct List* list)
static void insert_item(struct List* list, int index);
-static struct Item yank_register;
-static int yank_full;
+static struct Item yank_register[MAX_ITEMS];
+static int yank_count;
static enum Pending pending_action;
@@ -468,17 +524,24 @@ static int replace_start;
static void paste_item(struct List* list, int index, int* cursor)
{
- if (list->count >= MAX_ITEMS || !yank_full)
+ if (yank_count <= 0 || list->count + yank_count > MAX_ITEMS)
return;
+ if (index < 0)
+ index = 0;
+
+ if (index > list->count)
+ index = list->count;
+
history_record(list);
- insert_item(list, index);
- if (index >= 0 && index < list->count)
- {
- list->items[index] = yank_register;
- *cursor = 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];
+
+ *cursor = index;
}
static char lower(char c)
@@ -509,6 +572,28 @@ static int contains_fold(const char* haystack, const char* needle)
return 0;
}
+static int match_at(const char* text, int pos, const char* needle)
+{
+ int k = 0;
+
+ while (needle[k] != '\0' && text[pos + k] != '\0' && lower(text[pos + k]) == lower(needle[k]))
+ k++;
+
+ return needle[k] == '\0' ? k : 0;
+}
+
+static int match_offset(const char* text, const char* needle)
+{
+ if (needle[0] == '\0')
+ return -1;
+
+ for (int i = 0; text[i] != '\0'; i++)
+ if (match_at(text, i, needle) > 0)
+ return i;
+
+ return -1;
+}
+
static int find_match(const struct List* list, int from, int direction, const char* query)
{
if (list->count == 0 || query[0] == '\0')
@@ -610,6 +695,16 @@ static int screen_rows(void)
return 24;
}
+static int screen_cols(void)
+{
+ struct winsize window;
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) == 0 && window.ws_col > 0)
+ return window.ws_col;
+
+ return 80;
+}
+
static int content_rows(void)
{
int rows = screen_rows() - 4;
@@ -617,6 +712,121 @@ static int content_rows(void)
return rows < 1 ? 1 : rows;
}
+static int emit_wrapped(char* buffer, int length, int cols,
+ const char* background, const char* background_reset,
+ const char* prefix, int prefix_cols, int first_col,
+ const char* style, const char* style_reset,
+ const char* text, const char* search, int* emitted, int visible,
+ int is_cursor, int caret_cols, int* cursor_row, int* cursor_column)
+{
+ int total = (int) strlen(text);
+ int byte = 0;
+ int line = 0;
+ int text_col = 0;
+
+ char mask[MAX_TEXT];
+ memset(mask, 0, sizeof mask);
+
+ if (search != NULL && search[0] != '\0')
+ for (int i = 0; i < total && i < MAX_TEXT; i++)
+ {
+ int m = match_at(text, i, search);
+
+ for (int k = 0; k < m && i + k < MAX_TEXT; k++)
+ mask[i + k] = 1;
+ }
+
+ do
+ {
+ while (line > 0 && byte < total && text[byte] == ' ')
+ {
+ byte++;
+ text_col++;
+ }
+
+ if (line > 0 && byte >= total)
+ break;
+
+ 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;
+ }
+
+ if (line == 0)
+ length += snprintf(buffer + length, 64 * 1024 - (size_t) length,
+ "%s%s%s", background, prefix, style);
+ else
+ length += snprintf(buffer + length, 64 * 1024 - (size_t) length,
+ "%s%*s%s", background, prefix_cols, "", style);
+
+ for (int i = byte; i < end;)
+ {
+ int j = i;
+
+ while (j < end && mask[j] == mask[i])
+ j++;
+
+ if (mask[i])
+ length += snprintf(buffer + length, 64 * 1024 - (size_t) length,
+ "\x1b[7m%.*s\x1b[27m", j - i, text + i);
+ else
+ length += snprintf(buffer + length, 64 * 1024 - (size_t) length,
+ "%.*s", j - i, text + i);
+
+ i = j;
+ }
+
+ length += snprintf(buffer + length, 64 * 1024 - (size_t) length,
+ "%s\x1b[K%s\r\n", style_reset, background_reset);
+
+ if (is_cursor && *cursor_row < 0
+ && caret_cols >= line_start_col && caret_cols <= line_start_col + width)
+ {
+ *cursor_row = row;
+ *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)
{
static const int values[] = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
@@ -633,21 +843,15 @@ static void to_roman(int number, char* buffer, size_t size)
}
}
-static void render(const struct List* list, int cursor, int scroll, enum Mode mode, int edit_pos, const char* command)
+static int wrap_column(void)
{
- int rows = screen_rows();
- int visible = content_rows();
+ int cols = screen_cols();
- int total_items = 0;
- int done = 0;
-
- for (int i = 0; i < list->count; i++)
- if (!is_section(&list->items[i]) && !is_subtask(&list->items[i]))
- {
- total_items++;
- done += list->items[i].state == STATE_DONE;
- }
+ return cols > WRAP_COLUMN ? WRAP_COLUMN : cols;
+}
+static int compute_number_width(const struct List* list)
+{
int max_run = 0;
int run = 0;
@@ -655,7 +859,7 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
{
if (is_section(&list->items[i]))
run = 0;
- else if (!is_subtask(&list->items[i]) && ++run > max_run)
+ else if (!is_subtask(&list->items[i]) && !is_note(&list->items[i]) && ++run > max_run)
max_run = run;
}
@@ -664,6 +868,29 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
for (int count = max_run; count >= 10; count /= 10)
number_width++;
+ return number_width;
+}
+
+static int render(const struct List* list, int cursor, int scroll, enum Mode mode, int edit_pos, const char* command, const char* search, int measure)
+{
+ int rows = screen_rows();
+ int cols = wrap_column();
+ int visible = content_rows();
+
+ const char* matches = mode == MODE_SEARCH ? command : search;
+
+ int total_items = 0;
+ int done = 0;
+
+ for (int i = 0; i < list->count; i++)
+ if (!is_section(&list->items[i]) && !is_subtask(&list->items[i]) && !is_note(&list->items[i]))
+ {
+ total_items++;
+ done += list->items[i].state == STATE_DONE;
+ }
+
+ int number_width = compute_number_width(list);
+
char date[64];
char clock[32];
time_t now = time(NULL);
@@ -686,13 +913,14 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
int index = scroll;
int prev_group = -1;
int cursor_row = -1;
+ int cursor_column = -1;
while (emitted < visible && index < list->count)
{
const struct Item* item = &list->items[index];
- const char* background = index == cursor ? "\x1b[48;5;235m" : "";
- const char* background_reset = index == cursor ? "\x1b[49m" : "";
+ const char* background = index == cursor ? HIGHLIGHT : "";
+ const char* background_reset = index == cursor ? HIGHLIGHT_RESET : "";
if (is_section(item))
{
@@ -768,6 +996,26 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT;
+ if (is_note(item))
+ {
+ int indent = base_indent + number_width + 6;
+ const char* name = item->text;
+
+ char prefix[16];
+ (void) snprintf(prefix, sizeof prefix, "%*s", indent, "");
+
+ int caret = 0;
+
+ if (index == cursor)
+ caret = utf8_width(name, edit_pos);
+
+ length = emit_wrapped(buffer, length, cols, background, background_reset,
+ prefix, indent + 2, indent, "\x1b[2;3m", "\x1b[22;23m", name,
+ matches, &emitted, visible, index == cursor, caret, &cursor_row, &cursor_column);
+ index++;
+ continue;
+ }
+
const char* box = item->state == STATE_DONE ? "[x]"
: item->state == STATE_DOING ? "[-]"
: "[ ]";
@@ -787,12 +1035,21 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
name++;
}
+ char prefix[32];
+ (void) snprintf(prefix, sizeof prefix, "%*s%s ", indent, "", box);
+
+ int caret = 0;
+
if (index == cursor)
- cursor_row = 4 + emitted;
+ {
+ int offset = (int) (name - item->text);
+ int byte = edit_pos - offset < 0 ? 0 : edit_pos - offset;
+ caret = utf8_width(name, byte);
+ }
- length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "%s%*s%s %s%s%s\x1b[K%s\r\n",
- background, indent, "", box, dim, name, dim_reset, background_reset);
+ length = emit_wrapped(buffer, length, cols, background, background_reset,
+ prefix, indent + 4, indent + 4, dim, dim_reset, name,
+ matches, &emitted, visible, index == cursor, caret, &cursor_row, &cursor_column);
}
else
{
@@ -812,7 +1069,7 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
int number = 0;
for (int k = index; k >= 0 && !is_section(&list->items[k]); k--)
- if (!is_subtask(&list->items[k]))
+ if (!is_subtask(&list->items[k]) && !is_note(&list->items[k]))
number++;
const char* task_box = is_optional(item)
@@ -828,16 +1085,25 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
name++;
}
+ char prefix[48];
+ (void) snprintf(prefix, sizeof prefix, "%*s\x1b[2m%*d.\x1b[22m %s ",
+ base_indent, "", number_width, number, task_box);
+
+ int caret = 0;
+
if (index == cursor)
- cursor_row = 4 + emitted;
+ {
+ int offset = (int) (name - item->text);
+ int byte = edit_pos - offset < 0 ? 0 : edit_pos - offset;
+ caret = utf8_width(name, byte);
+ }
- length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "%s%*s\x1b[2m%*d.\x1b[22m %s %s%s%s\x1b[K%s\r\n",
- background, base_indent, "", number_width, number,
- task_box, dim, name, dim_reset, background_reset);
+ length = emit_wrapped(buffer, length, cols, background, background_reset,
+ prefix, base_indent + number_width + 6, base_indent + number_width + 6,
+ dim, dim_reset, name,
+ matches, &emitted, visible, index == cursor, caret, &cursor_row, &cursor_column);
}
- emitted++;
index++;
}
@@ -863,67 +1129,53 @@ static void render(const struct List* list, int cursor, int scroll, enum Mode mo
if (mode == MODE_COMMAND || mode == MODE_SEARCH)
{
- int cursor_column = LEFT_MARGIN + 2 + (int) strlen(command);
+ 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);
}
else if (mode != MODE_CONFIRM && cursor >= 0 && cursor < list->count && cursor_row >= 0)
{
- int cursor_column;
- int caret = utf8_width(list->items[cursor].text, edit_pos);
-
if (is_section(&list->items[cursor]))
{
int level = section_level(&list->items[cursor]);
int section_indent = LEFT_MARGIN + (level - 1) * SECTION_INDENT;
+ int caret = utf8_width(list->items[cursor].text, edit_pos);
cursor_column = is_editing(mode) ? section_indent + 1 + caret : section_indent + 1;
}
- else
- {
- int heading_level = 0;
-
- for (int k = cursor - 1; k >= 0; k--)
- if (is_section(&list->items[k]))
- {
- heading_level = section_level(&list->items[k]);
- break;
- }
-
- int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT;
- int text_start;
-
- if (is_subtask(&list->items[cursor]))
- text_start = base_indent + number_width + SUBTASK_INDENT + 5;
- else
- text_start = base_indent + number_width + 7;
-
- cursor_column = text_start + caret;
- }
const char* shape = mode == MODE_REPLACE ? "\x1b[4 q"
: mode == MODE_INSERT ? "\x1b[6 q"
: "\x1b[2 q";
- length += snprintf(buffer + length, sizeof buffer - (size_t) length,
- "\x1b[%d;%dH%s\x1b[?25h", cursor_row, cursor_column, shape);
+ if (cursor_column >= 0)
+ length += snprintf(buffer + length, sizeof buffer - (size_t) length,
+ "\x1b[%d;%dH%s\x1b[?25h", cursor_row, cursor_column, shape);
}
else
{
length += snprintf(buffer + length, sizeof buffer - (size_t) length, "\x1b[?25l");
}
- (void) write(STDOUT_FILENO, buffer, (size_t) length);
+ if (!measure)
+ (void) write(STDOUT_FILENO, buffer, (size_t) length);
+
+ return cursor_row;
}
-static int read_key(void)
+static int pending_key = -1;
+
+static void unget_key(int key)
{
- static int pending = -1;
+ pending_key = key;
+}
- if (pending != -1)
+static int read_key(void)
+{
+ if (pending_key != -1)
{
- int key = pending;
- pending = -1;
+ int key = pending_key;
+ pending_key = -1;
return key;
}
@@ -935,9 +1187,6 @@ static int read_key(void)
if (character != 27)
return (unsigned char) character;
- /* an escape: peek for an arrow sequence without blocking. a byte that is
- * not part of a sequence is pushed back so a lone escape followed by a
- * command (e.g. :wq) never swallows the following keys. */
int flags = fcntl(STDIN_FILENO, F_GETFL);
(void) fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
@@ -968,15 +1217,307 @@ static int read_key(void)
}
(void) fcntl(STDIN_FILENO, F_SETFL, flags);
- pending = (unsigned char) first;
+ pending_key = (unsigned char) first;
return 27;
}
+static const char* item_display(const struct List* list, int index, int number_width,
+ int* first_col, int* cont_col)
+{
+ const struct Item* item = &list->items[index];
+ int heading_level = 0;
+
+ for (int k = index - 1; k >= 0; k--)
+ if (is_section(&list->items[k]))
+ {
+ heading_level = section_level(&list->items[k]);
+ break;
+ }
+
+ int base_indent = LEFT_MARGIN + heading_level * SECTION_INDENT;
+
+ if (is_note(item))
+ {
+ int indent = base_indent + number_width + 6;
+ *first_col = indent;
+ *cont_col = indent + 2;
+ return item->text;
+ }
+
+ if (is_subtask(item))
+ {
+ int indent = base_indent + number_width + SUBTASK_INDENT;
+ *first_col = *cont_col = indent + 4;
+ const char* name = item->text + 1;
+
+ while (*name == ' ')
+ name++;
+
+ return name;
+ }
+
+ *first_col = *cont_col = base_indent + number_width + 6;
+ const char* name = item->text;
+
+ if (is_optional(item))
+ {
+ name = item->text + 1;
+
+ while (*name == ' ')
+ name++;
+ }
+
+ return name;
+}
+
+static int wrap_lines(const char* name, int cols, int first_col, int cont_col,
+ int* starts, int* ends, int max_lines)
+{
+ int total = (int) strlen(name);
+ int byte = 0;
+ int line = 0;
+
+ do
+ {
+ while (line > 0 && byte < total && name[byte] == ' ')
+ byte++;
+
+ if (line > 0 && byte >= total)
+ break;
+
+ int base = line == 0 ? first_col : cont_col;
+ int avail = cols - base;
+
+ if (avail < 1)
+ avail = 1;
+
+ int end = byte;
+ int width = 0;
+ int space_pos = -1;
+
+ while (end < total && width < avail)
+ {
+ if (name[end] == ' ')
+ space_pos = end;
+
+ end++;
+
+ while (end < total && ((unsigned char) name[end] & 0xC0) == 0x80)
+ end++;
+
+ width++;
+ }
+
+ if (end < total && name[end] != ' ' && space_pos > byte)
+ end = space_pos;
+
+ if (line < max_lines)
+ {
+ starts[line] = byte;
+ ends[line] = end;
+ }
+
+ byte = end;
+ line++;
+ }
+ while (byte < total);
+
+ if (line == 0)
+ {
+ if (max_lines > 0)
+ {
+ starts[0] = 0;
+ ends[0] = 0;
+ }
+
+ line = 1;
+ }
+
+ return line;
+}
+
+static 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)
+ width++;
+
+ return width;
+}
+
+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)
+ i++;
+
+ width++;
+ }
+
+ return i;
+}
+
+static void place_on_line(const struct List* list, int index, int number_width, int cols,
+ int last, int goal, int* edit_pos)
+{
+ if (is_section(&list->items[index]))
+ {
+ *edit_pos = text_start(&list->items[index]);
+ return;
+ }
+
+ int first_col;
+ int cont_col;
+ const char* name = item_display(list, index, number_width, &first_col, &cont_col);
+ int name_offset = (int) (name - list->items[index].text);
+
+ int starts[MAX_TEXT];
+ int ends[MAX_TEXT];
+ int lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT);
+ int line = last ? lines - 1 : 0;
+ int base = line == 0 ? first_col : cont_col;
+ int want = goal - base;
+
+ if (want < 0)
+ want = 0;
+
+ *edit_pos = name_offset + offset_at_width(name, starts[line], ends[line], want);
+}
+
+static void move_visual(struct List* list, int* cursor, int* edit_pos, int number_width, int cols, int down)
+{
+ if (*cursor < 0 || *cursor >= list->count)
+ return;
+
+ const struct Item* item = &list->items[*cursor];
+ int line = 0;
+ int lines = 1;
+ int goal = 0;
+
+ if (!is_section(item))
+ {
+ int first_col;
+ int cont_col;
+ const char* name = item_display(list, *cursor, number_width, &first_col, &cont_col);
+ int name_offset = (int) (name - item->text);
+ int name_length = (int) strlen(name);
+
+ int starts[MAX_TEXT];
+ int ends[MAX_TEXT];
+ lines = wrap_lines(name, cols, first_col, cont_col, starts, ends, MAX_TEXT);
+
+ int pos = *edit_pos - name_offset;
+
+ if (pos < 0)
+ pos = 0;
+
+ if (pos > name_length)
+ pos = name_length;
+
+ for (int i = 0; i < lines; i++)
+ if (starts[i] <= pos)
+ line = i;
+
+ int base = line == 0 ? first_col : cont_col;
+ goal = base + display_width(name, starts[line], pos);
+
+ if (down && line < lines - 1)
+ {
+ int want = goal - cont_col;
+
+ if (want < 0)
+ want = 0;
+
+ *edit_pos = name_offset + offset_at_width(name, starts[line + 1], ends[line + 1], want);
+ return;
+ }
+
+ if (!down && line > 0)
+ {
+ int target = line - 1;
+ int target_base = target == 0 ? first_col : cont_col;
+ int want = goal - target_base;
+
+ if (want < 0)
+ want = 0;
+
+ *edit_pos = name_offset + offset_at_width(name, starts[target], ends[target], want);
+ return;
+ }
+ }
+
+ if (down && *cursor + 1 < list->count)
+ {
+ (*cursor)++;
+ place_on_line(list, *cursor, number_width, cols, 0, goal, edit_pos);
+ }
+ else if (!down && *cursor > 0)
+ {
+ (*cursor)--;
+ place_on_line(list, *cursor, number_width, cols, 1, goal, edit_pos);
+ }
+}
+
+static int matching_close(int open)
+{
+ switch (open)
+ {
+ case '(': return ')';
+ case '[': return ']';
+ case '{': return '}';
+ case '<': return '>';
+ }
+
+ return 0;
+}
+
+static int matching_open(int close)
+{
+ switch (close)
+ {
+ case ')': return '(';
+ case ']': return '[';
+ case '}': return '{';
+ case '>': return '<';
+ }
+
+ return 0;
+}
+
+static void apply_change(struct List* list, int cursor, enum Mode* mode, int* edit_pos, int from, int to)
+{
+ struct Item* item = &list->items[cursor];
+ int length = (int) strlen(item->text);
+
+ if (to > length)
+ to = length;
+
+ if (from < 0 || to < from)
+ return;
+
+ history_record(list);
+ memmove(&item->text[from], &item->text[to], (size_t) (length - to) + 1);
+ *edit_pos = from;
+ *mode = MODE_INSERT;
+}
+
static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit_pos,
- char* command, const char* search, int count, int key)
+ char* command, char* search, int count, int key)
{
switch (key)
{
+ case 27:
+ search[0] = '\0';
+ break;
+
case ':':
command[0] = '\0';
*mode = MODE_COMMAND;
@@ -992,7 +1533,13 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
int match = find_match(list, *cursor, 1, search);
if (match >= 0)
+ {
*cursor = match;
+ int off = match_offset(list->items[match].text, search);
+
+ if (off >= 0)
+ *edit_pos = off;
+ }
break;
}
@@ -1002,27 +1549,43 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
int match = find_match(list, *cursor, -1, search);
if (match >= 0)
+ {
*cursor = match;
+ int off = match_offset(list->items[match].text, search);
+
+ if (off >= 0)
+ *edit_pos = off;
+ }
break;
}
case 'y':
{
- char next;
+ int next = read_key();
- if (read(STDIN_FILENO, &next, 1) == 1 && next == 'y'
- && *cursor >= 0 && *cursor < list->count)
+ if (next != 'y')
{
- yank_register = list->items[*cursor];
- yank_full = 1;
+ unget_key(next);
+ break;
+ }
+
+ if (*cursor >= 0 && *cursor < list->count)
+ {
+ int start = *cursor;
+ int end = item_block_end(list, start);
+
+ yank_count = 0;
+
+ for (int i = start; i < end && yank_count < MAX_ITEMS; i++)
+ yank_register[yank_count++] = list->items[i];
}
break;
}
case 'p':
- paste_item(list, *cursor + 1, cursor);
+ paste_item(list, item_block_end(list, *cursor), cursor);
break;
case 'P':
@@ -1031,13 +1594,27 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
case 'j':
case KEY_DOWN:
- *cursor += count;
+ {
+ int number_width = compute_number_width(list);
+ int cols = wrap_column();
+
+ for (int repeat = 0; repeat < count; repeat++)
+ move_visual(list, cursor, edit_pos, number_width, cols, 1);
+
break;
+ }
case 'k':
case KEY_UP:
- *cursor -= count;
+ {
+ int number_width = compute_number_width(list);
+ int cols = wrap_column();
+
+ for (int repeat = 0; repeat < count; repeat++)
+ move_visual(list, cursor, edit_pos, number_width, cols, 0);
+
break;
+ }
case 'h':
case KEY_LEFT:
@@ -1055,6 +1632,18 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
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)
{
@@ -1108,7 +1697,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
break;
case ' ':
- if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor]))
+ if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor]) && !is_note(&list->items[*cursor]))
{
history_record(list);
struct Item* item = &list->items[*cursor];
@@ -1119,23 +1708,34 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
break;
case 'x':
- if (*cursor >= 0 && *cursor < list->count && !is_section(&list->items[*cursor]))
+ if (*cursor >= 0 && *cursor < list->count
+ && *edit_pos < (int) strlen(list->items[*cursor].text))
{
history_record(list);
+
struct Item* item = &list->items[*cursor];
- item->state = item->state == STATE_DONE ? STATE_TODO : STATE_DONE;
- commit(list);
+
+ for (int repeat = 0; repeat < count && *edit_pos < (int) strlen(item->text); repeat++)
+ {
+ int next = utf8_right(item->text, *edit_pos);
+ memmove(&item->text[*edit_pos], &item->text[next],
+ strlen(item->text) - (size_t) next + 1);
+ }
}
break;
case 'o':
+ {
history_record(list);
- insert_item(list, *cursor + 1);
- *cursor = *cursor + 1 < list->count ? *cursor + 1 : list->count - 1;
+
+ int at = item_block_end(list, *cursor);
+ insert_item(list, at);
+ *cursor = at < list->count ? at : list->count - 1;
*edit_pos = 0;
*mode = MODE_INSERT;
break;
+ }
case 'A':
history_record(list);
@@ -1159,15 +1759,10 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
{
history_record(list);
- if (is_section(&list->items[*cursor]))
- {
- int position = 1;
-
- while (list->items[*cursor].text[position] == ' ')
- position++;
+ int start = text_start(&list->items[*cursor]);
- *edit_pos = position;
- }
+ if (*edit_pos < start)
+ *edit_pos = start;
*mode = MODE_INSERT;
}
@@ -1193,12 +1788,193 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
break;
+ case 'c':
+ {
+ if (*cursor < 0 || *cursor >= list->count)
+ break;
+
+ int op = read_key();
+
+ const char* text = list->items[*cursor].text;
+ int length = (int) strlen(text);
+ int low = text_start(&list->items[*cursor]);
+
+ if (op == 'w' || op == 'e' || op == 'b' || op == '$'
+ || op == '0' || op == 'c' || op == 'h' || op == 'l')
+ {
+ int from = *edit_pos;
+ int to = *edit_pos;
+
+ if (op == 'w' || op == 'e')
+ {
+ int pos = *edit_pos;
+
+ for (int r = 0; r < count; r++)
+ {
+ while (pos < length && text[pos] == ' ')
+ pos++;
+
+ while (pos < length && text[pos] != ' ')
+ pos++;
+ }
+
+ to = pos;
+ }
+ else if (op == 'b')
+ {
+ int pos = *edit_pos;
+
+ for (int r = 0; r < count; r++)
+ {
+ if (pos > low)
+ pos--;
+
+ while (pos > low && text[pos] == ' ')
+ pos--;
+
+ while (pos > low && text[pos - 1] != ' ')
+ pos--;
+ }
+
+ from = pos;
+ }
+ else if (op == '$')
+ {
+ to = length;
+ }
+ else if (op == '0')
+ {
+ from = low;
+ }
+ else if (op == 'c')
+ {
+ from = low;
+ to = length;
+ }
+ else if (op == 'l')
+ {
+ for (int r = 0; r < count; r++)
+ to = utf8_right(text, to);
+ }
+ else
+ {
+ for (int r = 0; r < count; r++)
+ from = utf8_left(text, from);
+ }
+
+ apply_change(list, *cursor, mode, edit_pos, from, to);
+ break;
+ }
+
+ if (op != 't' && op != 'f' && op != 'i' && op != 'a')
+ {
+ unget_key(op);
+ break;
+ }
+
+ int target = read_key();
+
+ if (target < 32 || target >= 256)
+ break;
+
+ if (op == 't' || op == 'f')
+ {
+ int found = -1;
+
+ for (int i = *edit_pos + 1; i < length; i++)
+ if ((unsigned char) text[i] == target)
+ {
+ found = i;
+ break;
+ }
+
+ if (found >= 0)
+ apply_change(list, *cursor, mode, edit_pos,
+ *edit_pos, op == 'f' ? found + 1 : found);
+
+ break;
+ }
+
+ int open_char = target;
+ int close_char = matching_close(target);
+
+ if (close_char == 0)
+ {
+ int open = matching_open(target);
+
+ if (open != 0)
+ {
+ open_char = open;
+ close_char = target;
+ }
+ else
+ {
+ close_char = target;
+ }
+ }
+
+ int open_at = -1;
+ int close_at = -1;
+
+ for (int i = *edit_pos; i >= low; i--)
+ if ((unsigned char) text[i] == open_char)
+ {
+ open_at = i;
+ break;
+ }
+
+ if (open_at >= 0)
+ {
+ for (int i = open_at + 1; i < length; i++)
+ if ((unsigned char) text[i] == close_char)
+ {
+ close_at = i;
+ break;
+ }
+
+ if (close_at < *edit_pos)
+ {
+ open_at = -1;
+ close_at = -1;
+ }
+ }
+
+ if (open_at < 0)
+ {
+ for (int i = *edit_pos; i < length; i++)
+ if ((unsigned char) text[i] == open_char)
+ {
+ open_at = i;
+ break;
+ }
+
+ for (int i = open_at + 1; open_at >= 0 && i < length; i++)
+ if ((unsigned char) text[i] == close_char)
+ {
+ close_at = i;
+ break;
+ }
+ }
+
+ if (open_at < 0 || close_at < 0)
+ break;
+
+ if (op == 'i')
+ apply_change(list, *cursor, mode, edit_pos, open_at + 1, close_at);
+ else
+ apply_change(list, *cursor, mode, edit_pos, open_at, close_at + 1);
+
+ break;
+ }
+
case 'g':
{
- char next;
+ int next = read_key();
- if (read(STDIN_FILENO, &next, 1) == 1 && next == 'g')
+ if (next == 'g')
*cursor = 0;
+ else
+ unget_key(next);
break;
}
@@ -1231,17 +2007,31 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
case 'd':
{
- char next;
+ int next = read_key();
- if (read(STDIN_FILENO, &next, 1) == 1 && next == 'd'
- && *cursor >= 0 && *cursor < list->count)
+ if (next != 'd')
+ {
+ unget_key(next);
+ break;
+ }
+
+ if (*cursor >= 0 && *cursor < list->count)
{
history_record(list);
- yank_register = list->items[*cursor];
- yank_full = 1;
- for (int repeat = 0; repeat < count && *cursor < list->count; repeat++)
- delete_item(list, *cursor);
+ int start = *cursor;
+ int end = start;
+
+ 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);
}
@@ -1253,8 +2043,6 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
if (*cursor < 0 || *cursor >= list->count)
break;
- history_record(list);
-
if (is_section(&list->items[*cursor]))
{
int start = *cursor;
@@ -1262,6 +2050,7 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
if (end < list->count)
{
+ history_record(list);
int next_end = block_end(list, end);
reverse_items(list, start, end);
reverse_items(list, end, next_end);
@@ -1269,8 +2058,25 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
*cursor = start + (next_end - end);
}
}
- else if (*cursor + 1 < list->count && !is_section(&list->items[*cursor + 1]))
+ else if (!is_subtask(&list->items[*cursor]) && !is_note(&list->items[*cursor]))
{
+ int start = *cursor;
+ int end = task_block_end(list, start);
+
+ if (end < list->count && !is_section(&list->items[end]))
+ {
+ history_record(list);
+ int next_end = task_block_end(list, end);
+ reverse_items(list, start, end);
+ reverse_items(list, end, next_end);
+ reverse_items(list, start, next_end);
+ *cursor = start + (next_end - end);
+ }
+ }
+ else if (*cursor + 1 < list->count
+ && (is_subtask(&list->items[*cursor + 1]) || is_note(&list->items[*cursor + 1])))
+ {
+ history_record(list);
struct Item moved = list->items[*cursor];
list->items[*cursor] = list->items[*cursor + 1];
list->items[*cursor + 1] = moved;
@@ -1283,14 +2089,13 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
if (*cursor < 0 || *cursor >= list->count)
break;
- history_record(list);
-
if (is_section(&list->items[*cursor]))
{
int start = *cursor;
if (start > 0)
{
+ history_record(list);
int previous = start - 1;
while (previous >= 0 && !is_section(&list->items[previous]))
@@ -1304,8 +2109,29 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
*cursor = previous_start;
}
}
- else if (*cursor > 0 && !is_section(&list->items[*cursor - 1]))
+ else if (!is_subtask(&list->items[*cursor]) && !is_note(&list->items[*cursor]))
{
+ int start = *cursor;
+ int previous = start - 1;
+
+ while (previous >= 0
+ && (is_subtask(&list->items[previous]) || is_note(&list->items[previous])))
+ previous--;
+
+ if (previous >= 0 && !is_section(&list->items[previous]))
+ {
+ history_record(list);
+ int end = task_block_end(list, start);
+ reverse_items(list, previous, start);
+ reverse_items(list, start, end);
+ reverse_items(list, previous, end);
+ *cursor = previous;
+ }
+ }
+ else if (*cursor > 0
+ && (is_subtask(&list->items[*cursor - 1]) || is_note(&list->items[*cursor - 1])))
+ {
+ history_record(list);
struct Item moved = list->items[*cursor];
list->items[*cursor] = list->items[*cursor - 1];
list->items[*cursor - 1] = moved;
@@ -1318,10 +2144,23 @@ static int normal_key(struct List* list, int* cursor, enum Mode* mode, int* edit
{
history_record(list);
int kept = 0;
+ int i = 0;
- for (int i = 0; i < list->count; i++)
- if (list->items[i].state != STATE_DONE)
- list->items[kept++] = list->items[i];
+ while (i < list->count)
+ {
+ int end = item_block_end(list, i);
+
+ int drop = !is_section(&list->items[i])
+ && !is_subtask(&list->items[i])
+ && !is_note(&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;
+ }
list->count = kept;
commit(list);
@@ -1360,8 +2199,9 @@ static void insert_key(struct List* list, int* cursor, enum Mode* mode, int* edi
if (key == '\r' || key == '\n')
{
- insert_item(list, *cursor + 1);
- (*cursor)++;
+ int at = item_block_end(list, *cursor);
+ insert_item(list, at);
+ *cursor = at;
*edit_pos = 0;
return;
}
@@ -1444,6 +2284,15 @@ static int command_key(struct List* list, const char* path, enum Mode* mode, cha
return 1;
}
+ if (strcmp(command, "reorder") == 0)
+ {
+ history_record(list);
+ commit(list);
+ command[0] = '\0';
+ *mode = MODE_NORMAL;
+ return 1;
+ }
+
int write = strchr(command, 'w') != NULL || strcmp(command, "x") == 0;
int quit = strchr(command, 'q') != NULL || strcmp(command, "x") == 0;
@@ -1479,7 +2328,7 @@ static int command_key(struct List* list, const char* path, enum Mode* mode, cha
return 1;
}
-static void search_key(const struct List* list, int* cursor, enum Mode* mode, char* command, char* search, int key)
+static void search_key(const struct List* list, int* cursor, int* edit_pos, enum Mode* mode, char* command, char* search, int key)
{
size_t length = strlen(command);
@@ -1499,7 +2348,13 @@ static void search_key(const struct List* list, int* cursor, enum Mode* mode, ch
int match = find_match(list, *cursor, 1, search);
if (match >= 0)
+ {
*cursor = match;
+ int off = match_offset(list->items[match].text, search);
+
+ if (off >= 0)
+ *edit_pos = off;
+ }
return;
}
@@ -1563,37 +2418,40 @@ static void run(struct List* list, const char* path)
for (;;)
{
- int visible = content_rows();
-
if (cursor >= list->count)
cursor = list->count - 1;
if (cursor < 0)
cursor = 0;
- if (cursor < scroll)
- scroll = cursor;
-
- if (cursor >= scroll + visible)
- scroll = cursor - visible + 1;
-
if (list->count > 0)
{
const char* text = list->items[cursor].text;
int text_length = (int) strlen(text);
int caret_max = is_editing(mode) ? text_length : (text_length > 0 ? text_length - 1 : 0);
+ int caret_min = is_editing(mode) ? 0 : text_start(&list->items[cursor]);
+
+ if (caret_min > caret_max)
+ caret_min = caret_max;
if (edit_pos > caret_max)
edit_pos = caret_max;
- if (edit_pos < 0)
- edit_pos = 0;
+ if (edit_pos < caret_min)
+ edit_pos = caret_min;
- while (edit_pos > 0 && ((unsigned char) text[edit_pos] & 0xC0) == 0x80)
+ while (edit_pos > caret_min && ((unsigned char) text[edit_pos] & 0xC0) == 0x80)
edit_pos--;
}
- render(list, cursor, scroll, mode, edit_pos, command);
+ if (cursor < scroll)
+ scroll = cursor;
+
+ while (scroll < cursor
+ && render(list, cursor, scroll, mode, edit_pos, command, search, 1) < 0)
+ scroll++;
+
+ render(list, cursor, scroll, mode, edit_pos, command, search, 0);
int key = read_key();
@@ -1629,7 +2487,7 @@ static void run(struct List* list, const char* path)
break;
case MODE_SEARCH:
- search_key(list, &cursor, &mode, command, search, key);
+ search_key(list, &cursor, &edit_pos, &mode, command, search, key);
break;
case MODE_CONFIRM:
@@ -1654,15 +2512,21 @@ static void print_help(void)
" %s -v | --version\n"
"\n"
"keys:\n"
- " h j k l / arrows move w / b words { / } sections\n"
- " gg / G top/bottom o new item i insert at cursor\n"
- " A append text R overtype (replace) 3j 2dd counts\n"
- " x toggle done space cycle state\n"
+ " h j k l / arrows move w / b words 0 / $ line ends\n"
+ " { / } sections gg / G top/bottom\n"
+ " o new item i insert at cursor A append text\n"
+ " R overtype (replace) 3j 2dd counts\n"
+ " x delete char under cursor space cycle state\n"
+ " cw ce cb c0 c$ cc change word / to start / end / whole\n"
+ " 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"
- " J / K move item or section / n N search u ctrl-r undo/redo\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"
" :w :q :wq :x write / quit :reset all back to todo :clear empty\n"
+ " :reorder re-sort the list\n"
"\n"
- "items: '=' section, '==' subsection, '-' subtask, '*' optional, else a task.\n"
+ "items: '=' section, '==' subsection, '-' subtask, '*' optional, '>' note.\n"
"\n"
"by hachem <im@hachem.wtf>\n",
TODO_PROGRAM, TODO_VERSION, TODO_PROGRAM, TODO_PROGRAM, TODO_PROGRAM);