#!/bin/bash trap "exit 1" TERM export MYPID=$$ # ANSI stuff BOLD="\033[1m" ITALIC="\033[3m" STRIKE="\033[9m" PLAIN="\033[0m" UNDERLINE="\033[4m" RED="\033[31m" MAGENTA="\033[35m" GREEN="\033[32m" YELLOW="\033[33m" BLUE="\033[34m" CYAN="\033[36m" GREY="\033[2;37m" LINK="$BLUE$UNDERLINE" brack="$CYAN" HILITE="\033[43;1m" TOPTITLE="\033[44;1m" LEFTTITLE="\033[31;1m" function die() { kill -s TERM $MYPID } function getalts() { local verb alt var x verb=$1 alt="" var="words_$verb" IFS='|' for x in ${!var}; do if [[ $x != $verb ]]; then [[ -z $alt ]] && alt=$x || alt="$alt,$x" fi done IFS=' ' echo "$alt" } function usage() { local titleformat format local usage_var desc_var format="%22s %-40s %s\n" echo "usage: $0 COMMAND [commandopts]" echo printf "$format" "COMMAND" "DESCRIPTION" "SYNONYMS" for x in $valid_modes; do usage_var="usage_$x" desc_var="desc_$x" printf "$format" "${!usage_var}" "${!desc_var}" "$(getalts $x)" done } function inc() { indent=$((indent + $indentamt)) } function dec() { indent=$((indent - $indentamt)) } function getuid_witherror() { # [noun] local x rv noun if [[ $$ -ge 2 ]]; then noun=$2 else noun="task" fi x=$(getuid $1) rv=$? if [[ $rv -ne 0 ]]; then error "Specified $noun #$1 does not exist." die fi echo "$x" return $rv } function getuid() { local res id uid id=$1 res="" for uid in ${!idmap[@]}; do if [[ ${idmap[$uid]} == $id ]]; then res=${uid} break fi done echo $res if [[ -z $res ]]; then return 1 fi return 0 } function getid() { local res uid uid=$1 res=${idmap[$uid]} if [[ -z $res ]]; then return 1 fi echo $res return 0 } function getorgenid() { local res uid uid=$1 res=$(getid $uid) if [[ -z $res ]]; then res=$nextid idmap[$uid]=$nextid nextid=$((nextid + 1)) needsave=1 fi return $res } function loadids() { local LINE n uid id max if [[ ! -e $idfile ]]; then return 1 fi max=0 while read LINE; do uid=${LINE%:*} id=${LINE#*:} idmap[$uid]=$id [[ $id -gt $max ]] && max=$id done < "$idfile" nextid=$((max + 1)) return 0 } function saveids() { local uid cp /dev/null $idfile for uid in ${!idmap[@]}; do echo "$uid:${idmap[$uid]}" >> $idfile done } function dumpids() { local uid for uid in ${!idmap[@]}; do echo "$uid --> ${idmap[$uid]}" done } function loadtasks() { local f ess maxid=-1 ntasks=0 dblog "Loading tasks..." for f in $dir/*.vcf; do loadtask $f ntasks=$((ntasks + 1)) done numwid=${#maxid} [[ $ntasks -eq 1 ]] && ess="" || ess="s" dblog "Finished loading $ntasks task$ess." } function dblog() { [[ $DEBUG -eq 1 ]] && echo "$(date) $*" } function action() { echo -e "$BOLD$YELLOW* $PLAIN$YELLOW$*$PLAIN" } function error() { echo -e "$BOLD${RED}ERROR: $PLAIN$RED$*$PLAIN" >/dev/stderr } function info() { echo -e "$BOLD${CYAN}>> $PLAIN$CYAN$*$PLAIN" } function confirm() { local yn echo -en "$BOLD${MAGENTA}CONFIRM: $PLAIN$MAGENTA$* (y/N)? $PLAIN" read yn if [[ $yn == "y" ]]; then return 0 fi return 1 } function loadtask() { local uid sum checked desc file id parent res i file="$1" res=$(cat $file | awk -F: 'BEGIN { checked=0; } /^UID:/ { printf("uid©%s©",$2); } /^SUMMARY:/ { sub("SUMMARY:",""); printf("sum©%s©",$0); } /^DESCRIPTION:/ { sub("DESCRIPTION:",""); printf("desc©%s©",$0); } /^RELATED-TO/ { sub("^RELATED-TO.*:",""); printf("parent©%s©",$0); } /^STATUS:COMPLETED/ { checked=1; } END { printf("checked©%d\n",checked); }') IFS='©' read -ra tok <<< "$res" parent="" i=0 while [[ ! -z ${tok[$i]} ]]; do if [[ ${tok[$i]} == "uid" ]]; then uid=${tok[$((i + 1))]} getorgenid $uid id=$? elif [[ ${tok[$i]} == "sum" ]]; then sum="${tok[$((i + 1))]}" elif [[ ${tok[$i]} == "desc" ]]; then desc="${tok[$((i + 1))]}" elif [[ ${tok[$i]} == "parent" ]]; then parent="${tok[$((i + 1))]}" elif [[ ${tok[$i]} == "checked" ]]; then checked="${tok[$((i + 1))]}" fi i=$((i + 2)) done IFS=' ' taskuid[$uid]=$uid taskid[$uid]=$id tasksum[$uid]=$sum taskdesc[$uid]=$desc taskchecked[$uid]=$checked taskparent[$uid]=$parent [[ $id -gt $maxid ]] && maxid=$id [[ ${#sum} -gt $taskwid ]] && taskwid=${#sum} [[ ${#desc} -gt $taskwid ]] && taskwid=${#desc} } function marknotdone() { local uid rv file sedrv local children childuid ischild noun uid=$1 [[ $# -gt 1 ]] && ischild=1 || ischild=0 [[ $ischild -eq 1 ]] && noun=subtask || noun=task # already not done? if [[ ${taskchecked[$uid]} -eq 0 ]]; then error "$noun #${taskid[$uid]} is already unfinished" return 1 fi file=$dir/$uid.vcf sed -i '/^COMPLETED:/d;/^STATUS:COMPLETED/d;/PERCENT-COMPLETE:100/d' $file sedrv=$((sedrv + $?)) set_lastmodified $uid sedrv=$((sedrv + $?)) if [[ $sedrv -eq 0 ]]; then action "Marked $noun #${taskid[$uid]} as completed." processed="$processed ${uid}" loadtask $file rv=0 children=$(getchildren $uid) for childuid in ${children}; do marknotdone $childuid noprint rv=$((rv + $?)) done else error "failed to mark $noun #${taskid[$uid]} as unfinished" rv=1 fi return $rv } function delq_clear() { deluids="" } function delq_add() { local x if [[ ! $deluids == *\ $1\ * ]]; then deluids="$deluids $1" for x in $(getchildren $1); do delq_add $x done fi } function delq_process() { local uidlist uid rv yn file count ess wantshow rv=0 # Remove duplicates deluids=$(echo $deluids | tr ' ' '\n' | sort -u | tr '\n' ' ') info "The following tasks will be removed:" needtitle=0 for uid in $deluids; do wantshow=0 # don't show if it's a subtask of one already in the list if [[ -z ${taskparent[$uid]} ]]; then wantshow=1 elif [[ $deluids == *${taskparent[$uid]}\ * ]]; then wantshow=0 else wantshow=1 fi if [[ $wantshow -eq 1 ]]; then showit $uid | sed -e 's/^/ /' fi done confirm "Really remove these tasks" if [[ $? -eq 0 ]]; then count=0 for uid in $deluids; do file=$dir/$uid.vcf rm -f $file unset idmap[$uid] count=$((count + 1)) done saveids [[ $count -eq 1 ]] && ess="" || ess="s" action "$count task$ess removed." else action "Aborted." rv=1 fi return $rv } function set_lastmodified() { local uid file now uid=$1 file=$dir/$uid.vcf now=$(date -u +'%Y%m%dT%H%M%SZ') sed -i "s/^LAST-MODIFIED:.*/LAST-MODIFIED:$now/" $file return $? } function move_in() { # move_in uid local uid rv parent parent2 sedrv uid=$1 rv=0 parent=${taskparent[$uid]} if [[ -z $parent ]]; then error "Can't shift task #${taskid[$uid]} any further left." fi parent2=${taskparent[$parent]} taskparent[$uid]="$parent2" file=$dir/$uid.vcf sed -i "s/^RELATED-TO.*/RELATED-TO;RELTYPE=PARENT:${taskparent[$uid]}/" $file sedrv=$? set_lastmodified $uid sedrv=$((sedrv + $?)) if [[ $sedrv -eq 0 ]]; then action "Shifted task #${taskid[$uid]} inwards/left." loadtask $file showit ${taskparent[$uid]} $uid rv=0 else error "Failed to shift task #${taskid[$uid]} left." rv=1 fi return $rv } function move_out() { # move_out uid new_parent local uid rv newparent sedrv uid=$1 newparent=$2 rv=0 taskparent[$uid]="$newparent" file=$dir/$uid.vcf sed -i "s/^RELATED-TO.*/RELATED-TO;RELTYPE=PARENT:${taskparent[$uid]}/" $file sedrv=$? set_lastmodified $uid sedrv=$((sedrv + $?)) if [[ $sedrv -eq 0 ]]; then action "Shifted task #${taskid[$uid]} underneath #${taskid[${taskparent[$uid]}]} ('${tasksum[${taskparent[$uid]}]}')." loadtask $file showit ${taskparent[$uid]} $uid rv=0 else error "Failed to shift task #${taskid[$uid]} under #${taskparent[$uid]}." rv=1 fi return $rv } function setnote() { # setnote uid new note goes here local uid newnote sedrv rv file str uid=$1 rv=0 shift newnote="$*" file=$dir/$uid.vcf if grep -q ^DESCRIPTION: $file; then sed -i "s/^DESCRIPTION:.*/DESCRIPTION:$newnote/" $file sedrv=$? else str="END:VTODO" toadd="DESCRIPTION:$newnote" sed -i "/$str/i $toadd\n" $file sedrv=$? fi set_lastmodified $uid sedrv=$((sedrv + $?)) if [[ $sedrv -eq 0 ]]; then action "Set note of task #${taskid[$uid]} to '$newnote'." loadtask $file rv=0 else error "Failed to set note of task #${taskid[$uid]}." rv=1 fi return $rv } function rename() { # rename uid new name goes here local uid newname sedrv rv file uid=$1 rv=0 shift newname="$*" file=$dir/$uid.vcf sed -i "s/^SUMMARY:.*/SUMMARY:$newname/" $file sedrv=$? set_lastmodified $uid sedrv=$((sedrv + $?)) if [[ $sedrv -eq 0 ]]; then action "Renamed task #${taskid[$uid]} to '$newname'." loadtask $file rv=0 else error "failed to rename task #${taskid[$uid]}." rv=1 fi return $rv } function markdone() { local uid sedrv rv file toadd1 toadd2 toadd3 str now local children childuid ischild noun uid=$1 [[ $# -gt 1 ]] && ischild=1 || ischild=0 [[ $ischild -eq 1 ]] && noun=subtask || noun=task # already conplete? if [[ ${taskchecked[$uid]} -eq 1 ]]; then error "$noun #${taskid[$uid]} is already completed" return 1 fi str="END:VTODO" now=$(date -u +'%Y%m%dT%H%M%SZ') toadd1="STATUS:COMPLETED" toadd2="PERCENT-COMPLETE:100" toadd3="COMPLETED:$now" file=$dir/$uid.vcf sed -i "/$str/i $toadd1\n$toadd2\n$toadd3" $file sedrv=$? set_lastmodified $uid sedrv=$((sedrv + $?)) if [[ $sedrv -eq 0 ]]; then action "Marked $noun #${taskid[$uid]} as completed." processed="$processed ${uid}" loadtask $file rv=0 children=$(getchildren $uid) for childuid in ${children}; do markdone $childuid noprint rv=$((rv + $?)) done else error "failed to mark $noun #${taskid[$uid]} as completed" rv=1 fi return $rv } function randomuid() { local dupe uid dupe=1 while [[ $dupe -eq 1 ]]; do uid=$(( $RANDOM % 4 + 1)) uid="$uid$(( $RANDOM % 5 + 1))" while [[ ${#uid} -lt 19 ]]; do uid="$uid$(( $RANDOM % 9 + 1))" done dupe=0 for x in ${!taskuid[@]}; do if [[ $x == $uid ]]; then dupe=1 break fi done done echo $uid } function addtask() { local uid parent file if [[ $1 == "-p" ]]; then parentuid=$2 shift 2 else parentuid="" fi uid=$(randomuid) idmap[$uid]=$nextid taskid[$uid]=$nextid taskuid[$uid]=$nextid tasksum[$uid]="$*" taskdesc[$uid]="" taskchecked[$uid]=0 taskparent[$uid]=$parentuid file=$dir/$uid.vcf now=$(date -u +'%Y%m%dT%H%M%SZ') cat >$file <>$file fi cat >>$file <" "Complete a task" "done|x|complete" addcmd "notdone" "notdone " "Uncomplete a task" "notdone|o|incomplete|uncomplete|clear" addcmd "add" "add [parent] " "Add a new task [as subtask of parent]" "a|add|new|create" addcmd "del" "del [id1] .. [idX]" "Delete given task(s)" "del|rm|delete" addcmd "cleanup" "cleanup" "Delete all completed tasks" "cleanup|clean|flush|dc" addcmd "rename" "rename " "Rename given task" "rename" addcmd "in" "in " "Decrease indent of given task" "in|left|h" addcmd "out" "out " "Move task below the given parent" "out|right|l|mv" addcmd "note" "note " "Change notes for given task" "note|desc|description|comment" addcmd "sync" "sync" "Sync tasks using vdirsyncer" "sync" DEBUG=0 TESTMODE=0 ARGS="hdt" while getopts "$ARGS" i; do case "$i" in h) usage; exit 1; ;; d) DEBUG=1 ;; t) DEBUG=1 TESTMODE=1 ;; *) error "invalid argument: $i"; usage; exit 1 ;; esac done shift $((OPTIND - 1)) mode="" for x in $valid_modes; do thisone="words_$x" tomatch=${!thisone} if [[ $1 =~ ^(${tomatch})$ ]]; then mode=$x shift fi done if [[ -z $mode ]]; then mode=list fi # Commands that don't need tasks loaded if [[ $mode == "sync" ]]; then res=$(${VDIRSYNCER} sync 2>&1) rv=$? if [[ $rv -eq 0 ]]; then upcount=$(echo "$res" | grep -v ^Sync | grep -c ${VDS_REMOTE}) downcount=$(echo "$res" | grep -v ^Sync | grep -c ${VDS_LOCAL}) [[ $upcount -eq 1 ]] && upess="" || upess="s" [[ $downcount -eq 1 ]] && downess="" || downess="s" if [[ $upcount -eq 0 && $downcount -eq 0 ]]; then action "Sync complete - no changes." elif [[ $upcount -eq 0 ]]; then action "Sync complete - $downcount change$downess pulled." elif [[ $downcount -eq 0 ]]; then action "Sync complete - $upcount change$upess pushed." else action "Sync complete - $upcount change$upess pushed, $downcount change$downess pulled." fi else error "sync failed. Output:" echo "$res" | sed -e 's/^/ /' fi exit $rv fi loadids loadtasks [[ $TESTMODE -eq 1 ]] && exit 1 [[ $needsave -eq 1 ]] && saveids # single-task commands if [[ $mode == "add" ]]; then if [[ $1 =~ ^[0-9]+$ ]]; then parentid=$1 parentuid=$(getuid_witherror $parentid "parent task") shift 1 else parentid="" parentuid="" fi newtasksum="$*" if [[ -z $parentuid ]]; then addtask "$newtasksum" else addtask -p $parentuid "$newtasksum" fi elif [[ $mode == "rename" ]]; then uid=$(getuid_witherror $1) shift rename $uid "$*" elif [[ $mode == "note" ]]; then uid=$(getuid_witherror $1) shift setnote $uid "$*" elif [[ $mode == "in" ]]; then uid=$(getuid_witherror $1) shift move_in $uid elif [[ $mode == "out" ]]; then uid=$(getuid_witherror $1) puid=$(getuid_witherror $2 "parent task") shift 2 move_out $uid $puid elif [[ $mode == "cleanup" ]]; then action "Deleting all completed tasks." for uid in ${!taskuid[@]}; do [[ ${taskchecked[$uid]} -eq 1 ]] && delq_add $uid done delq_process $uid # show results if [[ ! -z $processed ]]; then for uid in ${uids}; do showit $uid done fi else # multi-task commands filter=$* uids="" if [[ -z $filter ]]; then for uid in ${!taskuid[@]}; do [[ -z ${taskparent[$uid]} ]] && uids="$uids $uid" done elif [[ $filter =~ ^[0-9\ ]+$ ]]; then # list of task IDs for uid in ${!taskuid[@]}; do for wantid in $filter; do if [[ ${taskid[$uid]} == $wantid ]]; then uids="$uids $uid" fi done done else # text to match a=$( echo ${filter} | tr 'A-Z' 'a-z') for uid in ${!taskuid[@]}; do b=$( echo ${tasksum[$uid]} | tr 'A-Z' 'a-z') if [[ ${b} == *"${a}"* ]]; then uids="$uids $uid" fi done fi needtitle=1 if [[ -z $uids ]]; then error "no matching tasks found." exit 1 else processed="" for uid in ${uids}; do if [[ $mode == "list" ]]; then showit $uid elif [[ $mode == "done" ]]; then markdone $uid elif [[ $mode == "notdone" ]]; then marknotdone $uid elif [[ $mode == "del" ]]; then delq_add $uid fi done # do queued actions if [[ $mode == "del" ]]; then delq_process $uid fi # show results if [[ ! -z $processed ]]; then for uid in ${uids}; do showit $uid done fi fi fi