#!/bin/sh # clikan.sh -- CLI Kanban ver='2022-09-08/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 <"$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 <"$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"` echo ::::: $cl ::::: $edate # if date field exists and is lessequal than today if test -n "$edate" -a "$edate" -le "$stoday" # save kanban entry for later then echo "$cl" >> $buff 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 <>$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 } ;; h) 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