Building a Better Command-Line Todo App: A Journey in Shell Scripting
Have you ever wanted a simple, lightweight todo app that works right in your terminal? One that doesn’t require a graphical interface, cloud syncing, or complex dependencies? That’s exactly what I set out to build with “Terminal Todo Tracker” (ttt), a command-line application written in shell script that makes task management effortless for developers and terminal enthusiasts.
Why Another Todo App?
As developers, we spend much of our day in the terminal. Switching contexts to manage tasks can break our flow and reduce productivity. By creating a todo app that lives in the terminal, we can seamlessly integrate task management into our existing workflow.
The goal was simple: create a tool that’s:
- Fast and lightweight
- Works across operating systems
- Requires no external dependencies
- Has an intuitive, colorful interface
- Stores data in plain text
The Terminal Todo Tracker
The result is “ttt” — a terminal-based todo application with a surprisingly rich feature set despite its minimalist approach:
Key Features
- Colorful Interface: Uses ANSI color codes to create a visually appealing experience
- Due Date Organization: Tasks are automatically grouped by due date
- Inline Completed Tasks: Completed tasks remain visible with strikethrough formatting
- Keyboard Shortcuts: Single-key commands for common actions
- Cross-Platform: Works on Linux, macOS, and Windows (via PowerShell or Batch)
- Smart Defaults: Today’s date is pre-populated for quick task entry
Technical Implementation
The application is written in two versions:
- A Bash script for Linux and macOS users
- A POSIX-compliant script for environments without Bash
Data Storage
Data is stored in a simple text file at $HOME/.ttt/tasks.txt using a pipe-delimited format:
TODO:|31/05/23|Submit quarterly report
DONE:|30/05/23|Schedule team meeting
This approach ensures data can be easily backed up, version controlled, or manually edited if needed.
User Interface
The UI is built entirely with ANSI escape codes for formatting and colors:
# Colors and formatting
RESET="\033[0m"
BOLD="\033[1m"
STRIKETHROUGH="\033[9m"
ITALIC="\033[3m"
GREEN="\033[32m"
YELLOW="\033[33m"
# …and more
These codes allow us to create a rich terminal interface without any external dependencies.
Smart User Experience
Today’s Date as Default
A small but significant UX improvement was adding today’s date as the default option:
# Get today's date in dd/mm/yy format
today=$(date +%d/%m/%y)
echo -e "\n${BOLD}Enter due date (${YELLOW}dd/mm/yy${RESET}${BOLD}) [${GREEN}Today: $today${RESET}${BOLD}]:${RESET} "
read due_date
# Use today's date if user didn't enter anything
if [ -z "$due_date" ]; then
due_date="$today"
echo -e "${BLUE}Using today's date: $today${RESET}"
fi
This simple enhancement dramatically improves the experience for adding tasks that need to be completed today — just press Enter and you’re done.
Inline Completed Tasks
Rather than hiding completed tasks in a separate section, they’re displayed with strikethrough formatting alongside pending tasks:
# Display both active and completed tasks for this date
grep "|$date|" "$TASKS_FILE" | while IFS='|' read -r status date task; do
if [ "$status" = "DONE:" ]; then
echo " ""$BOLD""$BLUE""[$task_num]""$RESET"" ""$STRIKETHROUGH""$task""$RESET"" ""$GRAY""(done)""$RESET"
else
echo " ""$BOLD""$BLUE""[$task_num]""$RESET"" $task"
fi
task_num=$((task_num + 1))
done
This approach helps users maintain context about what they’ve accomplished alongside what’s still pending.
Cross-Platform Compatibility
To ensure the application works seamlessly across different environments, I created platform-specific wrappers:
- Linux/macOS: Native Bash and POSIX-compliant scripts
- Windows: PowerShell and Batch scripts that invoke the POSIX version
The PowerShell wrapper ensures Windows users get the same experience:
# PowerShell wrapper for ttt
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
& $env:SystemRoot\System32\bash.exe -c "$scriptPath/ttt.sh $args"
Lessons Learned
Building this application taught me several valuable lessons:
1. Embrace POSIX for Compatibility
By creating a POSIX-compliant version, I ensured the application works in minimal environments like Docker containers or older systems.
2. Text-Based Data Storage Is Underrated
Simple text files enable users to interact with their data using standard Unix tools:
$ grep "Meeting" ~/.ttt/tasks.txt
TODO:|01/06/23|Team planning meeting
3. Special Characters Require Special Handling
One challenging bug involved task removal failing with special characters. The solution was to improve how we select lines for removal:
# Function to remove a task line from file
remove_line_from_file() {
task_line="$1"
temp_file="$TASKS_FILE.tmp"
# Write all lines except the one to remove to a temp file
awk -v line="$task_line" '$0 != line' "$TASKS_FILE" > "$temp_file"
# Replace original with temp file
mv "$temp_file" "$TASKS_FILE"
}
This approach is more robust than the initial sed implementation that struggled with escaping.
Conclusion
The Terminal Todo Tracker demonstrates that simple tools can be highly effective when tailored to specific workflows. By embracing the terminal environment rather than fighting against it, we can create applications that enhance productivity without unnecessary complexity.
Building command-line tools forces us to focus on what truly matters: core functionality, user experience, and reliability. Sometimes the best tool isn’t the one with the most features, but the one that fits perfectly into your existing workflow.
Have you built your own command-line tools? Are there features you’d add to this todo tracker? I’d love to hear your thoughts in the comments below.
The complete source code for Terminal Todo Tracker is available on GitHub.