Part of the "How I organize myself" series:
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
Let's go over each one.
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:
#!/bin/python 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") continue summary = str(apt) if start.hour == 0 and start.minute == 0: start_fmt = start.strftime("%m/%d/%Y") print(f"{start_fmt} [1] {summary}") else: start_fmt = start.strftime("%m/%d/%Y @ %H:%M") if apt['due']: end_fmt = apt['due'].strftime("%m/%d/%Y @ %H:%M") else: 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 continue 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}") else: 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 key presses 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:
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.
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) segunda.update({ '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, cur_sched.domingo] gran = 30 max_minute = 60 - gran def get_cur_wday_time(): weekday = datetime.datetime.today().weekday() hour = datetime.datetime.today().hour minute = (datetime.datetime.today().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}" else: 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): try: return scheds[weekday][format_time(hour, m)] except: pass for h in range(hour - 1, 1, -1): for m in range(max_minute, -1, -gran): try: return scheds[weekday][format_time(h, m)] except: pass return '' def get_new(): weekday, hour, minute = get_cur_wday_time() try: return scheds[weekday][format_time(hour, minute)] except: 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:
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:
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):
#!/bin/python 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='') print() 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: break 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))), end='') else: print(f"{text:15}", end='') print()
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):
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).
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 😃.