LITT (or tt
for short) is intended to be a simple, fast (as in workflow), and terse time tracking tool with a CLI frontend and a simple JSON file as a backend database. It was spawned out of a dissatisfaction with existing time tracking tools that were primarily web based, and required too many different input mechanisms (keyboard, mouse, app, etc...).
Using tt
can be extremely terse, and could even be bound to a keyboard macro button. At it's shortest, it can be used as a simple stopwatch that logs to a file.
The following command will start a stopwatch, if one isn't currently running, or stop the currently running stopwatch.
tt sw
Note that since you have not supplied any information about what you were doing during that time, it cannot fill it in however you can amend these recorded time periods after the fact with more detail. If you want to supply information about what you are doing, you can either supply a description explicitly, as in:
tt sw "Writing documentation"
Or make use of aliases which allow you to match a set of tags, a description, and other information to a string. You can create an alias with the following:
tt alias --key dev.docs --tag Development --tag Documentation --description "Writing documentation for dev work"
Note that the previous tt sw
command example is actually doing more than just adding a description. It is checking to see if the supplied string matches an alias key and, if so, using the values associated with that alias; if no alias is found matching that key, then the string is treated as a task description.
You can use aliases with the exact same command syntax as above:
tt sw dev.docs
That's the general idea. If these examples seem like this is a tool you're interested in trying, give the rest of the detailed use cases a read.
LITT makes use of the Python dateparser
package, so you'll need to install that either from pip or your OS packages.
Full package dependencies are in requirements.txt.
The time tracking DB, as well as the configuration files, are stored in ~/.litt
on Linux, and %HOMEDRIVE%\%HOMEPATH\.litt
on Windows.
tt
accepts a few global configuration parameters, which are set persistently using tt config
, but can be set on a per-use basis by supplying the same options to any other tt
command (that is, the following options are accepted by any tt
command, and if given explicitly will override the persistent settings).
--output-format
- Accepts one of:
json
,json-compact
,yaml
- Defaults to:
json-compact
- Note:
yaml
output is only available if the PyYAML package is installed, and is dynamically detected based on an attempt to import the package.
- Accepts one of:
The usage of tt
is pretty straight forward:
- You can use it as a stopwatch with at most one concurrent interruption
- Example: You start a stopwatch (
tt start
) to work on documentation, but you get an urgent bug report that you want to respond to. Instead of stopping (tt stop
) and restarting your stopwatch, you can interrupt (tt interrupt
ortt i
) it to respond to the bug report, and then resume (tt resume
ortt r
) the stopwatch. - The choice of not supporting arbitrarily nested interruptions is a workflow one, to encourage less churn in task selection.
- Example: You start a stopwatch (
- You can use it as a ledger to record time after the fact.
- Example: You keep track of time on a handwritten notepad to take between client sites. At the end of the day, you can record all of those time allocations with
tt track
- Example: You keep track of time on a handwritten notepad to take between client sites. At the end of the day, you can record all of those time allocations with
There are two additional commands that allow you to edit time records (tt amend
) and create aliases for commonly used tasks (tt alias
) that round out the functionality.
Viewing tracked data is done with tt ls
which allows for filtering, viewing, sorting (sometimes, based on the chosen output format) and optionally saving the data for use with another application.
Note: To support terse interaction, all options have short and long forms, and in many cases positional arguments are supported where the meaning is either unambiguous or can be derived.
The Python script supports the execution of arbitrary executables at specific points in the logic, called hooks. See the section on Hooks for more information.
A time record, as tracked by tt
, has several properties:
Identifier
: The string that uniquely identifies this time record, allowing it to be uniquely identified when amending properties. This is decided at commit-time (seeCommitTime
) and can either be user-provided, or is auto-generated. This is only used by the user to amend the time record after it has been committed, and can safely be left as auto-generated unless there is a specific reason for explicit IDs.StartTime
: The unix timestamp that the time record starts.EndTime
: The unix timestamp that the time record ends.CommitTime
: The unix timestamp that the time record was committed to the persistent ledger.Description
: A short one-line description of the work performed.Detail
: A detailed description of the work performed.Tags
: A collection of strings that are associated with this time record, useful for filtering, grouping, and aggregating. Any number of tags can be attached to a time record.StructuredData
: A string of data that has some structural interpretation. Internally tott
this is just saved as a base64 string, but this is useful if you have applications that interface withtt
(such as storing TaskWarrior task IDs, or other information).
The stopwatch mode of operation of tt
assumes that it is being used in-line with workflows, and so the timestamps of events are all taken to be the time the command is run. If this is incorrect, the resulting time records can be amended (see Amending Time Records).
Three are several commands that control the behaviour of tt
when using it as a stopwatch.
tt
prints the status of the currently active stopwatch and, if active, the interrupt timer.tt start
starts a stopwatch, and will error (gracefully) if a stopwatch is already running.tt stop
stops a currently running stopwatch, and will error (gracefully) if no stopwatch is currently running.tt sw
will start a stopwatch if one is not running, and will stop a stopwatch if one is currently running.tt interrupt
(tt i
) will interrupt a currently running stopwatch (if one is running, otherwise it will act as an alias tott start
), which pauses the running stopwatch and starts a new one.tt resume
(tt r
) will stop the stopwatch started bytt interrupt
and resume the stopwatch (if there was one, otherwise it will act as an alias tott stop
) that was running whentt interrupt
was called.tt cancel
will top the stopwatch (or interruption, if one is running) without committing the record to the ledger (effectively discarding the time). If no stopwatch is running, this operation does nothing. If an interruption is running, the interruption is canceled as thoughtt interrupt
was never issued (but otherwise leaves the stopwatch intact). It takes no options.
tt sw
is a context-aware alias to tt start
or tt stop
that will happily do what you tell it to (such as accidentally clobber a running stopwatch if issued by accident), and is provided as a terse alternative for brave users.
tt start
and tt stop
are the recommended commands for using the stopwatch, especially to start.
All of the above commands support the following options as well as exactly one positional argument:
-d
/--description
-t
/--tag
-D
/--detail
-S
/--structured-data
-a
/--alias
When provided, the positional argument is checked against the list of known alias keys. If an alias key matching the positional argument is found, then the positional argument is treated as the value of --alias
. If no alias key matching the positional argument is found, then it is treated as the value of --description
.
For more on aliases, see the Aliases section.
tt stop
, tt resume
, and tt sw
(when stopping a stopwatch) are commit-level operations, and options specified with these commands override (or add to, in the case of --tag
) the values of the options given to the corresponding command that started the current stopwatch (or interruption interval). When the positional argument is provided to these commands, the interpretation is the same as in other stopwatch commands. Additionally these commands support the following options:
-i
/--id
-u
/--untag
Note that the --id
parameter provides an opportunity for the user to explicitly dictate the ID that should be used for this time record, which must be unique among all time records tracked so far.
There is only one command for using tt
as a ledger, tt track
, which takes all of the same options as tt start
and tt stop
, supports the positional argument semantic of tt start
, and has these additional options:
-s
/--start
-e
/--end
--dryrun
--start
and --end
take any absolute or relative time or date specification, and their values are parsed by the dateparser Python module. If no timezone is given, then the local timezone is assumed. At least one of --start
and --end
must be specified, and if only one is provided then the other is assumed to be the time the command is invoked. It is an error for the value of --end
to precede (or equal) the value of --start
.
--dryrun
is provided to allow you to see the dates and times that are being parsed from your provided date specifications without committing to record to the ledger.
Note that --id
has the same interpretation here as it does in tt stop
.
Since tt interrupt
and tt resume
are only used with stopwatch time tracking, there is no way to insert interruptions to a block of time added with tt track
. See the note about Mutating History for suggestions on how you might go about adding interruptions to these blocks of time manually.
Aliases are ways of pairing commonly used options (description, detail, tags, etc...) with a shorter, easily remembered key. Recall the example from above:
tt alias --key dev.docs --tag Development --tag Documentation --description "Writing documentation for dev work"
This alias can now be referenced in any of tt start
, tt interrupt
, or tt sw
(when starting a stopwatch).
When a valid alias key is given to a command, the properties defined by the alias are set first, and if any other options are provided, those values override the value set by the alias. For example, using the above alias:
tt start dev.docs -d "Proof-reading documentation"
Would produce a time record with the Development and Documentation tags, but instead of the alias' description, the one provided on the command line will be used.
To view all aliases configured, use tt alias
without arguments, and to replace an alias run with key AKey, use tt alias --key AKey {Options}
. To remove an alias with key AKey, simply overwrite it with a new alias that specifies no options (i.e. tt alias --key AKey
).
Time records committed to the ledger are not immutable, and changes can be made with tt amend
which takes the same options as tt track
without the positional argument. Note that --id
has a different meaning to tt amend
as it does in tt stop
; that is for tt amend
, the --id
option is mandatory, and indicates which time record the edits should be applied to.
Values to options given to tt amend
will replace the values on the specified time record with the exception of --tag
which will append to the tags associated with the specified time record. Any values set in the specified record that are not explicitly overridden on the tt amend
command line will be left unmodified.
Note that LITT takes some pointers from Mercurial and does not include significant tools for editing history in complex or detailed ways. For example, interruptions to stopwatch tracked time periods cannot be edited with tt amend
. Since the authoritative ledger is a JSON file, if you need to do complex edits to history you will want to do so with other tools (such as jq
), or a text editor.
Reading records from the ledger can be done with tt ls
, which supports the following options:
-i
/--id
-s
/--sort-by
-f
/--filter
-c
/--csv
-w
/--with-structured-data
-D
/--without-detail
--dryrun
Sorting with --sort-by
allows the records to be sorted by some key that is present in a standard record only when the output format is not one of json
/json-compact
/yaml
. This is because those formats output a dictionary that keys on record ID, and there is no guarantee that serializing that structure will remain ordered on import and export. When printing the data as a CSV or in human-readable form, the sorting works as expected. By default, records are sorted by CommitTime
.
By default, the structured data is not included in the output, however this can be changed with --with-structured-data
(which leaves it in the base64 encoded form). Similarly, the --without-detail
option will omit the detailed text field (Detail
) from the output, useful for summary tables or reports where the CSV output is being consumed directly (and not being send to another application for processing.)
If --id
is given then only the exact specified time record is returned, and any values of --filter
and the presence of --csv
are ignored.
The --csv
option takes no arguments, and will generate a time-sheet-style CSV, with each record on a line, and one column per tag (with marks in the appropriate rows and columns indicating which records were tagged in which way). This overrides any setting of --output-format
, either persistent or on the command line.
The --filter
option can be specified multiple times, and records must match all filters to be contained in the output (that is separate filters are combined with a logical AND). The --filter
options takes JSON documents that describe the filters, with conditions specified in the same JSON documenting being combined with a logical OR (that is, a record matching ANY condition in a single --filter
expression will be returned, but final results must pass every expression provided with a --filter
option)
{
"Tags": [ "string", ... ],
"StartTime": [
{
"Condition": "<"|"<="|"=="|">="|">"|"!=",
"Timespec": "string"
},
...
],
"EndTime": [
{
"Condition": "<"|"<="|"=="|">="|">"|"!=",
"Timespec": "string"
},
...
],
"Description": [ r"regex", ... ],
"Detail": [ r"regex", ... ]
}
For the tag-based filtering, for a given record to match, the set of tags attached to the record must have a non-empty intersection with the given list of tags, or the two sets must be equal (this permits finding untagged records by asking for an empty tag list).
For regular expression based matching, the python re.search
function is used, which allows matching patterns anywhere in the given string if no anchors are specified.
As with tt track
, the timespecs passed to these filters are parsed by dateparser
, and so can be relative or absolute. As with tt track
, --dryrun
is provided to provide transparency in how your timespecs are being parsed.
Find all records, displayed human-readably, and sorted by the time they ended.
tt --output-format human ls --sort-by EndTime
Find all of the untagged records.
tt ls --filter '{"Tags": []}'
Find all of the records tagged with both Personal and Gardening:
tt ls --filter '{"Tags": ["Personal"])' --filter '{"Tags": ["Gardening"]}'
Finding all of the records tagged with Work since the start of the work week, not counting anything in progress.
tt ls --filter '{"StartTime":[{"Condition": ">=", "Timespec": "monday"}]}' \
--filter '{"EndTime":[{"Condition": "<=", "Timespec": "now"}]}' \
--filter '{"Tags": ["Work"]}'
Hooks are executable files placed in the subdirectories of ~/.litt/hooks
, where the subdirectory is named for the hook event that it should be invoked on. Files found in a hook directory are executed in lexicographical order. When hook events are fired and executables are invoked, the hook event name is passed as the first, and only, command line parameter. Additionally, any contextual information is passed in as JSON on stdin; which information this is is indicated below. Supported hook events are:
-
pre_load
: Before the JSON DB file is loaded from disk- Context:
null
- Context:
-
pre_commit
: After all changes are made to the state, but before the state is written to disk.-
Context: The old and new images of any changed items.
- For Aliases (if the OldImage value is
null
, then the alias did not exist before this command; if the NewImage value isnull
then the alias was deleted by the command run):
{ "OldImage": { "AliasKey": { ...<properties> } }, "NewImage": { "AliasKey": { ...<properties> } } }
- For Records (if the OldImage value is
null
, then the alias did not exist before this command; if the NewImage value isnull
then the alias was deleted by the command run):
{ "OldImage": { "RecordId": { ...<properties> } }, "NewImage": { "RecordId": { ...<properties> } } }
- For Aliases (if the OldImage value is
-
-
post_commit
: After all changes are made to the state, and after the state is written to disk.- Context: Same as
pre_commit
- Context: Same as
-
pre_config_write
: After all changes are made to the persistent configuration, but before the config is written to disk.- Context:
null
- Context:
-
post_config_write
: After all changes are made to the persistent configuration, and after the config is written to disk.- Context:
null
- Context:
Sometimes it's useful to be able to edit the JOSN file by hand, but the timestamps are hard to read and interpret for a human. This shell pipeline will insert a new field into each record that adds a human-readable, local-timezone timestamp for each time field in the record.
cat events.json | jq . | grep -v 'TimeHuman":' | \
while read line
do
echo "$line"
echo "$line" | grep -c 'Time":' > /dev/null && \
echo "\"$(echo "$line" | cut -d '"' -f2 | sed 's/$/Human/')\": \
\"$(date -d "@`echo "$line" | cut -d ' ' -f2 | tr -d ','`")\","
done | less