Join the conversation on the Discussions tab
❌ Preliminary and incomplete. This notice will be removed when the code is ready for general use.
tklr began life in 2013 as etm-qt sporting a gui based on Qt. The intent was to provide an app supporting GTD (David Allen's Getting Things Done) and exploiting the power of python-dateutil. The name changed to etmtk in 2014 when Tk replaced Qt. Development of etmtk continued until 2019 when name changed to etm-dgraham, to honor the PyPi naming convention, and the interface changed to a terminal based one based on prompt_toolkit. In 2025 the name changed to "tklr", the database to SQLite3 and the interface to Click (CLI) and Textual. Features have changed over the years but the text based interface and basic format of the reminders has changed very little. The goal has always been to be the Swiss Army Knife of tools for managing reminders.
tklr offers a simple way to manage your events, tasks and other reminders.
Rather than filling out fields in a form to create or edit reminders, a simple text-based format is used. Each reminder in tklr begins with a type character followed by the subject of the reminder and then, perhaps, by one or more @key value pairs to specify other attributes of the reminder. Mnemonics are used to make the keys easy to remember, e.g, @s for scheduled datetime, @l for location, @d for description and so forth.
The 4 types of reminders in tklr with their associated type characters:
type | char |
---|---|
event | * |
task | ~ |
project | ^ |
goal | + |
note | % |
draft | ? |
-
A task, ~: pick up milk.
~ pick up milk
-
An event reminder, *: have lunch with Ed [s]tarting next Tuesday at 12pm with an extent of 1 hour and 30 minutes, i.e., lasting from 12pm until 1:30pm.
* Lunch with Ed @s tue 12p @e 1h30m
-
A note reminder, %: a favorite Churchill quotation that you heard at 2pm today with the quote itself as the description.
% Give me a pig - Churchill @s 2p @d Dogs look up at you. Cats look down at you. Give me a pig - they look you in the eye and treat you as an equal.
The subject, "Give me a pig - Churchill" in this example, follows the type character and is meant to be brief - analogous to the subject of an email. The optional description follows the "@d" and is meant to be more expansive - analogous to the body of an email.
-
A project reminder, ^: build a dog house, with component @~ tasks.
^ Build dog house @~ pick up materials &r 1 &e 4h @~ cut pieces &r 2: 1 &e 3h @~ assemble &r 3: 2 &e 2h @~ sand &r 4: 3 &e 1h @~ paint &r 5: 4 &e 4h
The "&r X: Y" entries set "X" as the label for the task and the task labeled "Y" as a prerequisite. E.g., "&r 3: 2" establishes "3" as the label for assemble and "2" (cut pieces) as a prerequisite. The "&e extent" entries give estimates of the times required to complete the various tasks.
-
A draft reminder, !: meet Alex for coffee Friday.
! Coffee with Alex @s fri @e 1h
This can be changed to an event when the details are confirmed by replacing the ! with an * and adding the time to
@s
. This draft will appear highlighted on the current day until you make the changes to complete it.
-
An appointment (event) for a dental exam and cleaning at 2pm on Feb 5 and then again, @+, at 9am on Sep 3.
* dental exam and cleaning @s 2p feb 5 @e 45m @+ 9am Sep 3
-
A reminder (task) to fill the bird feeders starting Friday of the current week and repeat (do over) thereafter 4 days after the previous completion.
~ fill bird feeders @s fri @o 4d
-
The full flexibility of the superb Python dateutil package is supported. Consider, for example, a reminder for Presidential election day which starts in November, 2020 and repeats every 4 years on the first Tuesday after a Monday in November (a Tuesday whose month day falls between 2 and 8 in the 11th month). In tklr, this event would be
-
* Presidential election day @s nov 1 2020 @r y &i 4 &w TU &m 2, 3, 4, 5, 6, 7, 8 &M 11
This guide walks you through setting up a development environment for tklr
using uv
and a local virtual environment. Eventually the normal python installation procedures using pip or pipx will be available.
This step will create a directory named tklr-dgrham in your current working directory that contains a clone of the github repository for tklr.
git clone https://github.com/dagraham/tklr-dgraham.git
cd tklr-dgraham
which uv || curl -LsSf https://astral.sh/uv/install.sh | sh
This will create a .venv/
directory inside your project to hold all the relevant imports.
uv venv
uv pip install -e .
You have two options for activating the virtual environment for the CLI:
source .venv/bin/activate
Then you can run:
tklr --version
tklr add "- test task @s 2025-08-01"
tklr ui
To deactivate:
deactivate
brew install direnv # macOS
sudo apt install direnv # Ubuntu/Debian
eval "$(direnv hook zsh)" # or bash
Restart your shell or run source ~/.zshrc
.
echo 'export PATH="$PWD/.venv/bin:$PATH"' > .envrc
direnv allow
Now every time you cd
into the project, your environment is activated automatically and, as with the manual option, test your setup with
tklr --version
tklr add "- test task @s 2025-08-01"
tklr ui
You're now ready to develop, test, and run tklr
locally with full CLI and UI support.
To update your local copy of Tklr to the latest version:
# Navigate to your project directory
cd ~/Projects/tklr-dgraham # adjust this path as needed
# Pull the latest changes from GitHub
git pull origin master
# Reinstall in editable mode (picks up new code and dependencies)
uv pip install -e .
Tklr needs a home directory to store its files - most importantly these two:
- config.toml: An editable file that holds user configuration settings
- tkrl.db: An SQLite3 database file that holds all the records for events, tasks and other reminders created when using tklr
Any directory can be used for home. These are the options:
-
If started using the command
tklr --home <path_to_home>
and the directory<path_to_home>
exists then tklr will use this directory and, if necessary, create the filesconfig.toml
andtklr.db
in this directory. -
If the
--home <path_to_home>
is not passed to tklr then the home will be selected in this order:- If the current working directory contains files named
config.toml
andtklr.db
then it will be used as home - Else if the environmental variable
TKLR_HOME
is set and specifies a path to an existing directory then it will be used as home - Else if the environmental variable
XDG_CONFIG_HOME
is set, and specifies a path to an existing directory which contains a directory namedtklr
, then that directory will be used. - Else the directory
~/.config/tklr
will be used.
- If the current working directory contains files named
When an @s
scheduled entry specifies a date without a time, i.e., a date instead of a datetime, the interpretation is that the task is due sometime on that day. Specifically, it is not due until 00:00:00
on that day and not past due until 00:00:00
on the following day. The interpretation of @b
and @u
in this circumstance is similar. For example, if @s 2025-04-06
is specified with @b 3d
and @u 2d
then the task status would change from waiting to pending at 2025-04-03 00:00:00
and, if not completed, to deleted at 2025-04-09 00:00:00
.
When an item is specified with an @r
entry, an @s
entry is required and is used as the DTSTART
entry in the recurrence rule. E.g.,
* datetime repeating @s 2024-08-07 14:00 @r d &i 2
is serialized (stored) as
{
"itemtype": "*",
"subject": "datetime repeating",
"rruleset": "DTSTART:20240807T140000\nRRULE:FREQ=DAILY;INTERVAL=2",
}
Note: The datetimes generated by the rrulestr correspond to datetimes matching the specification of @r
which occur on or after the datetime specified by @s
. The datetime corresponding to @s
itself will only be generated if it matches the specification of @r
.
On the other hand, if an @s
entry is specified, but @r
is not, then the @s
entry is stored as an RDATE
in the recurrence rule. E.g.,
* datetime only @s 2024-08-07 14:00 @e 1h30m
is serialized (stored) as
{
"itemtype": "*",
"subject": "datetime only",
"e": 5400,
"rruleset": "RDATE:20240807T140000"
}
The datetime corresponding to @s
itself is, of course, generated in this case.
When @s
is specified, an @+
entry can be used to specify one or more, comma separated datetimes. When @r
is given, these datetimes are added to those generated by the @r
specification. Otherwise, they are added to the datetime specified by @s
. E.g., is a special case. It is used to specify a datetime that is relative to the current datetime. E.g.,
* rdates @s 2024-08-07 14:00 @+ 2024-08-09 21:00
would be serialized (stored) as
{
"itemtype": "*",
"subject": "rdates",
"rruleset": "RDATE:20240807T140000, 20240809T210000"
}
This option is particularly useful for irregular recurrences such as annual doctor visits. After the initial visit, subsequent visits can simply be added to the @+
entry of the existing event once the new appointment is made.
Note: Without @r
, the @s
datetime is included in the datetimes generated but with @r
, it is only used to set the beginning of the recurrence and otherwise ignored.
[[timezones.md]]
When a datetime is specified, the timezone is assumed to be the local timezone. The datetime is converted to UTC for storage in the database. When a datetime is displayed, it is converted back to the local timezone.
This would work perfectly but for recurrence and daylight savings time. The recurrence rules are stored in UTC and the datetimes generated by the rules are also in UTC. When these datetimes are displayed, they are converted to the local timezone.
- fall back @s 2024-11-01 10:00 EST @r d &i 1 &c 4
rruleset_str = 'DTSTART:20241101T140000\nRRULE:FREQ=DAILY;INTERVAL=1;COUNT=4'
item.entry = '- fall back @s 2024-11-01 10:00 EST @r d &i 1 &c 4'
{
"itemtype": "-",
"subject": "fall back",
"rruleset": "DTSTART:20241101T140000\nRRULE:FREQ=DAILY;INTERVAL=1;COUNT=4"
}
Fri 2024-11-01 10:00 EDT -0400
Sat 2024-11-02 10:00 EDT -0400
Sun 2024-11-03 09:00 EST -0500
Mon 2024-11-04 09:00 EST -0500
Since urgency values are used ultimately to give an ordinal ranking of tasks, all that matters is the relative values used to compute the urgency scores. Accordingly, all urgency scores are constrained to fall within the interval from -1.0 to 1.0. The default urgency is 0.0 for a task with no urgency components.
There are some situations in which a task will not be displayed in the "urgency list" and there is no need, therefore, to compute its urgency:
- Completed tasks are not displayed.
- Hidden tasks are not displayed. The task is hidden if it has an
@s
entry and an@b
entry and the date corresponding to@s - @b
falls sometime after the current date. - Waiting tasks are not displayed. A task is waiting if it belongs to a project and has unfinished prerequisites.
- Only the first unfinished instance of a repeating task is displayed. Subsequent instances are not displayed.
There is one other circumstance in which urgency need not be computed. When the pinned status of the task is toggled on in the user interface, the task is treated as if the computed urgency were equal to 1.0
without any actual computations.
All other tasks will be displayed and ordered by their computed urgency scores. Many of these computations involve datetimes and/or intervals and it is necessary to understand both are represented by integer numbers of seconds - datetimes by the integer number of seconds since the epoch (1970-01-01 00:00:00 UTC) and intervals by the integer numbers of seconds it spans. E.g., for the datetime "2025-01-01 00:00 UTC" this would be 1735689600
and for the interval "1w" this would be the number of seconds in 1 week, 7*24*60*60 = 604800
. This means that an interval can be subtracted from a datetime to obtain another datetime which is "interval" earlier or added to get a datetime "interval" later. One datetime can also be subtracted from another to get the "interval" between the two, with the sign indicating whether the first is later (positive) or earlier (negative). (Adding datetimes, on the other hand, is meaningless.)
Briefly, here is the essence of this method used to compute the urgency scores using "due" as an example. Here is the relevant section from config.toml with the default values:
[urgency.due]
# The "due" urgency increases from 0.0 to "max" as now passes from
# due - interval to due.
interval = "1w"
max = 8.0
The "due" urgency of a task with an @s
entry is computed from now (the current datetime), due (the datetime specified by @s
) and the interval and max settings from urgency.due. The computation returns:
0.0
ifnow < due - interval
max * (1.0 - (now - due) / interval)
ifdue - interval < now <= due
max
ifnow > due
For a task without an @s
entry, the "due" urgency is 0.0.
Other contributions of the task to urgency are computed similarly. Depending on the configuration settings and the characteristics of the task, the value can be either positive or negative or 0.0 when missing the requisite characteristic(s).
Once all the contributions of a task have been computed, they are aggregated into a single urgency value in the following way. The process begins by setting the initial values of variables Wn = 1.0
and Wp = 1.0
. Then for each of the urgency contributions, v
, the value is added to Wp
if v > 0
or abs(v)
is added to Wn
if v
negative. Thus either Wp
or Wn
is increased by each addition unless v = 0
. When each contribution has been added, the urgency value of the task is computed as follows:
urgency = (Wp - Wn) / (Wp + Wn)
Equivalently, urgency can be regarded as a weighted average of -1.0
and 1.0
with Wn/(Wn + Wp)
and Wp/(Wn + Wp)
as the weights:
urgency = -1.0 * Wn / (Wn + Wp) + 1.0 * Wp / (Wn + Wp) = (Wp - Wn) / (Wn + Wp)
Observations from the weighted average perspective and the fact that Wn >= 1
and Wp >= 1
:
-1.0 < urgency < 1
urgency = 0.0
if and only ifWn = Wp
urgency
is always increasing inWp
and always decreasing inWn
urgency
approaches1.0
asWn/Wp
approaches0.0
- asWp
increases relative toWn
urgency
approaches-1.0
asWp/Wn
approaches0.0
- asWn
increases relative toWp
Thus positive contributions always increase urgency and negative contributions always decrease urgency. The fact that the urgency derived from contributions is always less than 1.0
means that pinned tasks with urgency = 1
will always be listed first.
These are the default settings in config.toml:
# DO NOT EDIT TITLE
title = "Tklr Configuration"
[ui]
# theme: str = 'dark' | 'light'
theme = "dark"
# ampm: bool = true | false
# Use 12 hour AM/PM when true else 24 hour
ampm = false
# dayfirst and yearfirst settings
# These settings are used to resolve ambiguous date entries involving
# 2-digit components. E.g., the interpretation of the date "12-10-11"
# with the various possible settings for dayfirst and yearfirst:
#
# dayfirst yearfirst date interpretation standard
# ======== ========= ======== ============== ========
# True True 12-10-11 2012-11-10 Y-D-M ??
# True False 12-10-11 2011-10-12 D-M-Y EU
# False True 12-10-11 2012-10-11 Y-M-D ISO 8601
# False False 12-10-11 2011-12-10 M-D-Y US
#
# The defaults:
# dayfirst = false
# yearfirst = true
# correspond to the Y-M-D ISO 8601 standard.
# dayfirst: bool = true | false
dayfirst = false
# yearfirst: bool = true | false
yearfirst = true
[alerts]
# dict[str, str]: character -> command_str.
# E.g., this entry
# d: '/usr/bin/say -v Alex "[[volm 0.5]] {subject}, {when}"'
# would, on my macbook, invoke the system voice to speak the subject
# of the reminder and the time remaining until the scheduled datetime.
# The character "d" would be associated with this command so that, e.g.,
# the alert entry "@a 30m, 15m: d" would trigger this command 30
# minutes before and again 15 minutes before the scheduled datetime.
# ─── Urgency Configuration ─────────────────────────────────────
[urgency.due]
# The "due" urgency increases from 0.0 to "max" as now passes from
# due - interval to due.
interval = "1w"
max = 8.0
[urgency.pastdue]
# The "pastdue" urgency increases from 0.0 to "max" as now passes
# from due to due + interval.
interval = "2d"
max = 2.0
[urgency.recent]
# The "recent" urgency decreases from "max" to 0.0 as now passes
# from modified to modified + interval.
interval = "2w"
max = 4.0
[urgency.age]
# The "age" urgency increases from 0.0 to "max" as now increases
# from modified to modified + interval.
interval = "26w"
max = 10.0
[urgency.extent]
# The "@e extent" urgency increases from 0.0 when extent = "0m" to "max"
# when extent >= interval.
interval = "12h"
max = 4.0
[urgency.blocking]
# The "blocking" urgency increases from 0.0 when blocked = 0 to "max"
# when blocked >= count. Blocked is the integer count of tasks in a project for which the given task is an unfinished prerequisite.
count = 3
max = 6.0
[urgency.tags]
# The "tags" urgency increases from 0.0 when tags = 0 to "max" when
# when tags >= count. Tags is the count of "@t" entries given in the task.
count = 3
max = 3.0
[urgency.priority]
# The "priority" urgency corresponds to the value from "1" to "5" of `@p`
# specified in the task. E.g, with "@p 3", the value would correspond to
# the "3" entry below. Absent an entry for "@p", the value would be 0.0.
"1" = -5.0
"2" = 2.0
"3" = 5.0
"4" = 8.0
"5" = 10.0
# In the default settings, a priority of "1" is the only one that yields
# a negative value, `-5`, and thus reduces the urgency of the task.
[urgency.description]
# The "description" urgency equals "max" if the task has an "@d" entry and
# 0.0 otherwise.
max = 2.0
[urgency.project]
# The "project" urgency equals "max" if the task belongs to a project and
# 0.0 otherwise.
max = 3.0