Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 279 lines (247 sloc) 9.67 KB
#!/bin/bash
#
# commandline asciii kanban board for minimalist productivity bash hackers (csv-based)
#
# Usage:
#
# kanban add # add item interactive (adviced)
# kanban show [status] .... # show ascii kanban board [with status]
# kanban <id> # edit or update item
# kanban <id> <status> # update status of todo id (uses $EDITOR as preferred editor)
# kanban <status> ..... # list only todo items with this status(es)
# kanban list # list all todos (heavy)
# kanban tags # list all submitted tags
# kanban add <status> <tag> <description> # add item (use quoted strings for args)
# kanban stats <status|tag> [<str>] # generates stats
#
# NOTE #1: statuses can be managed in ~/.kanban.conf
# NOTE #2: the database csv can be found in ~/.kanban.csv
#
# Examples:
#
# kanban add TODO projectX "do foo"
# kanban TODO DOING HOLD
# kanban stats status projectX
# kanban stats tag projectX
#
# Environment:
#
# You can switch context (e.g. work vs home vs project x ) like so:
#
# KANBANFILE=~/.kanban.foo.csv kanban show
# KANBANFILE=~/.kanban.foo.csv KANBANCONF=~/.kanban.foo.conf kanban show
#
# KANBANFILE env-var is not needed when a .kanban.csv file is present in the current working dir
# KANBANCONF is created automatically if not found
#
# Copyright (C) 2015, Leon van Kammen / Coder of Salvation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
BOX=(┌ ─ ┐ └ ┘ │ ┤ ┴ ┬ ├ ┼)
BAR=(▁ ▂ ▃ ▄ ▅ ▆ ▆ ▇ ▇ '' '')
TMP=~/.kanban.tmp
locale | grep -q "UTF-8" && UTF8=1
X=$(tput cols) # get size of terminal window
Y=$(tput lines) #
#SMALLSCREEN=('HOLD' 'DOING' 'CODE') # uncomment to get simplified kanban board
[[ ! -n $EDITOR ]] && {
which nano &>/dev/null && EDITOR=nano
which pico &>/dev/null && EDITOR=pico
which vi &>/dev/null && EDITOR=vi
which vim &>/dev/null && EDITOR=vim
which emacs &>/dev/null && EDITOR=emacs
}
[[ ! -n "${KANBANCONF}" ]] && KANBANCONF=~/".kanban.conf"
if [[ ! -n "${KANBANFILE}" ]]; then
[[ -f ".kanban.csv" ]] && KANBANFILE="$(pwd)/.kanban.csv"
[[ ! -n "${KANBANFILE}" ]] && KANBANFILE=~/".kanban.csv";
fi
[[ ! -f "${KANBANFILE}" ]] && echo '"status","tag","description","history","date"' > "${KANBANFILE}"
config_example="# kanban config file
# kanban config file
statuses=('BACKLOG' 'HOLD' 'DOING' 'CODE' 'DONE')
# maximum amount of todos within status (triggers warning when exceeds)
declare -a maximum_todo
maximum_todo['HOLD']=5
maximum_todo['DOING']=4
maximum_todo['CODE']=3
"
# usage: fb put <x> <y> <string>
put() { printf "\x1B["$2";"$1"f$3" "$4"; }
strtoline(){ for((i=0;i<${#1};i++)); do printf "$2"; done; }
draw_line(){
for((i=0;i<$1;i++)); do printf "-"; done
}
# usage: fb box <x> <y> <width> <height>
draw_topline(){
w=$((X-2));
printf ${BOX[0]}; draw_line $w; printf "${BOX[2]}\n"
}
createconfig(){
[[ -f "${KANBANCONF}" ]] && {
read -p "overwrite current config? (y/n)" overwrite;
[[ ! "$overwrite" == "y" ]] && echo "aborted" && exit 1;
}
echo "$config_example" > "${KANBANCONF}"
}
tags(){
cat "${KANBANFILE}" | awk -F',' '{ print $2 }' | sed 's/,.*//g;s/"//g' | tail -n+2 | sort | uniq | tr '\n' ' '
}
get_statuses(){
echo ${statuses[@]}
}
add_interactive(){
echo "enter description:"
read -p "> " description
echo "enter one of statuses: ${statuses[@]}"
read -p "> " status
echo "enter one of tags: $(tags)"
read -p "> " tag
add "$status" "$tag" "$description"
}
add(){
[[ ! -n $1 ]] && { add_interactive "$@"; return 0; }
[[ ! "${statuses[*]}" =~ "$1" ]] && echo "invalid status $1 (possible: ${statuses[*]})" && exit 1
csvline="$( for col in "$@"; do printf "%s" "\"$col\","; done )\"${1:0:1}\",\"$(get_current_date)\"\""
echo "${csvline:0:$((${#csvline}-1))}" >> "${KANBANFILE}"
}
stats(){
[[ ! -n $1 ]] && exit 1
create_index
field=$1; shift; tags="$*"
greppattern="(${tags// /\|})"
[[ "$field" == "status" ]] && field=2
[[ "$field" == "tag" ]] && field=3
[[ "$field" == "history" ]] && field=5
[[ -n $2 ]] && WIDTH=$2 || WIDTH=20;
[[ -n $3 ]] && PADDING=$3 || PADDING=20;
{
if [[ -n $PADDING ]]; then
cat "${TMP}".index | grep -E "$greppattern" | gawk -vFS='^"|","|"$|",|,"|,' '{h[$'"$field"']++}END{for(i in h){print h[i],i|"sort -rn|head -20"}}' |gawk '!max{max=$1;}{r="";i=s='$WIDTH'*$1/max;while(i-->0)r=r"'"${BAR[5]}"'";printf "%'$PADDING's %5d %s %s",$2,$1,r,"\n";}'
else
cat "${TMP}".index | grep -E "$greppattern" | gawk -vFS='^"|","|"$|",|,"|,' '{h[$'"$field"']++}END{for(i in h){print h[i],i|"sort -rn|head -20"}}' |gawk '!max{max=$1;}{r="";i=s='$WIDTH'*$1/max;while(i-->0)r=r"'"${BAR[5]}"'";printf "%s %s: %5d\n",r,$2,$1;}' | tr -s " "
fi
} | grep -v 'tag\|status\|history\|-[ ]\+1' | grep -v '^[ ]\+1' # remove header rows
}
init(){
trap "tput cnorm -- normal" 0 1 5 # reset terminal colors to normal
}
list(){
tags="$*"
greppattern="(${tags// /\|})"
create_index
cat "${TMP}".index | grep -E "$greppattern" | sort -k2 -t, | head -n-1 | HEADER='"id","status","tag","description","history","start","touched"\n"-","-","-","-","-"'"\n" printcsv 6 | cut -c 1-$(tput cols)
rm "${TMP}".*
}
do_alert(){
nlines=$(echo "$1" | wc -l ) ; maximum=$2
if [[ ${#maximum} > 0 ]] && [[ $nlines > $maximum ]]; then
echo -e "\n\n!!! alert: number of '$3' items (max $maximum) slows down average productionrate !!!" >> "$4"
fi
}
create_index(){
rm "${TMP}".index &>/dev/null
cat -n "${KANBANFILE}" | sed 's/^[ ]\+//g;s/\t/,/g' >> "${TMP}".index
}
columnize(){
i=1; lines="$(cat -)"; header="$( echo "$lines" | head -n0 )"; output="";
rm "${TMP}".col.* &>/dev/null
echo -e "$header";
for status in "${statuses[@]}"; do
[[ -n $SMALLSCREEN ]] && ! [[ "${SMALLSCREEN[@]}" =~ $status ]] && continue
echo -e "|_$status\n|+$(strtoline "$status" "~")~\n" > "${TMP}".col.$i
cat "${TMP}".index | grep "$status" | printcsv 5 | sed 's/["]\?'$status'["]\?//g' | sed 's/^/| /g' | unexpand >> "${TMP}".col.$i
#echo "$( cat "${TMP}".col.$i )" > "${TMP}".col.$i
i=$((i+1))
done
pr -m -t -w$X "${TMP}".col.* | lines
rm "${TMP}".col.* # print and cleanup
}
lines(){
if [[ -n $UTF8 ]]; then
cat - | sed 's/| /├╴/g;s/~/━/g;s/|+/┕━/g;s/|_/│ /g' # nice utf8 lines
else cat - | sed 's/~/-/g;s/|+/|/g;s/|+/| /g;s/|_/| /g'; fi
}
align(){
cat - | awk '{ for(i=3;i<=NF;i++){ $2=$2" "$i } printf "%-5s %s\n", $1,$2 ; }'
}
show(){
[[ ! -f "${KANBANFILE}" ]] && touch "${KANBANFILE}"
create_index
if [[ -n $1 ]]; then
statuses=(); for status in $*; do statuses+=($status); done
fi
{
echo "$1"
if [[ -n $1 ]]; then cat "${TMP}".index | grep "$1"; else cat "${TMP}".index; fi
} | columnize | more
echo ""
}
get_current_date(){ date "+%Y-%m-%d@%H:%M"; }
update_item_status(){
item="$( cat "${KANBANFILE}" | awk "{ if (NR==$1) print \$0 }" )"
[[ ${#item} == 0 ]] && echo "item $1 not found" && exit 1
if [[ -n "$2" ]]; then # status change
status="$(echo "$item" | awk -F',' '{ print $1 }' | sed 's/"//g' )"
flags="$(echo "$item" | awk -F',' '{ print $4 }' | sed 's/"//g' )"
dates="$(echo "$item" | awk -F',' '{ print $5 }' | sed 's/"//g' )"
newflags="$flags${2:0:1}"
newdates="$dates $(get_current_date)"
[[ "$2" =~ "DONE" ]] && date="$(get_current_date)"
newitem="$item"
newitem="${newitem/$status/$2}"
newitem="${newitem/$flags/$newflags}"
newitem="${newitem/$dates/$newdates}"
KANBANITEMS="$(<$KANBANFILE)"
echo "${KANBANITEMS//"$item"/"$newitem"}" > "${KANBANFILE}"
echo "$status -> $2"
fi
}
update_item(){
item="$( cat "${KANBANFILE}" | awk "{ if (NR==$1) print \$0 }" )"
[[ ${#item} == 0 ]] && echo "item $1 not found" && exit 1
status="$(echo "$item" | awk -F',' '{ print $1 }')"
echo '#
# STATUSES ARE: '${statuses[*]}'
#
'"$item" > "${TMP}".update
${EDITOR} "${TMP}".update
KANBANITEMS="$(<$KANBANFILE)"
newitem="$(cat "${TMP}".update | tail -n1 )"
echo "${KANBANITEMS//"$item"/"$newitem"}" > "${KANBANFILE}"
echo "updated item $1"
}
printcsv(){
csv="$HEADER$(cat -)"
[[ ! -n $1 ]] && max=999999 || max=$1
[[ ! -n $2 ]] && min=1 || min=$1
echo -e "$csv" | sed 's/,"",/," ",/g' | gawk -vFS='^"|","|"$|",|,"' \
'{out=""; for(i='$min';i<NF+1&&i<max;i++) out=out"\t"$i; print out }' \
max=$max | sed 's/""/"/g' | column -t -s $'\t'
}
# source config
[[ ! -f "${KANBANCONF}" ]] && { createconfig; }
source "${KANBANCONF}"
# execute main
init
if [[ -n "$1" ]]; then
[[ "${statuses[*]}" =~ "$1" ]] && { list "$@" ; exit 0; }
case "$1" in
[0-9]*) [[ -n $2 ]] && [[ "${statuses[*]}" =~ "$2" ]] && update_item_status "$@" && exit 0
update_item "$@"
;;
*) "$@"
;;
esac
else grep -A29 "^# Usage:" "$0" | sed 's/^# //g'; fi