~kris/dots

srice

ref: e9b48d06a8541f3eda5c4db90382ab3c77183afb srice/.local/bin/misc/todo -rwxr-xr-x 13.9 KiB
e9b48d06 — Kris Yotam xprofile: systemd-aware pipewire start + blueman-applet; sb-internet: tolerate missing /proc/net/wireless 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
#!/bin/bash
# todo - aesthetic terminal todo list backed by SQLite
# Inspired by todo.txt-cli. Data lives in ~/.config/scripts/scripts.db
# Usage: todo [command] [args...]

DB="${XDG_CONFIG_HOME:-$HOME/.config}/scripts/scripts.db"

# --- colors ---
C_RESET='\033[0m'
C_BOLD='\033[1m'
C_DIM='\033[2m'
C_ID='\033[33m'         # yellow
C_PRI_A='\033[1;31m'    # bold red
C_PRI_B='\033[1;33m'    # bold yellow
C_PRI_C='\033[1;34m'    # bold blue
C_PRI='\033[1;36m'      # bold cyan (D+)
C_DONE='\033[9;2m'      # strikethrough + dim
C_LIST='\033[35m'       # magenta
C_DATE='\033[2m'        # dim
C_COUNT='\033[1;32m'    # bold green
C_HEADER='\033[1;4m'    # bold underline

# --- init ---
mkdir -p "$(dirname "$DB")"
sqlite3 "$DB" "
CREATE TABLE IF NOT EXISTS todo_lists (
	name TEXT PRIMARY KEY,
	created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
INSERT OR IGNORE INTO todo_lists (name) VALUES ('inbox');
CREATE TABLE IF NOT EXISTS todo (
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	task TEXT NOT NULL,
	priority TEXT DEFAULT '',
	list TEXT DEFAULT 'inbox',
	done INTEGER DEFAULT 0,
	created_at TEXT DEFAULT (datetime('now', 'localtime')),
	done_at TEXT DEFAULT ''
);"

# --- helpers ---
die() { printf '%s\n' "$1" >&2; exit 1; }

esc() { printf '%s' "$1" | sed "s/'/''/g"; }

pri_color() {
	case "$1" in
		A) printf '%b' "$C_PRI_A" ;;
		B) printf '%b' "$C_PRI_B" ;;
		C) printf '%b' "$C_PRI_C" ;;
		?*) printf '%b' "$C_PRI" ;;
		*) ;;
	esac
}

fmt_task() {
	id="$1"; task="$2"; pri="$3"; list="$4"; done="$5"; created="$6"
	if [ "$done" = "1" ]; then
		printf "${C_DIM}%4s ${C_DONE}%s${C_RESET}\n" "$id" "$task"
		return
	fi
	pri_str=""
	if [ -n "$pri" ]; then
		pri_str="$(pri_color "$pri")(${pri})${C_RESET} "
	fi
	list_str=""
	if [ "$list" != "inbox" ]; then
		list_str=" ${C_LIST}@${list}${C_RESET}"
	fi
	printf "${C_ID}%4s${C_RESET} ${pri_str}%s${list_str}\n" "$id" "$task"
}

# --- commands ---

cmd_add() {
	[ -z "$1" ] && die "Usage: todo add \"task text\" [-p PRI] [-l LIST]"
	task=""; pri=""; list="inbox"
	while [ $# -gt 0 ]; do
		case "$1" in
			-p) shift; pri=$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]'); shift ;;
			-l) shift; list="$1"; shift ;;
			*) [ -n "$task" ] && task="$task $1" || task="$1"; shift ;;
		esac
	done
	[ -z "$task" ] && die "No task text provided"
	sqlite3 "$DB" "INSERT OR IGNORE INTO todo_lists (name) VALUES ('$(esc "$list")');"
	new_id=$(sqlite3 "$DB" "INSERT INTO todo (task, priority, list) VALUES ('$(esc "$task")', '$(esc "$pri")', '$(esc "$list")'); SELECT last_insert_rowid();")
	printf "${C_COUNT}+${C_RESET} %s ${C_DIM}#%s${C_RESET}\n" "$task" "$new_id"
}

cmd_list() {
	filter_list=""; search=""; show_done=0; show_all=0
	while [ $# -gt 0 ]; do
		case "$1" in
			-l) shift; filter_list="$1"; shift ;;
			-a) show_all=1; shift ;;
			--done) show_done=1; shift ;;
			*) search="$1"; shift ;;
		esac
	done

	where="1=1"
	if [ "$show_all" = "0" ] && [ "$show_done" = "0" ]; then
		where="done = 0"
	elif [ "$show_done" = "1" ]; then
		where="done = 1"
	fi
	[ -n "$filter_list" ] && where="$where AND list = '$(esc "$filter_list")'"
	[ -n "$search" ] && where="$where AND task LIKE '%$(esc "$search")%'"

	order="CASE WHEN priority = '' THEN 1 ELSE 0 END, priority, id"

	result=$(sqlite3 -separator '|' "$DB" "SELECT id, task, priority, list, done, created_at FROM todo WHERE $where ORDER BY $order;")

	if [ -z "$result" ]; then
		printf "${C_DIM}No tasks found${C_RESET}\n"
		return
	fi

	printf '%s\n' "$result" | while IFS='|' read -r id task pri list done created; do
		fmt_task "$id" "$task" "$pri" "$list" "$done" "$created"
	done

	total=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 0;")
	done_count=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 1;")
	printf "\n${C_DIM}─── ${C_COUNT}%s${C_DIM} open · %s done ───${C_RESET}\n" "$total" "$done_count"
}

cmd_do() {
	[ -z "$1" ] && die "Usage: todo do ID [ID ...]"
	for id in "$@"; do
		task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id AND done = 0;")
		[ -z "$task" ] && { printf "${C_DIM}#%s not found or already done${C_RESET}\n" "$id"; continue; }
		sqlite3 "$DB" "UPDATE todo SET done = 1, done_at = datetime('now', 'localtime') WHERE id = $id;"
		printf "${C_DONE}${C_RESET} ${C_DIM}#%s${C_RESET} %s\n" "$id" "$task"
	done
}

cmd_undo() {
	[ -z "$1" ] && die "Usage: todo undo ID"
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $1 AND done = 1;")
	[ -z "$task" ] && die "#$1 not found or not done"
	sqlite3 "$DB" "UPDATE todo SET done = 0, done_at = '' WHERE id = $1;"
	printf "${C_COUNT}${C_RESET} ${C_DIM}#%s${C_RESET} %s\n" "$1" "$task"
}

cmd_rm() {
	[ -z "$1" ] && die "Usage: todo rm ID [ID ...]"
	for id in "$@"; do
		task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
		[ -z "$task" ] && { printf "${C_DIM}#%s not found${C_RESET}\n" "$id"; continue; }
		sqlite3 "$DB" "DELETE FROM todo WHERE id = $id;"
		printf "${C_DIM}× #%s %s${C_RESET}\n" "$id" "$task"
	done
}

cmd_pri() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo pri ID PRIORITY"
	id="$1"
	pri=$(printf '%s' "$2" | tr '[:lower:]' '[:upper:]')
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
	[ -z "$task" ] && die "#$id not found"
	sqlite3 "$DB" "UPDATE todo SET priority = '$(esc "$pri")' WHERE id = $id;"
	printf "$(pri_color "$pri")(%s)${C_RESET} ${C_DIM}#%s${C_RESET} %s\n" "$pri" "$id" "$task"
}

cmd_depri() {
	[ -z "$1" ] && die "Usage: todo depri ID"
	sqlite3 "$DB" "UPDATE todo SET priority = '' WHERE id = $1;"
	printf "${C_DIM}Removed priority from #%s${C_RESET}\n" "$1"
}

cmd_edit() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo edit ID \"new task text\""
	id="$1"; shift
	new_task="$*"
	sqlite3 "$DB" "UPDATE todo SET task = '$(esc "$new_task")' WHERE id = $id;"
	printf "${C_DIM}Edited #%s →${C_RESET} %s\n" "$id" "$new_task"
}

cmd_mv() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo mv ID LIST"
	id="$1"; list="$2"
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
	[ -z "$task" ] && die "#$id not found"
	sqlite3 "$DB" "INSERT OR IGNORE INTO todo_lists (name) VALUES ('$(esc "$list")'); UPDATE todo SET list = '$(esc "$list")' WHERE id = $id;"
	printf "${C_DIM}#%s →${C_RESET} ${C_LIST}@%s${C_RESET} %s\n" "$id" "$list" "$task"
}

cmd_lists() {
	printf "${C_HEADER}Lists${C_RESET}\n"
	sqlite3 -separator '|' "$DB" "
		SELECT l.name,
			COALESCE(SUM(CASE WHEN t.done = 0 THEN 1 ELSE 0 END), 0) as open,
			COALESCE(SUM(CASE WHEN t.done = 1 THEN 1 ELSE 0 END), 0) as done
		FROM todo_lists l
		LEFT JOIN todo t ON t.list = l.name
		GROUP BY l.name ORDER BY open DESC, l.name;
	" | while IFS='|' read -r list open done_n; do
		printf "  ${C_LIST}@%-15s${C_RESET} ${C_COUNT}%3s${C_RESET} open  ${C_DIM}%3s done${C_RESET}\n" "$list" "$open" "$done_n"
	done
}

cmd_mklist() {
	[ -z "$1" ] && die "Usage: todo mklist NAME"
	name="$1"
	exists=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$name")';")
	[ "$exists" != "0" ] && die "@$name already exists"
	sqlite3 "$DB" "INSERT INTO todo_lists (name) VALUES ('$(esc "$name")');"
	printf "${C_COUNT}+${C_RESET} ${C_LIST}@%s${C_RESET} created\n" "$name"
}

cmd_rmlist() {
	[ -z "$1" ] && die "Usage: todo rmlist NAME"
	name="$1"
	[ "$name" = "inbox" ] && die "Cannot delete @inbox"
	exists=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$name")';")
	[ "$exists" = "0" ] && die "@$name not found"
	count=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE list = '$(esc "$name")' AND done = 0;")
	if [ "$count" != "0" ]; then
		printf "${C_DIM}@%s has %s open tasks. Move them to inbox? [y/N]${C_RESET} " "$name" "$count"
		read -r ans
		case "$ans" in
			y|Y) sqlite3 "$DB" "UPDATE todo SET list = 'inbox' WHERE list = '$(esc "$name")';" ;;
			*) die "Aborted" ;;
		esac
	fi
	sqlite3 "$DB" "DELETE FROM todo WHERE list = '$(esc "$name")'; DELETE FROM todo_lists WHERE name = '$(esc "$name")';"
	printf "${C_DIM}× @%s deleted${C_RESET}\n" "$name"
}

cmd_renlist() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo renlist OLD NEW"
	old="$1"; new="$2"
	[ "$old" = "inbox" ] && die "Cannot rename @inbox"
	exists=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$old")';")
	[ "$exists" = "0" ] && die "@$old not found"
	taken=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$new")';")
	[ "$taken" != "0" ] && die "@$new already exists"
	sqlite3 "$DB" "
		UPDATE todo_lists SET name = '$(esc "$new")' WHERE name = '$(esc "$old")';
		UPDATE todo SET list = '$(esc "$new")' WHERE list = '$(esc "$old")';
	"
	printf "${C_LIST}@%s${C_RESET} ${C_DIM}${C_RESET} ${C_LIST}@%s${C_RESET}\n" "$old" "$new"
}

cmd_search() {
	[ -z "$1" ] && die "Usage: todo search TERM"
	cmd_list "$1"
}

cmd_archive() {
	n=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 1;")
	[ "$n" = "0" ] && { printf "${C_DIM}Nothing to archive${C_RESET}\n"; return; }
	sqlite3 "$DB" "DELETE FROM todo WHERE done = 1;"
	printf "${C_DIM}Archived %s completed tasks${C_RESET}\n" "$n"
}

cmd_count() {
	open=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 0;")
	done_n=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 1;")
	total=$((open + done_n))
	printf "${C_COUNT}%s${C_RESET} open  ${C_DIM}%s done  %s total${C_RESET}\n" "$open" "$done_n" "$total"
}

# --- dmenu modes ---

cmd_dmenu_add() {
	# pick list
	lists=$(sqlite3 "$DB" "SELECT name FROM todo_lists ORDER BY name;")
	[ -z "$lists" ] && lists="inbox"
	list=$(printf '%s\nNew list...' "$lists" | dmenu -i -l 10 -p "List:" || exit 0)
	[ -z "$list" ] && exit 0

	if [ "$list" = "New list..." ]; then
		list=$(printf '' | dmenu -l 1 -p "List name:" || exit 0)
		[ -z "$list" ] && exit 0
		sqlite3 "$DB" "INSERT OR IGNORE INTO todo_lists (name) VALUES ('$(esc "$list")');"
	fi

	# pick priority
	pri=$(printf 'None\nA\nB\nC\nD' | dmenu -i -l 5 -p "Priority:" || exit 0)
	[ -z "$pri" ] && exit 0
	[ "$pri" = "None" ] && pri=""

	# task text
	task=$(printf '' | dmenu -c -l 1 -h 40 -p "Todo:" || exit 0)
	[ -z "$task" ] && exit 0

	sqlite3 "$DB" "INSERT INTO todo (task, priority, list) VALUES ('$(esc "$task")', '$(esc "$pri")', '$(esc "$list")');"
	notify-send "Todo added" "$task → @$list"
}

cmd_dmenu_view() {
	# pick a list
	lists=$(sqlite3 "$DB" "SELECT list || ' (' || SUM(CASE WHEN done=0 THEN 1 ELSE 0 END) || ')' FROM todo GROUP BY list ORDER BY list;")
	[ -z "$lists" ] && { notify-send "Todo" "No lists found"; exit 0; }

	selected=$(printf '%s' "$lists" | dmenu -i -l 15 -p "View list:" || exit 0)
	[ -z "$selected" ] && exit 0

	list=$(printf '%s' "$selected" | sed 's/ ([0-9]*)$//')

	tasks=$(sqlite3 -separator '|' "$DB" "SELECT priority, task FROM todo WHERE list = '$(esc "$list")' AND done = 0 ORDER BY CASE WHEN priority = '' THEN 1 ELSE 0 END, priority, id;")

	if [ -z "$tasks" ]; then
		notify-send "@$list" "No open tasks"
		exit 0
	fi

	# format for dunst
	body=""
	n=0
	printf '%s\n' "$tasks" | while IFS='|' read -r pri task; do
		n=$((n + 1))
		if [ -n "$pri" ]; then
			line="($pri) $task"
		else
			line="    $task"
		fi
		if [ -n "$body" ]; then
			body="$body
$line"
		else
			body="$line"
		fi
		# dunst gets it all at the end
		printf '%s\n' "$line"
	done | {
		body=$(cat)
		count=$(printf '%s\n' "$body" | wc -l)
		notify-send -t 10000 "@$list  [$count]" "$body"
	}
}

cmd_dmenu_do() {
	tasks=$(sqlite3 -separator ' | ' "$DB" "SELECT id, CASE WHEN priority != '' THEN '(' || priority || ') ' ELSE '' END || task || ' @' || list FROM todo WHERE done = 0 ORDER BY CASE WHEN priority = '' THEN 1 ELSE 0 END, priority, id;")
	[ -z "$tasks" ] && { notify-send "Todo" "No open tasks"; exit 0; }

	selected=$(printf '%s' "$tasks" | dmenu -i -l 20 -p "Complete:" || exit 0)
	[ -z "$selected" ] && exit 0

	id=$(printf '%s' "$selected" | cut -d'|' -f1 | tr -d ' ')
	sqlite3 "$DB" "UPDATE todo SET done = 1, done_at = datetime('now', 'localtime') WHERE id = $id;"
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
	notify-send "Done" "$task"
}

# --- main ---

cmd_help() {
	printf '%b' "\
${C_BOLD}todo${C_RESET} - terminal todo list

${C_HEADER}Tasks${C_RESET}
  todo add \"task\" [-p PRI] [-l LIST]    Add a task
  todo ls [-l LIST] [-a] [--done] [Q]   List tasks
  todo do ID [ID ...]                   Mark task(s) done
  todo undo ID                          Reopen a task
  todo rm ID [ID ...]                   Delete task(s)
  todo pri ID A-Z                       Set priority
  todo depri ID                         Remove priority
  todo edit ID \"new text\"               Edit task text
  todo mv ID LIST                       Move task to a list
  todo search TERM                      Search tasks
  todo archive                          Purge completed tasks
  todo count                            Task counts

${C_HEADER}Lists${C_RESET}
  todo lists                            Show all lists
  todo mklist NAME                      Create a new list
  todo rmlist NAME                      Delete a list
  todo renlist OLD NEW                  Rename a list

${C_HEADER}Dmenu${C_RESET}
  todo -d                               Dmenu add mode
  todo -D                               Dmenu view list → dunst
  todo -x                               Dmenu complete mode

${C_HEADER}Notes${C_RESET}
  Default list is ${C_LIST}@inbox${C_RESET}. Use ${C_DIM}-l${C_RESET} to filter or add to a list.
  Priorities: ${C_PRI_A}(A)${C_RESET} ${C_PRI_B}(B)${C_RESET} ${C_PRI_C}(C)${C_RESET} highest → lowest.
"
}

case "$1" in
	add|a)       shift; cmd_add "$@" ;;
	ls|list)     shift; cmd_list "$@" ;;
	do|done)     shift; cmd_do "$@" ;;
	undo)        shift; cmd_undo "$@" ;;
	rm|del)      shift; cmd_rm "$@" ;;
	pri|p)       shift; cmd_pri "$@" ;;
	depri|dp)    shift; cmd_depri "$@" ;;
	edit)        shift; cmd_edit "$@" ;;
	mv|move)     shift; cmd_mv "$@" ;;
	lists)       cmd_lists ;;
	mklist)      shift; cmd_mklist "$@" ;;
	rmlist)      shift; cmd_rmlist "$@" ;;
	renlist)     shift; cmd_renlist "$@" ;;
	search|s)    shift; cmd_search "$@" ;;
	archive)     cmd_archive ;;
	count)       cmd_count ;;
	-d|--dmenu)  cmd_dmenu_add ;;
	-D|--dview)  cmd_dmenu_view ;;
	-x|--ddo)    cmd_dmenu_do ;;
	-h|--help|help) cmd_help ;;
	"")          cmd_list ;;
	*)           printf "Unknown command: %s (try todo -h)\n" "$1" ;;
esac