Commit Diff


commit - /dev/null
commit + 3767d523ff012f697d21cc2795f82f42c5f0abad
blob - /dev/null
blob + 30c3791c3dfc584693b6928f9f6191f5ee5859e6 (mode 644)
--- /dev/null
+++ painlist
@@ -0,0 +1,256 @@
+#!/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