Blob


1 #! /bin/sh
3 # Copyright (c) 2015-2024 Paul W. Rankin <rnkn@rnkn.xyz>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <https://www.gnu.org/licenses/>.
18 usage() {
19 cat <<EOF
20 usage:
21 t [-aDehn]
22 t [-T] STRING
23 t [-aD] [-s REGEX_STRING] [-d [INTEGER|REGEX_STRING]]
24 t [-aD] [-s REGEX_STRING] [-k [INTEGER|REGEX_STRING]]
25 t [-aD] [-s REGEX_STRING] [-b [INTEGER|REGEX_STRING]]
26 t [-aD] [-s REGEX_STRING] [-z [INTEGER|REGEX_STRING]]
28 examples:
29 t print incomplete todos
30 t -a print all todos
31 t -D print all done todos
32 t -s call print all todos matching "call"
33 t -s "call|email" print all todos matching "call" or "email"
34 t -D -s read print all done todos matching "read"
35 t -d 12 mark todo item 12 as done
36 t -s read -d 3 mark todo item 3 within todos matching "read" as done
37 t -d burn mark all todos matching "burn" as done
38 t -s burn -d . same as above
39 t -k 7 delete todo item 7
40 t -k bunnies delete all todos matching "bunnies"
41 t -s bunnies -k . same as above
42 t -e edit TODO_FILE in $EDITOR
43 t -T sell horse add todo "sell horse" due today
44 t -n print unnumbered output (suitable for redirection)
45 EOF
46 exit 1
47 }
49 re_todo_file='[Tt][Oo][Dd][Oo].*'
50 re_todo='^- \[ ] '
51 re_done='^- \[[xX]] '
52 re_either='^- \[[ xX]] '
53 re_date='[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
55 for f in *; do
56 expr "$f" : "$re_todo_file" > /dev/null && todo_file="$f" && break
57 done
59 if [ ! "$todo_file" ]; then
60 if [ -r "$TODO_FILE" ]; then
61 todo_file="$TODO_FILE"
62 else
63 echo 'No todo file found'
64 exit 1
65 fi
66 fi
68 # t_read(query)
69 # returns: sorted list of matching todos
70 t_read() {
71 query="$*"
72 casematch=
73 expr "$query" : '\(.*[A-Z].*\)' > /dev/null || casematch='-i'
74 todo_list=$(grep $wholeword $casematch "$re_prefix.*$*" "$todo_file")
76 if [ -n "$todo_list" ]; then
77 due_list=$(echo "$todo_list" | grep "$re_date")
78 todo_list=$(echo "$todo_list" | grep -v "$re_date")
79 due_list=$(echo "$due_list" | sed -E "s/.*($re_date).*/\1&/" |
80 sort -n | sed -E "s/^$re_date//")
82 # printf '%s\n%s\n' "$due_list" "$todo_list"
83 if [ -n "$due_list" ] && [ -n "$todo_list" ]; then
84 printf '%s\n%s\n' "$due_list" "$todo_list"
85 elif [ -n "$due_list" ]; then
86 printf '%s\n' "$due_list"
87 else
88 printf '%s\n' "$todo_list"
89 fi
90 fi
91 }
93 # t_print(prefix)
94 # returns: todo list printed to stdout
95 t_print() {
96 input=$(cat)
97 if [ -n "$input" ]; then
98 n=1
99 n_width=$(echo "$input" | wc -l | xargs | wc -c)
101 echo "$input" | while read -r todo; do
102 date=$(expr "$todo" : ".*\($re_date\)" | sed 's/-//g')
103 today=
105 if [ -n "$date" ] && [ -z "$onlydone$showall" ]; then
106 today=$(date +%Y%m%d)
107 if [ "$today" -ge "$date" ]; then
108 todo=$(echo "$todo" |
109 sed -E "s/($re_prefix)(.*)/\1** \2 **/")
110 fi
111 fi
113 if [ -n "$export" ]; then
114 printf "%s\n" "${todo}"
115 else
116 printf "%${n_width}s %s\n" "$n" "${todo#- }"
117 fi
118 n=$(( n + 1 ))
119 done
120 fi
123 # t_select(number|regex)
124 # returns: selected todos
125 t_select() {
126 if expr "$1" : ^[0-9]*$ > /dev/null; then
127 sed -n "$1p"
128 else
129 casematch=
130 expr "$1" : '.*[A-Z].*' > /dev/null || casematch='-i'
131 grep $casematch "$*"
132 fi
135 # t_done(number|regex)
136 # returns: altered todo_file
137 t_done() {
138 t_select "$1" |
139 while read -r todo; do
140 tmp=$(mktemp)
141 awk -v str="$todo" \
142 '$0 == str { gsub (/- \[ ]/, "- [X]") } { print }' \
143 "$todo_file" > "$tmp"
144 mv "$tmp" "$todo_file"
145 done
148 # t_kill()
149 t_kill() {
150 t_select "$1" |
151 while read -r todo; do
152 tmp=$(mktemp)
153 awk -v str="$todo" '$0 != str' "$todo_file" > "$tmp"
154 mv "$tmp" "$todo_file"
155 done
158 t_toggle() {
159 t_select "$1" |
160 while read -r todo; do
161 tmp=$(mktemp)
162 check=
163 expr "$todo" : "$re_done" > /dev/null &&
164 check='- [ ]' || check='- [X]'
165 awk -v str="$todo" -v check="$check" \
166 '$0 == str { gsub (/- \[[ xX]]/, check) } { print }' \
167 "$todo_file" > "$tmp"
168 mv "$tmp" "$todo_file"
169 done
172 t_openurl() {
173 t_select "$1" | grep -Eo "https?://[^ ]+" | xargs open
176 while getopts :ab:Dd:ehk:nS:s:Tz: opt; do
177 case $opt in
178 (h) usage ;;
179 (a) showall=0 ;;
180 (b) openurl=$OPTARG ;;
181 (D) onlydone=0 ;;
182 (d) markdone=$OPTARG ;;
183 (e) ${EDITOR:-vi} "$todo_file"; exit 0 ;;
184 (k) kill=$OPTARG ;;
185 (n) export=0 ;;
186 (S) query=$OPTARG; wholeword=-w ;;
187 (s) query=$OPTARG ;;
188 (T) due=" $(date +%F)" ;;
189 (z) toggle=$OPTARG ;;
190 (:) printf "t: option -%s requires an argument\n" "$OPTARG"; exit 2 ;;
191 (*) printf "t: unrecognized option -%s\n\n" "$OPTARG"; usage ;;
192 esac
193 done
195 shift "$(( OPTIND - 1 ))"
197 if [ -n "$onlydone" ]; then
198 re_prefix="$re_done"
199 elif [ -n "$showall" ]; then
200 re_prefix="$re_either"
201 else
202 re_prefix="$re_todo"
203 fi
205 if [ -n "$markdone" ]; then t_read "$query" | t_done "$markdone"
206 elif [ -n "$toggle" ]; then t_read "$query" | t_toggle "$toggle"
207 elif [ -n "$kill" ]; then t_read "$query" | t_kill "$kill"
208 elif [ -n "$openurl" ]; then t_read "$query" | t_openurl "$openurl"
209 elif [ -n "$query" ]; then t_read "$query" | t_print "$re_prefix"
210 elif [ -n "$*" ]; then
211 echo "- [ ] $*${due}" >> "$todo_file"
212 else t_read | t_print "$re_prefix"
213 fi