180 lines
5.5 KiB
Python
180 lines
5.5 KiB
Python
#!/bin/sh
|
|
# encoding: utf-8
|
|
from __future__ import print_function
|
|
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
|
|
''''which python >/dev/null 2>&1 && exec python "$0" "$@" # '''
|
|
''''exec echo "Error: I can't find python anywhere" # '''
|
|
# Author: Tim Pulver
|
|
# Date: 2015
|
|
# https://github.com/timpulver/todo.txt-graph
|
|
#
|
|
# Visualizes the completed tasks in todo.txt with horizontal bar graphs
|
|
# Items are coloured green when there are more than 5 completed tasks per day,
|
|
# grey otherwise to motivate completing stuff.
|
|
# Have a look at Jerry Seinfeld's "Don't break the chain" methodoligy:
|
|
# https://dontbreakthechain.com/what
|
|
# You can change the threshold by adding this line to your todo.cfg file:
|
|
# export TODOTXT_GRAPH_THRESHOLD=123
|
|
#
|
|
# Params:
|
|
# - TODO_TXT_PATH: Where the todo.txt / done.txt file is located
|
|
# - NUMBER_OF_DAYS: How many days should be visualized (optional, default: 7)
|
|
|
|
import datetime
|
|
import sys
|
|
import os
|
|
import re
|
|
import collections
|
|
|
|
TICK_CHAR = '■'
|
|
DEFAULT_THRESHOLD = 5 # grey bar < DEFAULT_THRESHOLD >= green bar
|
|
WIDTH = 60 # Graph width
|
|
DONE = "done.txt"
|
|
TODO = "todo.txt"
|
|
|
|
# try to use xrange, which is faster
|
|
try:
|
|
range = xrange
|
|
except NameError:
|
|
pass
|
|
|
|
|
|
|
|
# Bash color codes and color helper class from:
|
|
# https://github.com/emilerl/emilerl/blob/master/pybash/bash/__init__.py
|
|
RESET = '\033[0m'
|
|
CCODES = {
|
|
'black' :'\033[0;30m',
|
|
'blue' :'\033[0;34m',
|
|
'green' :'\033[0;32m',
|
|
'cyan' :'\033[0;36m',
|
|
'red' :'\033[0;31m',
|
|
'purple' :'\033[0;35m',
|
|
'brown' :'\033[0;33m',
|
|
'light_gray' :'\033[0;37m',
|
|
'dark_gray' :'\033[0;30m',
|
|
'light_blue' :'\033[0;34m',
|
|
'light_green' :'\033[0;32m',
|
|
'light_cyan' :'\033[0;36m',
|
|
'light_red' :'\033[0;31m',
|
|
'light_purple' :'\033[0;35m',
|
|
'yellow' :'\033[0;33m',
|
|
'white' :'\033[0;37m',
|
|
}
|
|
|
|
# A helper class to colorize strings
|
|
class Colors(object):
|
|
def __init__(self, state = False):
|
|
self.disabled = state
|
|
|
|
def disable(self):
|
|
self.disabled = True
|
|
|
|
def enable(self):
|
|
self.disabled = False
|
|
|
|
def __getattr__(self,key):
|
|
if key not in CCODES.keys():
|
|
raise AttributeError("Colors object has no attribute '%s'" % key)
|
|
else:
|
|
if self.disabled:
|
|
return lambda x: x
|
|
else:
|
|
return lambda x: RESET + CCODES[key] + x + RESET
|
|
|
|
def __dir__(self):
|
|
return self.__class__.__dict__.keys() + CCODES.keys()
|
|
|
|
|
|
|
|
# Graph based on:
|
|
# https://github.com/mkaz/termgraph/blob/master/termgraph.py
|
|
def print_blocks(label, count, step):
|
|
# when nothing has been done, step equals 0 so the number
|
|
# of blocks/ticks can't be computed but also doesn't
|
|
# matter, hence set blocks = 0 in that case.
|
|
blocks = int(count / step) if step > 0 else 0
|
|
print("{}: ".format(label), end="")
|
|
threshold = int(os.getenv('TODOTXT_GRAPH_THRESHOLD', DEFAULT_THRESHOLD))
|
|
|
|
c = Colors()
|
|
|
|
for i in range(blocks):
|
|
if count >= threshold:
|
|
sys.stdout.write(c.green(TICK_CHAR))
|
|
else:
|
|
sys.stdout.write(c.light_gray(TICK_CHAR))
|
|
|
|
# format the text (number of tasks done)
|
|
if count < 10:
|
|
print("{:>2.0f}".format(count))
|
|
elif count < 100:
|
|
print("{:>3.0f}".format(count))
|
|
else:
|
|
print("{:>4.0f}".format(count))
|
|
|
|
# Initialize dictionary with all days to also display days
|
|
# where no tasks were completed
|
|
def initialize_dic(cutoffDays = 7):
|
|
base = datetime.datetime.today().date()
|
|
dic = {(base - datetime.timedelta(days=x)).isoformat() : 0
|
|
for x in range(0, cutoffDays)}
|
|
return dic
|
|
|
|
# Based on Lately Addon:
|
|
# https://github.com/emilerl/emilerl/tree/master/todo.actions.d
|
|
def main(todo_file, done_file, cutoffDays = 7):
|
|
lines = []
|
|
files = [todo_file, done_file]
|
|
for filename in files:
|
|
with open(filename, 'r') as f:
|
|
lines.extend(f.readlines())
|
|
today = datetime.datetime.today()
|
|
cutoff = today - datetime.timedelta(days=cutoffDays)
|
|
dic = initialize_dic(cutoffDays)
|
|
|
|
for line in lines:
|
|
m = re.match("x ([\d]{4}-[\d]{2}-[\d]{2}).*", line)
|
|
if m is not None:
|
|
done = m.group(1)
|
|
year, month, day = m.group(1).split("-")
|
|
completed = datetime.datetime(int(year),int(month),int(day))
|
|
if completed >= cutoff:
|
|
#print c.green(m.group(1)) + " " + c.blue(line.replace("x %s" % m.group(1), "").strip())
|
|
if m.group(1) in dic:
|
|
oldVal = dic.get(m.group(1), 1)
|
|
dic.update({m.group(1): oldVal+1})
|
|
else:
|
|
dic[m.group(1)] = 1
|
|
|
|
# find out max value
|
|
max = 0
|
|
for key, value in dic.items():
|
|
if value > max:
|
|
max = value
|
|
|
|
maxf = float(max)
|
|
step = maxf / WIDTH
|
|
|
|
orderedDic = collections.OrderedDict(sorted(dic.items()))
|
|
|
|
# display graph
|
|
print()
|
|
for key, value in orderedDic.items():
|
|
print_blocks(key, value, step)
|
|
print()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) < 3:
|
|
print("Usage: graph.py [TODO_FILE] [DONE_FILE] <days back>")
|
|
sys.exit(1)
|
|
|
|
if os.path.isfile(sys.argv[1]) and os.path.isfile(sys.argv[2]):
|
|
if len(sys.argv) == 4:
|
|
main(sys.argv[1], sys.argv[2], int(sys.argv[3]))
|
|
else:
|
|
main(sys.argv[1], sys.argv[2])
|
|
else:
|
|
print("Error: %s or %s doesn't exist" % (sys.argv[1], sys.argv[2]))
|
|
sys.exit(1)
|