task_cli/t.sh

1748 lines
44 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"
brackfold="$CYAN"
HILITE="\033[43;1m"
GREYBG="\033[48;2;30;30;30m"
TOPTITLE="\033[44;1m"
LEFTTITLE="\033[31;1m"
#FOLDBG="\033[100m"
FOLDBG="$BOLDD"
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 [OPTIONS] [commandopts]"
echo
echo " -f Assume 'yes' to all questions."
echo " -l xx Specify name of local vdirsyncer storage name (default: $DEFAULT_VDS_LOCAL)"
echo " -r xx Specify name of remote vdirsyncer storage name (default: $DEFAULT_VDS_REMOTE)"
echo " -s Auto-sync with server when complete."
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 getid() {
local res id uid
uid=$1
res=""
for id in ${!idmap[@]}; do
if [[ ${idmap[$id]} == $uid ]]; then
res=${id}
break
fi
done
echo $res
if [[ -z $res ]]; then
return 1
fi
return 0
}
function getuid() {
local res id
id=$1
res=${idmap[$id]}
if [[ -z $res ]]; then
return 1
fi
echo $res
return 0
}
function loadids() {
local LINE n uid id max count
count=0
if [[ ! -e $idfile ]]; then
error "No ID file found (looking here: $idfile)"
return 1
fi
max=0
while read LINE; do
uid=${LINE%:*}
id=${LINE#*:}
idmap[$id]=$uid
if [[ -z $uid || -z $id ]]; then
error "invalid idmapping line: '$LINE'"
return 1
fi
[[ $id -gt $max ]] && max=$id
count=$((count + 1))
done < "$idfile"
nextid=$((max + 1))
dblog "got $count IDs from $idfile"
return 0
}
function saveids() {
local id verbose=0
local ess nids=0
[[ $1 == "-v" ]] && verbose=1
if [[ ${#idmap[@]} -gt 0 ]]; then
cp /dev/null "$idfile"
for id in ${!idmap[@]}; do
#dblog "save to $idfile: uid ${idmap[$id]} id $id sum ${tasksum[$id]}" >> $idfile
echo "${idmap[$id]}:$id" >> "$idfile"
nids=$((nids + 1))
done
[[ $nids -eq 1 ]] && ess="" || ess="s"
[[ $verbose -eq 1 ]] && action "Saved $nids ID mapping$ess to $idfile."
else
[[ $verbose -eq 1 ]] && warn "No ID mappings found to save"
return 1
fi
return 0
}
function dumpids() {
local id
for id in ${!idmap[@]}; do
echo "$id --> ${idmap[$id]}"
done
}
function loadtasks() {
local f ess x thislev par parid show thissum thisdesc
local verbose=0
maxid=-1
maxindent=-1
maxtaskwidth=-1
ntasks=0
while [[ $# -ge 1 ]]; do
[[ $1 == "-v" ]] && verbose=1 && shift 1
done
dblog "Loading tasks..."
for f in "$dir"/*.vcf; do
dblog "--> loading from $f"
loadtask "$f" || return 1
ntasks=$((ntasks + 1))
done
numwid=${#maxid}
dblog "Calculating max indent level"
# figure out max indent level
for x in ${!taskuid[@]}; do
show=1
par=${taskparent[$x]}
if [[ ! -z $par ]]; then
parid=$(getid $par)
if [[ ${taskfolded[$parid]} -eq 1 ]]; then
show=0
fi
fi
dblog " uid $x id [${taskid[$x]}] parent [$par] parent [$parid] show [$show]"
if [[ $show -eq 1 ]]; then
dblog " ->showing $x [${tasksum[$x]}]"
thislev=$(getindentlevel $x)
dblog " ->lv: $thislev"
[[ $thislev -gt $maxindent ]] && maxindent=$thislev
thissum=${tasksum[$x]}
thisdesc=${taskdesc[$x]}
[[ ${#thissum} -gt $maxtaskwidth ]] && maxtaskwidth=${#thissum}
[[ ${#thisdesc} -gt $maxtaskwidth ]] && maxtaskwidth=${#thisdesc}
dblog " ->sum: $thissum"
fi
done
[[ $ntasks -eq 1 ]] && ess="" || ess="s"
[[ $verbose -eq 1 ]] && action "Finished loading $ntasks task$ess."
dblog "Finished loading $ntasks task$ess."
return 0
}
function getindentlevel() { ## $0 id
local id thisid parentuid parentid level
id=$1
thisid=$id
#dblog "gil() id $id thisid $thisid"
level=0
while [ ! -z $parentuid ] ; do
parentuid=${taskparent[$thisid]}
if [[ ! -z $parentuid ]]; then
thisid=$(getid $parentuid)
dblog " parent uid: [$parentuid] id: [$thisid]"
level=$((level + 1))
fi
done
echo $level
}
function dblog() {
[[ $DEBUG -eq 1 ]] && echo "$(date) ${FUNCNAME[1]}() $*" >/dev/stderr
}
function action() {
echo -e "$BOLD$GREEN* $PLAIN$GREEN$*$PLAIN"
}
function warn() {
echo -e "$BOLD${YELLOW}WARNING: $PLAIN$YELLOW$*$PLAIN" >/dev/stderr
}
function error() {
echo -e "$BOLD${RED}ERROR: $PLAIN$RED$*$PLAIN" >/dev/stderr
}
function info() {
[[ $AUTOYES -eq 1 ]] && return
echo -e "$BOLD${CYAN}>> $PLAIN$CYAN$*$PLAIN"
}
function confirm() {
local yn
if [[ $AUTOYES -eq 1 ]]; then
return 0
fi
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 folded desc file id parent res i cats
dblog "open $1"
file="$1"
res=$(cat "$file" | awk -F: 'BEGIN { checked=0; folded=0; } /^UID:/ { printf("uid©%s©",$2); } /^SUMMARY:/ { sub("SUMMARY:",""); printf("sum©%s©",$0); } /^CATEGORIES:/ { sub("^CATEGORIES:",""); printf("cats©%s©",$0); } /^DESCRIPTION:/ { sub("DESCRIPTION:",""); printf("desc©%s©",$0); } /^RELATED-TO/ { sub("^RELATED-TO.*:",""); printf("parent©%s©",$0); } /^X-OC-HIDESUBTASKS:1/ { folded=1; } /^STATUS:COMPLETED/ { checked=1; } END { printf("checked©%d©folded©%d\n",checked,folded); }')
IFS='©'
read -ra tok <<< "$res"
parent=""
i=0
#dblog "res: '$res'"
#for x in ${!tok[@]}; do
#dblog "forloop tok $x is '${tok[$x]}'"
#done
# while [[ ! -z ${tok[$i]} ]]; do
#dblog "tok $i is '${tok[$i]}'"
# i=$((i + 1))
# done
while [[ ! -z ${tok[$i]} ]]; do
dblog "process token '${tok[$i]}'"
if [[ ${tok[$i]} == "uid" ]]; then
uid=${tok[$((i + 1))]}
#nexti=$((i + 1))
#dblog "i=$i tok[i]=${tok[$i]} i+1=$((i + 1)) tok[i+1]=${tok[$((i + 1))]} uid $uid id $id"
#dblog "i=$i tok[i]=${tok[$i]} i+1=$((i + 1)) tok[nexti]=${tok[$nexti]} uid $uid id $id"
id=$(getid $uid)
if [[ -z $id ]]; then
id=$nextid
idmap[$nextid]=$uid
nextid=$((nextid + 1))
needsave=1
fi
#dblog "i=$i tok[i]=${tok[$i]} uid $uid id $id"
elif [[ ${tok[$i]} == "sum" ]]; then
sum="${tok[$((i + 1))]}"
elif [[ ${tok[$i]} == "cats" ]]; then
cats="${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))]}"
elif [[ ${tok[$i]} == "folded" ]]; then
folded="${tok[$((i + 1))]}"
fi
i=$((i + 2))
done
IFS=' '
taskuid[$id]=$uid
taskid[$id]=$id
tasksum[$id]=$sum
taskcats[$id]=$cats
taskdesc[$id]=$desc
taskchecked[$id]=$checked
taskfolded[$id]=$folded
taskparent[$id]=$parent
if [[ $DEBUG -eq 1 ]]; then
echo "taskuid[$id]=$uid"
echo "taskid[$id]=$id"
echo "tasksum[$id]=$sum"
echo "taskcats[$id]=$cats"
echo "taskdesc[$id]=$desc"
echo "taskchecked[$id]=$checked"
echo "taskfolded[$id]=$folded"
echo "taskparent[$id]=$parent"
fi
[[ $id -gt $maxid ]] && maxid=$id
[[ ${#sum} -gt $taskwid ]] && taskwid=${#sum}
[[ ${#desc} -gt $taskwid ]] && taskwid=${#desc}
if [[ -z $uid ]]; then
error "task has no uid:"
echo "$res"
return 1
fi
return 0
}
function marknotdone() {
local id uid rv file sedrv
local children childuid ischild noun
uid=$1
id=$(getid $uid)
[[ $# -gt 1 ]] && ischild=1 || ischild=0
[[ $ischild -eq 1 ]] && noun=subtask || noun=task
# already not done?
if [[ ${taskchecked[$id]} -eq 0 ]]; then
error "$noun #${id} 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 #${id} as unfinished."
processed="$processed ${uid} "
loadtask $file
rv=0
children=$(getchildren $uid)
for childuid in ${children}; do
marknotdone $childuid noprint
rv=$((rv + $?))
done
else
failed="$failed ${uid} "
error "failed to mark $noun #${id} 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
local od
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
id=$(getid $uid)
# don't show if it's a subtask of one already in the list
if [[ -z ${taskparent[$id]} ]]; then
wantshow=1
elif [[ $deluids == *${taskparent[$id]}\ * ]]; 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
id=$(getid $uid)
unset idmap[$id]
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_left() { # move_left uid
local uid rv parent parentid parent2 sedrv
local id
uid=$1
id=$(getid $uid)
rv=0
parent=${taskparent[$id]}
if [[ -z $parent ]]; then
error "Can't shift task #${id} any further left."
exit 1
fi
parentid=$(getid ${taskparent[$id]})
parent2=${taskparent[$parentid]}
taskparent[$id]="$parent2"
file=$dir/$uid.vcf
sed -i "s/^RELATED-TO.*/RELATED-TO;RELTYPE=PARENT:${taskparent[$id]}/" $file
sedrv=$?
set_lastmodified $uid
sedrv=$((sedrv + $?))
if [[ $sedrv -eq 0 ]]; then
action "Shifted task #${id} upwards/left."
loadtask $file
showit ${taskparent[$id]} $uid
rv=0
else
error "Failed to shift task #${id} left."
rv=1
fi
return $rv
}
function move_right() { # move_right uid new_parent
local uid rv newparent sedrv
local id newparentid str toadd
uid=$1
id=$(getid $uid)
rv=0
newparent=$2
if [[ $newparent == "-" ]]; then
newparent=""
newparentid=""
else
newparentid=$(getid $newparent)
fi
rv=0
taskparent[$id]="$newparent"
file=$dir/$uid.vcf
if [[ -z $newparent ]]; then
sed -i "/^RELATED-TO.*/d" $file
sedrv=$?
else
if grep -q ^RELATED-TO $file; then
sed -i "s/^RELATED-TO.*/RELATED-TO;RELTYPE=PARENT:${taskparent[$id]}/" $file
sedrv=$?
else
str="END:VTODO"
toadd="RELATED-TO;RELTYPE=PARENT:${taskparent[$id]}"
sed -i "/$str/i $toadd\n" $file
sedrv=$?
fi
fi
set_lastmodified $uid
sedrv=$((sedrv + $?))
if [[ $sedrv -eq 0 ]]; then
if [[ -z $newparent ]]; then
str="to toplevel."
else
str="underneath #${newparentid} ('${tasksum[$newparentid]}')."
fi
action "Shifted task #${id} $str"
loadtask $file
showit ${taskparent[$id]} $uid
rv=0
else
if [[ -z $newparent ]]; then
str="to toplevel."
else
str="under #${newparentid}."
fi
error "Failed to shift task #${id} $str"
rv=1
fi
return $rv
}
function setnote() { # setnote uid new note goes here
local uid newnote sedrv rv file str
local id
uid=$1
id=$(getid $uid)
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
if [[ -z $newnote ]]; then
action "Removed note from task #${id}."
else
action "Set note of task #${id} to '$newnote'."
fi
loadtask $file
rv=0
else
error "Failed to set note of task #${id}."
rv=1
fi
return $rv
}
function modtag() { # setnote uid add|del new tag goes here
local uid arg sedrv rv file str
local id action x catarr newcat
uid=$1
id=$(getid $uid)
rv=0
shift
action=$1
shift
arg="$*"
file=$dir/$uid.vcf
if [[ -z $arg ]]; then
if [[ $action == "del" ]]; then
sed -i "/^CATEGORIES:/d" $file
sedrv=$?
else
error "No tag specified to add."
return 1
fi
else
IFS=","
catarr=( ${taskcats[$id]} )
IFS=" "
for x in ${!catarr[@]} ; do
if [[ $action == "add" || ${catarr[$x]} != $arg ]]; then
[[ -z $newcat ]] && newcat="${catarr[$x]}" || newcat="$newcat,${catarr[$x]}"
fi
done
if [[ $action == "add" ]]; then
[[ -z $newcat ]] && newcat="${arg}" || newcat="$newcat,$arg"
fi
if grep -q ^CATEGORIES: $file; then
sed -i "s/^CATEGORIES:.*/CATEGORIES:${newcat}/" $file
sedrv=$?
else
str="END:VTODO"
toadd="CATEGORIES:${newcat}"
sed -i "/$str/i $toadd\n" $file
sedrv=$?
fi
fi
set_lastmodified $uid
sedrv=$((sedrv + $?))
if [[ $sedrv -eq 0 ]]; then
if [[ $action == "del" ]]; then
if [[ -z $arg ]]; then
action "Removed all tags from task #${id}."
else
action "Removed tag '$arg' from task #${id}."
fi
else
action "Added tag '$arg' to task #${id}."
fi
loadtask $file
rv=0
else
error "Failed to set/remove tag on task #${id}."
rv=1
fi
return $rv
}
function rename() { # rename uid new name goes here
local uid newname sedrv rv file
local id
uid=$1
id=$(getid $uid)
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 #${id} to '$newname'."
loadtask $file
rv=0
else
error "failed to rename task #${id}."
rv=1
fi
return $rv
}
function toggle() {
local uid id rv children
uid=$1
id=$(getid $uid)
# already done this one?
if [[ $processed == *\ $uid\ * || $failed == *\ $uid\ * ]]; then
return 1
fi
# not a parent?
children=$(getchildren $uid)
if [[ -z ${children} ]]; then
error "Can't fold/unfold task #${id} with no children."
return 1
fi
if [[ ${taskfolded[$id]} -eq 1 ]]; then
unfold $uid
rv=$?
else
fold $uid
rv=$?
fi
return $rv
}
function fold() {
local uid id sedrv rv file str
local children
uid=$1
id=$(getid $uid)
# not a parent?
children=$(getchildren $uid)
if [[ -z ${children} ]]; then
error "Can't fold task #${id} with no children."
return 1
fi
# already folded?
if [[ ${taskfolded[$id]} -eq 1 ]]; then
error "Task #${id} is already folded."
return 1
fi
file=$dir/$uid.vcf
str="END:VTODO"
if grep -q ^X-OC-HIDESUBTASKS: $file ; then
sed -i "s/X-OC-HIDESUBTASKS:.*/X-OC-HIDESUBTASKS:1/" $file
sedrv=$?
else
sed -i "/$str/i X-OC-HIDESUBTASKS:1" $file
sedrv=$?
fi
set_lastmodified $uid
sedrv=$((sedrv + $?))
if [[ $sedrv -eq 0 ]]; then
action "Folded task #${id}."
processed="$processed ${uid} "
loadtask $file
rv=0
else
failed="$failed ${uid} "
error "failed to fold task #${id}"
rv=1
fi
return $rv
}
function unfold() {
local uid id sedrv rv file str
local children
uid=$1
id=$(getid $uid)
# not a parent?
children=$(getchildren $uid)
if [[ -z ${children} ]]; then
error "Can't unfold task #${id} with no children."
return 1
fi
# not folded?
if [[ ${taskfolded[$id]} -ne 1 ]]; then
error "Task #${id} is not folded."
return 1
fi
file=$dir/$uid.vcf
str="END:VTODO"
if grep -q ^X-OC-HIDESUBTASKS: $file ; then
sed -i "s/X-OC-HIDESUBTASKS:.*/X-OC-HIDESUBTASKS:0/" $file
sedrv=$?
else
sed -i "/$str/i X-OC-HIDESUBTASKS:0" $file
sedrv=$?
fi
set_lastmodified $uid
sedrv=$((sedrv + $?))
if [[ $sedrv -eq 0 ]]; then
action "Unfolded task #${id}."
processed="$processed ${uid} "
loadtask $file
rv=0
else
failed="$failed ${uid} "
error "failed to unfold task #${id}"
rv=1
fi
return $rv
}
function markdone() {
local uid sedrv rv file toadd1 toadd2 toadd3 str now
local children childuid ischild noun id
uid=$1
id=$(getid $uid)
[[ $# -gt 1 ]] && ischild=1 || ischild=0
[[ $ischild -eq 1 ]] && noun=subtask || noun=task
# already complete?
if [[ ${taskchecked[$id]} -eq 1 ]]; then
error "$noun #${id} 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 #${id} as completed."
processed="$processed ${uid} "
loadtask $file
rv=0
children=$(getchildren $uid)
for childuid in ${children}; do
markdone $childuid noprint
rv=$((rv + $?))
done
else
failed="$failed ${uid} "
error "failed to mark $noun #${id} as completed"
rv=1
fi
return $rv
}
function viewtask() {
local uid id file
uid=$1
id=$(getid $uid)
file=$dir/$uid.vcf
action "Viewing task #$id ($file):"
cat $file | sed -e 's/^/ /'
return 0
}
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 parentid parentuid file thisid
if [[ $1 == "-p" ]]; then
parentuid=$2
parentid=$(getid $parentuid)
shift 2
else
parentuid=""
fi
uid=$(randomuid)
thisid=$nextid
idmap[$thisid]=$uid
taskid[$thisid]=$thisid
taskuid[$thisid]=$uid
tasksum[$thisid]="$*"
taskdesc[$thisid]=""
taskchecked[$thisid]=0
taskfolded[$thisid]=0
taskparent[$thisid]=$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[$thisid]}
EOF
if [[ ! -z ${taskparent[$thisid]} ]]; then
echo "RELATED-TO;RELTYPE=PARENT:${taskparent[$thisid]}" >>$file
fi
cat >>$file <<EOF2
PRIORITY:9
X-APPLE-SORT-ORDER:611376630
X-OC-HIDESUBTASKS:0
END:VTODO
END:VCALENDAR
EOF2
nextid=$((nextid + 1))
saveids
needtitle=1
if [[ -z ${parentuid} ]]; then
action "Created new task #${taskid[$thisid]}."
showit $uid
else
action "Created new subtask #${taskid[$thisid]} under '${tasksum[${parentid}]}'."
showit ${taskparent[$thisid]} $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 col wid screenwid id ilev
local children nchildren charcol
local notecol bcol bl br space pre end
local showkids
local reset catstr catcol
reset="$PLAIN"
catcol="\033[38;2;255;165;0m"
showkids=1
if [[ $1 == "--nochildren" ]]; then
showkids=0
shift
fi
uid=$1
id=$(getid $uid)
children=$(getchildren $uid)
childrenarray=( $children )
nchildren=${#childrenarray[@]}
if [[ $# -ge 2 ]]; then
hilite=$2
fi
if [[ $needtitle -eq 1 ]]; then
cols=$(tput cols)
screenwid=$((cols - $numwid - 2 - 1))
wid=$(echo "scale=0; 4 + ($maxindent * $indentamt) + $maxtaskwidth + 3" | bc)
[[ $wid -gt $screenwid ]] && wid=$screenwid
printf "$TOPTITLE%-${numwid}s %-${wid}s$reset\n" "ID" "Task"
needtitle=0
fi
col="$reset"
notecol="$YELLOW"
if [[ ${taskchecked[$id]} -eq 1 ]]; then
char="✓"
col="$col$STRIKE"
notecol="$notecol$STRIKE"
else
char=" "
fi
if [[ $uid == $hilite ]]; then
col="$col$HILITE"
notecol="$notecol$HILITE"
fi
charcol="$GREEN"
if [[ $nchildren -gt 0 && ${taskfolded[$id]} -eq 1 ]]; then
bcol=$brackfold
bl="("
br=")"
#col="$col$FOLDBG"
space=" "
pre=""
end=" $reset$CYAN${BOLD}[$nchildren subtasks]"
if [[ $char == " " ]]; then
char="+"
charcol="$BOLD$CYAN"
fi
else
bcol=$brack
bl="["
br="]"
space=" "
pre=""
end=""
fi
if [[ -z ${taskcats[$id]} ]]; then
catstr=""
else
catstr="${catcol} (${taskcats[$id]}$reset$catcol)"
fi
printf "${LEFTTITLE}%-${numwid}d${reset}" ${id}
printf "$space$space"
if [[ $space == " " ]]; then
printf "%*s" $indent
elif [[ $indent -gt 0 ]]; then
for x in $(eval echo {1..${indent} ); do
echo -n "$space"
done
fi
ilev=$(getindentlevel $id)
printf "$reset"
printf "$bcol$bl$reset$charcol%s$reset$bcol$br$reset ${col}${pre}%s$end${reset}${catstr}${reset}\n" "${char}" "${tasksum[$id]}"
nextline
if [[ ! -z ${taskdesc[$id]} ]]; then
printf "${LEFTTITLE}%*s${reset} " $numwid
printf "%*s" $(( $indent + 4))
printf "${notecol}${ITALIC}%s${reset}\n" "${taskdesc[$id]}"
nextline
fi
if [[ $showkids -eq 1 && ${taskfolded[$id]} -ne 1 ]]; then
inc
for childuid in ${children}; do
showit $childuid $hilite
done
dec
fi
}
function addcmd() { # name usage description alt1|alt2|etc supportsall
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}\""
if [[ $* == *SUPPORTS_ALL* ]]; then
if [[ -z $modes_that_support_all ]]; then
modes_that_support_all="$cmd"
else
modes_that_support_all="$modes_that_support_all|$cmd"
fi
fi
if [[ $* == *PASSIVE* ]]; then
if [[ -z $passive_modes ]]; then
passive_modes="$cmd"
else
passive_modes="$passive_modes|$cmd"
fi
fi
}
function getchildren() {
local uid childuid children id
uid=$1
children=""
for id in ${!taskid[@]}; do
[[ ${taskparent[$id]} == $uid ]] && children="$children ${taskuid[$id]}"
done
[[ -z $children ]] && return 1
echo "$children"
return 0
}
function makedir() {
local d
d="$1"
if [[ -d "$d" ]]; then
info "Directory $d already exists"
else
mkdir -p "${d}"
if [[ $? -eq 0 ]]; then
action "Created directory ${BOLD}$d${PLAIN}"
else
error "Couldn't mkdir ${BOLD}$d${PLAIN}${RED}"
return 1
fi
fi
return 0
}
function init() {
local gotvdconf=0 d url count cals cals_arr thiscal selcal
action "Initialising configration"
if ! which -s vdirsyncer; then
error "Can't find ${BOLD}vdirsyncer${PLAIN}${RED} - please install it."
return 1
fi
if [[ -e $vdirsyncconfig ]]; then
confirm "Use existing vdirsyncer config in ${BOLD}$vdirsyncconfig${PLAIN}${MAGENTA}"
if [[ $? -eq 0 ]]; then
gotvdconf=1
fi
fi
if [[ $gotvdconf -ne 1 ]]; then
for d in "$vdirsyncdir" "$vdircaldir" "$vdirstatdir"; do
makedir "$d" || return 1
done
echo -e "${MAGENTA}Please provide the URL of your caldav server.${PLAIN}"
echo -en "${BOLD}${MAGENTA}==> ${PLAIN}"
read url
if [[ ! $url =~ ^http.* ]]; then
error "Provided URL 'url' appears invalid."
return 1
fi
echo -en "${BOLD}${MAGENTA}Caldav username: ${PLAIN}"
read u
echo -en "${BOLD}${MAGENTA}Caldav password: ${PLAIN}"
read -s p
cat >"${vdirsyncconfig}" <<EOF
[general]
status_path = "$vdirstatdir"
[pair cal]
a = "cal_local"
b = "cal_remote"
collections = ["from a", "from b"]
[storage cal_local]
type = "filesystem"
path = "$vdircaldir"
fileext = ".vcf"
[storage cal_remote]
type = "caldav"
url = "$url"
username = "$u"
password = "$p"
EOF
unset u
unset p
info "Created vdirsyncer config in ${vdirsyncconfig}"
fi
cals=$(ls -1q $vdircaldir 2>/dev/null)
count=$(echo "$cals" | wc -l | tr -d ' ')
if [[ $count -eq 0 ]]; then
action "Running initial vdirsyncer discover..."
vdirsyncer discover
action "Running initial vdirsyncer sync..."
vdirsyncer sync
fi
getcals count cals_arr
makedir "$confdir" || return 1
if [[ -e "${allcalsdir}" ]] ; then
warn "Calendar symlink ${allcalsdir} already exists. Not overwriting it."
else
ln -s "${vdircaldir}" "${allcalsdir}"
action "Created symlink to $vdircaldir"
fi
if [[ ! -e "${defaultcalfile}" ]] ; then
selcal=""
if [[ $count -gt 0 ]]; then
local sel
echo
for d in ${!cals_arr[@]}; do
echo -e "${MAGENTA}${BOLD}$((d + 1)). ${PLAIN}${MAGENTA}${cals_arr[$d]}${PLAIN}"
done
sel=-1
while [[ $sel -le 0 || $sel -gt ${#cals_arr[@]} ]]; do
echo -en "${MAGENTA}${BOLD}Select calendar: ${PLAIN}"
read sel
done
selcal=${cals_arr[$(($sel - 1))]}
echo "$selcal" >"$defaultcalfile"
else
warn "No calendars found."
fi
fi
if [[ -z $selcal ]]; then
warn "No calendar selected "
echo "${YELLOW}Please put default calendar name in ${defaultcalfile}"
echo "${YELLOW}then: touch \"${idfile}_\$(cat $defaultcalfile)\""
selcal=""
else
selcal=${selcal// /_}
if [[ -e "${idfile}_${selcal}" ]]; then
warn "ID mapping file for $selcal already exists. Not overwriting it."
else
touch "${idfile}_${selcal}"
fi
fi
info "Initialisation complete"
[[ -z $selcal ]] && selcal="<none>"
echo -e "${CYAN} vdirsyncer config is in ${BOLD}${vdirsyncdir}${PLAIN}"
echo -e "${CYAN} task_cli config is in ${BOLD}${confdir}${PLAIN}"
echo -e "${CYAN} selected calendar is '${BOLD}${selcal}${PLAIN}'"
echo
}
function tasksync() {
local res upcount downcount noun
if [[ $1 == "-a" ]]; then
noun="Auto-sync"
else
noun="Sync"
fi
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 "$noun complete - no changes."
elif [[ $upcount -eq 0 ]]; then
action "$noun complete - $downcount change$downess pulled."
elif [[ $downcount -eq 0 ]]; then
action "$noun complete - $upcount change$upess pushed."
else
action "$noun complete - $upcount change$upess pushed, $downcount change$downess pulled."
fi
else
error "$noun failed. Output:"
echo "$res" | sed -e 's/^/ /'
fi
}
function getcals() { # countvar arrayvar
local loc_cals loc_count thiscal
local countvar arrayvar
countvar=$1
arrayvar=$2
loc_cals=$(ls -1q $vdircaldir 2>/dev/null)
loc_count=$(echo "$loc_cals" | wc -l | tr -d ' ')
eval "$countvar=$loc_count"
eval "$arrayvar=()"
while read thiscal; do
eval "$arrayvar+=(\"$thiscal\")"
done <<< "$loc_cals"
}
function getcalname() { # cal_name_or_number
local lc larr d found=""
getcals lc larr
for d in ${!larr[@]}; do
if [[ ${larr[$d]} == $1 ]]; then
found="${larr[$d]}"
break
elif [[ $1 =~ ^[0-9]+$ && $d -eq $(($1 - 1)) ]]; then
# make this the default
found="${larr[$d]}"
break
fi
done
echo "$found"
if [[ -z $found ]]; then
return 1
fi
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"
###
#
#
# Ask:
# - where is this script located?
# - remote<->local task id mapping file? [default: $thisdir/idmappings.txt ]
# - url for your remote caldav
# - username for your remote caldav
# - password for your remote caldav
# - vdirsyncer dir [default ${HOME}/.config/vdirsyncer]
# - vdirsyncer status dir [default ${vdirsyncer_dir/status}
# - vdirsyncer config file [default ${vdirsyncer_dir}/config]
# - vdirsyncer calendar dir [default ${vdirsyncer_dir/calendar}
###### - vdirsyncer name for your local calender [default cal_local]
###### - vdirsyncer name for your remote calender [default cal_remote]
VDIRSYNCER=/usr/local/bin/vdirsyncer
DEFAULT_VDS_LOCAL=cal_local
DEFAULT_VDS_REMOTE=cal_remote
basedir=/Users/rpearce/code/task_cli
confdir=${HOME}/.task_cli
allcalsdir=$confdir/allcals # symlink to vdirsyncer/calendar/
defaultcalfile=$confdir/defaultcal
idfile=$confdir/idmappings
nextid=1
indent=0
indentamt=4
needsave=0
mode=list
modes_that_support_all=""
passive_modes="" # modes that do not change data
vdirsyncdir="${HOME}/.config/vdirsyncer"
vdircaldir="${HOME}/.config/vdirsyncer/calendar"
vdirstatdir="${HOME}/.config/vdirsyncer/status"
vdirsyncconfig="${vdirsyncdir}/config"
# for pager
printed=0
rows=$(tput lines)
addcmd "list" "list [id1] .. [idX]" "List [just the specified] tasks" "list|ls|show" SUPPORTS_ALL PASSIVE
addcmd "fold" "fold <id>" "Fold a parent task (hide its children)" "fold|f|zc" SUPPORTS_ALL
addcmd "unfold" "unfold <id>" "Unfold a parent task (show its children)" "unfold|u|zo" SUPPORTS_ALL
addcmd "toggle" "toggle <id>" "Fold/Unfold a parent task" "toggle|z|zz" SUPPORTS_ALL
addcmd "done" "done <taskid>" "Complete a task" "done|x|complete" SUPPORTS_ALL
addcmd "notdone" "notdone <taskid>" "Uncomplete a task" "notdone|o|incomplete|uncomplete|clear" SUPPORTS_ALL
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 "left" "left <id>" "Decrease indent of given task" "left|h|out|up"
addcmd "right" "right <id> <parent>" "Move task below the given parent" "right|l|mv|in"
addcmd "note" "note <id> <notes>" "Change notes for given task" "note|desc|description|comment"
addcmd "tag" "tag <id> <tag>" "Add a tag to the given task" "tag|t"
addcmd "untag" "untag <id> <tag>" "Remove a tag from the given task" "untag|ut"
addcmd "sync" "sync" "Sync tasks using vdirsyncer" "sync"
addcmd "view" "view <id>" "Show detailed info for given task" "v|view|info|vcal" PASSIVE
addcmd "showcals" "showcals" "List available calendars" "cals|showcals|cls" PASSIVE
addcmd "setcal" "setcal" "Set the default calendar." "setcal|sc"
DEBUG=0
TESTMODE=0
AUTOSYNC=0
CALID=$(cat "$confdir/defaultcal")
ARGS="fhic:dstyl:r:"
while getopts "$ARGS" i; do
case "$i" in
h)
usage;
exit 1;
;;
i)
init
exit $?
;;
c)
CALID="$OPTARG"
;;
l)
VDS_LOCAL="$OPTARG"
;;
r)
VDS_REMOTE="$OPTARG"
;;
d)
DEBUG=1
;;
s)
AUTOSYNC=1
;;
t)
DEBUG=1
TESTMODE=1
;;
y|f)
AUTOYES=1
;;
*)
error "invalid argument: $i";
usage;
exit 1
;;
esac
done
shift $((OPTIND - 1))
# validate CALID
foundcal=$(getcalname "$CALID")
if [[ -n $foundcal ]]; then
CALID="$foundcal"
else
error "Calendar '$CALID' does not exist."
exit 1
fi
dir=$allcalsdir/$CALID
idfile="$confdir/idmappings_${CALID// /_}"
if [[ ! -e $idfile ]]; then
warn "No ID mapping file found for calendar '$CALID' ($idfile)"
confirm "Create one from existing tasks?"
if [[ $? -eq 0 ]]; then
loadtasks -v
saveids -v || exit 1
else
die
fi
fi
[[ -z $VDS_LOCAL ]] && VDS_LOCAL="$DEFAULT_VDS_LOCAL"
[[ -z $VDS_REMOTE ]] && VDS_REMOTE="$DEFAULT_VDS_REMOTE"
mode=""
for x in $valid_modes; do
thisone="words_$x"
tomatch=${!thisone}
if [[ $1 =~ ^(${tomatch})$ ]]; then
mode=$x
shift
break
fi
done
if [[ -z $mode ]]; then
mode=list
fi
# Commands that don't need tasks loaded
if [[ $mode == "sync" ]]; then
tasksync
exit $rv
fi
loadids || exit 1
loadtasks || exit 1
[[ $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 == "showcals" ]]; then
getcals count cals_arr
if [[ $count -gt 0 ]]; then
echo -e "${BOLD}${MAGENTA}Available Calendars:$PLAIN"
for d in ${!cals_arr[@]}; do
echo -en "${MAGENTA}${BOLD}$((d + 1)). ${PLAIN}${MAGENTA}${cals_arr[$d]}${PLAIN}"
[[ ${cals_arr[$d]} == $CALID ]] && echo -e "$BOLD$MAGENTA <- default$PLAIN" || echo
done
else
error "No calendars found."
fi
elif [[ $mode == "setcal" ]]; then
if [[ -z $1 ]]; then
error "No calendar name provided."
exit 1
fi
foundcal=$(getcalname $1)
if [[ -n $foundcal ]]; then
action "Default calendar set to '${foundcal}'"
echo "${foundcal}" > "$defaultcalfile"
else
error "Calendar '$1' not found"
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 == "tag" ]]; then
uid=$(getuid_witherror $1)
shift
modtag $uid add "$*"
elif [[ $mode == "untag" ]]; then
uid=$(getuid_witherror $1)
shift
modtag $uid del "$*"
elif [[ $mode == "left" ]]; then
uid=$(getuid_witherror $1)
shift
move_left $uid
elif [[ $mode == "right" ]]; then
uid=$(getuid_witherror $1)
if [[ $2 == "-" ]]; then
puid="-"
else
puid=$(getuid_witherror $2 "parent task")
fi
shift 2
move_right $uid $puid
elif [[ $mode == "view" ]]; then
uid=$(getuid_witherror $1)
viewtask $uid
elif [[ $mode == "cleanup" ]]; then
action "Deleting all completed tasks."
for id in ${!taskuid[@]}; do
uid=$(getuid $id)
[[ ${taskchecked[$id]} -eq 1 ]] && delq_add $uid
done
delq_process
# show results
if [[ ! -z $processed ]]; then
for uid in ${processed}; do
showit $uid
done
fi
else
# multi-task commands
filter=$*
uids=""
if [[ -z $filter ]]; then
#for id in ${!taskid[@]}; do
# [[ -z ${taskparent[$id]} ]] && uids="$uids ${taskuid[$id]}"
#done
filter=all
fi
if [[ $filter == "all" ]]; then
na=0
if [[ ! $mode =~ $modes_that_support_all ]]; then
error "Command '$mode' doesn't support 'all'"
exit 1
fi
kids=""
for id in ${!taskid[@]}; do
match=0
uid=$(getuid $id)
children=$(getchildren $uid)
[[ -z ${taskparent[$id]} ]] && hasparent=0 || hasparent=1
[[ -z $children ]] && haschildren=0 || haschildren=1
if [[ $mode == "fold" && $haschildren -eq 1 && ${taskfolded[$id]} -ne 1 ]]; then
match=1
kids="--nochildren"
elif [[ $mode == "unfold" && ${taskfolded[$id]} -eq 1 ]]; then
match=1
kids="--nochildren"
elif [[ $mode == "done" && ${taskchecked[$id]} -eq 0 ]]; then
match=1
elif [[ $mode == "notdone" && ${taskchecked[$id]} -eq 1 ]]; then
match=1
elif [[ $mode == "list" && $hasparent -eq 0 ]]; then
match=1
fi
if [[ $match -eq 1 ]]; then
uids="$uids ${taskuid[$id]}"
fi
done
if [[ ! $mode =~ $passive_modes ]]; then
info "About to run '$mode' on the following tasks:"
needtitle=0
for uid in $uids; do
wantshow=0
id=$(getid $uid)
# don't show if it's a subtask of one already in the list
if [[ -z ${taskparent[$id]} ]]; then
wantshow=1
elif [[ $uids == *${taskparent[$id]}\ * ]]; then
wantshow=0
else
wantshow=1
fi
if [[ $wantshow -eq 1 ]]; then
showit $kids $uid | sed -e 's/^/ /'
fi
done
confirm "Really proceed"
if [[ $? -ne 0 ]]; then
action "Aborted."
exit 1
fi
fi
elif [[ $filter =~ ^[0-9\ ]+$ ]]; then
# list of task IDs
for wantid in $filter; do
uids="$uids ${taskuid[$wantid]}"
done
else
# text to match
a=$( echo ${filter} | tr 'A-Z' 'a-z')
for id in ${!taskid[@]}; do
b=$( echo ${tasksum[$id]} | tr 'A-Z' 'a-z')
if [[ ${b} == *"${a}"* ]]; then
uids="$uids ${taskuid[$id]}"
fi
done
fi
needtitle=1
if [[ -z $uids ]]; then
error "no matching tasks found."
exit 1
else
processed=""
failed=""
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 == "fold" ]]; then
fold $uid
elif [[ $mode == "unfold" ]]; then
unfold $uid
elif [[ $mode == "toggle" ]]; then
toggle $uid
elif [[ $mode == "del" ]]; then
delq_add $uid
fi
done
# do queued actions
if [[ $mode == "del" ]]; then
delq_process
fi
# remove duplicates where list contains both parent
# and child of parent
if [[ ! -z $processed ]]; then
for uid in ${processed}; do
thisid=$(getid $uid)
parentuid=${taskparent[$thisid]}
# is this NOT the child of something in the list
if [[ -z $parentuid || $processed != *$parentuid* ]]; then
newprocessed="$newprocessed $uid"
fi
done
processed="$newprocessed"
fi
if [[ ! -z $processed ]]; then
# show results
for uid in ${processed}; do
showit $uid
done
fi
fi
fi
if [[ $AUTOSYNC -eq 1 && $mode != "sync" ]]; then
tasksync -a
fi