├── .gitignore ├── bootstrap.py ├── drawme.dot ├── pip.requirements ├── readme.md ├── scripts ├── .gitignore └── recover.py ├── settings.py ├── taskwarrior ├── .gitignore ├── hooks │ ├── on-exit.py │ ├── on-modify.py │ └── utils.py └── taskrc ├── timewarrior ├── .gitignore ├── extensions │ ├── duration.py │ ├── last.sh │ ├── pomo_msg.py │ ├── pomo_stat.py │ ├── toggle.py │ └── utils.py └── timewarrior.cfg └── 读我.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.yaml 4 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import os 4 | import sys 5 | import shutil 6 | 7 | 8 | def mkdir(d): 9 | try: 10 | os.makedirs(d) 11 | except Exception: 12 | pass 13 | 14 | 15 | home = os.path.expanduser('~') 16 | taskd = os.path.join(home, '.task') 17 | taskrc = os.path.join(home, '.taskrc') 18 | timed = os.path.join(home, '.timewarrior') 19 | timecfg = os.path.join(timed, 'timewarrior.cfg') 20 | 21 | # Keep data in a target folder like cloud-disk 22 | if len(sys.argv) > 1: 23 | dst = os.path.realpath(sys.argv[1]) 24 | dtaskd = os.path.join(dst, 'taskwarrior') 25 | dtimed = os.path.join(dst, 'timewarrior') 26 | map(mkdir, (dtaskd, dtimed)) 27 | 28 | # # Migrate existing data of taskwarrior 29 | if os.path.isdir(taskd): 30 | for fn in os.listdir(taskd): 31 | if fn.endswith('.data'): 32 | shutil.copy(os.path.join(taskd, fn), dtaskd) 33 | with open(taskrc, 'a') as file: 34 | file.write('\ndata.location=%s' % dtaskd) 35 | 36 | # # Migrate existing data of timewarrior 37 | timedd = os.path.join(timed, 'data') 38 | dtimedd = os.path.join(dtimed, 'data') 39 | if os.path.isdir(timedd): 40 | shutil.move(timedd, dtimed) 41 | else: 42 | mkdir(dtimedd) 43 | os.path.isdir(timed) or mkdir(timed) 44 | os.symlink(dtimedd, timed) 45 | 46 | taskd = dtaskd 47 | timed = dtimed 48 | 49 | map(mkdir, (taskd, timed)) 50 | 51 | # Install hooks and extensions of Pomodoro-Warriors 52 | base = os.path.dirname(os.path.realpath(sys.argv[0])) 53 | btaskd = os.path.join(base, 'taskwarrior') 54 | btimed = os.path.join(base, 'timewarrior') 55 | btimecfg = os.path.join(btimed, 'timewarrior.cfg') 56 | 57 | # # Backup and setup hooks of taskwarrior 58 | hooks = os.path.join(taskd, 'hooks') 59 | os.path.isdir(hooks) and shutil.move(hooks, hooks + '.bak') 60 | os.symlink(os.path.join(btaskd, 'hooks'), hooks) 61 | with open(taskrc, 'a') as file: 62 | file.write('\ninclude %s/taskrc' % btaskd) 63 | 64 | # # Backup and setup extensions of timewarrior 65 | exts = os.path.join(timed, 'extensions') 66 | os.path.isdir(exts) and shutil.move(exts, exts + '.bak') 67 | os.symlink(os.path.join(btimed, 'extensions'), exts) 68 | with open(timecfg, 'a') as file: 69 | file.write('\n%s' % open(btimecfg).read()) 70 | -------------------------------------------------------------------------------- /drawme.dot: -------------------------------------------------------------------------------- 1 | /* 2 | ## the triggered hooks and inputs/outputs when adding/modifying task(s) 3 | 4 | ``` 5 | # [1] task task1,task2 modify ... 6 | # [2] task add task3 7 | # [3] task next 8 | # 9 | # +---------------------+ 10 | # | on-add | 11 | # | [stdin] | 12 | # | added task3 json | 13 | # | [stdout] | 14 | # | added task3 json | 15 | # | feedback text | ---------+ 16 | # +---------------------+ | 17 | # ^ | 18 | # | [2] | 19 | # | | 20 | # +---------------------+ +---------------------+ | 21 | # | on-modify | | | | 22 | # | [stdin] | | | | 23 | # | original task2 json | | on-launch | | 24 | # | modified task2 json | | [stdout] | | 25 | # | [stdout] | | feedback text | | 26 | # | modified task2 json | [1] | | | 27 | # | feedback text | <----- | | -+ | 28 | # +---------------------+ +---------------------+ | | 29 | # | | | | 30 | # | | [1] | | 31 | # | v | | 32 | # | +---------------------+ | | 33 | # | | on-modify | | | 34 | # | | [stdin] | | | 35 | # | | original task1 json | | | 36 | # | | modified task1 json | | | 37 | # | | [stdout] | | | 38 | # | | modified task1 json | | | 39 | # | | feedback text | | | 40 | # | +---------------------+ | | 41 | # | | | | 42 | # | | [1] | [3] | [2] 43 | # | v v v 44 | # | +-------------------------------------+ 45 | # | | on-exit | 46 | # | [1] | [stdout] | 47 | # +--------------------------> | feedback text | 48 | # +-------------------------------------+ 49 | ``` 50 | */ 51 | 52 | digraph { 53 | label="[1] task task1,task2 modify ...\l[2] task add task3\l[3] task next" 54 | labeljust=l; 55 | launch [label=" 56 | on-launch 57 | \l[stdout] 58 | \lfeedback text 59 | "]; 60 | task1 [label=" 61 | on-modify 62 | \l[stdin] 63 | \loriginal task1 json 64 | \lmodified task1 json 65 | \l[stdout] 66 | \lmodified task1 json 67 | \lfeedback text 68 | "]; 69 | task2 [label=" 70 | on-modify 71 | \l[stdin] 72 | \loriginal task2 json 73 | \lmodified task2 json 74 | \l[stdout] 75 | \lmodified task2 json 76 | \lfeedback text 77 | "]; 78 | task3 [label=" 79 | on-add 80 | \l[stdin] 81 | \ladded task3 json 82 | \l[stdout] 83 | \ladded task3 json 84 | \lfeedback text 85 | "]; 86 | exit [label=" 87 | on-exit 88 | \l[stdout] 89 | \lfeedback text 90 | "] 91 | edge [label="[1]"]; 92 | launch -> {task1 task2} -> exit; 93 | edge [label="[2]"]; 94 | launch -> task3 -> exit; 95 | edge [label="[3]"]; 96 | launch -> exit; 97 | } 98 | -------------------------------------------------------------------------------- /pip.requirements: -------------------------------------------------------------------------------- 1 | pyaml==16.12.2 2 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Pomodoro-warriors 2 | 3 | [中文文档](./读我.md) 4 | 5 | ## About 6 | 7 | Pomodoro-warriors is the integration of [taskwarrior](https://taskwarrior.org/docs/) and [timewarrior](https://taskwarrior.org/docs/timewarrior/) which helps you to: 8 | 9 | * Split tasks into smaller ones. 10 | * Track the time spent on every task. 11 | * Do in Pomodoro Mode. 12 | * Review and report in various ways. 13 | 14 | ## Installation 15 | 16 | ### Install to local path 17 | 18 | 1. Run `python2 bootstrap.py`; 19 | 2. Install [taskwarrior](https://taskwarrior.org/download/); 20 | 3. Install [timewarrior](https://taskwarrior.org/docs/timewarrior/download.html). 21 | 22 | ### Install with cloud storage services 23 | 24 | Take OneDrive as an example and suppose you've already installed both taskwarrior and timewarrior: 25 | 26 | ```bash 27 | python2 bootstrap.py ~/OneDrive/task 28 | ``` 29 | 30 | ## Usage 31 | 32 | Since pomodoro-warriors is the integration of [taskwarrior](https://taskwarrior.org/docs/) and [timewarrior](https://taskwarrior.org/docs/timewarrior/), the following usages go with the hypothesis that the readers are skilled at both of them. 33 | 34 | ### 1. `task split ` 35 | 36 | Create a new subtask with `` which blocks the former one. 37 | 38 | If a project name is given to the subtask. The project name of the parent task is prepended to it to make it in hierarchy. 39 | Else, the project name of the subtask is simply inherited from the parent task. 40 | 41 | Therefor, a task with no project cannot be splitted. 42 | 43 | For example. 44 | 45 | ```bash 46 | >>> task add project:test "I'm a parent task" 47 | Created task 1. 48 | >>> task 1 split +next "I'm a child task" 49 | Created task 2. 50 | >>> task 1 split project:sub "I'm another child task" 51 | Created task 3. 52 | >>> task _get 1.depends 53 | b4eb87e6-54f5-422e-939a-f03c673de23e,8dd2e258-525f-4ff0-a7dc-b80fbca8387c 54 | >>> task _get {2,3}.project 55 | test test.sub 56 | ``` 57 | 58 | ### 2. `task timew ...` 59 | 60 | This is a shortcut to execute `timew ... ` which makes the tracking and reporting of tasks much more convenient. 61 | 62 | E.g. `task timew start`. 63 | 64 | ### 3. Pomodoro Mode 65 | 66 | Tracking with a special tag `pomodoro` tells timewarrior that you are in Pomodoro Mode. 67 | 68 | So you can start a Pomodoro when doing a specific task by executing `task timew start pomodoro`. 69 | 70 | ### 4. Reports 71 | 72 | * `timew last`. Show info of current tracking or last tracked task. 73 | * When a task is done or deleted, show it's tracked time. 74 | * `timew pomo_stat`. Export statistics on Pomodoro Mode. 75 | * `timew pomo_msg`. Show current state in Pomodoro Mode. Can be integrated with `tmux` or `powerline`. 76 | * `timew duration`. Output the total duration. 77 | 78 | If you are using [tmux](https://github.com/tmux/tmux) you can append the following line to `~/.tmux.conf`: 79 | 80 | ```bash 81 | set-option -g status-left "#(timew pomo_msg.py :day)" 82 | ``` 83 | 84 | If you are using [powerline](https://github.com/powerline/powerline) you can add this to the segments: 85 | 86 | ```json 87 | { 88 | "function": "powerline.lib.shell.run_cmd", 89 | "priority": 80, 90 | "args": { 91 | "cmd": ["timew", "pomo_msg.py", ":day"] 92 | } 93 | } 94 | ``` 95 | 96 | See branch [ks](https://github.com/cf020031308/pomodoro-warriors/tree/ks) to view my personal reports as examples. 97 | 98 | ### 5. Other improvements 99 | 100 | * `task tiny`. Display tasks in tiny spaces like panes in tmux. 101 | * A User Defined Attribute `estimate` to store an estimate for the costing duration of a task. 102 | * `timew toggle [ ...]`. Start a new track with tags appended to / removed from the tags of current track. 103 | 104 | ## Example Workflow 105 | 106 | Mainly it's the combination of the GTD Theory and the Pomodoro Technology. 107 | 108 | ### Collect 109 | 110 | * `task add ` 111 | 112 | ### Process 113 | 114 | 1. Get stuff with `task -PROJECT` and process the pieces one by one; 115 | 2. Put the measurable goals in annotation by `task annotate ` (or description if it's short) and set project, priority, scheduled, due, etc with `task modify `; 116 | 3. Split the task into detailed subtasks with `task split `. The more actionable the subtasks are the better; 117 | 4. Estimate the number of pomodoros every subtask would take. If any one is going to cost more than 8, keep splitting it. Else, record it with `task modify estimate:`. 118 | 119 | ### Arrange 120 | 121 | * Plan what you want to do at the beginning of a day. Use `task start` or add due dates to make the tasks obvious in your task list. 122 | 123 | ### Do 124 | 125 | * normal way 126 | + `task timew start`. Start a task and track it. 127 | + `timew stop`. Stop tracking a task. 128 | + `task done`. Complete a task and stop tracking it. 129 | * Pomodoro Mode 130 | + `task timew start pomodoro`. Start a pomodoro on a task. 131 | + `timew toggle pomodoro` or `timew stop pomodoro`. Stop a pomodoro and keep tracking the task. 132 | + If something important interrupts, use `task modify +next` to boost its urgency. After the current pomodoro, handle it. 133 | + With the integration of tmux or powerline, the state of Pomodoro Mode is displayed in the status-line. 134 | 135 | ### Review 136 | 137 | * Daily: `timew day` 138 | * Weekly: `timew week` 139 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .report.eml 2 | settings.yaml 3 | -------------------------------------------------------------------------------- /scripts/recover.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Occasionally the majority of data in the files completed.data and 3 | pending.data would be purged. 4 | Maybe it is because of the use of cloud storage I don't know. 5 | Using taskserver or simply git to manage the data is recommended. 6 | And here comes a script to recovery completed.data and pending.data 7 | from backlog.data. 8 | ''' 9 | 10 | import os 11 | import json 12 | import time 13 | import datetime 14 | from collections import OrderedDict 15 | 16 | 17 | def utc2time(date): 18 | return int( 19 | time.mktime( 20 | datetime.datetime.strptime(date, '%Y%m%dT%H%M%SZ').timetuple()) - 21 | time.timezone) 22 | 23 | 24 | def recover(input_folder, output_folder=None): 25 | assert os.path.isdir(input_folder) 26 | if output_folder: 27 | assert os.path.isdir(output_folder) 28 | else: 29 | output_folder = input_folder 30 | tasks = OrderedDict() 31 | with open(os.path.join(input_folder, 'backlog.data')) as f: 32 | for line in f.readlines(): 33 | task = json.loads(line) 34 | for k in task: 35 | try: 36 | task[k] = utc2time(task[k]) 37 | except: 38 | pass 39 | if task.get('tracked') and task['tracked'][0] == 'P': 40 | del task['tracked'] 41 | if task.get('tags'): 42 | task['tags'] = ','.join(task['tags']) 43 | if task.get('annotations'): 44 | for anno in task.pop('annotations'): 45 | task['annotation_%s' % utc2time(anno['entry'])] = anno[ 46 | 'description'] 47 | for k in task: 48 | task[k] = json.dumps( 49 | task[k] if isinstance(task[k], basestring) 50 | else str(task[k]), 51 | ensure_ascii=False) 52 | tasks[task['uuid']] = task 53 | 54 | for path in ('completed.data', 'pending.data'): 55 | with open(os.path.join(input_folder, path)) as f: 56 | for line in f.read().decode('utf8').splitlines(): 57 | row = line.split('" ') 58 | row[0] = row[0][1:] 59 | row[-1] = row[-1][:-2] 60 | task = dict((col + '"').split(':', 1) for col in row) 61 | tasks[task['uuid']] = task 62 | 63 | pending, completed = [], [] 64 | for task in tasks.values(): 65 | _task = '[%s]' % ' '.join( 66 | sorted(['%s:%s' % kv for kv in task.items()])).encode('utf8') 67 | if task['status'] in ('"pending"', '"waiting"'): 68 | pending.append((task['entry'], _task)) 69 | else: 70 | completed.append((task['entry'], _task)) 71 | pending = [x[1] for x in sorted(pending, key=lambda x: x[0])] 72 | completed = [x[1] for x in sorted(completed, key=lambda x: x[0])] 73 | 74 | with open(os.path.join(output_folder, 'completed.data'), 'w') as f: 75 | f.write('\n'.join(completed)) 76 | with open(os.path.join(output_folder, 'pending.data'), 'w') as f: 77 | f.write('\n'.join(pending)) 78 | 79 | 80 | if __name__ == '__main__': 81 | recover( 82 | os.path.join(os.getenv('HOME'), '.task'), 83 | os.path.dirname(os.path.realpath(__file__))) 84 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import yaml 4 | 5 | 6 | # With configured tag `pomodoro` timewarrior tracks time in Pomodoro Mode. 7 | # Concentrating for 25 minutes achieves a pomodoro. 8 | # Have a break for 5 or 30 minutes every time completing 1 or 4 pomodoro(es). 9 | # An interrupt longer than 2 minutes makes the active pomodoro aborted. 10 | # Reset Pomodoro Combo if a break is longer than 12 minutes. 11 | POMODORO_TAG = 'pomodoro' 12 | POMODORO_DURATION = 1500 13 | POMODORO_SHORT_BREAK = 300 14 | POMODORO_LONG_BREAK = 1500 15 | POMODORO_SET_COUNT = 4 16 | POMODORO_ABORT_GAP = 120 17 | POMODORO_COMBO_GAP = 300 18 | 19 | # load settings from settings.yaml, which is ignoed in git 20 | ypath = os.path.join( 21 | os.path.dirname(os.path.abspath(__file__)), 'settings.yaml') 22 | if os.path.isfile(ypath): 23 | with open(ypath) as yfile: 24 | globals().update(yaml.load(yfile)) 25 | -------------------------------------------------------------------------------- /taskwarrior/.gitignore: -------------------------------------------------------------------------------- 1 | *.data 2 | -------------------------------------------------------------------------------- /taskwarrior/hooks/on-exit.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import os 4 | import commands 5 | 6 | import utils 7 | 8 | 9 | RESERVED_TAGS = set('nocolor nonag nocal next'.split()) 10 | 11 | 12 | def main(): 13 | inputs = utils.format_inputs() 14 | args, cmd = inputs['args'], inputs['command'] 15 | if cmd == 'split': 16 | # task split 17 | pre, mods = args.split(cmd, 1) 18 | _id = int(pre.split()[1]) 19 | mods = ( 20 | (mods + ' ') 21 | .replace(' project: ', '') 22 | .replace(' project:', ' project:{}.') 23 | .strip()) 24 | if ' project:' not in mods: 25 | mods += ' project:{}' 26 | subid = commands.getoutput( 27 | 'task _get %s.project | ' 28 | 'xargs -I{} task add %s | ' 29 | 'grep -o "[0-9]\\+"' % 30 | (_id, mods)) 31 | if subid: 32 | print 'Created task %s.' % subid 33 | os.system('task %s modify depends:%s' % (_id, subid)) 34 | else: 35 | print 'You can only split a task when it is a project.' 36 | elif cmd == 'timew': 37 | # task timew ... 38 | pre, timew = args.split(cmd, 1) 39 | _id = int(pre.split()[1]) 40 | tags, proj, uuid = commands.getoutput( 41 | 'task _get %s.tags %s.project %s.uuid' % (_id, _id, _id) 42 | ).split(' ', 2) 43 | tags = [t for t in tags.split(',') if t and t not in RESERVED_TAGS] 44 | while proj: 45 | tags.append(proj) 46 | proj = proj.rpartition('.')[0] 47 | tags.append(uuid) 48 | tags = ' '.join('"%s"' % t for t in tags) 49 | os.system('timew %s %s && task %s start' % (timew, tags, _id)) 50 | 51 | 52 | main() 53 | -------------------------------------------------------------------------------- /taskwarrior/hooks/on-modify.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import json 4 | import commands 5 | 6 | import utils 7 | 8 | 9 | def main(): 10 | inputs = utils.format_inputs() 11 | task = inputs['task'] 12 | ret = [json.dumps(task)] 13 | if 'end' in task and 'end' not in inputs['prior']: 14 | timew = json.loads(commands.getoutput('timew get dom.tracked.1.json')) 15 | cmd = 'timew duration "%(uuid)s" from %(entry)s - %(end)s' % task 16 | if 'end' not in timew and task['uuid'] in timew['tags']: 17 | cmd = 'timew stop :quiet && ' + cmd 18 | if 'estimate' in task: 19 | ret.append( 20 | 'Estimate Duration: %s' % 21 | utils.parse_duration(task['estimate'])) 22 | ret.append('Total Duration: %s' % commands.getoutput(cmd)) 23 | if len(ret) == 1: 24 | ret.append('') 25 | print('\n'.join(ret)) 26 | 27 | 28 | main() 29 | -------------------------------------------------------------------------------- /taskwarrior/hooks/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import re 4 | import os 5 | import sys 6 | import json 7 | import datetime 8 | 9 | 10 | DURATION_PATTERN = re.compile( 11 | r'^P(\d+Y)?(\d+M)?(\d+D)?(?:T(\d+H)?(\d+M)?(\d+S)?)?$') 12 | 13 | 14 | def parse_duration(duration): 15 | ds = DURATION_PATTERN.findall(duration)[0] 16 | assert ds, 'not a duration compliant with ISO-8601: %s' % duration 17 | y, m, d, H, M, S = [int(d[:-1]) if d else 0 for d in ds] 18 | s = (((y * 365 + m * 30 + d) * 24 + H) * 60 + M) * 60 + S 19 | return datetime.timedelta(seconds=s) 20 | 21 | 22 | def format_inputs(): 23 | ''' 24 | // returned inputs example 25 | 26 | { 27 | "version": "2.5.1", 28 | "api": "2", 29 | "rc": "~/.taskrc", 30 | "data": "~/.task/work", 31 | "filename": "on-exit_print.py", 32 | "command": "undo", 33 | "args": "task undo", 34 | "prior": {}, // exists only in on-modify scripts 35 | "task": {} // exists only in on-add/on-modify scripts 36 | } 37 | ''' 38 | inputs = dict(arg.split(':', 1) for arg in sys.argv[1:]) 39 | assert inputs['api'] == '2', 'API: %s is not supported' % inputs['api'] 40 | 41 | filename = os.path.split(sys.argv[0])[-1] 42 | inputs['filename'] = filename 43 | 44 | if filename.startswith('on-add'): 45 | inputs['task'] = json.load(sys.stdin) 46 | elif filename.startswith('on-modify'): 47 | inputs['prior'] = json.loads(sys.stdin.readline()) 48 | inputs['task'] = json.loads(sys.stdin.readline()) 49 | 50 | return inputs 51 | -------------------------------------------------------------------------------- /taskwarrior/taskrc: -------------------------------------------------------------------------------- 1 | verbose=label,new-id 2 | bulk=0 3 | search.case.sensitive=no 4 | 5 | weekstart=Monday 6 | calendar.details=full 7 | calendar.holidays=sparse 8 | 9 | uda.estimate.type=duration 10 | uda.estimate.label=Estimate 11 | 12 | report.tiny.description="a next-like tiny report to display in tiny panels" 13 | report.tiny.columns=id,project,estimate,description.truncated_count 14 | report.tiny.labels=ID,Project,E,Description 15 | report.tiny.filter=status:pending limit:page 16 | report.tiny.sort=urgency- 17 | default.command=tiny 18 | 19 | # placeholders for DIY commands 20 | report.split.columns= 21 | report.timew.columns= 22 | 23 | context.inbox=-PROJECT 24 | context.backlog=+PROJECT -SCHEDULED -ACTIVE 25 | context.kanban=( ( +PROJECT ( +SCHEDULED or +ACTIVE ) ) or due.before:tomorrow ) 26 | -------------------------------------------------------------------------------- /timewarrior/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /timewarrior/extensions/duration.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import datetime 4 | 5 | import utils 6 | 7 | 8 | def main(): 9 | _, entries = utils.format_inputs() 10 | now = datetime.datetime.now() 11 | duration = datetime.timedelta() 12 | for entry in entries: 13 | start = utils.parse_utc(entry['start']) 14 | end = utils.parse_utc(entry['end']) if 'end' in entry else now 15 | duration += (end - start) 16 | print duration 17 | 18 | 19 | main() 20 | -------------------------------------------------------------------------------- /timewarrior/extensions/last.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | timew get dom.tracked.1.json | python -c " 5 | import json 6 | import sys 7 | for tag in json.load(sys.stdin)['tags']: 8 | if len(tag) == 36 and tag.count('-') == 4: 9 | print(tag) 10 | break 11 | " | xargs -I{} task uuid:{} info 12 | -------------------------------------------------------------------------------- /timewarrior/extensions/pomo_msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | from pomo_stat import stat 4 | 5 | 6 | pomos = stat() 7 | content = '[POMO] ' + { 8 | 'ACTIVE': 'Active-%(combo)s: %(desc)s', 9 | 'INTERRUPT': 'Interrupt-%(combo)s: %(desc)s', 10 | 'COMPLETE': 'Complete. Achieved: %(achieved)d, Combo: %(combo)d', 11 | 'INACTIVE': 'Inactive. Achieved: %(achieved)d, MaxCombo: %(max_combo)d', 12 | }.get(pomos['status'], '%(status)s, Combo: %(combo)d') % pomos 13 | print(content) 14 | -------------------------------------------------------------------------------- /timewarrior/extensions/pomo_stat.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import json 4 | import datetime 5 | import commands 6 | 7 | import utils 8 | from utils import settings 9 | 10 | 11 | def stat(): 12 | _, entries = utils.format_inputs() 13 | entries = [ 14 | entry for entry in entries if settings.POMODORO_TAG in entry['tags']] 15 | ret = { 16 | 'status': 'INACTIVE', # ACTIVE|INTERRUPT|COMPLETE|INACTIVE|BREAK 17 | 'desc': '', # Description of last tracked task 18 | 'seconds': 0.0, # Total tracking seconds in Pomodoro Mode 19 | 'interrupt': 0, # Times of interruptions 20 | 'aborted': 0, # The count of aborted pomodoroes 21 | 'achieved': 0, # The count of achieved pomodoroes 22 | 'combo': 0, # The count of archieved pomodoroes in combo 23 | 'max_combo': 0 # 24 | } 25 | if not entries: 26 | return ret 27 | 28 | seconds, end, now = 0.0, None, datetime.datetime.now() 29 | for entry in entries + [{}]: 30 | start = utils.parse_utc(entry['start']) if 'start' in entry else now 31 | gap = (start - end).total_seconds() if end else 0.0 32 | if start == end: 33 | ret['seconds'] += seconds 34 | if seconds < settings.POMODORO_DURATION: 35 | ret['status'] = 'ACTIVE' 36 | else: 37 | ret['status'] = 'COMPLETE' 38 | ret['combo'] += 1 39 | ret['achieved'] += 1 40 | if ret['max_combo'] < ret['combo']: 41 | ret['max_combo'] = ret['combo'] 42 | seconds = 0.0 43 | elif seconds < settings.POMODORO_DURATION: 44 | ret['interrupt'] += 1 45 | if gap < settings.POMODORO_ABORT_GAP: 46 | ret['status'] = 'INTERRUPT' 47 | else: 48 | ret['status'] = 'INACTIVE' 49 | ret['aborted'] += 1 50 | ret['combo'] = 0 51 | ret['seconds'] += seconds 52 | seconds = 0 53 | else: 54 | ret['seconds'] += seconds 55 | ret['combo'] += 1 56 | ret['achieved'] += 1 57 | if ret['max_combo'] < ret['combo']: 58 | ret['max_combo'] = ret['combo'] 59 | seconds = 0.0 60 | break2 = end + datetime.timedelta(seconds=( 61 | settings.POMODORO_SHORT_BREAK 62 | if ret['combo'] % settings.POMODORO_SET_COUNT 63 | else settings.POMODORO_LONG_BREAK)) 64 | if (start - break2).total_seconds() >= settings.POMODORO_COMBO_GAP: 65 | ret['combo'] = 0 66 | ret['status'] = 'INACTIVE' 67 | else: 68 | ret['status'] = break2.strftime('BREAK TO %H:%M') 69 | end = utils.parse_utc(entry['end']) if 'end' in entry else now 70 | seconds += (end - start).total_seconds() 71 | else: 72 | ret['seconds'] += seconds 73 | 74 | for tag in entries[-1]['tags']: 75 | if utils.is_uuid(tag): 76 | ret['desc'] = commands.getoutput('task _get %s.description' % tag) 77 | break 78 | 79 | return ret 80 | 81 | 82 | if __name__ == '__main__': 83 | print(json.dumps(stat(), indent=2, sort_keys=True)) 84 | -------------------------------------------------------------------------------- /timewarrior/extensions/toggle.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import os 4 | import json 5 | import commands 6 | 7 | import utils 8 | 9 | 10 | tracked = json.loads(commands.getoutput('timew get dom.tracked.1.json')) 11 | if 'end' in tracked: 12 | print 'There is no active time tracking.' 13 | exit() 14 | 15 | tags = tracked['tags'] 16 | for tag in utils.format_inputs()[0]['temp']['report']['tags'].split(','): 17 | while tag[0] == tag[-1] and tag[0] in '\'"': 18 | tag = tag[1:-1] 19 | (tags.remove if tag in tags else tags.append)(tag) 20 | 21 | os.system('timew start %s' % ' '.join('"%s"' % tag for tag in tags)) 22 | -------------------------------------------------------------------------------- /timewarrior/extensions/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import sys 4 | import json 5 | import time 6 | import datetime 7 | 8 | 9 | basedir = os.path.realpath(__file__) 10 | for i in range(3): 11 | basedir = os.path.dirname(basedir) 12 | if basedir not in sys.path: 13 | sys.path.append(basedir) 14 | import settings 15 | 16 | 17 | DURATION_PATTERN = re.compile( 18 | r'^P(\d+Y)?(\d+M)?(\d+D)?(?:T(\d+H)?(\d+M)?(\d+S)?)?$') 19 | 20 | 21 | def parse_duration(duration): 22 | ds = DURATION_PATTERN.findall(duration)[0] 23 | assert ds, 'not a duration compliant with ISO-8601: %s' % duration 24 | y, m, d, H, M, S = [int(d[:-1]) if d else 0 for d in ds] 25 | s = (((y * 365 + m * 30 + d) * 24 + H) * 60 + M) * 60 + S 26 | return datetime.timedelta(seconds=s) 27 | 28 | 29 | def parse_utc(utcdate): 30 | return ( 31 | datetime.datetime.strptime(utcdate, '%Y%m%dT%H%M%SZ') - 32 | datetime.timedelta(seconds=time.timezone)) 33 | 34 | 35 | def utc2tz(utcdate): 36 | return parse_utc(utcdate).strftime('%Y-%m-%dT%H:%M:%S') 37 | 38 | 39 | def format_inputs(): 40 | configs = {} 41 | while 1: 42 | line = sys.stdin.readline() 43 | if line == '\n': 44 | break 45 | ks, v = line.split(':', 1) 46 | c, ks = configs, ks.strip().split('.') 47 | for k in ks[:-1]: 48 | c = c.setdefault(k, {}) 49 | c[ks[-1]] = v.strip() 50 | return configs, json.load(sys.stdin) 51 | 52 | 53 | def is_uuid(s): 54 | '269795eb-57a4-46d0-b636-4d2ff5ad5c49' 55 | return len(s) == 36 and s.count('-') == 4 56 | -------------------------------------------------------------------------------- /timewarrior/timewarrior.cfg: -------------------------------------------------------------------------------- 1 | reports.day.lines=2 2 | reports.week.lines=1 3 | reports.month.lines=1 4 | -------------------------------------------------------------------------------- /读我.md: -------------------------------------------------------------------------------- 1 | # 番茄武士 2 | 3 | [English Document](./readme.md) 4 | 5 | ## 简介 6 | 7 | 蕃茄武士是一个集成了 [任务管理工具 taskwarrior](https://taskwarrior.org/docs/) 和 [时间统计工具 timewarrior](https://taskwarrior.org/docs/timewarrior/) 的工具,并在此基础上增加了如下主要功能: 8 | 9 | * 任务拆分; 10 | * 记录任务消耗的时间; 11 | * 支持番茄工作法; 12 | * 生成统计报告。 13 | 14 | ## 安装 15 | 16 | ### 安装到本地 17 | 18 | 1. 运行 `python2 bootstrap.py`; 19 | 2. 安装 [taskwarrior](https://taskwarrior.org/download/); 20 | 3. 安装 [timewarrior](https://taskwarrior.org/docs/timewarrior/download.html)。 21 | 22 | ### 安装到网盘 23 | 24 | 比如想用 OneDrive 同步数据,且 OneDrive 同步盘的路径为 `~/OneDrive/`,则执行下面语句: 25 | 26 | ```bash 27 | python2 bootstrap.py ~/OneDrive/task 28 | ``` 29 | 30 | ### 关于网盘 31 | 32 | 对于国内用户,我非常推荐使用[坚果云](https://www.jianguoyun.com)。对比 OneDrive 等大厂产品的优势如下: 33 | 34 | * 全平台:我因此几个不同系统的电脑有相同的开发环境和数据。 35 | * 国内网络:速度快,不用翻墙。 36 | * 数据有多个版本供回滚:`taskwarrior` 有时候数据会出问题,我因此还写了 `scripts/recover.py`,有了坚果云就用不到了。 37 | * 配置灵活:不需要像 `bootstrap.py` 里那样把数据放到同步文件夹里,而是配置哪些文件(夹)是要同步的。 38 | 39 | ### 用法 40 | 41 | 因为番茄武士是在 [taskwarrior](https://taskwarrior.org/docs/) 和 [timewarrior](https://taskwarrior.org/docs/timewarrior/) 里通过 hooks 添加了一些方便的功能,所以使用番茄武士的前提是你已经掌握了这两个工具。 42 | 43 | ### 1. 拆分任务:`task split ` 44 | 45 | 为 `` 所表示的任务添加一个子任务,子任务会继承其 project 属性并将其 block(所以被拆分的任务必须有 project 属性)。 46 | 比如说: 47 | 48 | ```bash 49 | >>> task add project:test "I'm a parent task" 50 | Created task 1. 51 | >>> task 1 split +next "I'm a child task" 52 | Created task 2. 53 | >>> task 1 split project:sub "I'm another child task" 54 | Created task 3. 55 | >>> task _get 1.depends 56 | b4eb87e6-54f5-422e-939a-f03c673de23e,8dd2e258-525f-4ff0-a7dc-b80fbca8387c 57 | >>> task _get {2,3}.project 58 | test test.sub 59 | ``` 60 | 61 | ### 2. 记录任务时间:`task timew ...` 62 | 63 | 这个命令会被展开成 `tiemw ... `,所以用来记录某一任务消耗的时间是很方便的,比如 `task timew start`。 64 | 65 | ### 3. 番茄工作法 66 | 67 | 记录时间时,如果有为 `pomodoro` 的 tag,就会进入番茄模式(具体请搜索“番茄工作法”,就不赘述了)。 68 | 69 | 所以在使用番茄工作法做某一项任务时可以使用 `task timew start pomodoro`。 70 | 71 | ### 4. 统计报告 72 | 73 | * `timew last`. 当前或最近时间统计的任务的详情; 74 | * 完成或删除一个任务时显示其消耗的时间; 75 | * `timew pomo_stat`. 使用番茄工作法产生的统计数据,主要用来给下一个命令用; 76 | * `timew pomo_msg`. 当前番茄工作法所处状态,用来与 `tmux` 或 `powerline` 集成。 77 | 78 | 与 [tmux](https://github.com/tmux/tmux) 集成可在 `~/.tmux.conf` 中添加下面的配置: 79 | 80 | ```bash 81 | set-option -g status-left "#(timew pomo_msg.py :day)" 82 | ``` 83 | 84 | 与 [powerline](https://github.com/powerline/powerline) 集成可以添加这个 `segment`: 85 | 86 | ```json 87 | { 88 | "function": "powerline.lib.shell.run_cmd", 89 | "priority": 80, 90 | "args": { 91 | "cmd": ["timew", "pomo_msg.py", ":day"] 92 | } 93 | } 94 | ``` 95 | 96 | 可参考分支 [ks](https://github.com/cf020031308/pomodoro-warriors/tree/ks),里面有我个人使用的报告。 97 | 98 | ### 5. 其它改进 99 | 100 | * `task tiny`. 适合小窗口(如 tmux 的 panes)的任务列表; 101 | * 任务可设置 estimate 属性,用来记录自己预估任务完成所需的时间; 102 | * `timew toggle [ ...]`. 从目前时间日志的 tags 中增加或去除指定 tags,以此开始一条新的时间日志。 103 | 104 | ## 工作流举例 105 | 106 | 结合 GTD 理论和番茄工作法 107 | 108 | ### 收集 109 | 110 | * `task add `. 只简单写任务描述就可以。 111 | 112 | ### 处理 113 | 114 | 1. 因为收集时没设置 project,可以用 `task -PROJECT` 筛选出来,一条条处理; 115 | 2. 设定一个可量化的目标,通过 `task annotate ` 批注(如果比较短也可以直接改描述),并通过 `task modify ` 设置好 project, priority, scheduled, due 等属性; 116 | 3. 用 `task split ` 将任务拆分成更小的子任务,越流程化越好; 117 | 4. 估计子任务所需时间(最好不要超过 8 个番茄时间),用 `task modify estimate:` 记下。 118 | 119 | ### 安排 120 | 121 | * 每天早上安排下当天要做的事情,结合 `task start` 命令与 due 属性使其能在任务列表中被轻易区分出来。 122 | 123 | ### 执行 124 | 125 | * 普通执行 126 | + 使用 `task timew start` 开始统计一项任务的时间; 127 | + 使用 `timew stop` 停止统计时间; 128 | + 使用 `task done` 结束一项任务并停止统计时间。 129 | * 番茄工作法 130 | + 使用 `task timew start pomodoro` 开始一个番茄; 131 | + 使用 `timew toggle pomodoro` 或 `timew stop pomodoro` 结束一个番茄 (但继续统计任务时间); 132 | + 中途如果有重要任务插入,可以用 `task modify +next` 给该任务添加一个较高的紧急度,完成当前番茄后处理; 133 | + 与 tmux 或 powerline 集成后可在其 status-line 看到当前番茄状态。 134 | 135 | ### 回顾 136 | 137 | * 每天: `timew day` 138 | * 每周: `timew week` 139 | --------------------------------------------------------------------------------