Tiempo#
Synopsys#
t -h|--help
t help SUBCOMMAND
t SUBCOMMAND -h|--help
t SUBCOMMAND [OPTIONS]
Description#
Tiempo is a command-line time tracker. Register the start and end times of your activities and get results by day, week, month or custom periods of time in different formats, including custom ones.
Entries are optionally organized in timesheets which you can use to represent projects, clients etc. Most commands (notably the ones that display entries) work on the current or active timesheet.
The entries are stored in a local Sqlite3 database whose location you can learn
by looking at your config file (whose location you can learn running t c
).
See Files, paths and the environment for more info on setting your config file.
tiempo stores entries as UTC and then converts to the local timezone for displaying and querying. This means that even if you change timezones your recorded times will be correct, although their displayed results might vary if grouping by day for example.
This tool is compatible with a previous one with the same purpose called
timetrap
. The most notable difference being that timetrap stores times as
local timezone times, which makes it messy if you switch timezones. Another
difference is that in timetrap you used the internal ruby API to add custom
display formats and in tiempo
you’ll use standard input/output and
command-line arguments in any language of your choice.
Subcommands#
Bellow you can see the full list of subcommands. All of them can be abbreviated
to their first letter, so for example t backend
can also be typed t b
.
Command |
Description |
---|---|
Archive entries to a hidden timesheet. |
|
Open an sqlite shell to the database |
|
Configure tiempo in-place or get path to config file. |
|
Display all entries in the current sheet. |
|
Edit an entry. |
|
Start an entry in the current timesheet. |
|
Delete an entry or an entire timesheet. |
|
List existing sheets. |
|
Display entries starting this month. |
|
Show all running entries. |
|
End the active entry in the current timesheet. |
|
Restart the timer for an entry. |
|
Switch to a new or existing timesheet. |
|
Display entries that started today. |
|
Display entries starting last monday or later. |
|
Display entries that started yesterday. |
Tutorial#
Life Cycle#
Most of the time you’ll be using these commands:
t in
starts time tracking for an activity.t out
ends time tracking for the current activity.t resume
resumes time tracking for a previous activity.t edit
allows to quickly edit an entry’s details.t archive
archives entries without deleting them from the database.t kill
permanently deletes entries.
You can see the state of your activities using:
t list
shows time tracking summary for all timesheets.t now
shows running entries for all timesheets.t display
shows more detailed information about activities in the current timesheet. Variants of this subcommand includet today
,t yesterday
,t week
andt month
.
To begin, let’s start a task with:
$ t in 'task 1'
Checked into sheet "default".
At any time see what’s happening with:
$ t display
Timesheet: default
Day Start End Duration Notes
Sun Oct 30, 2022 11:24:18 - 0:00:03 first task
0:00:03
----------------------------------------------------------------
Total 0:00:03
After some time of hard work you can stop tracking with:
$ t out
Checked out of sheet "default".
If later (say, after lunch) you decide to continue with this activity you may do so with:
$ t resume
Resuming "task1"
Checked into sheet "default".
Which by default resumes the last task of the timesheet.
After some time (days, weeks, months…) of activities being recorded and queried you might want to archive some of them. For that use:
$ t archive
A total of 2 entries accounting for 9m 38s will be archived.
Proceed? [y/N] y
If you do list
or display
, no activities are going to be active. If you
want to see archived activities, you can do it with:
$ t display full
Timesheet: _default
Day Start End Duration Notes
Mon Oct 24, 2022 18:57:03 - 19:01:25 0:04:21 task1
19:06:49 - 19:12:05 0:05:16 task1
0:09:38
-----------------------------------------------------------
Total 0:09:38
Using sheets#
Usually you need to track activities for different projects or clients. That’s what timesheets are for.
To switch (and create if necessary) to a new timesheet use:
$ t sheet 'My Amazing Project'
Switching to sheet 'My Amazing Project'
Then, start a new activity in the new timesheet with:
$ t in 'An Amazing Activity'
Checked into sheet "My Amazing Project".
Just like before you can use t display
to see your progress. To see a
summary of all of your timesheets use:
$ t list
Timesheet Running Today Total Time
* My Amazing Project 0:00:00 0:00:00 0:00:00
- default 0:00:00 0:00:05 0:00:05
-----------------------------------------------------
0:00:00 0:00:05 0:00:05
You can switch timesheets with:
$ t sheet default
Switching to sheet 'default'
If you switch very often between two timesheets pass -
to switch to the
previously active timesheet.
Next steps#
See the list of subcommands and their options bellow to really take advantage of tiempo. Learn about the different settings (see Settings) and how to use the different formatters (see Default formatters) that are shipped by default or learn how to make your own (see Custom formatters).
Happy time tracking!
Subcommands#
t archive#
t a|archive [OPTIONS] [SHEET]
To archive means to move entries from the current or specified timesheet to a different “hidden” sheet so that when you query the current sheet you don’t see them but they are kept in the database. Archived entries are stored in a sheet with the same name of the original with an underscore as prefix. Use t list to see archived sheets.
By default this command will ask for you confirmation, then archive ALL entries
in the current timesheet. Pass --fake
to only display the entries using the
text
formatter (useful if you want to make sure that the archived
time/entries are correct).
Using --start
and --end
you can further limit the period of time that
will be archived (See Specifying times section of the docs on the
accepted formats). You can use --grep
to only archive certain entries that
match the given regular expression.
If what you need is to archive a specific elapsed time, for example only 15
hours out of however many the timesheet has use --time
with the amount of
hours you want to archive.
Options
- -f, --fake
Don’t actually archive the entries, just display them
- -e, --end <TIME>
Include entries that start on this date or earlier
- -g, --grep <REGEXP>
Include entries where the note matches this regexp.
- -s, --start <TIME>
Include entries that start on this date or later
- -t, --time <HOURS>
Time in hours to archive. Archived time will be equal or less than this.
SHEET
Archive entries from this timesheet instead of the current one.
t backend#
t b|backend
Open a sqlite3 shell into the database. Sqlite3 must be installed and in the
PATH
variable for this command to work.
t configure#
t c|configure [OPTIONS]
If called without arguments t c
will print the absoulte path to the config
file. If you pass arguments they’ll be used to edit your config file in place.
Warning
This will erase any comments you might have added to your toml or yaml file. It is better to edit the config file manually with your preferred text editor:
vim $(t c)
Options
- --auto-checkout
Checkout of current running entry when starting a new one. Not implemented.
- --no-auto-checkout
Starting a new entry fails if one is running (default).
- --no-require-note
Entries can be created without a note.
- --require-note
Prompt for a note if one isn’t provided when checking in (default).
- --append-notes-delimiter <DELIMITER>
delimiter used when appending notes via t edit –append. Default: ‘ ‘ (space).
- --database-file <PATH>
Set the absoulte path to the sqlite database. A new database will be created at this location if one does not exist.
- --default-formatter <FORMATTER>
The format to use when display commands are invoked without a
--format
option. Defaults to'text'
. See Settings section of the docs for per-command default formatter.
--formatter-search-paths <PATH>[,<PATH>...]
comma separated paths to search for user defined fomatters. See Custom formatters section for more info on this.
- --interactive-entries <N>
How many unique previous notes to show when selecting interactively. An example of an interactive command is
t resume -i
.- --note-editor <EDITOR>
Specify a custom command used to edit an entry’s note. Default: $EDITOR
- --round-in-seconds <SECONDS>
The duration of time to use for rounding with the -r flag. Default: 900 (15 m). Not implemented.
- --week-start <DAY>
The day of the week to use as the start of the week for t week. Default: monday [possible values: monday, tuesday, wednesday, thursday, friday, saturday, sunday]
t display#
t d|display [OPTIONS] [SHEET|all|full]
Display all entries in the current timesheet using the specified formatter (by
default this is text
). For more information on formatters see
Default formatters. For information on custom formatters see
Custom formatters.
It is worh noting that BOTH the --start
and --end
arguments act on
the start time of the entry.
Options
- -v, --ids
Print database ids (for use with edit).
- -e, --end <TIME>
Include entries that start on this date or earlier.
- -f, --format <FORMAT>
The output format. Valid built-in formats are ‘chart’, ‘text’, ‘ical’, ‘csv’, ‘json’, ‘ids’ and ‘activities’. See Default formatters for more info. See Custom formatters to learn how to make your own.
- -g, --grep <REGEXP>
Only display entries whose notes match this regular expression.
- -s, --start <TIME>
Include entries that start on this date or later.
- --sort <COLUMN>
Sort entries by this in the activities formatter. Options are text, time and last_started. Add a - (minus sign) before the column name to make it a descending order. Default is -last_started.
<SHEET>
The sheet to display. Pass ‘all’ to see entries from all sheets or ‘full’ to see hidden entries.
New in 1.7
Pass partial formatter names. For example pass t as argument to commands that accept –format and it will be understood as text formatter. It uses a prefix match.
t edit#
t e|edit [OPTIONS] [NOTE]
If run without arguments this command will launch a text editor with the text of
the current timesheet’s last entry’s note. Upon saving and exiting the new text
will be set. If a string is given as command-line argument it will be used as
the new note and no editor will be launched. If used with --append
the
new text will be appended to the previous one using whatever string is set as
append_notes_delimiter in the settings file.
The last entry is defined as the one that was started last, not necessarily the one with the highest ID in the current timesheet.
You can also leave the note untouched and edit other parts of the entry like its
start and end times using --start
and --end
respectively, see
Specifying times. Or you can move an entry that was created in the wrong
timesheet by using --move
.
Options
- -z, --append
Append the new text to the current note instead of replacing it.
- -e, --end <TIME>
Set this as the end time.
- --id <ID>
Edit entry with this ID instead of the last one in the current sheet.
- -m, --move <SHEET>
Move entry to another sheet.
- -s, --start <TIME>
Set this as the start time.
<NOTE>
Set this as the entry’s note. It will replace the previous one unless
--append
is given.
t help#
t help SUBCOMMAND
Get help on the specified subcommand. It is equivalent to passing -h
to any
subcommand.
t in#
t i|in [--at TIME] [NOTE]
Start an entry in the current timesheet. If used without arguments and
require_note is set to true
in the settings then it will launch a
text editor for you to write a note. Pass --at
to set a specific time of
start instead of now, like in case you forgot to start an activity five minutes
ago:
t i --at '5 min ago' 'writing the codez'
t kill#
t k|kill (--id ID|--last|SHEET)
Use this to completely delete entries. Pass a timesheet name to delete all
entries from it or use --id
to delete an entry by its ID. IDs from entries
can be shown passing -v
to ‘display’ subcommand and variants. To delete only
the last entry from the sheet pass --last
as single argument.
This command will ask for confirmation before touching the database.
If you’d prefer to keep record of entries but have them out of the way use t archive.
Options
- -l, --last
Delete the last entry of the current sheet
- --id <ID>
Delete entry with this ID instead of sheet
<SHEET>
Delete an entire sheet by its name
t list#
t l|list [-a|--all] [-f|--flat]
Lists existing timesheets. Pass --all
to also list hidden (archived) ones.
The output includes 4 columns: the name of the timesheet, the elapsed time of
the running entry, the accumulated time for the day and the total time (sum of
all entries per timesheet).
Timesheet Running Today Total Time
default 0:00:00 0:00:00 2:39:38
- foo 0:00:00 0:30:01 0:30:01
* pink 0:00:00 0:05:04 0:05:04
--------------------------------------------
0:00:00 0:35:06 3:14:44
An asterisk (*) at the left will indicate the current timesheet and a dash (-) indicates the previously used one. This is relevant for t sheet where you can use - as the timesheet name to just switch to the previous one.
New in 1.5.3
Pass --flat
to only display timesheet names one per line. Useful for passing
the list to other commands (it is used for the different completion scripts).
$ t l --flat
default
foo
pink
t month#
t m|month [OPTIONS] [SHEET|all|full]
Like t display but only display entries that started this month in the
current timezone. Pass --month MONTH
to display entries of different months
or the special strings this
(default), current
or last
. It accepts
whole month names or just the first three letters as in jun
or sep
.
Just like t-display
this accepts --grep
, --ids
, and --format
.
t now#
t n|now
Show only timesheets with running entries. Since every timesheet can only have one running entry at a time this command displays its note.
Timesheet Running Activity
* default 0:00:01 reading
- pink 0:00:21 gardening
Just like t list it displays an asterisk for the current timesheet and a dash for the previous one.
t out#
t o|out [--at TIME]
End the active entry in the current timesheet, if any. End time is set as the
current time unless --at
is passed, case in which the specified time is set
as end time for the entry. See Specifying times to learn the accepted
formats.
t resume#
t r|resume [OPTIONS]
If called without arguments this command will start a new entry with the same note as the last started entry in the current sheet. It is an easy way of continuing with a task that was suspended.
To restart an entry with one of the last 10 unique notes pass --interactive
which will show an UI like this:
Latest entries of sheet 'dafault':
# Note Total time Last started
5 try to find a pair for this sock 11:35:35 1 week ago
4 pensar en la inmortalidad del cangrejo 1:31:09 1 week ago
3 cook fried rice 0:50:03 4 days ago
2 investigate quantum mechanics 10:59:17 11 hours ago
1 turn lead into gold 1:24:14 10 hours ago
Enter a number to select, write q or send EOF to cancel, or write a note wrapped
in '' or \"\" to start a new entry.
>>
The number of unique entries shown is configurable with the interactive_entries setting. To proceed pass any of the shown numbers of the left column and press intro or enter q or Ctrl+D to exit. It is also possible (from tiempo 1.9.0 onwards) to start a new entry by writing its note wrapped in double or single quotes in the prompt.
To restart an entry whose ID you know pass --id ID
.
Options
- -i, --interactive
Select one of the latest unique entries interactively
- --id ID
Resume entry by its ID
t sheet#
t s|sheet [SHEET]
Switch to a timesheet with name SHEET
, creating it if it doesn’t exist. If
used without arguments this command is an alias to t list so you can see
what timesheets you have.
tiempo
remembers the last sheet you used (unless it doesn’t exist anymore),
so you can switch quickly between the current and the previous one passing -
(a dash) as the sheet name.
t today#
t t|today [OPTIONS] [SHEET|all|full]
Like t display but only display entries that started today in the current
timezone. Pass --end
to further filter entries that started earlier than the
given time.
Just like t-display
this accepts --grep
, --ids
, and --format
.
t week#
t w|week [OPTIONS] [SHEET|all|full]
Like t display but only display entries that started this week in the
computer’s timezone. The first day of the week by default is monday
but this
is configurable. See Settings and t configure.
Just like t-display
this accepts --grep
, --ids
, and --format
.
t yesterday#
t y|yesterday [OPTIONS] [SHEET|all|full]
Like t display but only display entries that started yesterday in the computer’s current timezone.
Just like t-display
this accepts --grep
, --ids
, and --format
.
Settings#
Tiempo settings are kept in a single TOML
file. You can learn its location
by running t c
. Here is the full list with descriptions.
append_notes_delimiter#
Type: string Default: " "
String used to concatenate text when --append
is passed to t edit.
auto_checkout#
Type: bool Default: false
Not implemented. Kept for compatibility with timetrap
. Tracking issue
https://gitlab.com/categulario/tiempo-rs/-/issues/11
auto_sheet#
Type: string Default: "dotfiles"
Not implemented. Kept for compatibility with timetrap
. Tracking issue:
https://gitlab.com/categulario/tiempo-rs/-/issues/9
auto_sheet_search_paths#
Type: [string] Default Depends on OS, irrelevant anyways
Not implemented. Kept for compatibility with timetrap
. Tracking issue:
https://gitlab.com/categulario/tiempo-rs/-/issues/9
commands#
tiempo
allows having per-command configuration. For now it limits to
per-command default formatters introduced in 1.4.0
. To set a default
formatter other than the default text
use something like this in your
settings file:
In toml
[commands.week]
default_formatter = "chart"
In yaml
commands:
week:
default_formatter: chart
To set a default formatter for every command use default_formatter.
database_file#
Type: string Default: Depends on OS
Absolute path to the database file.
default_command#
Type: string|null Default: null
Not implemented. Kept for compatibility with timetrap
. Implementation
planned. Tracking issue: https://gitlab.com/categulario/tiempo-rs/-/issues/10
default_formatter#
Type: string Default: "text"
Formatter used for t display subcommand and variants when no --format
argument is passed. Included formatters are explained in the
Default formatters section, they are: ‘chart’, ‘text’, ‘ical’, ‘csv’,
‘json’, ‘ids’, ‘activities’.
To set a default formatter for only one command see the commands config above.
formatter_search_paths#
Type: [string] Default: Depends on OS, but not empty
Array of paths to search for custom formatters. See Custom formatters to learn how to write them. Custom formatters must be executable files.
formatters#
The chart formatter introduced in 1.4.0
has a few settings.
It is also possible to store settings for your custom formatters in this
variable. Take a look at the following example:
[formatters.earnings]
currency = "USD"
hourly_rate = 400
[formatters.chart]
daily_goal_hours = 4
weekly_goal_hours = 20
character_equals_minutes = 30
formatters:
earnings:
currency: MXN
hourly_rate: 280
chart:
daily_goal_hours: 4
weekly_goal_hours: 20
character_equals_minutes: 30
Check Custom formatters to learn how to read this settings in your formatters.
Available settings for the chart formatter are:
daily_goal_hours
(integer, default: 0) Number of hours you set as your daily goal. Will display a uniform gray bar at the set hours that will turn green as you make progress.weekly_goal_hours
(integer, default: 0) Number of hours you expect to work in a week. Controls wether the weekly hour count displays in green color.character_equals_minutes
(integer, default: 30). Controls how many minutes each character of the chart represents. Set lower for wider charts. Set to 0 to make tiempo crash.
interactive_entries#
Type: integer Default 10
Number of unique entries to show when interactive commands like t resume are invoked.
note_editor#
Type: string|null Default null
Specify an editor to use when starting an entry without a note and when
require_note is true. By default tiempo
respects the $EDITOR
variable so you can leave this setting as null
most of the time.
require_note#
Type: bool Default true
Wether or not entries require a note to be recorded. If true then running t in without arguments will launch the text editor configured in note_editor
round_in_seconds#
Type: integer Default: 900 (15 min)
Not implemented, kept for compatibility with timetrap. Tracking issue: https://gitlab.com/categulario/tiempo-rs/-/issues/6
week_start#
Type: string Default "monday"
Day of the week that should be considered as the start for t week.
Options are "monday"
, "tuesday"
, "wednesday"
, "thursday"
,
"friday"
, "saturday"
, "sunday"
.
Files, paths and the environment#
Tiempo uses the same directory to store the database and configuration. It’s location depends on the operative system, but it’s likely one of these options:
On Linux
tiempo respects XDG
variables. So most likely the config directory is
$HOME/.config/tiempo
.
On windows
{FOLDERID_RoamingAppData}
On mac
$HOME/Library/Application Support
To be 100% sure run t c
.
You can set the config file to a different location by setting the
TIEMPO_CONFIG
envornment variable to an absolute path that includes
the filename like this:
TIEMPO_CONFIG=/path/to/my_config.toml
Inside the config directory you’ll find two files: config.toml
and
database.sqlite3
which are your settings file and database respectively.
Tiempo also recognises traditional timetrap
config, environment and
directories, which in all platforms should be: $HOME/.timetrap.yml
for the
config file, $HOME/.timetrap.db
for the database and
TIMETRAP_CONFIG_FILE
to set the database to a different location.
From within the config file you can set the paths to the database and custom formatters as explained in the Settings.
Specifying times#
Some arguments accept a time as value, like t in
’s --at
or t d
--start
. These are the accepted formats:
Something similar to ISO format will be parsed as a time in the computer’s timezone.
2021-01-13
a date2019-05-03 11:13
a date with portions of a time
ISO format with offset or UTC will be parsed as a time in the specified
timezone. Use Z
for UTC
and an offset for everything else
2021-01-13Z
2005-10-14 19:20:35+05:00
something that looks like an hour will be parsed as a time in the current
day in the computer’s timezone. Add Z
or an offset to specify the timezone.
11:30
23:50:45
(with seconds)
some human times, like time ago or yesterday times:
an hour ago
a minute ago
50 min ago
1h30m ago
two hours thirty minutes ago
yesterday at 15:20
(from1.8.0
)now
(from1.9.0
)
Default formatters#
Tiempo comes with a handful of formatters that display your entries in different ways. Here’s the full list.
chart#
(New in 1.4.0) Displays a nice chart of the weekly progress:
Date Day Chart Hours
Sep 5 Mon ####### 3.8
6 Tue #### 2.3
7 Wed ####### 3.9
8 Thu 0.0
9 Fri ######### 4.5
10 Sat ######### 4.7
11 Sun 0.0
Week 19.3/20.0
Timesheet: default
This formatter comes with a few settings. See formatters for more.
text#
Displays nicely the entries grouped by day. This is the default formatter. If
multiple sheets are displayed at once a column with the sheet name is added.
Pass -v
to also reveal each entry’s id:
Timesheet: foo
Day Start End Duration Notes
Tue Sep 20, 2022 09:09:58 - 09:40:13 0:30:15 write tiempo's docs
09:41:01 - 10:11:20 0:30:18 work on pizarra.categulario.xyz
1:00:34
-------------------------------------------------------------------------------------
Total 1:00:34
ical#
This formatter’s output can be redirected to a file and then uploaded to a calendar app:
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
METHOD:PUBLISH
PRODID:tiempo-rs
VERSION:2.0
BEGIN:VEVENT
DESCRIPTION:write tiempo's docs
DTEND:20220920T134013Z
DTSTAMP:20220920T130958Z
DTSTART:20220920T130958Z
SEQUENCE:0
SUMMARY:write tiempo's docs
UID:tiempo-10@abraham-lenovo-t470s
END:VEVENT
BEGIN:VEVENT
DESCRIPTION:work on pizarra.categulario.xyz
DTEND:20220920T141120Z
DTSTAMP:20220920T134101Z
DTSTART:20220920T134101Z
SEQUENCE:0
SUMMARY:work on pizarra.categulario.xyz
UID:tiempo-11@abraham-lenovo-t470s
END:VEVENT
END:VCALENDAR
csv#
Dump the entries as CSV to stdout. Useful for passing entries to a different
tool. Pass -v
to add an id
column.
start,end,note,sheet
2022-09-20T13:09:58.805280Z,2022-09-20T13:40:13.837994Z,write tiempo's docs,foo
2022-09-20T13:41:01.478854Z,2022-09-20T14:11:20.461322Z,work on pizarra.categulario.xyz,foo
json#
Dump the entries in JSON format:
[{"id":10,"note":"write tiempo's docs","start":"2022-09-20T13:09:58.805280283Z","end":"2022-09-20T13:40:13.837994932Z","sheet":"foo"},{"id":11,"note":"work on pizarra.categulario.xyz","start":"2022-09-20T13:41:01.478854071Z","end":"2022-09-20T14:11:20.461322041Z","sheet":"foo"}]
ids#
Dump only the ids in a single line:
2196 2197 2198 2199 2200 2201
activities#
Show a list of activities grouped by note with the accumulated time and how long ago it was last started.
Note Time Last started
elaboración de reporte 0:48:21 2 hours ago
construcción de la interfaz de grafos 23:08:59 5 days ago
contribuir a nested_inline 0:37:53 3 weeks ago
Pass --sort COLUMN
to sort the table by one of time
, note
and
last_started
. Prepend a -
to sort in descending order.
Custom formatters#
You can implement your own formatters for all subcommands that display entries
(like t display
, t week
etc.). It is as easy as creating an executable
file written in any programming language (interpreted or compiled) and placing
it in a path listed in the config value for formatter_search_paths
.
This executable will be given as standard input a csv stream with each row
representing a time entry with the same structure as the csv
formatter
output. It will also be given a command line argument representing user settings
for this formatter stored in the config file and formatted as JSON.
Example#
Let’s create a simple earnings
formatter. It is as easy as creating the
following python script under the name earnings
(no extension) inside one of
the paths listed in your formatter_search_paths setting and marking it as
executable (chmod +x earnings
):
#!/usr/bin/env python3
import sys, json, csv
from datetime import datetime, timezone, timedelta
from math import ceil
# Settings for this formatter are passed as the first argument
config = json.loads(sys.argv[1])
reader = csv.DictReader(
sys.stdin, # entries are received from stdin
fieldnames=['id', 'start', 'end', 'note', 'sheet'],
)
total = timedelta(seconds=0)
for line in reader:
# times are formatted as ISO format
start = datetime.strptime(line['start'], '%Y-%m-%dT%H:%M:%S.%fZ')
# entries that are still running don't have a value in the 'end' column
if not line['end']:
end = datetime.utcnow()
else:
end = datetime.strptime(line['end'], '%Y-%m-%dT%H:%M:%S.%fZ')
total += end - start
hours = total.total_seconds() / 3600
# use the settings
earnings = hours * config['hourly_rate']
currency = config['currency']
print(f'You have earned: ${earnings:.2f} {currency}')
You’ll also need to add these settings to your config file:
[formatters.earnings]
hourly_rate = 300
currency = "USD"
Now if you run t display -f earnings
you will get something like:
You have earned: 2400 USD