Organization beyond Taskwarrior

Translations: br
Jan 20, 2021

Part of the "How I organize myself" series:

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

In the previous article of this series, I went into all my Taskwarrior and VIT customizations, and my workflow with them, that enables me to organize my tasks and get them done. Tasks, however, aren't the whole story when getting organized.

Another crucial component of organization is having a calendar. It enables you to be aware of time-sensitive tasks and events, and also to make informed decisions when scheduling new events. Of course this is nothing new, and is even part of GTD.

Something that isn't in GTD, though, and that I missed, is something to keep track of a schedule. GTD is a great system to keep track of all moving parts of projects in your life, and to get them done, but it isn't at all concerned with reserving different portions of your day or week to do the basics (like eating), doing something regularly, or advancing in your general tasks. It just organizes what to do and in which contexts, not precisely when, which may be lacking, if one is trying to use their time well.

So, besides having VIT as my central tasks organizer, my system also needs

  • a decent calendar, somehow integrated with VIT to also show due dates of tasks, in addition to normal appointments
  • a way to set a schedule, and to be constantly reminded of it

Let's go over each one.

Calendar using calcurse

First of all, Taskwarrior actually does have a calendar, which can be shown with task calendar, but it's honestly useless. You can only see which days have tasks, but not which tasks those are.

I wanted a calendar that gave me a good monthly and weekly overview of appointments, that was lightweight (preferably on the terminal), and customizable enough to be integrated with VIT. I ended up using calcurse.

Now, the big question was: How can I have my tasks show up in calcurse? Well, since calcurse uses a text file to hold all appointments using a simple syntax, I just needed a script that reads all tasks from Taskwarrior and outputs the appointments using calcurse's syntax. And this is the script I wrote:


from tasklib import TaskWarrior
import sys

tw = TaskWarrior('/home/nfraprado/.task/data')

# Parse appointments
apts = tw.tasks.filter('(status:pending or status:waiting or status:completed)', type='cal')
for apt in apts:
    start = apt['scheduled']
    if start is None:
        sys.stderr.write(f"Apt '{apt}' has no sched date!\n")

    summary = str(apt)

    if start.hour == 0 and start.minute == 0:
        start_fmt = start.strftime("%m/%d/%Y")
        print(f"{start_fmt} [1] {summary}")
        start_fmt = start.strftime("%m/%d/%Y @ %H:%M")

        if apt['due']:
            end_fmt = apt['due'].strftime("%m/%d/%Y @ %H:%M")
            end_fmt = start_fmt

        print(f"{start_fmt} -> {end_fmt}|{summary}")

# Parse due dates for next actions and projects
tasks = tw.tasks.filter('(status:pending or status:waiting) and (type:next or '
                       'type:objective or type:standby)')
for task in tasks:
    for date_type, label in [('due', "Prazo final: "),
                             ('scheduled', "Prazo inicial: ")]:
        if not task[date_type]: # Skip tasks with no date
        start = task[date_type]

        proj = "Projeto: " if task['type'] == "objective" else ""

        summary = label + proj + str(task)

        if start.hour == 0 and start.minute == 0:
            start_fmt = start.strftime("%m/%d/%Y")
            print(f"{start_fmt} [1] {summary}")
            start_fmt = start.strftime("%m/%d/%Y @ %H:%M")
            end_fmt = start_fmt
            print(f"{start_fmt} -> {end_fmt}|{summary}")

This script is basically concerned with which tasks will be shown in the calendar, and in which format. First, there are the cal tasks, which, if you remember from last post, are my appointments, and they are the main thing that should be shown in the calendar. Each one of them is converted into an entry for calcurse, with the scheduled date being used as the start time, and the due date as the end time. The appointment text is just the plain task description.

Then, there are also the normal tasks, which I want to show not as a continued event in the calendar, but rather I want to have an entry for the start date, to show me when I can start doing the task, and another one for the due date, to show me before when it needs to be done. I also want custom labels prepended to these entries, so I can tell them apart from the appointments. So this script prepends "Prazo inicial: " to the scheduled date entry, and "Prazo final: " to the due date entry. Additionally, if the task is an objective task, "Projeto: " is also added to the description's entry, meaning the date is relevant for the whole project rather than a single next action task.

Initially, that was it. I mapped a key in VIT to run this script, and then reloaded calcurse's appointments with R. So, I needed two keypresses in two separate windows to see the calendar updated.

I later discovered that calcurse also supports hooks (like Taskwarrior, as shown in the previous post), and added a pre-load hook with the following:

taskwarrior-task2cal > /home/nfraprado/.calcurse/apts

Meaning when I type R in calcurse, it automatically runs my script to export the tasks to calcurse's file, so it is now a single key in calcurse to see my calendar updated! 🙂

The following gif shows both a next and a cal task being added in Taskwarrior and automatically showing up in calcurse:

Tasks being added in Taskwarrior and automatically shown in calcurse


Another little thing is that I have calcurse's notification.command configured with the following:

calcurse --next | sed -n -e '2s/.*\] \(.*\)/\1/p' | xargs -I '{}' notify-send '  Upcomming appointment' '{}'

This makes so that it shows a notification in my system some time (configurable, I use 10 minutes) before every appointment, with its description.

Schedule using python

The first step in maintaining a schedule is, of course, to create it.

I wanted a simple and easy way to define and later edit my schedule, so I implemented it using dictionaries in python, with the schedule of each day of the week being given by a separate dictionary.

The idea is that the key gives the start time of an action, and the corresponding value is the action itself. The action is considered the current one from that time until the time of the next action. For example, I have the following base schedule dictionary:

base = {
    '08': "Banho+café",
    '12': "Almoçar",
    '15': "Piano",
    '19': "Jantar",
    '24': "Dormir",

If this were used as a schedule, it would mean that the schedule starts with "Banho+café" from 8 in the morning until 12 PM, when it turns into "Almoçar", and so on.

Like any python dictionary, I can then extend it to implement the schedule of a day, like

segunda = dict(base)
    '09': "Tarefas",

Now, considering segunda as the schedule, "Banho+café" only goes until 9 AM, when it turns into "Tarefas", which in turn goes until 12 PM, when "Almoçar" starts, like before, and so on.

A value can also be deleted, like always, using del segunda['09'], for example.

To define my weekly schedule I just need to create one dictionary for each day of the week using specific variable names (segunda, terça, quarta, quinta, sexta, sábado and domingo).

I like this system because I can add actions simply adding its name and start time, and also because I can add common actions to a base dictionary that is extended by each day's dictionary, reducing redundancy.

Next, I have a python module that knows how to parse each dictionary to return the information I'm interested in:

import datetime

import cur_sched

scheds = [cur_sched.segunda, cur_sched.terça, cur_sched.quarta,
          cur_sched.quinta, cur_sched.sexta, cur_sched.sábado,

gran = 30
max_minute = 60 - gran

def get_cur_wday_time():
    weekday =
    hour =
    minute = ( // gran) * gran
    if hour == 0:
        hour = 24

    return weekday, hour, minute

def format_time(hour, minute):
    if minute > 0:
        time = f"{hour:02}h{minute:02}"
        time = f"{hour:02}"

    return time

def get_current():
    weekday, hour, minute = get_cur_wday_time()
    return get_sched(weekday, hour, minute)

def get_sched(weekday, hour, minute):
    for m in range(minute, -1, -gran):
            return scheds[weekday][format_time(hour, m)]
    for h in range(hour - 1, 1, -1):
        for m in range(max_minute, -1, -gran):
                return scheds[weekday][format_time(h, m)]
    return ''

def get_new():
    weekday, hour, minute = get_cur_wday_time()
        return scheds[weekday][format_time(hour, minute)]
        return ''

get_current() returns the current schedule action based on the current time. get_new() does the same, but only if the action just started. For example, if "Piano" goes from 3 to 4 PM, and considering a granularity of 30 minutes (which I'm currently using), it will return "Piano" only between 3 and 3:30 PM.

To always be able to easily see what's the current action based on my schedule, I have an i3blocks block in my status bar specific for it:

Status bar showing current schedule action: "Piano"

It just calls get_current() from the previous python module.

But only knowing the current action isn't enough, I need to be notified when the current schedule changes. That's why I also have a cron job running every 30 minutes and calling get_new() to check if the scheduled action changed and if so, showing me a notification:

Notification showing current schedule action: "Schedule change: Piano"

Finally, it's also useful to see the weekly schedule as a whole sometimes, so I have a script that prints it, using a different color for each action in the schedule, with the colors themselves being random (so they change on every new run):


import schedule
import colored

weekday_name = ["2a", "3a", "4a", "5a", "6a", "Sáb", "Dom"]
color = True

print("      ", end='')
for weekday in range(0, 7):
    print(f"{weekday_name[weekday]:15}", end='')

def get_color(text):
    hex_num = hex(hash(text) % (16 ** 6))
    hex_num6 = hex_num[:2] + hex_num[2:].rjust(6, '0')
    return hex_num6.replace('0x', '#')

for hour in range(8, 25):
    for minute in range(0, schedule.max_minute + 1, schedule.gran):
        if hour == 24 and minute != 0:
        print(f"{schedule.format_time(hour, minute):5}" + " ", end='')
        for weekday in range(0, 7):
            text = schedule.get_sched(weekday, hour, minute)
            if color:
                print(colored.stylize(f"{text:15}", colored.fg(get_color(text))),
                print(f"{text:15}", end='')

Extra: tasks on the status bar

Since I already use i3blocks as my system's status bar, I also added a block with task information to help me to stay on top of my tasks and to regularly review them (and not only during the weekly review):

Status bar showing the tasks summary

The string in the beginning shows by current context, in this case, sp. The three numbers following are the number of pending in tasks (in yellow), the number of "stuck" projects (in magenta) and the number of tasks due this week (in red).

Future improvements

And that's all there is to my organization system. It's basically VIT on top of Taskwarrior to organize my tasks, calcurse to show my calendar, and blocks in the status bar and notifications to help me track and to warn me about my tasks, schedule and appointments.

This system serves me well, though there are still things to improve. Mainly integration with my phone. As I previously noted, this isn't an issue right now since I'm always home, but as soon as the pandemic is over, I need a good way to have my tasks on my phone synced with my computer. I need to at least be able to easily add in tasks, and see all of my reports, optionally with some filter. I will also need a calendar with the same integration with Taskwarrior I have on my computer. Perhaps with the whole "Convergence" theme going on with Purism, I may end up buying a Librem 5 and having a similar setup on both devices 😃.