Managing my tasks using VIT

Translations: br
Dec 22, 2020

Part of the "How I organize myself" series:

  1. Managing my tasks using VIT
  2. Organization beyond Taskwarrior

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.

Taskwarrior configuration

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.

VIT configuration

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>.

VIT variable replacements

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?

Hooks and taskpirate

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.

Demonstration

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 /.

VIT navigation and task creation using my custom bindings

At asciinema.org

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.

Task annotation and usage of taskopen in VIT

At asciinema.org

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.

Usage of my custom zp, aN and gP bindings to ease project tracking in VIT

At asciinema.org

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.