task_cli/t.sh.orig

913 lines
21 KiB
Bash
Executable File

#!/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 <<EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-111003//EN
BEGIN:VTODO
DTSTAMP:$now
UID:$uid
CREATED:$now
LAST-MODIFIED:$now
SUMMARY:${tasksum[$uid]}
EOF
if [[ ! -z ${taskparent[$uid]} ]]; then
echo "RELATED-TO;RELTYPE=PARENT:${taskparent[$uid]}" >>$file
fi
cat >>$file <<EOF2
PRIORITY:9
X-APPLE-SORT-ORDER:611376630
END:VTODO
END:VCALENDAR
EOF2
nextid=$((nextid + 1))
saveids
needtitle=1
if [[ -z ${parentuid} ]]; then
action "Created new task #${taskid[$uid]}."
showit $uid
else
action "Created new subtask #${taskid[$uid]} under '${tasksum[${taskparent[$uid]}]}'."
showit ${taskparent[$uid]} $uid
fi
}
function nextline() {
local dummy morestring
morestring="<------ more ------>"
printed=$((printed + 1));
if [[ $printed -ge $((rows - 2)) ]]; then
echo -en "${CYAN}${morestring}${PLAIN}"
read -n1 -s dummy
printf "\r%*s\r" ${#morestring}
printed=0
needtitle=1
fi
}
function showit() {
local uid childuid children char hilite cols wid
uid=$1
if [[ $# -ge 2 ]]; then
hilite=$2
fi
if [[ $needtitle -eq 1 ]]; then
cols=$(tput cols)
wid=$((cols - $numwid - 2 - 1))
printf "$TOPTITLE%-${numwid}s %-${wid}s$PLAIN\n" "ID" "Task"
needtitle=0
fi
if [[ ${taskchecked[$uid]} -eq 1 ]]; then
char="✓"
col="$GREY$STRIKE"
else
char=" "
col="$PLAIN"
fi
if [[ $uid == $hilite ]]; then
col="$col$HILITE"
fi
printf "${LEFTTITLE}%-${numwid}d${PLAIN} %*s" ${taskid[$uid]} $indent
printf "$brack[$PLAIN$GREEN%s$PLAIN$brack]$PLAIN ${col}%s${PLAIN}\n" "${char}" "${tasksum[$uid]}"
nextline
if [[ ! -z ${taskdesc[$uid]} ]]; then
printf "${LEFTTITLE}%*s${PLAIN} " $numwid
printf "%*s" $(( $indent + 4))
printf "${col}${ITALIC}%s${PLAIN}\n" "${taskdesc[$uid]}"
nextline
fi
inc
children=$(getchildren $uid)
for childuid in ${children}; do
showit $childuid $hilite
done
dec
}
function addcmd() { # name usage description alt1|alt2|etc
local cmd usage desc alts
cmd="$1"
usage="$2"
desc="$3"
alts="$4"
[[ -z $valid_modes ]] && valid_modes=$cmd || valid_modes="$valid_modes $cmd"
eval "usage_${cmd}=\"${usage}\""
eval "desc_${cmd}=\"${desc}\""
eval "words_${cmd}=\"${alts}\""
}
function getchildren() {
local uid childuid children
uid=$1
children=""
for childuid in ${!taskuid[@]}; do
[[ ${taskparent[$childuid]} == $uid ]] && children="$children $childuid"
done
[[ -z $children ]] && return 1
echo "$children"
return 0
}
# Should be in config file:
#basedir=/Users/rpearce/scripts/t
#dir=$basedir/docs
#idfile=$basedir/idmappings.txt
#VDS_LOCAL=$cal_local
#VDS_REMOTE=$cal_remote
#VDIRSYNCER=/usr/local/bin/vdirsyncer
#VDS_BASEDIR="~/.config/vdirsyncer"
#VDS_CONFIG="${VDS_BASEDIR}/config"
#VDS_STATUSDIR="${VDS_BASEDIR}/status"
#VDS_CALDIR="${VDS_BASEDIR}/calendar"
# Base vdirsycner config in ${VDS_CONFIG}:
#[general]
#status_path = "${VDS_STATUSDIR}/"
#
#[pair cal]
#a = "${VDS_LOCAL}"
#b = "${VDS_REMOTE}"
#collections = ["from a", "from b"]
#
#[storage ${VDS_LOCAL}]
#type = "filesystem"
#path = "${VDS_CALDIR}/"
#fileext = ".vcf"
#
#[storage ${VDS_REMOTE}]
#type = "caldav"
#url = "https://tasks.haven.family/caldav.php/haven/"
#username = "xxx"
#password = "xxx"
VDIRSYNCER=/usr/local/bin/vdirsyncer
VDS_LOCAL=$cal_local
VDS_REMOTE=$cal_remote
basedir=/Users/rpearce/scripts/t
dir=$basedir/docs
idfile=$basedir/idmappings.txt
nextid=1
indent=0
indentamt=4
needsave=0
mode=list
# for pager
printed=0
rows=$(tput lines)
addcmd "list" "list [id1] .. [idX]" "List [just the specified] tasks" "list|ls|show"
addcmd "done" "done <taskid>" "Complete a task" "done|x|complete"
addcmd "notdone" "notdone <taskid>" "Uncomplete a task" "notdone|o|incomplete|uncomplete|clear"
addcmd "add" "add [parent] <name>" "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 <id> <newname>" "Rename given task" "rename"
addcmd "in" "in <id>" "Decrease indent of given task" "in|left|h"
addcmd "out" "out <id> <parent>" "Move task below the given parent" "out|right|l|mv"
addcmd "note" "note <id> <notes>" "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