How to Build a To-Do List CLI App Using Python in Termux

Meta Description: Learn how to build a to-do list CLI app using Python in Termux. Get the full working script, line-by-line breakdown, auto-run setup, and data storage tips.

How to Build a To-Do List CLI App Using Python in Termux

Typing tasks into bulky mobile productivity apps often feels slow. Most commercial task managers are loaded with splash screens, background trackers, sync delays, and unnecessary animations. If you want something fast, private, and distraction-free, you can build your own command-line task manager right on your Android phone using Termux and Python.

This python cli todo app termux tutorial walks you through building a persistent, keyboard-driven task organizer from scratch. You will get a full, copy-paste-ready Python script, a thorough breakdown of how every function works, instructions on how to launch the app automatically whenever you open Termux, and key storage practices to keep your data safe.


Quick Summary: Termux Python CLI To-Do App

A termux todo app python project is a lightweight, terminal-based task organizer that runs directly inside the Termux environment on Android. It stores your tasks locally in a structured JSON file, lets you create, view, complete, and delete tasks without touching a graphical interface, and requires zero external pip libraries. Because it runs purely on Python's standard library, it executes instantly and consumes virtually zero battery.


Why Run a CLI To-Do App in Termux?

Termux turns an Android smartphone into an authentic Linux environment. Running your daily workflow inside a terminal shell provides several clear advantages:

  • Zero Latency: No loading spinners. The application launches in a fraction of a second.
  • Complete Privacy: Your tasks never touch third-party cloud servers. Your personal to-do list remains on your local file system.
  • Zero Bloat: By relying solely on the Python standard library, you avoid the dependency management headaches, virtual web views, and storage bloat that come with full-stack frameworks like Flask or heavy GUI toolkits.
  • Keyboard-First Efficiency: If you use an external Bluetooth keyboard or an on-screen keyboard like Hacker's Keyboard, you can update your task list without taking your fingers off the home row.

Data Storage Options: Plain Text vs. JSON vs. SQLite

When developing a console-based task manager on Android, choosing the right file persistence model is critical. Here is how the three most common approaches compare:

Storage Format Pros Cons Ideal Use Case
Plain Text (.txt) Human-readable with any text editor; extremely simple to append lines. Difficult to store metadata like completion status, unique IDs, or timestamps. Quick scratchpads and bare-bones notes.
JSON (.json) Structured data mapping (dictionaries/lists); built-in Python module; easy inspection. Requires rewriting the whole file on update; not suited for millions of rows. CLI To-Do Lists (Best Choice)
SQLite (.db) ACID compliant; handles relational data and complex querying effortlessly. Binary file format cannot be quickly inspected via cat; overkill for small lists. Large-scale logging or complex multi-table applications.

For this project, JSON strikes the perfect balance. It keeps the script simple, lets you inspect your tasks directly using terminal utilities like cat or jq, and natively maps to Python lists and dictionaries.


Prerequisites and Environment Setup

Before writing the code, ensure your Termux environment is updated and that Python 3 is installed. Open Termux and run the following commands step by step.

Step 1: Update Package Repositories

Ensure your local package index is current to prevent broken package errors:

pkg update && pkg upgrade -y

Step 2: Install Python and Storage Tools

Install the official Python package along with nano (a beginner-friendly command-line text editor):

pkg install python nano -y

Step 3: Verify the Installation

Check that Python installed correctly by querying its version:

python --version

You should see an output such as Python 3.11.x or Python 3.12.x.

Step 4: Create a Dedicated Project Directory

Keep your workspace organized by creating a dedicated folder for your script and data file:

mkdir -p ~/todo-cli
cd ~/todo-cli

The Complete Working Python Script

Below is the complete, production-ready script for your CLI to-do manager. It handles file initialization, user input validation, task additions, completions, and deletions while preventing crashes from corrupt or missing files.

#!/usr/bin/env python3
"""
CLI To-Do List Application for Termux
File: todo.py
Author: termuxgenius.com
"""

import json
import os
import sys

# Define storage location in the user's home directory
DATA_DIR = os.path.expanduser("~/.todo_cli")
DATA_FILE = os.path.join(DATA_DIR, "tasks.json")

# ANSI terminal colors for clean styling
COLOR_RESET = "\033[0m"
COLOR_GREEN = "\033[92m"
COLOR_YELLOW = "\033[93m"
COLOR_RED = "\033[91m"
COLOR_CYAN = "\033[96m"
COLOR_BOLD = "\033[1m"


def ensure_storage_exists():
    """Ensure data directory and JSON file exist."""
    if not os.path.exists(DATA_DIR):
        os.makedirs(DATA_DIR, exist_ok=True)
    if not os.path.exists(DATA_FILE):
        with open(DATA_FILE, "w", encoding="utf-8") as f:
            json.dump([], f)


def load_tasks():
    """Read tasks from the JSON storage file."""
    ensure_storage_exists()
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        print(f"{COLOR_RED}Warning: Storage file corrupted. Initializing empty list.{COLOR_RESET}")
        return []


def save_tasks(tasks):
    """Save tasks list to the JSON storage file."""
    ensure_storage_exists()
    try:
        with open(DATA_FILE, "w", encoding="utf-8") as f:
            json.dump(tasks, f, indent=4)
    except IOError as e:
        print(f"{COLOR_RED}Error saving tasks: {e}{COLOR_RESET}")


def list_tasks(tasks):
    """Display all current tasks formatted in a readable list."""
    if not tasks:
        print(f"\n{COLOR_YELLOW}No tasks found. Your list is clear!{COLOR_RESET}\n")
        return

    print(f"\n{COLOR_BOLD}{COLOR_CYAN}--- CURRENT TASKS ---{COLOR_RESET}")
    for idx, task in enumerate(tasks, start=1):
        status = f"{COLOR_GREEN}[DONE]{COLOR_RESET}" if task["done"] else f"{COLOR_RED}[PENDING]{COLOR_RESET}"
        title = task["title"]
        print(f"{idx}. {status} {title}")
    print()


def add_task(tasks):
    """Prompt the user for a new task title and append it."""
    title = input(f"{COLOR_BOLD}Enter task description: {COLOR_RESET}").strip()
    if not title:
        print(f"{COLOR_RED}Task description cannot be empty.{COLOR_RESET}")
        return
    tasks.append({"title": title, "done": False})
    save_tasks(tasks)
    print(f"{COLOR_GREEN}Task added successfully!{COLOR_RESET}")


def mark_done(tasks):
    """Mark an existing task as completed."""
    list_tasks(tasks)
    if not tasks:
        return

    try:
        choice = int(input(f"{COLOR_BOLD}Enter task number to mark as completed: {COLOR_RESET}"))
        if 1 <= choice <= len(tasks):
            tasks[choice - 1]["done"] = True
            save_tasks(tasks)
            print(f"{COLOR_GREEN}Task #{choice} marked as complete.{COLOR_RESET}")
        else:
            print(f"{COLOR_RED}Invalid task number.{COLOR_RESET}")
    except ValueError:
        print(f"{COLOR_RED}Please enter a valid integer.{COLOR_RESET}")


def delete_task(tasks):
    """Remove a task permanently from the list."""
    list_tasks(tasks)
    if not tasks:
        return

    try:
        choice = int(input(f"{COLOR_BOLD}Enter task number to delete: {COLOR_RESET}"))
        if 1 <= choice <= len(tasks):
            removed = tasks.pop(choice - 1)
            save_tasks(tasks)
            print(f"{COLOR_YELLOW}Removed task: '{removed['title']}'{COLOR_RESET}")
        else:
            print(f"{COLOR_RED}Invalid task number.{COLOR_RESET}")
    except ValueError:
        print(f"{COLOR_RED}Please enter a valid integer.{COLOR_RESET}")


def clear_screen():
    """Clear terminal screen for clean navigation."""
    os.system("clear")


def main_menu():
    """Main interactive terminal loop."""
    while True:
        tasks = load_tasks()
        print(f"{COLOR_BOLD}{COLOR_CYAN}=== TERMUX TO-DO MANAGER ==={COLOR_RESET}")
        print("1. View Tasks")
        print("2. Add Task")
        print("3. Mark Task as Done")
        print("4. Delete Task")
        print("5. Exit")

        choice = input(f"{COLOR_BOLD}Select an option (1-5): {COLOR_RESET}").strip()

        if choice == "1":
            list_tasks(tasks)
            input(f"{COLOR_YELLOW}Press Enter to return to menu...{COLOR_RESET}")
            clear_screen()
        elif choice == "2":
            add_task(tasks)
            input(f"{COLOR_YELLOW}Press Enter to return to menu...{COLOR_RESET}")
            clear_screen()
        elif choice == "3":
            mark_done(tasks)
            input(f"{COLOR_YELLOW}Press Enter to return to menu...{COLOR_RESET}")
            clear_screen()
        elif choice == "4":
            delete_task(tasks)
            input(f"{COLOR_YELLOW}Press Enter to return to menu...{COLOR_RESET}")
            clear_screen()
        elif choice == "5":
            print(f"\n{COLOR_GREEN}Goodbye! Keep crushing your goals.{COLOR_RESET}\n")
            sys.exit(0)
        else:
            print(f"{COLOR_RED}Invalid choice. Please select 1 through 5.{COLOR_RESET}\n")


if __name__ == "__main__":
    clear_screen()
    main_menu()

Line-by-Line Code Breakdown

Understanding what your code does is essential for maintaining and extending it. Here is an explanation of the core blocks in this script.

1. Imports and Directory Setup

import json
import os
import sys

DATA_DIR = os.path.expanduser("~/.todo_cli")
DATA_FILE = os.path.join(DATA_DIR, "tasks.json")

The standard modules json, os, and sys provide all the functionality we need. We use os.path.expanduser("~/.todo_cli") to guarantee that our data files are saved inside the Termux user's root home folder (/data/data/com.termux/files/home/.todo_cli). This hidden directory ensures your to-do data remains intact even if you move or rename your project script.

2. ANSI Color Escapes

COLOR_RESET = "\033[0m"
COLOR_GREEN = "\033[92m"
COLOR_YELLOW = "\033[93m"
COLOR_RED = "\033[91m"
COLOR_CYAN = "\033[96m"
COLOR_BOLD = "\033[1m"

Termux fully supports standard ANSI terminal escape sequences. Using these constants allows us to colorize status tags—such as turning completed items green and pending items red—without requiring bulky third-party libraries like curses or colorama.

3. Data Persistence: load_tasks() and save_tasks()

def load_tasks():
    ensure_storage_exists()
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        return []

The load_tasks() function handles missing or corrupt files defensively. If the JSON file is empty or accidentally malformed, Python intercepts the json.JSONDecodeError instead of terminating the app with an ugly traceback.

In save_tasks(), the indent=4 parameter makes the generated tasks.json easily readable if you ever choose to inspect or edit it using standard command-line tools like cat or nano.

4. Task Manipulation Logic

Each operation uses simple data structures:

  • add_task(): Prompts for input, strips whitespace to avoid blank tasks, and appends a dictionary object: {"title": "...", "done": False}.
  • mark_done(): Prompts the user for a 1-based index, converts it to Python's 0-based index (choice - 1), and updates the boolean flag to True.
  • delete_task(): Uses Python's built-in tasks.pop(index) method, which both deletes the item from memory and returns the deleted dictionary so we can print the removed task name to the user.

5. The Main Interactive Loop

def main_menu():
    while True:
        tasks = load_tasks()
        ...

The while True: loop keeps the interface active until the user selects option 5. Reading fresh data via load_tasks() at the top of each iteration guarantees that the in-memory representation matches the storage file on disk at all times.


How to Install and Execute the Script

Follow these steps to write and execute the application inside your Termux terminal.

  1. Open the Nano editor:
    nano ~/todo-cli/todo.py
  2. Paste the code: Paste the complete script from above into the Nano window.
  3. Save and exit: Press Ctrl + O, then press Enter to write the file. Then press Ctrl + X to exit the editor.
  4. Make the script executable:
    chmod +x ~/todo-cli/todo.py
  5. Run the application:
    python ~/todo-cli/todo.py

How to Auto-Run the App in Termux

If you use Termux primarily to keep track of tasks, you can configure the app to launch automatically whenever you open a new terminal session, or create a convenient short alias for it.

Method 1: Create a Global Terminal Alias (Recommended)

Instead of typing the full path every time, you can create a simple command shortcut like todo:

  1. Open your shell runcom configuration file:
    nano ~/.bashrc
  2. Scroll to the bottom and add this alias:
    alias todo="python ~/todo-cli/todo.py"
  3. Save and exit (Ctrl + O, Enter, Ctrl + X).
  4. Reload your shell configuration:
    source ~/.bashrc

Now, typing todo from any folder immediately opens your task manager.

Method 2: Launch Automatically on Termux Startup

If you want your task list displayed immediately upon opening the Termux application:

  1. Edit ~/.bashrc:
    nano ~/.bashrc
  2. Append the script invocation at the very bottom:
    # Auto-run To-Do CLI on shell launch
    if [ -f ~/todo-cli/todo.py ]; then
        python ~/todo-cli/todo.py
    fi
  3. Save and exit. When you exit the to-do app (Option 5), Termux will drop you directly into your standard command prompt.

Safety, Permissions, and Data Protection

While this script is lightweight and self-contained, working inside Termux requires understanding a few key storage and safety principles.

1. Sandboxed App Storage vs. Shared Android Storage

By default, Termux stores all files inside its private application sandbox:

/data/data/com.termux/files/home/

This sandbox isolates your files from malicious apps, but it introduces one major risk: if you clear the Termux app data or uninstall Termux from Android Settings, your tasks file will be permanently deleted.

2. Creating Backups on Shared Android Storage

To protect your tasks against accidental data loss, request Android storage access:

termux-setup-storage

Grant the permission prompt on your screen. This creates a symlink named ~/storage/shared that points to your public storage. You can create an automated backup script or run a one-line command to copy your tasks to your phone's Documents directory:

cp ~/.todo_cli/tasks.json ~/storage/shared/Documents/tasks_backup.json

3. Security Considerations

Because this script uses standard Python libraries without network sockets, it runs completely offline. It requires no elevated permissions, does not need root access (Superuser), and transmits zero telemetry data.


Common Mistakes and Troubleshooting

Issue 1: ModuleNotFoundError

If you receive an error stating that Python cannot be found, ensure you are not running the command under a bare shell without Python installed. Run:

pkg install python -y

Issue 2: Permission Denied Error

If running ./todo.py gives a permission error, ensure you have added execution permissions using chmod:

chmod +x ~/todo-cli/todo.py

Alternatively, invoke Python directly: python todo.py.

Issue 3: Broken Terminal Formatting

If ANSI escape characters show up as raw text (such as [92m), your active shell or terminal profile does not have full 256-color support enabled. Termux supports ANSI out of the box, but running inside unsupported sub-shells or custom multiplexers can occasionally disrupt formatting. You can strip the COLOR_* constants from the code to restore pure monochrome output.


Frequently Asked Questions (FAQ)

Can I run this to-do app completely offline?

Yes. The script relies solely on Python's built-in modules (json, os, and sys). Once Python is installed in Termux, you can manage your tasks with zero active internet connection.

Does this script require root access?

No. Termux operates as an unprivileged Linux user within Android's sandboxed permissions. All commands and file writes occur inside your user space without requiring root privileges.

How do I sync my tasks between my phone and computer?

Since the storage file is plain JSON, you can initialize a private Git repository inside ~/.todo_cli/ and push your tasks to GitHub or GitLab. Alternatively, you can copy the file to your shared storage folder and sync it using tools like Syncthing.

Can I add task priorities and due dates?

Yes. You can expand the task dictionary structure inside add_task() to include additional keys, such as "due_date": "2026-04-01" or "priority": "HIGH", and update the list_tasks() formatting function to display those new fields.


Conclusion

Building your own tools gives you complete control over your productivity workflow. With fewer than 150 lines of clean Python code, you now have a responsive, privacy-focused task manager running natively in Termux. It launches instantly, saves data reliably in structured JSON, and works entirely offline.

Try extending the script: add priority flags, build a search filter, or write a shell alias that automatically displays pending tasks whenever you start a terminal session. If you ran into any issues setting this up, leave a comment below or check out our other Termux terminal workflow guides on termuxgenius.com.