Logo banner

Introduction

TaskLite is a CLI task manager built with Haskell and SQLite.

$ tl add Buy milk +groceries
πŸ†• Added task "Buy milk" with id "01dd62xryn5fnzjgynkcy06spb"

$ tl add Go running
πŸ†• Added task "Go running" with id "01dd62yjtrtmaph23knff6mbsj"

$ tl
Id  Prio  Opened UTC  Body
pb   2    2019-06-12  Buy milk  +groceries
sj   0    2019-06-12  Go running

$ tl do pb
βœ… Finished task "Buy milk" with id "01dd62xryn5fnzjgynkcy06spb"

The Code is available on GitHub.

For help and ideas please come visit us at our GitHub Discussions!

Latest Versions

  • CLI version: 0.3.0.0
  • App version: 0.3.0.0

Full Changelog

Installation

Check out the following pages for instructions on how to install the different versions of TaskLite.

Updating

When you update to a newer version of TaskLite, all your data will automatically migrated to the latest version. This also works if you skip a few releases.

Nevertheless, it's highly recommended to run tasklite backup before installing a new version.

Also be aware that rollbacks are currently not supported.

CLI Tool



Prebuilt Binaries

MacOS

Install it via my Homebrew tap:

brew cask install ad-si/tap/tasklite

You can also get this (and previous) versions from the releases page.

Make sure to download the artifacts with curl or wget as macOS prevents the execution of files downloaded via a browser.

Furthermore you can get the latest CI builds on GitHub's Actions page. They are quite stable since only tested and production ready code gets merged into the master branch.

Linux

You can get the latest versions on GitHub's Releases page.

Furthermore you can get the latest CI builds on GitHub's Actions page. They are quite stable since only tested and production ready code gets merged into the master branch.

Prebuilt Docker Image

Another easy way to get started is using the prebuilt Docker image:

docker run --rm adius/tasklite sh
tasklite help

When exiting the container all data will be discarded.

For repeated local usage run following command, but make sure to replace $TASKLITE_PATH with the path to your TaskLite installation as defined in your config.yaml file. Per default it's created in the XDG base directory: $HOME/.local/share/tasklite.

docker run \
  --rm \
  --volume "$TASKLITE_PATH":/root/.local/share/tasklite \
  adius/tasklite

To make it easier to use, create an alias like:

alias tl="docker run …"

Providing your own config.yaml file to the docker container is not yet supported.

From Source

To build TaskLite from source, you need to install Stack first.

git clone https://github.com/ad-si/TaskLite
cd TaskLite
stack install tasklite-core

To test the installation run:

tasklite help

Configuration

It's a good idea to customize your config file at ~/.config/tasklite/config.yaml afterwards.

Check out the example config file for infos about available settings.

Desktop App



Native GTK App

Attention: This is still early alpha

A few dependencies must be available to build the app. To install them on macOS run:

brew install \
  gtk+3 \
  libffi \
  gobject-introspection \
  gdk-pixbuf
git clone https://github.com/ad-si/TaskLite
cd TaskLite
stack install tasklite-app

It might be necessary to add the package "libffi" to the pkg-config search path before installation. For example with fish:

set -x PKG_CONFIG_PATH /usr/local/opt/libffi/lib/pkgconfig

Start it with:

tasklite-app

DB Browser for SQLite

Alternatively you can use the DB Browser for SQLite to view and modify your tasks directly in the SQLite database.

Web App



Datasette

The web app is currently based on Datasette and can only be used to view tasks, but not to create new ones.

In combination with the Docker container the web frontend for the SQLite database can be served in following way:

docker run \
  --rm \
  --entrypoint datasette \
  --publish 8001:8001 \
  --volume ~/TaskLite:/root/tasklite \
  --volume "$PWD"/datasette:/root/datasette \
  adius/tasklite \
  serve \
    --host 0.0.0.0 \
    --metadata /root/datasette/metadata.json \
    --reload \
    /root/tasklite/main.db

Attention: Make sure that the IP address matches with your host's.

There is a predefined query for a tl head like overview: http://0.0.0.0:8001/main/tasks_pretty

Generate custom view by appending the SQL query to http://0.0.0.0:8001/main?sql=. For example http://0.0.0.0:8001/main?sql=select%20\*%20from%20tasks.

Some example views:

Equivalent to tl head:

select substr(ulid,22) as ulid,priority,body,due_utc,
  replace(tags,',',', ') as tags,notes,user
from tasks_view
where closed_utc is null
order by priority desc, due_utc asc, ulid desc
limit 50

Make sure to bookmark the views for easy access.

SQLite Web

Another way to host a simple web frontend is SQLite Web. While it's more bare bones than Datasette, it has the advantage that it also allows you to modify data.

docker run -it --rm \
  -p 8080:8080 \
  -v ~/TaskLite:/data \
  -e SQLITE_DATABASE=main.db \
  coleifer/sqlite-web

FAQs

Why another CLI task manager?

Answer:

Taskwarrior has been the gold standard for CLI task managers so far. However, I repeatedly lost tasks due to weird bugs and syncing issues. I also found several UI decisions inept and wanted something with a better workflow. But probably most importantly I couldn't see myself contributing to a C++ project. I had been working with C++ at university and it wasn't pleasant.

To sum it up: I finally wanted something which I could fully own and use until the end of days. That means:

  • Does not suddenly get bought by a bigger fish and get closed down or made unusable (looking at you Wunderlist)
  • Is written in a high-performance programming language, yet gives me lot's of guarantees about the code's stability and makes it easy for other developers to contribute
  • Free software
  • With a stable, future proof, powerful, and fast backend (currently SQLite, but support for plain files and Git is planned)

Why not Org-mode style?

Answer:

I don't like Org-mode's' unstructured combination of outlining, notes and tasks. Furthermore I don't like interactive document editing UIs in the terminal. I prefer REPL style apps which adhere to UNIX conventions and let me compose them easily with other CLI tools.

This, however, is just a personal preference and otherwise Org-mode is certainly a good solution. Also check out Smos, which is another powerful tree-based editor with extra focus on Getting Things Done.

What are your long term goals?

Answer:

For the product roadmap check out the dedicated page for it

However, this project is not just about the product, but just as well about the underlying values. Big companies are good at offering you fancy products without clarifying any of the adjacent issues which are crucial for an outstanding user experience beyond the product itself:

  • Will this service also be available in the future?
  • What are your incentives to keep this service alive?
  • What happens if the company dies?
  • Can I export my data in a practical format?
  • Who has data sovereignty?
  • How do you share / sell my data?
  • …

Will this service also be available in the future?

Answer:

The code is completely free open source software and compiling and using it is straight forward. Whatever happens to me, TaskLite will always be available in this way.

What are your incentives to keep this service alive?

Answer:

I'm using it daily and it has become something like my second brain. Since I'm not interested in abandoning my brain, it will be maintained and further developed in the future.

What happens if you die?

Answer:

A good friend has access to the repository and can transfer it to the community.

Can I export my data in a practical format?

Answer:

Not locking you in is one of the most important aspects of TaskLite. Therefore it supports numerous export formats:

  • JSON - All tasks as one JSON array
  • JSON Lines - One JSON object per task
  • CSV
  • SQL
  • Direct access to the SQLite database

For more information check out the export documentation

Who has data sovereignty?

Answer:

All your data is stored in your TaskLite database on your computer. No analytics data or data of any other kind gets transferred to a third party during the usage of TaskLite.

Do you share or sell my data?

Answer:

We do not have access to any of your data!

Answer:

There are several ways to stay up to date:

Roadmap



There is no time frame when the following enhancements will be implemented since the amount of time I can spare to work on TaskLite fluctuates heavily.

However, I'm using it daily and it has become something like my second brain. Since I'm not interested in abandoning my brain, rest assured that there will be steady progress on this in the future.

The enhancements are roughly listed with descending priority.

Feature Parity with Taskwarrior

Since TaskLite started out as a replacement for Taskwarrior, one of my main goals is to reach feature parity with it. Some of the major features still missing:

  • Hooks
  • History View
  • Burndown Chart
  • Calendar View

Check out the page "Differences to Taskwarrior" for a detailed comparison.

Tasklite Core

Currently the core of TaskLite and the CLI are somewhat mixed up in the code. I'd like to split them up, so that TaskLite Core can be used as a library in other applications which need to manage internal tasks / queues.

Check out the page Haskell API for Programmatic Usage for more information.

Syncing

There is no concrete plan for native synchronization support yet. I've been using Dropbox for synchronizing the TaskLite database among several computers, which has worked astonishingly well so far.

Another good solution could be Litestream, which continuously streams SQLite changes to S3-compatible storage.

Apps

Webapp

I'd like to be able to run TaskLite on a cheap v-server in a dedicated server mode and have it host a simple REST API, and an Elm webapp consuming it.

This would then be a good way to casually browse and edit the tasks in a more graphical way.

Mobile App

There are no plans for a native mobile app. The webapp should be sufficient for most use cases.

Desktop App

While there is already a prototype for a desktop app included in the code, I currently don't plan to further work on it any time soon. A webapp -- which could also be hosted locally -- should be sufficient for most use cases.

Concepts



General

TaskLite tries to adhere to the Unix philosophy: Do one thing and do it well. This, however, also means that it intentionally does not include certain features, which you might expect it to have.

For example there is no support for fuzzy timestamps like "in 2 weeks", as this should be handled by other CLI tools. Since CLI tools can be easily combosed, this is no disadvantage!

For example, I use tu to convert such fuzzy times to exact timestamps:

tl add Buy milk due:$(tu in 2 weeks)

And the big advantage of composing tools is, that you now can use tu in other contexts as well!

The power of the Unix philosophy:
Each new tool you learn gives you a compound interest on all other tools!

tip

If you use fish, it will be even shorter:

tl add Buy milk due:(tu in 2 weeks)

States

Instead of allowing one to explicitly set a state, TaskLite infers the current state from several other fields. There are primary, secondary, and tertiary states.

2 primary states:

  • Open - Waits to be done
  • Closed - Nothing left to be done

9 exclusive secondary states:

  • Asleep - Is hidden because it's not relevant yet
  • Awake - Has become relevant or will become soon
  • Ready - Is ready to be done (similar to Open)
  • Waiting - It's still unclear if the task needs to be done or really has been done. Regular checks are necessary until situation clears up.
  • Review - It's necessary to check if the task can finally be started or if it has finally been completed.
  • Done - Has been done
  • Obsolete - Has become obsolete or impossible to finish
  • Deletable - Not needed anymore and can be deleted (item in the trash)

Β 

State\Fieldawake_utcready_utcwaiting_utcreview_utcclosed_utcstate
Openβ”β”β”β”βŒβŒ
└─Asleep> now> now or ❌❌❌❌❌
└─Awake< now> now or ❌❌❌❌❌
└─Ready< now or ❌< now❌❌❌❌
└─Waiting❔❔< now> now or ❌❌❌
└─Review❔❔❔< now❌❌
Closedβ”β”β”β”βœ…β”
└─Doneβ”β”β”β”βœ…Done
└─Obsoleteβ”β”β”β”βœ…Obsolete
└─Deletableβ”β”β”β”βœ…Deletable

Legend:

  • βœ… = Set
  • ❌ = Not set
  • ❔ = Maybe set

3 exclusive tertiary states:

  • Repeating - If this task get completed, a duplicate will be created with the specified time offset. I.e. subsequent tasks get delayed (e.g. mowing the lawn)
  • Recurring - Task which needs to be done every day, week, etc. I.e. missed completions must be caught up immediately. (e.g. paying rent) The number of tasks which will be created in advance can be set via a config.
  • Frozen - Was previously repeating or recurring but has been stopped for the time being.
State\Fieldgroup_ulidrepetition_durationrecurrence_duration
Repeatingβœ…βœ…βŒ
Recurringβœ…βŒβœ…
Frozenβœ…βŒβŒ

A task is either recurring or repeating, but can't be both at the same time. For more information and examples check out the corresponding documentation page

Priority

The priority of a task is a decimal number between negative and positive inifity. It is automatically calculated based on the age, the due date, and several other values.

Priority

The idea is that you never have to manually set a priority, because it can be derived accurately from other values. This of course requires you to use the other available meta information adequately!

The exact calculation algorithm can be found in the taskViewQuery function in DbSetup.hs.

If you want to adjust the priority of selected tasks manually, you can use the tl boost [ulid] command to increase the priority by 1, or the tl hush [ulid] command to decrease it by 1.

Notes

A task can have several notes. Each note is identified by an ULID.

$ tl add Buy milk
πŸ†• Added task "Buy milk" with id "01dpgj8e9ws2dwgvsk5nmrvvg9"

$ tl note 'The vegan one from Super Buy' 01dpgj8e9ws2dwgvsk5nmrvvg9
πŸ—’  Added a note to task "Buy milk" with id "01dpgj8e9ws2dwgvsk5nmrvvg9"

$ tl info 01dpgj8e9ws2dwgvsk5nmrvvg9
awake_utc: null
review_utc: null
state: null
repetition_duration: null
recurrence_duration: null
body: Buy milk
user: adrian
ulid: 01dpgj8e9ws2dwgvsk5nmrvvg9
modified_utc: 2019-10-06 12:59:46
group_ulid: null
closed_utc: null
priority_adjustment: null
metadata: null
waiting_utc: null
ready_utc: null
due_utc: null
priority: 1.0
tags:

notes:
  - note: The vegan one from Super Buy
    ulid: 01dpgjf35pq74gchsgtcd6fgsa

Repetition and Recurrence

The difference between repetition and recurrence is the reference point from which future tasks are calculated from.

For repetition, it's the closing datetime. E.g. if a task gets completed, a duplicate will be created with the specified time offset added to the closing datetime. Check out the "Mow the law" example below for more details.

Recurring tasks are tasks which need to be done every day, week, etc. and must not be delayed. I.e. missed completions must be caught up immediately. Check out the "Pay the rent" example below for more details.

A task is either recurring or repeating, but can't be both at the same time. For both repetition and recurrence series, only the currently active task exists and subsequent tasks will be created on demand, once the current task is closed.

Example for Repetition

$ tl add Mow the lawn due:2020-10-01
πŸ†• Added task "Mow the lawn" with id "01eme2w9pqbmgme5zcwr9hpfbc"

$ tl repeat P1M 01eme2w9pqbmgme5zcwr9hpfbc
πŸ“… Set repeat duration of task "Mow the lawn"
with id "01eme2w9pqbmgme5zcwr9hpfbc" to "P1M"

$ tl info 01eme2w9pqbmgme5zcwr9hpfbc
Mow the lawn

   State: Open
Priority: 12.0
    ULID: 01eme2w9pqbmgme5zcwr9hpfbc

πŸ“…    Due     2020-10-01 00:00:00
       ⬇
πŸ†•  Created   2020-10-12 09:39:48
       ⬇
✏️   Modified  2020-10-12 09:39:58

Repetition Duration: P1M
Group Ulid: 01eme2wm3e4wcwacnw9ha7ffs1
User: adrian

$ tl do 01eme2w9pqbmgme5zcwr9hpfbc
βœ… Finished task "Mow the lawn" with id "01eme2w9pqbmgme5zcwr9hpfbc"
➑️  Created next task "Mow the lawn"
in repetition series "01eme2wm3e4wcwacnw9ha7ffs1"
with id "01eme2xe4vjv3yd3gkg0a9y8j8"

$ tl info 01eme2xe4vjv3yd3gkg0a9y8j8
Mow the lawn

   State: Open
Priority: 3.0
    ULID: 01eme2xe4vjv3yd3gkg0a9y8j8

✏️   Modified  2020-10-12 09:39:58
       ⬇
πŸ†•  Created   2020-10-12 09:40:25
       ⬇
πŸ“…    Due     2020-11-12 09:39:58

Repetition Duration: P1M
Group Ulid: 01eme2wm3e4wcwacnw9ha7ffs1
User: adrian

Example for Recurrence

$ tl add Pay rent due:2020-10-01
πŸ†• Added task "Pay rent" with id "01eme47dje7bpkmz01s5xdtw15"

$ tl recur P1M 01eme47dje7bpkmz01s5xdtw15
πŸ“… Set recurrence duration of task "Pay rent"
with id "01eme47dje7bpkmz01s5xdtw15" to "P1M"

$ tl info 01eme47dje7bpkmz01s5xdtw15
Pay rent

   State: Open
Priority: 12.0
    ULID: 01eme47dje7bpkmz01s5xdtw15

πŸ“…    Due     2020-10-01 00:00:00
       ⬇
πŸ†•  Created   2020-10-12 10:03:21
       ⬇
✏️   Modified  2020-10-12 10:03:32

Recurrence Duration: P1M
Group Ulid: 01eme47s0yy848wvbsyxh9mpj6
User: adrian

$ tl do 01eme47dje7bpkmz01s5xdtw15
βœ… Finished task "Pay rent" with id "01eme47dje7bpkmz01s5xdtw15"
➑️  Created next task "Pay rent"
in recurrence series "01eme47s0yy848wvbsyxh9mpj6"
with id "01eme487qmxj7jm4mtn5n59nbg"

$ tl info 01eme487qmxj7jm4mtn5n59nbg
Pay rent

   State: Open
Priority: 3.0
    ULID: 01eme487qmxj7jm4mtn5n59nbg

✏️   Modified  2020-10-12 10:03:32
       ⬇
πŸ†•  Created   2020-10-12 10:03:47
       ⬇
πŸ“…    Due     2020-11-01 00:00:00

Recurrence Duration: P1M
Group Ulid: 01eme47s0yy848wvbsyxh9mpj6
User: adrian

Import / Migration

This is a best effort list on how to import your tasks from other task managers to TaskLite.



YAML File

If you have all you tasks in one YAML file like this:

- id: 123
  body: Buy milk
  tags: [groceries]

- id: 456
  body: Go running
  tags: [sport]

Run following command to import it. Be sure to make yaml2json available in your path and to install jq first.

cat tasks.yaml \
| yaml2json \
| jq -c '.[]' \
| while read -r task
  do
    echo "$task" | tasklite importjson
  done

Taskwarrior

TaskLite supports all fields of Taskwarrior's export format. Therefore migration is really simple:

task export rc.json.array=off \
| while read -r task; \
  do echo $task | tasklite importjson; \
  done

Google Tasks

There is currently no proper way to export tasks.

A workaround is:

  1. Open the standalone view of Google Tasks
  2. Select all text with cmd + a and copy it
  3. Paste it in a text editor
  4. Format it properly
  5. Import it with a while loop as seen in the Taskwarrior section

Google Keep

You can export all tasks / notes from Google Keep via Google Takeout. It provides a Takeout/Keep directory with one .html and .json file per task.

To import the .json files, change into the directory and run following command:

find . -iname '*.json' \
| while read -r task
  do
    jq -c \
      '.textContent as $txt
        | .labels as $lbls
        | .title as $title
        | (if .isArchived then "done"
          elif .isTrashed then "deletable"
          else null
          end) as $state
        | {
            utc: .userEditedTimestampUsec,
            body: ((if $title and $title != "" then $title else $txt end)
              + (if .listContent
                then "\n\n" +
                  (
                    .listContent
                    | map("- [" + (if .isChecked then "x" else " " end) + "] "
                      + .text)
                    | join("\n")
                  )
                else ""
                end))

          }
        | if $lbls then . + {tags: ($lbls | map(.name))} else . end
        | if $title and $title != "" and $txt and $txt != ""
          then . + {notes: [{body: $txt}]}
          else .
          end
        | if $state then . + {state: $state} else . end
      ' \
      "$task" \
    | tl importjson
  done

The title of the Google Keep note becomes the body of the task and the note itself becomes a TaskLite note attached to the task. A list of sub-tasks will be converted to a GitHub Flavored Markdown task list.

Telegram

Telegram's "Saved Messages" -- a.k.a. messages to oneself -- are a pretty convenient inbox. Here is how to move them to TaskLite afterwards:

  1. Install Telegram Desktop brew install telegram-desktop

  2. Go to "Saved Messages"

  3. Click on 3 dots in the upper right corner

  4. Click on "Export chat history"

  5. Deselect all additional media and select "JSON" as output format

  6. Approve download on a mobile device

  7. Download the JSON file (if download window was closed, simply follow the previous steps again)

  8. Either import it directly as JSON or convert it first to YAML for cleanup.

    Import JSON directly:

    cat result.json \
    | jq -c '
      .messages
        | map(
            (if (.text | type) == "string"
            then .text
            else (.text
                | map(
                    if (. | type) == "string"
                    then .
                    else .text end
                  )
                | join(", ")
              )
            end) as $body
            | {
              utc: .date,
              body: $body,
              tags: ["telegram"]
            }
          )
        | .[]
      ' \
    | while read -r task
      do
        echo "$task" | tasklite importjson
      done
    

    Convert it to YAML for easier cleanup:

    jq '.messages' result.json | yq --yaml-output > out.yaml
    
  9. Clear chat history on Telegram

Apple Reminders

Use following Apple Script to display the reminders including their creation timestamp. Seen at discussions.apple.com/thread/8570915.

  1. Download the script: export-reminders.scpt
  2. Run it with:
    osascript export-reminders.scpt
    
  3. Enter the name of the list you want to export

    ⚠️ Warning
    It includes all reminders - even completed ones - in the list. If it's a long list, it will take a while. A better approach would be to create a new list and move all reminders you want to export to that list.

  4. Copy and paste the output into a tasks.json file
  5. Format it as proper JSON and manually add notes, and tags fields
  6. Import JSON file:
    cat tasks.json \
      | jq -c '.[]' \
      | while read -r task
        do
          echo $task | tasklite importjson
        done
    

Hooks

Hooks can either be specified via the config file or via hook files. But make sure that all hook files are executable, otherwise they won't be picked up by TaskLite.



Stages

Following stages are available:

  • pre-launch - After reading all configs, but before any TaskLite code is executed. Can be used to prevent execution of TaskLite.
  • post-launch - After reading CLI arguments, setting up the database and running all migrations.
  • pre-add - Right before adding a new task. Can be used to prevent addition of task.
  • post-add - After new task was added.
  • pre-modify - Right before a task gets modified. Can be used to prevent modification of task.
  • post-modify - After task was modified.
  • pre-exit - Pre printing results
  • post-exit - Last thing before program termination

The hooks receive data from TaskLite via stdin. Possible fields are:

{
  arguments: […],  // Command line arguments (after `tasklite`)
  taskOriginal: {},  // Task before any modifications by TaskLite
  taskModified: {},  // Modified task
}

After execution, every called hook must print a JSON object to stdout (even if it's empty). All fields of the JSON are optional.

Explanation of possible values:

{
  message: "…",  // A message to display on stdout
  taskModified: "…",  // New version of the task as computed by your script
  tasksToAdd: […],  // Additional tasks to add
}

Hooks can write to stderr at any time, but it is not recommended. Rather write a {message: ''} object to stdout and let TaskLite print the message with improved formatting and coloring.

Legend:

  • ❌ = Not available
  • -> = Must return following object (fields optional) on stdout
Event Input Success
(exitcode == 0)
Error
(exitcode != 0)
pre‑launch ❌
-> {
  message: "…",
}
-> {message: "…"}
Processing terminates
post‑launch
{
  arguments: […]
}
      
{
  message: "…"
}
      
{stderr: "…"}
Processing terminates
pre‑add
{
  arguments: […],
  taskToAdd: {}
}
      
{
  taskToAdd: {},
  message: "…"
}
      
{stderr: "…"}
Processing terminates
post‑add
{
  arguments: […],
  taskAdded: {}
}
      
{
  message: "…"
}
      
{stderr: "…"}
Processing terminates
pre‑modify
{
  arguments: […],
  taskOriginal: {}
}
      
{
  taskModified: {},
  message: "…"
}
      
{stderr: "…"}
Processing terminates
post‑modify
{
  arguments: […],
  taskOriginal: {},
  taskModified: {}
}
      
{
  taskModified: {},
  message: "…"
}
      
{stderr: "…"}
Processing terminates
pre‑exit
{}
      
{
  message: "…"
}
      
{stderr: "…"}
Processing terminates
post‑exit
{}
      
{
  message: "…"
}
      
{stderr: "…"}
Processing terminates

Β 

To see the JSON for a single task run:

tl ndjson | head -n 1 | jq

Usage

While the CLI interface is the main interface of TaskLite, it also supports several others (with varying amount of features).

Check out the following pages for more details.

CLI Tool



Add

To add a task run:

tl add Improve the TaskLite manual

It is also possible to immediately add tags when creating a task:

tl add Improve the TaskLite manual +tasklite +pc

And even to set certain fields:

tl add Buy milk +groceries due:2020-09-01 created:2020-08-27

Attention: The tags and special commands must be the last parameters, but their order doesn't matter.

Help

For a full overview of all supported subcommands run:

tasklite help

Screenshot of CLI output of help command

Context / Views

There is no first class support for views (or "context" in GTD slang), because it can be easily implemented with aliases / custom CLI commands and the SQL query API.

For example I have following work command in my $PATH:

#! /usr/bin/env bash

tasklite query \
  "(tags is null or tags not like '%feram%') \
    and state is 'Open' \
    order by priority desc, due_utc asc, ulid desc \
    limit 10"

Analyze and Filter Tasks

In order to further analyze and filter tasks TaskLite includes the ndjson command, which prints all tasks as newline delimited JSON objects.

This output can then easily be analyzed and filtered with standard UNIX tools. E.g. following example prints all tasks related to music:

tl ndjson | grep 'music' | jq

Import

TaskLite features a comprehensive and robust JSON importer.

For example to import a GitHub issue simply run:

curl https://api.github.com/repos/$OWNER/$REPO/issues/$NUM | tl import

Or to import a task from TaskWarrior:

task 123 export | tl import

In order to avoid data loss of fields which aren't directly supported by TaskLite, the whole imported JSON object is also stored in TaskLite's task metadata field. However, if the original JSON object already has a metadata field, its value is used instead.

warning

An import object's tags field must be of type [string], while a notes field must be of type {ulid?: string, body: string}.

warning

TaskLite does not properly support importing tasks which were created before 1970. While they can be imported, the creation date is set to 1970-01-01.

Export

Use one of following commands:

  • tl csv
  • tl ndjson
  • tl backup - Creates a backup at $TaskLiteDir/backups/YYYY-MM-DDtHHMM.db

Custom Views

The export commands in combination with other common CLI tools like csvkit can also be used for custom views of tasks.

tl csv \
| csvgrep --column tags --match tasklite \
| head -n 6 \
| csvcut --columns ulid,body,tags \
| csvlook --max-column-width 30

yields

| ulid                       | body                           | tags     |
| -------------------------- | ------------------------------ | -------- |
| 01chk64zwwjyybanvk7016hyyg | Add a burndown chart view      | tasklite |
| 01chk6c08h70xra2awd8dngtr7 | Add multi user support         | tasklite |
| 01chk6dxaxttwfyg019d3g3sze | Add a statistics view          | tasklite |
| 01chk6f3sq1mrskgkt1046fz7q | Add a calendar view            | tasklite |
| 01chk6vnm30ttvwc1qkasjaktm | Publish the TaskLite git re... | tasklite |

Metadata

The metadata field allows you to store additional data for each task which is not yet covered by TaskLite's core fields. It is stored as a JSON object and therefore supports all JSON datatypes.

This is similar to Taskwarrior's User Defined Attributes.

Metadata is especially useful for importing and migrating external tasks without losing any information.

If you, for example, want to import following task.json file, you will notice that it contains a field kanban-state, which has no equivalent in TaskLite:

{
  "created_at": "2020-02-08T20:02:32Z",
  "body": "Buy milk",
  "kanban-state": "backlog"
}

However, you can still simply import it, as the additional field will be stored in the metadata object:

$ tl import < task.json
πŸ“₯ Imported task "Buy milk" with ulid "01e0k6a1p00002zgzc0845vayw"

Inspecting it:

$ tl info 01e0k6a1p00002zgzc0845vayw
awake_utc: null
review_utc: null
state: null
repetition_duration: null
recurrence_duration: null
body: Buy milk
user: adrian
ulid: 01e0k6a1p00002zgzc0845vayw
modified_utc: 1970-01-01T00:00:00Z
group_ulid: null
closed_utc: null
priority_adjustment: null
metadata:
  body: Buy milk
  kanban-state: backlog
  created_at: 2020-02-08T20:02:32Z
waiting_utc: null
ready_utc: null
due_utc: null
priority: 0.0
tags:

notes:

To access the the metadata key programmatically you can do following:

$ tl ndjson \
  | grep 01e0k6a1p00002zgzc0845vayw \
  | jq -r '.metadata["kanban-state"]'
backlog

Or leverage SQL:

$ tl runsql "
    SELECT json_extract(metadata, '\$.kanban-state')
    FROM tasks
    WHERE ulid == '01e0k6a1p00002zgzc0845vayw'
  " \
  | tail -n 1
backlog

This can also be used to update metadata fields:

tl runsql "
    UPDATE tasks
    SET metadata=(
      SELECT json_set(tasks.metadata, '\$.kanban-state', 'sprint')
      FROM tasks
      WHERE ulid == '01e0k6a1p00002zgzc0845vayw'
    )
    WHERE ulid == '01e0k6a1p00002zgzc0845vayw'
  "

Another great use-case is to automatically extract and store data from the body of the task.

E.g. you could set up a CRON job to automatically extract any GitHub link from the body and store it in an extra github_url metadata field:

UPDATE tasks
SET metadata = json_insert(
  ifnull(metadata, '{}'),
  '$.github_url',
  substr(body, instr(body, 'https://github.com'),
    CASE
      WHEN instr(substr(body, instr(body, 'https://github.com')), ' ') == 0
      THEN length(substr(body, instr(body, 'https://github.com')))
      ELSE instr(substr(body, instr(body, 'https://github.com')), ' ') - 1
    END
  )
)
WHERE body LIKE '%https://github.com%'

External Commands

Like Git, TaskLite also supports external commands. This allows you to easily extend TaskLite's functionality with your own scripts.

For this to work, simply add an executable script (chmod +x) with the prefix tasklite- to your $PATH

For example, to add a grin command which simply prints a smiley:

$ cat /usr/local/bin/tasklite-grin
#! /usr/bin/env bash

echo '😁' "$@"

$ tasklite grin Hi
😁 Hi

Desktop App

The desktop app is still very early alpha and can currently only list the tasks. It's implemented with a declarative Haskell wrapper for GTK.

Screenshot of desktop app

Web App

The web app is currently provided by Datasette.

Screenshot of web app

REST API

The REST API is currently provided by Datasette.

All web views can be configured to deliver JSON by simply changing the file extension in the URL to .json.

For example:

curl --location http://0.0.0.0:8001/main/tasks_view.json

Haskell API for Programmatic Usage

While TaskLite is primarily a tool to manage your personal tasks, it can also be used as a dependency of other programms.

For example as a queue for processing tasks or a backend for a bookmarking service.

Automation

The real power of TaskLite gets unlocked when it's set up to be tightly integrated with other apps. This section contains several examples of how it can work together with other systems and services.



Folder Actions on MacOS

Folder actions are a feature on macOS to execute some code when files are added to a specific directory.

This can for example be used to import all Email files (.eml) which are saved in a directory.

Setup

  1. Open Automator and create a new Folder Action:

    Screenshot of Automator

  2. Specify the directory via the select field at the top

  3. Add a "Run Shell Script" block with following bash code:

    set -euo pipefail
    input=$(cat -)
    result=$(/Users/adrian/.local/bin/tasklite import "$input" 2>&1 || true)
    resultNorm=${result//[^a-zA-Z0-9 \/:.]/ }
    osascript -e \
        "display notification \"${resultNorm:0:80}\"
        with title \"Email was imported into TaskLite\""
    
  4. Save the folder action

    Screenshot of finished workflow

Differences to Taskwarrior



General

  • Simpler
    Taskwarrior has several redundant features and unnecessarily re-implements shell features like aliases.

  • More Robust & Stable
    Taskwarrior is plagued by numerous bugs due to its unnecessary complexity and non-optimal choice of programming languages. TaskLite's simple structure and Haskell's excellent correctness guarantees, however, yield a stable and robust piece of software.

  • Faster
    Haskell plus SQLite delivers excellent performance. Check out the section about performance for a simple benchmark.

  • More Powerful
    As all tasks are stored in an SQLite database, so you can use the most daring SQL queries to extract hidden insights. E.g. What is the average completion time for a task created on Monday tagged "sprint7" created by user "john-evil"?

    Furthermore, extensive tooling is available for SQLite to supercharge your TaskLite installation. For example Datasette as an instant REST API, or DB Browser for SQLite to view, manipulate, and plot your tasks in a GUI.

    Other 3rd party tools to edit SQLite databases are:

    • VisiData - Interactive CLI multitool for tabular data.
    • LiteCLI - CLI SQLite browser with auto-completion and syntax highlighting.

Command Comparison

TaskWarriorTaskLiteDescription & Differences
addaddAdd a new task
annotatenoteAdd a note / comment / annotation to a task
append- (edit)Append words to a task description
calc-Expression calculator
configconfigTL only displays it, TW allows modification
context-Manage contexts. TL uses tags instead.
countcountCount the tasks matching a filter
deletetrashMark a task as deletable
denotateunnoteRemove an annotation from a task
donedoComplete a task
duplicateduplicateClone an existing task
editeditLaunch your text editor to modify a task
execute-Execute an external command
exportndjsonExport tasks in NDJSON instead of JSON format
helphelpShow high-level help, a cheat-sheet
importimportAdditionally to JSON supports Email files (.eml)
loglogRecord an already-completed task
logo-Show the Taskwarrior logo
modify- (edit)Modify one or more tasks
prepend- (edit)Prepend words to a task description
purgedeleteCompletely remove task, rather than just change status
startstartStart working on a task, make active
stopstopStop working on a task, no longer active
synchronize-Syncs tasks with Taskserver
undo-Revert last change
versionversionVersion details and copyright
active-Started tasks
allallPending, completed and deleted tasks
blocked-Tasks that are blocked by other tasks
blocking-Tasks that block other tasks
completeddoneTasks that have been completed
listopenPending tasks
long-Pending tasks, long form
ls-Pending tasks, short form
minimal-Pending tasks, minimal form
newestnewMost recent pending tasks
nextheadMost urgent tasks
oldest-Oldest pending tasks
overdueoverdueOverdue tasks
readyreadyPending, unblocked, scheduled tasks
recurringrecurringPending recurring tasks
unblocked-Tasks that are not blocked
waitingwaitingHidden, waiting tasks
burndown.daily-Burndown chart, by day
burndown.monthly-Burndown chart, by month
burndown.weekly-Burndown chart, by week
calendar-Calendar and holidays
colors-Demonstrates all supported colors
columns-List of report columns and supported formats
commandshelpList of commands, with their behaviors
diagnostics-Show diagnostics, for troubleshooting
ghistory.annual-History graph, by year
ghistory.monthly-History graph, by month
ghistory.weekly-History graph, by week
ghistory.daily-History graph, by day
history.annual-History report, by year
history.monthly-History report, by month
history.weekly-History report, by week
history.daily-History report, by day
ids-Filtered list of task IDs
informationinfoAll attributes shown
projectsprojectsList of projects (project in TL = active tag)
reportshelpList of available reports
showconfigFiltered list of configuration settings
statsstatsFiltered statistics
summaryprojectsFiltered project summary
tagstagsFiltered list of tags
timesheet-Weekly timesheet report
udas-Details of all defined UDAs
uuids-Filtered list of UUIDs
_aliases-List of active aliases
_columns-List of supported columns
_commands-List of supported commands
_config-List of confguration setting names
_context-List of defined context names
_get-DOM accessor
_ids-Filtered list of task IDs
_projects-Filtered list of project names
_show-List of name=value configuration settings
_tags-Filtered list of tags in use
_udas-List of configured UDA names
_unique-List of unique values for the specified attribute
_urgency-Filtered list of task urgencies
_uuids-Filtered list of pending UUIDs
_version-Task version (and optional git commit)
_zshattributes-Zsh formatted task attribute list
_zshcommands-Zsh formatted command list
_zshids-Zsh formatted ID list
_zshuuids-Zsh formatted UUID list
  •           | random  | Show a random open task
    

Performance

Coming soon!

Development



Technologies

Check out the makefile for all development tasks

Generate Screenshots

Use asciinema to generate the terminal recording:

asciinema rec \
  --title 'TaskLite Help Page' \
  --command 'tasklite help' \
  --overwrite \
  screenshots/recording.json
asciinema rec \
  --title 'TaskLite "withtag" Command' \
  --command 'tasklite withtag tasklite' \
  --overwrite \
  screenshots/withtag.json

Change the size of the terminal in the recording.json file to:

  "width": 80,
  "height": 86,

Then use svg-term to generate the SVG image:

svg-term \
  --no-cursor \
  --at 99999 \
  --window \
  --term iterm2 \
  --profile ~/dotfiles/terminal/adius.itermcolors \
  < screenshots/recording.json \
  > screenshots/recording.svg

Ghcid

Ghcid with color output for GHC 8.4 (probably obsolete in 8.6):

ghcid \
  --command="stack ghci --ghci-options=-fdiagnostics-color=always"

Hlint

hlint \
  --ignore="Redundant do" \
  --ignore="Use list literal" \
  --ignore="Use String" \
  --ignore="Redundant bracket" \
  --ignore="Use camelCase" \
  .

Webapp

Build Images

Build base image for webapp runtime image:

docker build \
  --file tasklite-core/dockerfiles/haskell-datasette \
  --tag haskell-datasette \
  dockerfiles

Build runtime image:

stack image container
docker tag adius/tasklite-tasklite:latest adius/tasklite:latest

Deployment

On Google Cloud:

docker tag adius/tasklite-tasklite:latest gcr.io/deploy-219812/tasklite:latest
docker push gcr.io/deploy-219812/tasklite:latest
kubectl create -f kubernetes/deployment.yaml
kubectl port-forward tasklite-deployment-77884ff4f6-66sjf 8001

Open 127.0.0.1:8001

docker build \
  --file dockerfiles/nginx-proxy \
  --tag gcr.io/deploy-219812/nginx-proxy:latest \
  dockerfiles; \
and docker push gcr.io/deploy-219812/nginx-proxy:latest; \
and kubectl replace --filename kubernetes/deployment.yaml --force; \
and sleep 8;
and kubectl port-forward \
  (kubectl get pods --selector app=tasklite --output name) 8080

Afterwards change the health check URL to /healthcheck for the load balancer at https://console.cloud.google.com/compute/healthChecks.

Changelog

This document lists all changes to the functionality of TaskLite.



2020-03-01 - 0.3.0.0

  • Add edit command to edit YAML version of task in $EDITOR (1add89e)
  • Add several un* commands to erase fields (0f09c3d)
  • Only execute trigger to set closed_utc after state changed (395a8e0)
  • Show descriptive variable names in brief help text (10f8cf6)
  • Hide aliases from main help (c52df72)
  • Display alias errors even with subarguments (c52df72)
  • Fix parsing of timestamp part in ULIDs for small timestamp values (258df47)
  • Also display full version slug with tl version (0c292d1)
  • Remove unnecessary import logging (c04b894)
  • Add git hash to the version string (5f7b1ef)
  • Create config directory if it does not exist (8d0657c)

2019-10-04 - 0.2.2.0

  • Support optional filter expression after "count" command (61e87b7)
  • Automatically create a config file if it doesn't exist (7407f87)

2019-07-14 - 0.2.1.0

  • Fix creation of Docker image, extend documentation accordingly (fa4cad3)

2019-06-13 - 0.2.0.0

  • Initial release

Related



Comparison to Open Source Alternatives

If TaskLite isn't your cup of tea, maybe one of the other free task managers fits the bill:

NameDescription
BukuStore and manage your bookmarks from the command line
DstaskSingle binary CLI todo manager with git sync and markdown notes
DooitA TUI todo manager
EtmEvent and task manager
EurekaCLI tool to input and store ideas without leaving the terminal
nbCLI note-taking, bookmarking, archiving, and knowledge base app
NeorgTool for structured note taking and project/task management
Nvim-OrgModeOrgmode clone written in Lua for Neovim
Org modeNotes and todo lists powered by an Emacs based plain-text system
SmosPurely functional semantic tree-based editor (like Org mode)
TaskwarriorCommand line task management
td-cliCommand line todo list manager
Todo.txtSimple and extensible shell script for managing a todotxt file
TodomanSimple iCalendar + CalDAV based CLI todo manager
ToodlesProject management from the TODO's in your codebase
TTDLTerminal Todo List Manager
UnfogA simple CLI task and time manager
Vim-OrgModeText outlining and task management based on Emacs' Org-Mode

Stats

Updated on 2024-01-17.

NrNameStarsCommitsContrib.Code1. Commit
1nb6078739520Shell2014‑11‑12
2Buku5976202868Python2015‑11‑02
3Todo.txt539148366Shell2009‑03‑05
4Neorg52533121101Lua2021‑04‑11
5Org mode~4000~25000543EmacsLisp2003‑01‑01
6Taskwarrior364311578140C++2008‑04‑19
7Vim-OrgMode3066104447Python2010‑10‑09
8Nvim-OrgMode259493953Lua2021‑05‑13
9Dooit178890814Python2022‑04‑17
9Toodles96821113Haskell2018‑09‑04
10Eureka7182588Rust2017‑11‑20
11Dstask75287913Go2018‑12‑08
12Todoman44796536Python2015‑03‑29
13Smos274270923Haskell2018‑07‑29
14Unfog1811984Haskell2019‑10‑22
15td-cli1813015Python2018‑06‑03
16TTDL1782387Rust2018‑12‑30
17TaskLite1593985Haskell2018‑06‑04
18Etm4037734Python2017‑09‑02

Commercial Alternatives

For completeness sake here is also a list of popular commercial apps and SaaS providers:

  • Asana - Your team’s goals, plans, tasks, files together in one shared space.
  • Google Keep - Hybrid note taking and todo app.
  • Jira - Bug- & issue-tracking and project-management service.
  • Microsoft TODO - Cloud based task management application.
  • Nirvana - To-do app available on macOS, Android, Windows, and web.
  • Remember The Milk - Web based task- and time-management.
  • Tasker - Tool for managing tasks, processes and employees.
  • TaskPaper - Plain text to-do lists (macOS app).
  • Things - Personal task manager to achieve your goals (Apple only).
  • Todoist - SaaS to-do list to organize work and life.
  • Trello - Web based Kanban list-making application.

Archive

The following projects appear to be inactive or no longer actively maintained:

NameDescription
CommitTasksCombination between git commit and todo list
EagleMinimalistic todo app for command line
FfA distributed note taker and task manager
git-pendingGit plugin to list TODO, FIXME, … comments in a repository
PomodayKeyboard only task management web app
tMinimal command-line todo list manager
TaskbookTasks, boards & notes for the command-line habitat
TaskellCommand line Kanban board / task management
TracliCommand line app that tracks your time
UltralistOpen source task management system for the command line
WorkCLI TODO app written with Rust and SQLite
YokadiCommand line oriented, SQLite powered todo list

Stats

Updated on 2024-01-17.

NameStarsCommitsContrib.Code1. Commit
Taskbook885223228JavaScript2018‑02‑12
Taskell1639106410Haskell2017‑11‑15
Ultralist94141919Go2016‑04‑23
t737979Python2009‑08‑26
Pomoday57516514TypeScript2019‑10‑24
CommitTasks295944JavaScript2018‑08‑17
git-pending288372JavaScript2019‑06‑17
Ff18783310Haskell2017‑12‑29
Yokadi129115912Python2008‑08‑24
Tracli37623JavaScript2019‑07‑15
Eagle27461Python2018‑10‑28
Work24671Rust2020‑07‑15