Part of the "How I organize myself" series:
Two years ago I decided to get more organized about my life. During that time I read the Getting Things Done book and discovered Taskwarrior, a task manager for the terminal which doesn't get in the way.
I greatly appreciated Taskwarrior's simplicity and customizability, but after
some time, the need to write a command for every single action, like task
list
to list the tasks, gets tiresome. Even after using aliases to shorten the
commands, like tl
for task list
.
After searching for a TUI for Taskwarrior, I found VIT. I actually only stuck with VIT on the second try, because when I first found it, it was written in Perl and wasn't that great of an interface. But with the rewrite in python by thehunmonkgroup and the release of VIT 2.0, it became the perfect interface for Taskwarrior.
Before I start explaining my setup, keep in mind that I won't go into detail of GTD itself, so if you're not familiar with it, maybe take a look at GTD in 15 minutes. Things will make more sense.
To organize my tasks following the GTD method, I need to add some custom
attributes (called UDAs) and reports in my Taskwarrior configuration file
(.taskrc
).
My UDA definitions are the following:
uda.type.label = Type uda.type.values = in,next,objective,someday,standby,cal uda.priority.values = H,L, urgency.uda.priority.H.coefficient = 6.0 urgency.uda.priority.L.coefficient = -6.0 uda.difficulty.type = string uda.difficulty.label = Difficulty uda.difficulty.values = H,L,
This adds three different attributes. The first and most important is the
type
attribute. I use it to assign the task to one of the main lists defined
in GTD:
in
assigns the task to the "In" list, where I first collect my tasks to
take them out of my head.next
puts it in the "Next actions" list, where the tasks I need to be
doing next live.objective
assigns it to the "Projects" list, where each task describes
the objective of one of my current projects, and guides the tasks I create
on "Next actions" for each one of the projects.someday
puts the task in the "Some day/maybe" list. Tasks put there don't
need to be done anytime soon, and may even be uncertain ideas that won't ever
be done at all. Whenever I'm sure I don't want to do it, however, I delete the
task.standby
assigns it to the "Waiting for" list. That's where the tasks
that depend on the action of others sit.cal
puts the task in a "Calendar" list, so that it appears on my calendar
with the right date set.The other attribute is priority
, which I use to prioritize tasks. No
priority means medium priority. H
means "High" and L
means "Low", and
they increase and decrease the task's urgency, respectively. The next
report
sorts tasks by urgency, so setting a high priority makes the task appear higher
on the report, which makes it draw more of my attention as I skim on the report
top to bottom looking for what to do next.
The last attribute is difficulty
which tracks the difficulty of the task,
that is, how much energy it would cost me to complete it. At the time of this
writing, I seldom use it, but the idea is so I can, for example, filter the
next
report with only the L
difficulty
tasks (i.e., the easy ones)
whenever I'm tired.
I also think about adding a duration
UDA for tracking, also with H
or
L
, if I expect a task to take a lot or little time, so I can filter on
however much time I have available at the moment. I still haven't find the need
for it though.
Then, there are the reports:
report.next.columns = id,start.age,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description.count,urgency report.next.labels = ID,Active,P,Project,Tag,Recur,S,Due,Until,Description,Urg report.next.sort = urgency- report.all.columns = id,status.short,uuid.short,start.active,entry.age,end.age,type,depends.indicator,priority,project,tags.count,recur.indicator,wait.remaining,scheduled.remaining,due,until.remaining,description report.all.labels = ID,St,UUID,A,Age,Done,Type,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description report.all_valid.columns = id,status.short,uuid.short,start.active,entry.age,end.age,type,depends.indicator,priority,project,tags.count,recur.indicator,wait.remaining,scheduled.remaining,due,until.remaining,description report.all_valid.labels = ID,St,UUID,A,Age,Done,Type,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description report.all_valid.filter = (status:pending or status:waiting) report.in.columns = id,description report.in.description = Inbox report.in.filter = status:pending limit:page (type:in) report.in.labels = ID,Description report.someday.columns = id,description.count report.someday.description = Someday/Maybe report.someday.filter = limit: type:someday status:pending report.someday.labels = ID,Description report.standby.columns = id,priority,project,due.relative,description.count,urgency report.standby.description = WaitingFor report.standby.labels = ID,P,Project,Due,Description,Urgency report.standby.filter = limit: type:standby status:pending +READY report.standby.sort = urgency- report.objectives.columns = id,priority,project,description.count,urgency report.objectives.description = Projects report.objectives.labels = ID,P,Project,Description,Urgency report.objectives.filter = limit: type:objective status:pending +UNBLOCKED report.objectives.sort = urgency- report.type.columns = id,description,type report.type.description = Type report.type.filter = status:pending limit:page report.type.labels = ID,Description,Type report.cal.columns = id,entry.age,recur.indicator,scheduled,due,description report.cal.description = Calendar report.cal.filter = type:cal status:pending limit:page report.cal.labels = ID,Age,R,Scheduled,Due,Description report.cal.sort = scheduled
There is a report for each of the aforementioned types so that I can see the
list of tasks of each of them: in
, next
, objectives
(with an 's'),
someday
, standby
and cal
. Additionally, there's the all
report
which shows all the tasks, the all_valid
report which is like all
but
hides the completed and deleted tasks, and the type
report which shows each
of the tasks and its type.
If configuring Taskwarrior is all about setting up the attributes and reports to enable my workflow, configuring VIT is all about setting up bindings to make the workflow as fluid as possible.
My bindings (which are set in the config.ini
file inside the .vit
folder) are the following:
q = {ACTION_QUIT} a = {ACTION_NOOP} aa = {ACTION_TASK_ADD} ai = aatype:in<Space> an = aatype:next<Space> ap = aatype:objective project: as = aatype:someday<Space> aw = aatype:standby<Space> ac = aatype:cal schedule: aN = aatype:next project:{TASK_PROJECT}<Space> aP = aaproject:{TASK_PROJECT}<Space> gi = :in<Enter> gn = :next<Enter> gp = :objectives<Enter> gs = :someday<Enter> gw = :standby<Enter> gc = :cal<Enter> gl = :list<Enter> ga = :all_valid<Enter> gA = :all<Enter> gP = :all_valid project:{TASK_PROJECT}<Enter> M = m{TASK_DESCRIPTION} S = :!r task modify type:someday {TASK_UUID}<Enter> W = :!r task modify type:standby {TASK_UUID}<Enter> P = :!r task modify {TASK_UUID} priority: F = :!r task modify {TASK_UUID} difficulty: Y = :!r task duplicate {TASK_UUID}<Enter> #$ = :!r task sync<Enter> zp = gpf{STUCK_PROJS}<Enter> o = :! taskopen {TASK_UUID}<Enter> C = :!r taskwarrior-update_context<Enter> # Convenience mappings 1 = :1 2 = :2 3 = :3 4 = :4 5 = :5 6 = :6 7 = :7 8 = :8 9 = :9 - = m- + = m+
The main bindings are the ones starting with a
or g
. Those are the ones
I use all day. The ones starting with a
are for adding tasks, and there's
one for each task type, so I can quickly add a task of any type. The ones
starting with g
are for going to a report, so I can also quickly jump to a
specific report and see the tasks of that type.
Some of those are a bit special though. For example, aN
adds a task of type
next
and with the project of the currently selected task. I use this when
I'm reviewing my current projects on the objectives
report and want to add a
task for the next action of the project I'm currently selecting.
gP
is another one I use a lot. It shows all tasks with the same project of
the currently selected task. I use this when I'm looking at a task from a
project and want to see all other tasks of that project, like the objective
given by the objective
task, what are the next
tasks for it, if there
are things waiting on other people at standby
or something marked on the
calendar at cal
.
Then there are some utility bindings used less often. M
edits the selected
task description. S
and W
change the selected task's type to someday
and standby
, respectively. The former is useful for when I decide a task
should be done in the future rather than now, while the latter isn't used much.
P
edits the task's priority, while F
edits the task's difficulty. Y
duplicates the selected task.
The $
that is commented out is used to sync the tasks to a central
Taskwarrior server using Taskserver. It was essential to keep the tasks
synchronized between my computer and my phone when going out. But since going
out hasn't been a common theme recently, meaning I'm always on my computer, I
disabled it for the time being.
The zp
binding is one that is a bit more complicated but very handy. It
goes to the objectives
report, that shows my current projects, and filters
so that only the projects with no next
tasks are shown. It is important
with GTD to always make sure that all your projects have next actions assigned
to them, so that the next step in advancing them is obvious. With this binding,
during my weekly review of all the tasks, I can easily see the projects with no
next actions and then create one for each project using the aN
already
shown. If you're wondering what {STUCK_PROJS}
means, don't worry, I'll
explain it shortly.
o
uses taskopen on the currently selected task, and this is another one I
use all the time. What taskopen does is read through the task's annotations and
open one of them (asking which one, if there are multiple options). If the
annotation is an URL, it will be open on the web browser. If it is the string
"Notes", taskopen will open the text file associated with that task (or create
one if this is the first time) where you can write longer annotations. These two
are the ones I know and that I use all the time. Normal text annotations are
ignored by taskopen.
The C
binding runs a script to update the current Taskwarrior context, but I
normally don't need to run it since it runs automatically as explained in the
"Automatic context detection for Taskwarrior" post.
Finally, I have some convenience mappings. Each of the digits maps to :
followed by that digit, so I can jump to a task with one less keystroke. In
more detail, normally to jump to task 42 I would need to type :
, 4
,
2
and <Enter>
. With this binding I can skip the :
, so I type just
4
, 2
and <Enter>
. Since it is very common to jump to tasks in VIT,
this one less keystroke pays off. Additionally, -
and +
map to m-
and m+
, respectively, where m
is the default command to modify the task,
so to add a tag to a task I just press +
, type the tag name and press
<Enter>
.
You may have noticed some {SOMETHING}
in the VIT bindings above. I just
wanted to give a short explanation on those (since a full explanation should be
read in VIT's documentation) and also show my custom variable replacement.
First things first, the {ACTION_QUIT}
in the q
binding isn't even a
variable replacement, although the syntax is similar (the difference being that
it is the only thing after the =
). That's just one of VIT's actions that can
be mapped. A variable replacement occurs in the aN
binding for example:
aN = aatype:next project:{TASK_PROJECT}<Space>
Here, {TASK_PROJECT}
will be substituted by the project attribute of the
currently selected task. So that's why that binding does what it does. The
aa
in the beginning is mapped to the action to add a new task, then the type
is set to next
and the project to the selected task's project. All
{TASK_*}
are built-in VIT variable replacements, and can be used for any
task attribute (including UDAs).
Now, in the case of the zp
binding:
zp = gpf{STUCK_PROJS}<Enter>
{STUCK_PROJS}
is a custom variable replacement that I created. It was very
simple, I just followed VIT's CUSTOMIZE.md.
Inside my .vit
folder I added a keybinding/keybinding.py
with the
following:
from task_proj_stuck import get_stuck_proj_ids class Keybinding(): def replacements(self): def _custom_match(variable): if variable in ['STUCK_PROJS']: return [variable] def _custom_replace(task, arg): if arg == 'STUCK_PROJS': return ' '.join(list(get_stuck_proj_ids())) return [ { 'match_callback': _custom_match, 'replacement_callback': _custom_replace, }, ]
I then have a task_proj_stuck
python module with the following:
from tasklib import TaskWarrior tw = TaskWarrior() def get_stuck_projects(): """ Get taskwarrior projects that don't have any next actions assigned to them. Next actions here mean actions of type 'next', 'standby' or 'cal', either pending or waiting. """ projects = tw.tasks.pending().filter('+READY', type='objective') next_tasks = tw.tasks.filter('(status:pending or status:waiting) and type:next') standby_tasks = tw.tasks.filter('(status:pending or status:waiting) and type:standby') cal_tasks = tw.tasks.filter('(status:pending or status:waiting) and type:cal') for project in projects: count_next = len(next_tasks.filter(project=project['project'])) count_standby = len(standby_tasks.filter(project=project['project'])) count_cal = len(cal_tasks.filter(project=project['project'])) if count_next + count_standby + count_cal == 0: yield project def get_stuck_proj_ids(): return (str(project['id']) for project in get_stuck_projects())
So what happens is that the get_stuck_proj_ids()
function returns a
generator containing the id of each objective
task whose project doesn't
have any next
tasks. The {STUCK_PROJS}
custom variable replacement then
just calls this function and joins the ids with space.
For example, suppose there's project clean-bedroom
and project
write-vit-post
. Project clean-bedroom
's objective
task has id 42 and
project write-vit-post
's objective
task has id 99. Both of these
projects don't have any next
tasks, while all other projects have. Then, by
pressing zp
, VIT will execute gpf42 99<Enter>
, which goes to the
objectives
report and filters for just tasks 42 and 99, so I can focus on
adding next tasks for each one of these stuck projects with aN
. Pretty
convenient, right?
Another powerful feature of Taskwarrior that enables extensibility is the hooks API. If you use Git, you may already be familiar with this concept. It enables a custom script to be run when a certain event occurs in the program, in this case, in Taskwarrior.
Instead of just creating a Taskwarrior hook directly, I decided to use taskpirate, which makes the tasks more directly accessible in python. And as you may already know, I like python.
I currently have a single hook named pirate_add_inherit.py
, inside the
hooks
folder, and what it does is to make certain attributes inheritable
from the objectives
task of a project. The code is the following:
#!/bin/python from tasklib import TaskWarrior def hook_inherit(task): if task['project'] and task['type'] != 'objective': tw = TaskWarrior('/home/nfraprado/.task/data') try: obj_task = tw.tasks.filter(type='objective', project=task['project'])[0] except: return for field in ('due', 'priority'): if not task[field] and obj_task[field]: task[field] = obj_task[field]
Since it is an add
hook, it executes every time a new task is created. What
it does is the following: whenever a task is created with a project, its due
and priority
attributes are set equal to those attributes on the
objective
task of the project, except if those attributes are explicitly set
in the new task. This is very useful since then I can set the priority and due
date of a project on the objective
task and all tasks of that project will
have the same priority and due date automatically.
After talking so much, I owe you at least some gifs showing how this all works
out. It's worth saying the following aren't my real tasks, otherwise you'd be
looking at more than a hundred on the someday
report, for example.
In this first gif, I jump to each report (next
, in
, standby
,
objectives
and someday
) using the g
bindings, then add an in
task with ai
and use S
to move it to someday
. I also use the
<Enter>
default binding to inspect the task. You can also see me jumping to
tasks using their id and searching for a string using the default binding /
.
In this second gif, I use the default binding A
to annotate the task with
simple text and then with the string "Notes". I then use o
to make taskopen
open a note for the task where I input more annotations.
In this last gif, I use the zp
binding to show only the stuck projects, then
use aN
on two of them to create next
tasks. Finally, I use gP
on a
task to show only tasks of its project. Here you can also see the hook in
effect, since the Next action 1
task has the same priority
and due
attributes as the New Project 1
task, even though I didn't specify them.
And that's it. If this interested you, take a look at VIT. I only showed my specific bindings and workflow with it, but VIT is capable of a lot more.
Lastly, this post only covered the things related to the managing of tasks and VIT. There are still other very important parts of my organization left to be explained, like how I see my calendar and how I stay on schedule. I'll go over these on the next post.