#!/bin/sh # Part of sh-todo: # https://github.com/asb/sh-todo set -e . ~/.sh-todo SCRIPTNAME=$(basename $0) # Return 0 if $2 contains all tags in $1 and 1 otherwise. Returns 0 in the # case that $1 is empty. match_tags() { WANTEDTAGS=$1 LINETAGS="$2 " for TAG in $WANTEDTAGS; do if [ "${LINETAGS#*$TAG }" = "$LINETAGS" ]; then return 1 fi done return 0 } output_line() { LINE=$1 if [ $TODO_RAW ]; then printf "%s\n" "$LINE" elif [ "${LINE#DONE }" != "$LINE" ]; then printf "• $BEGIN_DONE%s$END_DONE\n" "${LINE#DONE }" else printf "• %s\n" "${LINE#TODO }" fi } parse_linetags() { LINETAGS="${1#*\[}" if [ "$LINETAGS" != "$LINE" ]; then LINETAGS="${LINETAGS%]*}" else LINETAGS="" fi printf "%s" "$LINETAGS" } # If given a todo, then add that to the list of tasks. Otherwise, print all # todo items which have the given tags do_todo() { [ -e "$TODO_F" ] || touch "$TODO_F" if [ -z "$INPUT" ]; then # No todo given, so print current tasks with matching tags while read -r LINE; do if match_tags "$ARGTAGS" "$(parse_linetags "$LINE")"; then output_line "$LINE" fi done < "$TODO_F" else # Must be adding a new todo printf "TODO %s%s\n" "$INPUT" "${ARGTAGS:+ [$ARGTAGS]}" >> "$TODO_F" fi exit 0 } do_todo_edit() { [ -n "$SAVEFILE" ] && > "$TODO_F.tmp" while read -r LINE; do if [ "${LINE#TODO *$INPUT}" != "$LINE" ] && match_tags "$ARGTAGS" "$(parse_linetags "$LINE")"; then LINETAGS=$(parse_linetags "$LINE") LINEPREFIX=${LINE% \[$LINETAGS\]} LINETAGS="$LINETAGS " # First apply REMTAGS for TAG in $REMTAGS; do if [ "${LINETAGS#*$TAG }" != "$LINETAGS" ]; then SFX="${LINETAGS#*$TAG }" LINETAGS="${LINETAGS%$TAG *}$SFX" fi done # Now apply ADDTAGS for TAG in $ADDTAGS; do if [ "${LINETAGS#*$TAG }" = "$LINETAGS" ]; then LINETAGS="$LINETAGS$TAG " fi done LINETAGS="${LINETAGS% }" LINE=$(printf "%s%s" "$LINEPREFIX" "${LINETAGS:+ [$LINETAGS]}") output_line "$LINE" fi [ -n "$SAVEFILE" ] && printf "%s\n" "$LINE" >> "$TODO_F.tmp" done < "$TODO_F" if [ -n "$SAVEFILE" ]; then cp "$TODO_F" "$TODO_F.old" mv "$TODO_F.tmp" "$TODO_F" fi exit 0 } # Enter an environment where all todo-* commands will already include the # given tasks. Useful for focusing on a certain subset of tasks. This relies # on bash, and is not portable do_todo_focus() { if [ -z "$ARGTAGS" ]; then printf "Error: must specify at least one tag to enter todo-focus\n" >&2 exit 1 fi TMPFILE=$(mktemp) TAGS="$*" cat < $TMPFILE . /etc/bash.bashrc . ~/.bashrc alias todo="todo $ARGTAGS" alias todone="todone $ARGTAGS" alias todone-archive="todone-archive $ARGTAGS" alias todone-view="todone-view $ARGTAGS" export PS1="todo-focus $ARGTAGS \$PS1" EOF bash --rcfile $TMPFILE -i rm $TMPFILE } # Mark any tasks matching the given pattern as done. Tags can also be # specified to limit the scope of the match do_todone() { if [ -z "$INPUT" ]; then printf "Error: no todo to mark done specified\n" >&2 exit 1 fi > "$TODO_F.tmp" HAD_MATCH=0 while read -r LINE; do if [ "${LINE#TODO *$INPUT}" != "$LINE" ] && match_tags "$ARGTAGS" "$(parse_linetags "$LINE")"; then HAD_MATCH=1 TASK="${LINE#TODO }" LINE=$(printf "DONE %s ($(date "+%Y-%m-%d %R"))" "$TASK") output_line "$LINE" printf "%s\n" "$LINE" >> "$TODONE_F" fi printf "%s\n" "$LINE" >> "$TODO_F.tmp" done < "$TODO_F" if [ $HAD_MATCH -eq 0 ]; then printf "Error: no tasks matched the given pattern\n" >&2 rm "$TODO_F.tmp" exit 1 fi mv "$TODO_F.tmp" "$TODO_F" } # Remove all DONE lines with the given tags from $TODO_F. All such lines will # already have been added to $TODONE_F do_todone_archive() { > "$TODO_F.tmp" while read -r LINE; do if [ "${LINE#DONE }" != "$LINE" ] && match_tags "$ARGTAGS" "$(parse_linetags "$LINE")"; then output_line "$LINE" else printf "%s\n" "$LINE" >> "$TODO_F.tmp" fi; done < "$TODO_F" mv "$TODO_F.tmp" "$TODO_F" } # Show a formatted list of completed tasks (can be limited by specifying # desired tags). # This function uses tac which is non-POSIX, as is the -d argument to date do_todone_view() { TAC=tac #TAC="tail -r" # For OSX/BSD $TAC "$TODONE_F" | while read -r LINE; do if ! match_tags "$ARGTAGS" "$(parse_linetags "$LINE")"; then continue; fi TRIMLINE=${LINE%(*)} # strip the timestamp TIMESTAMP=${LINE#"$TRIMLINE("} TIMESTAMP=${TIMESTAMP%)} DATE=${TIMESTAMP%% [0-9][0-9]:[0-9][0-9]} TIME=${TIMESTAMP#"$DATE "} if [ "$DATE" != "$LASTDATE" ]; then printf "\n$(date -d"$DATE" "+%A %d %B %Y")\n\n" fi printf "• %s($TIME)\n" "${TRIMLINE#DONE }" LASTDATE=$DATE done | less } # Parse arguments ARGTAGS="" INPUT="" for ARG in "$@"; do if [ "$SCRIPTNAME" = "todo-edit" ] && [ "$ARG" = "--save" ]; then SAVEFILE=1 elif [ "${ARG#@}" != "$ARG" ] ; then ARGTST="$ARGTAGS " if [ "${ARGTST#*$ARG }" = "$ARGTST" ]; then ARGTAGS="${ARGTAGS:+$ARGTAGS }$ARG" fi elif [ "$SCRIPTNAME" = "todo-edit" ] && [ "${ARG#-@}" != "$ARG" ]; then REMTAGS="${REMTAGS:+$REMTAGS }${ARG#-}" elif [ "$SCRIPTNAME" = "todo-edit" ] && [ "${ARG#+@}" != "$ARG" ]; then ADDTAGS="${ADDTAGS:+$ADDTAGS }${ARG#+}" else INPUT="${INPUT:+$INPUT }$ARG" fi done if [ "$SCRIPTNAME" = "todo" ]; then do_todo elif [ "$SCRIPTNAME" = "todo-edit" ]; then do_todo_edit elif [ "$SCRIPTNAME" = "todo-focus" ]; then do_todo_focus elif [ "$SCRIPTNAME" = "todone" ]; then do_todone elif [ "$SCRIPTNAME" = "todone-archive" ]; then do_todone_archive elif [ "$SCRIPTNAME" = "todone-view" ]; then do_todone_view else printf "Error: this is a multicall script, but was called with unrecognised script name '%s'\n" "$SCRIPTNAME" >&2 exit 1 fi