clikan/clikan.sh

529 lines
13 KiB
Bash
Executable File

#!/bin/sh
# clikan.sh -- CLI Kanban
ver='2022-09-14/HB9KNS'
conf="${CLIKANCONF:-$HOME/.clikanconf}"
defkanban="$HOME/clikanban.md"
editor="${CLIKANEDIT:-$VISUAL}"
editor="${editor:-$EDITOR}"
editor="${editor:-ed}"
myshell='/bin/sh -c'
filtr='.'
# pattern for target date and estimation: @YYYYMMDD:U.U
# (contains two groups in parentheses)
# if changed, also adjust line after TEPATT comment further down!
tepatt=' @\([0-9][0-9]*\):\([0-9][.0-9]*\)'
buff=`mktemp -t clikanbXXXXXX` || buff=${TMPDIR:-/tmp}/clikanb$$`date +%S%M%d`
self=`mktemp -t clikantXXXXXX` || self=${TMPDIR:-/tmp}/clikant$$`date +%S%M%d`
chmod 600 $self $buff
: >$self
if test ! -f "$conf"
then cat <<EOT
no config found, generating default config file $conf
and default kanban file $defkanban
(this should happen only once at first use)
EOT
cat <<EOT >"$conf"
# configuration file for $0
# (autogenerated at `date`)
# file name can be defined by env.var CLIKANCONF
#
# prompt is displayed when waiting for command
prompt |<
# maxshow defines the maximum number of cards/lines to be displayed
maxshow 23
# sortdir defines order of non-calendar kanban cards
# and can be one of up, down or none (default)
sortdir up
# if newdate is 1, yes or true (actually, begins with'[1tTyY]'),
# then the current date will be appended to new cards/entries
newdate true
# planunit defines the name used for units (purely informative)
planunit h/day
# total units per period of scheduled tasks, e.g hours per day
tunits 24
# planpart defines the percentage of time available for working on
# scheduled/planned tasks (integer value), for example
# '10' means 10% of total time is usable, like 2.4 h/day
# (tunits*planpart/100)
planpart 10
# kanban defines a kanban file, may be given several times
# kanban /some/path/to/jobkanban.txt
# kanban /another/path/to/privatekanban.md
kanban $defkanban
EOT
if test -f "$defkanban"
then cp "$defkanban" "$defkanban.bak"
echo found existing $defkanban,
echo saved as backup $defkanban.bak
fi
cat <<EOT >"$defkanban"
# my kanban file
* learn to use clikan
- improve clikan
+ install clikan
### titles work as comments
# might be a future card you don't yet want in waiting
EOT
fi
# get lines beginning with a value, and remove that column
# note SPC&TAB in patterns: make sure there is a SPC,
# and that the value is complete
getlines(){
sed -e 's/$/ /' | grep "^$1[ ]" | { while read _ values
# remove trailing added TAB
do echo "${values% }"
done
}
}
# extract date part of tepatt from argument
getsdate() {
local dp
dp=`echo "$*" | sed -e "s/.*$tepatt.*/\1/"`
if test "$dp" = "$*"
then echo ''
else echo $dp
fi
}
# display selection, and put selection list in selection file,
# input through stdin, with lines prepended with index number, TAB and arg1
showselect(){
local i
i=1
: >$self
while read l
do echo $i: $l
echo "$i $1$l" >>$self
i=$(( $i+1 ))
done
}
# get input from selection (showselect must be called before)
getselect(){
if test "$1" != ""
then echo "choice? (default: $1)" >&2
else echo "choice?" >&2
fi
read i
if test "$i" = ""
then echo "$1"
else getlines $i <$self
fi
}
# read config, set default values if missing
kbs="`getlines kanban <"$conf"`"
currkan="`echo "$kbs" | head -n 1`"
prompt=`getlines prompt <"$conf" | head -n 1`
maxshow=`getlines maxshow <"$conf" | head -n 1`
maxshow=${maxshow:-22}
sortdir=`getlines sortdir <"$conf" | head -n 1`
sortdir=${sortdir:-none}
nd=`getlines newdate <"$conf" | head -n 1`
case $nd in
1|[yY]*|[tT]*) newdate=true ;;
*) newdate=false ;;
esac
planpart=`getlines planpart <"$conf" | head -n 1`
planpart=${planpart:-10}
planunit=`getlines planunit <"$conf" | head -n 1`
planunit=${planunit:-h/day}
tunits=`getlines tunits <"$conf" | head -n 1`
tunits=${tunits:-24}
showprompt(){
if test "$prompt" != ""
then printf '%s ' "$prompt"
fi
}
# conditional sort
condsort(){
case $sortdir in
up) sort ;;
down) sort -r ;;
*) cat ;;
esac
}
# display all 'something' cards
showall(){
local f t ms
ms=$maxshow
case $1 in
d*) f='*' ;;
w*) f='-' ;;
# for archive/done, show a LOT of archived stuff
a*) f='+' ; ms=99999 ;;
*) f='' ;;
esac
echo
echo " $1 // `date`"
if test "$filtr" != "."
then echo " ($filtr)"
fi
if test "$f" = "*"
then t=`date +%m-%d`
# for "doing", sort filtered cards
cat $kbs | grep -i -e "$filtr" | getlines "$f" | condsort
# and also display '- ...(MM-DD)...' calendar entries
cat $kbs | grep "^[*+-] .*($t)" | sed -e 's/^. //'
# for waiting and archive, filter but keep saved card order
else cat $kbs | grep -i -e "$filtr" | getlines "$f"
# for all cases, select the cards and show the beginning of the list
fi | showselect "$f " | head -n $ms
}
# replace lines (in any kanban) containing some string
# arg.1=string, remainder=new line contents
repline(){
local strg newl
strg="$1"
shift
newl="$*"
for ff in $kbs
do cat "$ff" > $buff
cat $buff | { while read oldl
do if test "`echo \"$oldl\"`" = "$strg"
then echo "$newl"
else echo "$oldl"
fi
done
} > "$ff"
done
}
# put waiting cards of today or older into doing state
dotodays(){
local edate
: > $buff
# scan all kanbans for waiting entries (-) with matching dates
cat $kbs | getlines - | { while read cl
do
edate=`getsdate "$cl"`
# only process lines with date field
if test X$edate != X
then
# (if-thens splitted due to impossible "-le" comparison if edate is empty)
# if today or earlier
if test $edate -le $stoday
# save kanban entry for later
then echo "$cl" >> $buff
echo ": now doing $cl"
fi
fi
done
# if at least one entry was saved
if test -s $buff
then cat $buff | { while read cl
# change flag in all kanbans from waiting (-) to doing (*)
do repline "- $cl" "* $cl"
done
}
fi
}
}
showhelp(){ cat <<EOH
$0 : command line kanban
($ver)
configuration file: $conf
for help about commands, just hit RETURN
to show configuration, enter 'c'
EOH
read k
case $k in
c*) cat <<EOT
CLIKANCONF=$CLIKANCONF
configfile=$conf
editor=$editor
maxshow=$maxshow
configfile contents:
EOT
sed -e 's/^/ /' $conf
;;
*) cat <<EOH
command keys:
q: quit
d/w/a: show 'doing/waiting(todo)/archive(done)' cards
k: select target kanban for entry of new cards
n: add new card to target kanban, separated by whitespace
(e.g "n buy present!")
done N: archive card N as done
wait N: put card N back to waiting/todo state
do N: put card N into doing state
note: done/wait/do accept several arguments separated by white space
f F: filter for grep pattern F (case insensitive, empty='.', use _ for space)
p: show planning (time scheduling) for active (doing) cards containing
@YYMMDD/U.U target date (YYMMDD) and expected duration (U.U)
u N U.U [[YY]MMDD]: update expected duration for card N and
optionally set new target date [YY]MMDD
(asks for date if card is missing @YYMMDD)
t: put waiting cards with target date of today or older into doing state
ec: directly edit config file -- DANGER!
ek: directly edit kanban file -- DANGER!
!: execute arguments with "$myshell"
note: if the command does not match any of the
internal commands, and starts with a letter,
you can even omit the leading exclamation mark!
EOH
;;
esac
}
# simple date number calculation:
# assume each month has 30 days
datnum() {
local dn ds
# normalize to YYYYMMDD
if test "${1%????}" = ""
then ds=`date +%Y`$1
elif test "${1%??????}" = ""
then ds=$century$1
else ds=$1
fi
# 12*YYYY
dn=$(( 12*${ds%????} ))
# remove leading YYYY
ds=${ds#????}
# add 1MM, subtract 100, multiply by 30
dn=$(( 30*($dn+1${ds%??}-100) ))
# add 1DD, subtract 100
dn=$(( $dn+1${ds#??}-100 ))
echo $dn
}
showall doing
showprompt
while read com coa1 coa2 coa3 coar
do
year=`date +%y`
longyear=`date +%Y`
century=${longyear%??}
month=`date +%m`
day=`date +%d`
today=$year-$month-$day
stoday=$century$year$month$day
case $com in
q) break ;;
d) showall doing ;;
w) showall waiting/todo/backlog ;;
a) showall archived/done ;;
t) dotodays ;;
do|wait|done) if test "$coa1" = ""
then echo "card number?"
read coa1
fi
coar=`echo "$coa1 $coa2 $coa3 $coar" | tr -cd ' 0-9' | sed -e 's/ *$/ /'`
if test "$coar" = " "
then coar=''
fi
while test "$coar" != ""
do
coa1=`getlines ${coar%% *} <$self`
stamp=''
case $com in
done) coa2='+'
stamp=" // $today"
;;
wait) coa2='-' ;;
*) coa2='*' ;;
esac
coa3="$coa2${coa1#?}"
repline "$coa1" "$coa3$stamp"
coar=${coar#* }
done
showall doing
;;
k) echo "$kbs" | showselect
currkan=`getselect $currkan`
showall doing
;;
n) newc=`echo "$coa1 $coa2 $coa3 $coar" | sed -e 's/ *$//'`
if test "$newc" = ""
then
echo enter new card for kanban file $currkan :
read newc
else echo adding card for kanban file $currkan
fi
if test $newdate = true
then stamp=" // $today"
else stamp=''
fi
if test "$newc" != ""
then echo '*' "$newc$stamp" >>$currkan
else echo no content found, nothing added
fi
showall doing
;;
ec) echo calling "$editor $conf" ...
"$editor" "$conf"
echo please quit and restart to reload config ;;
ek) echo calling "$editor $currkan" ...
"$editor" "$currkan" ;;
f) filtr="${coa1:-.}"
echo "filtering for grep pattern '$filtr' (_ means SPC)"
filtr=`echo "$filtr" | sed -e 's/_/ /g'` ;;
p) echo "## planning for $planpart% of $tunits $planunit"
# generate list, but hidden
showall doing >/dev/null
# total current capacity in 0.1 units, to calculate limit
# (planpart is in %=0.01 units, divide by 10 to get in 0.1)
cap=$(( $tunits*$planpart/10 ))
now=`datnum $stoday`
echo "$planunit task"
# get all tasks with planning information, sorted
grep "$tepatt" $self | sort -k 2 | { while read nr flag task
do
# set weight for assigned time depending on priority value
case $task in
1*) w8=8 ;;
2*) w8=6 ;;
3*) w8=4 ;;
4*) w8=2 ;;
5*) w8=1 ;;
*) w8=3 ;;
esac
# get target date number of task, and normalize
tdate=`getsdate "$task"`
tnum=`datnum $tdate`
# difference in days
td=$(( $tnum-$now ))
# set minimum delta and mark
if test $td -gt 0
then tmark=' '
else
td=1
tmark='*'
fi
# get estimated units for task, append '.0' to force decimal notation,
# remove dot, keep one decimal and truncate additional figures
# (i.e multiply by 10), remove leading 0s
tunits=`echo "$task" | sed -e "s/.*$tepatt.*/\2/;s/$/.0/;s/[.]\([0-9]\).*/\1/;s/^0*//"`
# if empty, set to 0
tunits=${tunits:-0}
# assign 2*weight/8*tunits/days per task, but not more than estimated units
tcap=$(( $w8/4*$tunits/$td ))
if test $tcap -gt $tunits
then tcap=$tunits
# or at least 0.1
elif test $tcap -lt 1
then tcap=1
fi
# result: today's amount / (mark) / task nr / task
tres="$tcap $tmark$nr: $task"
if test $tcap -gt 99
then
# remove trailing figure for values of 10 (100) or higher
echo "$tres" | sed -e 's/. / /'
else
# for smaller, insert decimal and prepend 0 if missing
echo "$tres" | sed -e 's/\(.\) /.\1 /;s/^[.]/0./'
fi
if test $cap -gt 0
then cap=$(( $cap-$tcap ))
if test $cap -le 0
then echo ' (capacity limit reached)'
fi
fi
done
}
;;
u) if test "$coa1" = ""
then echo "card number?"
read coa1
fi
if test "$coa1" = ""
then showprompt
continue
fi
coa1=`echo "$coa1" | tr -cd '0-9'`
oldl=`getlines $coa1 <$self`
newl="$oldl"
echo ": ${oldl#??}"
if test "$coa2" = ""
then echo "new estimated time requirement?"
read coa2
fi
coa2=`echo "$coa2" | tr -cd '.0-9'`
if test "$coa2" = ""
then showprompt
continue
fi
# arg.3 will override existing target date
if test "$coa3" = ""
then
# search for ' @YYYYMMDD/U.U' and get YYYYMMDD
odat=`getsdate "$oldl"`
# if nothing found, ask for target date
if test "$odat" = "$oldl"
then
echo "target date [YYYY]MMDD?"
read coa3
# add dummy for later pattern replacement
newl="$newl @1:2"
else
coa3=$odat
fi
else
# remove old pattern and add dummy for later
newl=`echo "$oldl" | sed -e "s/\\(.*\\)$tepatt\\(.*\\)/\\1 @1:2\\4/"`
# force if nothing yet found
if test "$newl" = "$oldl"
then newl="$newl @1:2"
fi
fi
coa3=`echo $coa3 | tr -cd '0-9'`
if test "$coa3" = ""
then coa3=$stoday
echo "(empty/malformed date, using today=$coa3 instead)"
fi
# handle [YY]MMDD dates
# prepend 1 to prevent interpretation of leading 0 as octal
# (use 9999 instead of 1231 to cope with bogus inputs)
if test 1$coa3 -le 19999
then
# compare with today's MMDD
if test 1$coa3 -lt 1$month$day
# before, use next year, else this year
then coa3="$(( 1+$century$year ))$coa3"
else coa3="$century$year$coa3"
fi
elif test 1$coa3 -le 1999999
# prepend century if missing
then coa3=$century$coa3
fi
# TEPATT: adjust replacement string if $tepatt changed!
# (last reference is group 4 because of two groups in tepatt)
coar=`echo "$newl" | sed -e "s/\\(.*\\)$tepatt\\(.*\\)/\\1 @$coa3:$coa2\\4/"`
echo was: "$oldl"
echo now: "$coar"
repline "$oldl" "$coar"
showall doing
;;
!*) echo
$myshell "${com#!} $coa1 $coa2 $coa3 $coar"
echo ;;
[A-Za-z]*) echo
$myshell "$com $coa1 $coa2 $coa3 $coar"
echo ;;
*) showhelp
echo '(hit return to continue)'
read _
showall doing ;;
esac
showprompt
done
echo
rm -f $self $buff