Create a Task Tracker App for the Terminal with Python (Rich, Typer, Sqlite3)

Patrick Loeber

In this Python Tutorial we learn how to build a terminal application (CLI app) to manage our tasks and todos. We use Typer for building the CLI app, Rich for a colorized terminal output, and SQLite for the database.

Tech Stack:

Installation

We can install Rich and Typer with pip. SQLite3 is a built-in Python core module:

$ pip install rich typer

Get Started With Typer

First we setup Typer to create the CLI (Command Line Interface).

Create a new file todocli.py. Then create different functions to add, delete, update, complete, and show the todos.

Each todo should get a task name and a category.

import typer app = typer.Typer() @app.command(short_help='adds an item') def add(task: str, category: str): typer.echo(f"adding {task}, {category}") @app.command() def delete(position: int): typer.echo(f"deleting {position}") @app.command() def update(position: int, task: str = None, category: str = None): typer.echo(f"updating {position}") @app.command() def complete(position: int): typer.echo(f"complete {position}") @app.command() def show(): typer.echo(f"Todos") if __name__ == "__main__": app()

Now we can run the file in the terminal and automatically have support for all commands. For example try this:

$ python todocli.py --help $ python todocli.py add --help $ python todocli.py add "My first todo" "Study"

Try this for the other commands yourself!

Add Rich For Terminal Styling

We want to display the todos in a beautiful looking table layout with different colors. For this setup Rich and then modify the show() function:

from rich.console import Console from rich.table import console = Console() @app.command() def show(): tasks = [("My first todo", "Study", ("My second todo", "Sports")] console.print("[bold magenta]Todos[/bold magenta]!", "💻") table = Table(show_header=True, header_style="bold blue") table.add_column("#", style="dim", width=6) table.add_column("Todo", min_width=20) table.add_column("Category", min_width=12, justify="right") table.add_column("Done", min_width=12, justify="right") def get_category_color(category): COLORS = {'Learn': 'cyan', 'YouTube': 'red', 'Sports': 'cyan', 'Study': 'green'} if category in COLORS: return COLORS[category] return 'white' for idx, task in enumerate(tasks, start=1): c = get_category_color(task[1]) status = False is_done_str = '✅' if status else '❌' table.add_row(str(idx), task[0], f'[{c}]{task[1]}[/{c}]', is_done_str) console.print(table) if __name__ == "__main__": app()

For now we hard-coded the todos and its attributes. We also setup a dictionary with predefined categories and its corresponding colors.

Now run the show command and see how it looks:

$ python todocli.py show

Create A Todo Model

Now we want to setup an actual todo class.

Create a new file model.py and add the following:

import datetime class Todo: def __init__(self, task, category, date_added=None, date_completed=None, status=None, position=None): self.task = task self.category = category self.date_added = date_added if date_added is not None else datetime.datetime.now().isoformat() self.date_completed = date_completed if date_completed is not None else None self.status = status if status is not None else 1 # 1 = open, 2 = completed self.position = position if position is not None else None def __repr__(self) -> str: return f"({self.task}, {self.category}, {self.date_added}, {self.date_completed}, {self.status}, {self.position})"

Next to the task and category, it also gets fields for the created and completed date, a status to see if it's completed or not, and a position index.

Create The Database

Now we setup the SQLite database. Create a new table named "todos" with the same fields as the model class contains. Then implement all required functions to add and modify the todos.

Create a new file database.py and add the following:

import sqlite3 from typing import List import datetime from model import Todo conn = sqlite3.connect('todos.db') c = conn.cursor() def create_table(): c.execute("""CREATE TABLE IF NOT EXISTS todos ( task text, category text, date_added text, date_completed text, status integer, position integer )""") create_table() def insert_todo(todo: Todo): c.execute('select count(*) FROM todos') count = c.fetchone()[0] todo.position = count if count else 0 with conn: c.execute('INSERT INTO todos VALUES (:task, :category, :date_added, :date_completed, :status, :position)', {'task': todo.task, 'category': todo.category, 'date_added': todo.date_added, 'date_completed': todo.date_completed, 'status': todo.status, 'position': todo.position }) def get_all_todos() -> List[Todo]: c.execute('select * from todos') results = c.fetchall() todos = [] for result in results: todos.append(Todo(*result)) return todos def delete_todo(position): c.execute('select count(*) from todos') count = c.fetchone()[0] with conn: c.execute("DELETE from todos WHERE position=:position", {"position": position}) for pos in range(position+1, count): change_position(pos, pos-1, False) def change_position(old_position: int, new_position: int, commit=True): c.execute('UPDATE todos SET position = :position_new WHERE position = :position_old', {'position_old': old_position, 'position_new': new_position}) if commit: conn.commit() def update_todo(position: int, task: str, category: str): with conn: if task is not None and category is not None: c.execute('UPDATE todos SET task = :task, category = :category WHERE position = :position', {'position': position, 'task': task, 'category': category}) elif task is not None: c.execute('UPDATE todos SET task = :task WHERE position = :position', {'position': position, 'task': task}) elif category is not None: c.execute('UPDATE todos SET category = :category WHERE position = :position', {'position': position, 'category': category}) def complete_todo(position: int): with conn: c.execute('UPDATE todos SET status = 2, date_completed = :date_completed WHERE position = :position', {'position': position, 'date_completed': datetime.datetime.now().isoformat()})

Put Everything Together

Now we put everything together and use these database functions in the todocli.py.

Import all functions and call them in the corresponding command functions. Also replace the hardcoded todos in the show function with the actual todos.

This is the final todocli.py:

import typer from rich.console import Console from rich.table import Table from model import Todo from database import get_all_todos, delete_todo, insert_todo, complete_todo, update_todo console = Console() app = typer.Typer() @app.command(short_help='adds an item') def add(task: str, category: str): typer.echo(f"adding {task}, {category}") todo = Todo(task, category) insert_todo(todo) show() @app.command() def delete(position: int): typer.echo(f"deleting {position}") # indices in UI begin at 1, but in database at 0 delete_todo(position-1) show() @app.command() def update(position: int, task: str = None, category: str = None): typer.echo(f"updating {position}") update_todo(position-1, task, category) show() @app.command() def complete(position: int): typer.echo(f"complete {position}") complete_todo(position-1) show() @app.command() def show(): tasks = get_all_todos() console.print("[bold magenta]Todos[/bold magenta]!", "💻") table = Table(show_header=True, header_style="bold blue") table.add_column("#", style="dim", width=6) table.add_column("Todo", min_width=20) table.add_column("Category", min_width=12, justify="right") table.add_column("Done", min_width=12, justify="right") def get_category_color(category): COLORS = {'Learn': 'cyan', 'YouTube': 'red', 'Sports': 'cyan', 'Study': 'green'} if category in COLORS: return COLORS[category] return 'white' for idx, task in enumerate(tasks, start=1): c = get_category_color(task.category) is_done_str = '✅' if task.status == 2 else '❌' table.add_row(str(idx), task.task, f'[{c}]{task.category}[/{c}]', is_done_str) console.print(table) if __name__ == "__main__": app()

Congratulations! Now you should have a fully functioning CLI app. Now go ahead and try different commands to modify your own todo list!

The code is also available on GitHub.

Next Steps

The app now contains the basic functionality. Here are different ideas how you can now improve this:

The possibilities are endless! Let me know if you extend the functionality!

If you enjoyed this tutorial, please share it with your friends and on Twitter!

FREE VS Code / PyCharm Extensions I Use

✅ Write cleaner code with Sourcery, instant refactoring suggestions: Link *

* This is an affiliate link. By clicking on it you will not have any additional costs, instead you will support me and my project. Thank you! 🙏

Check out my Courses