commit - /dev/null
commit + 3767d523ff012f697d21cc2795f82f42c5f0abad
blob - /dev/null
blob + 30c3791c3dfc584693b6928f9f6191f5ee5859e6 (mode 644)
--- /dev/null
+++ painlist
+#!/bin/sh
+# simplistic pain list printer
+# pesco 2016-2019
+#
+# reads issue items on stdin, one per line, not indented. items are arbitrary
+# text, rated by adding ">AxBxC<" anywhere on the line, where A,B,C,... are
+# rating numbers in arbitrary categories. any number of categories can be used.
+#
+# if a pattern argument is given, it is interpreted as an extended regular
+# expression to filter for. the -v option inverts the filter.
+#
+# empty lines and those containing only a shell-style comment (#) are ignored.
+# in-line comments are not recognized; this allows the use of hash-tags.
+#
+# the rating scale is always 1-9; 9 should always be used for the highest level.
+# coarser scales should be mapped to 1-9 by ignoring some levels; for instance,
+# use 1,3,5,7,9 for a 5-level scale. it is recommended to document a clear
+# meaning for each level to make rating easy and unambiguous.
+#
+# an item's pain value is the geometric mean of its ratings.
+#
+# the output sorts items by pain value. a solid line visualizes a configurable
+# threshold (default -t 6). a dotted line shows the weighted median (half of
+# total pain). higher-value items are shown in a brighter shade.
+#
+#
+# == EXTRA FEATURES ==
+#
+# it is suggested to use the following features only in specific circumstances
+# as indicated.
+#
+# - text in square brackets [...] at the beginning of a line is highlighted in
+# the output. this can be used for example to mark items with a planned time.
+#
+# - items can be marked for urgency after time T by adding a tag of the form "!T"
+# where T is given in (fractional) megaseconds after the Unix epoch. yes, it is
+# awkward, but also simple. how to convert using GNU date:
+#
+# $ when() { echo $(($(date +%s -d "$1") / 1e6)); }
+# $ when tomorrow
+# 1459.43703
+#
+# (hint: round to the first decimal, a day is 86.4ks)
+#
+# urgent items are displayed in red. use this feature to flag items before it
+# becomes too late to act on them.
+#
+# - items marked with a tag of the form "^T" will be ignored until time T. use
+# this for items that cannot or should not be acted on before the given time.
+#
+# - indented (non-comment) lines below an item are interpreted as items pending
+# completion of their parent. for each child, a green plus sign is appended
+# to the parent item. pending items are not shown themselves but count toward
+# the pain value of their parent. use this to account for but otherwise hide
+# items that strictly depend on the completion of another.
+
+
+theta=6
+nocolor=1
+maxitems=0 # infinite
+
+# awk program for formatted output
+format='
+ function color(c) {
+ return nocolor? "" : sprintf("\033[3%dm", c)
+ }
+
+ function rgbcolor(r,g,b) {
+ return nocolor? "" : sprintf("\033[38;2;%d;%d;%dm", r,g,b)
+ }
+
+ function grey(l) {
+ return rgbcolor(l,l,l)
+ }
+
+ function reset() {
+ return nocolor? "" : "\033[m"
+ }
+
+ function bold() {
+ return nocolor? "" : "\033[1m"
+ }
+
+ {
+ p=0
+ if(match($0, /\++$/)) { p=RLENGTH }
+ if(substr($0,7,1) == "^") { npost++; npost+=p; next }
+ nplus += p
+ n++
+
+ urgent[n] = (substr($0,7,1) == "!")
+ if(urgent[n]) { nurg++ }
+
+ pain[n] = $1
+ if($1 >= theta) { nhi++ }
+ total += $1
+
+ if($1 == 0) {
+ sub(/^...../, " ")
+ nun++
+ }
+
+ # shorten -tag: fields; and do it without submatch references
+ # i lov^Whate awk :v
+ gsub(/[[:space:]]-[[:alnum:]]+:/, "&<-XXX->&")
+ gsub(/:<-XXX->[[:space:]]-[[:alnum:]]+:[^[:space:]]*/, "...")
+
+ item[n] = $0
+ }
+
+ END {
+ red = 1
+ yellow = 3
+ green = 2
+ white = 7
+
+ if(maxitems && n > maxitems) { n=maxitems }
+ for(i=1; i<=n; i++) {
+ it = item[i]
+ if(urgent[i]) {
+ it = color(red) it
+ } else {
+ if(pain[i] == 0) {
+ base = rgbcolor(235, 167, 231) # light orchid, #EBA7E7
+ } else if(pain[i] > 9) {
+ base = color(white)
+ } else {
+ base = grey(32+(pain[i]-1)*28)
+ }
+ it = base it
+ sub(/[! ] \[[^\]]*\]/, bold() color(yellow) "&" reset() base, it)
+ sub(/\++$/, color(green) "&", it)
+ }
+ print it
+
+ if(i == nhi) {
+ print color(red), "____________________________________" \
+ "_____________________________________"
+ }
+
+ acc += pain[i]
+ if(!half && acc >= total/2) {
+ print color(yellow), "...................................." \
+ "....................................."
+ half = 1
+ }
+ }
+
+ if(NR > 0) {
+ print reset() bold() # adds blank line
+ printf("%.1f pain \t%d active items", total, NR)
+ if(nun) printf(", %d unrated", nun)
+ if(nurg) printf(", %d urgent (%s!%s)", nurg,
+ color(red), reset() bold())
+ if(nplus) printf(" \t%d pending (%s+%s)", nplus,
+ color(green), reset() bold())
+ if(npost) printf(", %d postponed", npost)
+ print reset()
+ }
+ }
+'
+
+if test -t 1 # is stdout a tty?
+then
+ nocolor=0
+ maxitems=$(($(tput li) - 5))
+fi
+
+args=`getopt npvt:l: $*`
+if [ $? -ne 0 ]; then
+ echo "usage: $0 [-npv] [-t THRESHOLD] [-l MAX] [pattern]"
+ exit 1
+fi
+set -- $args
+while [ $# -ne 0 ]; do
+ case "$1" in
+ -n) nocolor=1; shift;;
+ -p) format='{print}'; shift;;
+ -t) theta="$2"; shift; shift;;
+ -v) invert=1; shift;;
+ -l) maxitems="$2"; shift; shift;;
+ --) shift; break;;
+ esac
+done
+
+# collect items and attach pain values
+awk '
+ function product(xs) {
+ p = 1
+ for(i in xs) { p *= xs[i] }
+ return p
+ }
+
+ function score(s) {
+ if(match(s, />[1-9](x[1-9])*</)) {
+ rating = substr(s, RSTART+1, RLENGTH-2)
+ split(rating, xs, /x/)
+
+ return product(xs) ^ (1 / length(xs))
+ }
+ return 0
+ }
+
+ function printit() {
+ #if(item && (up || urg)) {
+ if(item) {
+ if(plus) plus = " " plus
+ printf("%5.1f %s %s%s\n", pain, urg?"!":up?" ":"^", item, plus)
+ }
+ item = ""
+ }
+
+ BEGIN { "date +%s" | getline now }
+
+ # skip comments and blank lines
+ /^[[:space:]]*(#|$)/ { next }
+
+ # filter by pattern argument
+ {
+ if(xor(!match($0, pattern), invert)) next
+ }
+
+ # count pending (indented) items
+ /^[[:space:]]+[^[:space:]#]/ {
+ plus = plus "+"
+ pain = pain + score($0)
+ next
+ }
+
+ # process a new item
+ {
+ printit() # print previous item
+
+ item = $0
+ plus = ""
+ pain = score($0)
+ urg = 0
+ up = 1
+
+ if(match($0, / ![0-9]*(\.[0-9]+)?( |$)/)) {
+ urgtime = substr($0, RSTART+2, RLENGTH-2)
+ urg = (now >= urgtime * 1e6)
+ }
+
+ if(match($0, / \^[0-9]+(\.[0-9]+)?/)) {
+ uptime = substr($0, RSTART+2, RLENGTH-2)
+ up = (now >= uptime * 1e6)
+ }
+ }
+
+ END { printit() }
+' pattern="$*" invert=$invert |
+# sort by pain
+sort -rsn -k 1 |
+# format output
+awk "$format" theta=$theta nocolor=$nocolor maxitems=$maxitems