#!/usr/bin/env python3 """Tasked — terminal todo list manager.""" import argparse, json, sys, os from datetime import datetime, date from pathlib import Path def color(text, code): return f"\033[{code}m{text}\033[0m" if sys.stdout.isatty() else text DATA_FILE = Path.home() / ".tasked.json" def load_tasks(): if DATA_FILE.exists(): try: return json.loads(DATA_FILE.read_text()) except json.JSONDecodeError: return [] return [] def save_tasks(tasks): DATA_FILE.write_text(json.dumps(tasks, indent=2, default=str)) def next_id(tasks): return max([t.get("id", 0) for t in tasks], default=0) + 1 PRIORITY_COLORS = { "high": "31", "medium": "33", "low": "32", } def format_task(t, show_id=True): prefix = f"[{t['id']}] " if show_id else "" status = color("✓", "32") if t.get("done") else color("○", "31") pri = color(t.get("priority", "medium").upper()[:4], PRIORITY_COLORS.get(t.get("priority", "medium"), "37")) due = "" if t.get("due"): try: d = datetime.fromisoformat(t["due"]).date() delta = (d - date.today()).days if delta < 0: due = color(f" (OVERDUE {abs(delta)}d)", "31;1") elif delta == 0: due = color(" (DUE TODAY)", "33;1") elif delta == 1: due = color(" (due tomorrow)", "33") else: due = color(f" (due in {delta}d)", "90") except: due = f" ({t['due']})" cats = "" if t.get("categories"): cats = " " + " ".join(color(f"[{c}]", "36") for c in t["categories"]) return f" {status} {pri} {t['title']}{cats}{due}" def cmd_add(args): tasks = load_tasks() task = { "id": next_id(tasks), "title": " ".join(args.title), "done": False, "priority": args.priority or "medium", "categories": args.category or [], "created": datetime.now().isoformat(), "due": None, } if args.due: task["due"] = args.due tasks.append(task) save_tasks(tasks) print(f" {color('✓', '32')} Added task #{task['id']}: {task['title']}") def cmd_list(args): tasks = load_tasks() if not tasks: print(f" {color('No tasks found', '90')}") return # Filter if args.category: tasks = [t for t in tasks if args.category in t.get("categories", [])] if args.priority: tasks = [t for t in tasks if t.get("priority") == args.priority] # Separate done/not done pending = [t for t in tasks if not t.get("done")] completed = [t for t in tasks if t.get("done")] if args.all: show = tasks elif args.completed: show = completed else: show = pending if not show: print(f" {color('No matching tasks', '90')}") return print(f"\n {color('Tasks:', '1')} ({len(pending)} pending, {len(completed)} done)") print(f" {color('─' * 40, '90')}") for t in show: print(format_task(t)) print() def cmd_done(args): tasks = load_tasks() found = False for t in tasks: if t["id"] == args.id: t["done"] = True t["completed_at"] = datetime.now().isoformat() found = True print(f" {color('✓', '32')} Task #{args.id} completed!") break if not found: print(f" {color('✗', '31')} Task #{args.id} not found") save_tasks(tasks) def cmd_delete(args): tasks = load_tasks() before = len(tasks) tasks = [t for t in tasks if t["id"] != args.id] if len(tasks) < before: print(f" {color('✓', '32')} Deleted task #{args.id}") else: print(f" {color('✗', '31')} Task #{args.id} not found") save_tasks(tasks) def cmd_clear(args): tasks = load_tasks() before = len(tasks) if args.done: tasks = [t for t in tasks if not t.get("done")] print(f" {color('✓', '32')} Cleared {before - len(tasks)} completed tasks") else: tasks = [] print(f" {color('✓', '32')} Cleared all {before} tasks") save_tasks(tasks) def cmd_stats(args): tasks = load_tasks() total = len(tasks) done = sum(1 for t in tasks if t.get("done")) pending = total - done categories = {} for t in tasks: for c in t.get("categories", []): categories[c] = categories.get(c, 0) + 1 print(f"\n {color('Task Statistics', '1')}") print(f" {color('─' * 30, '90')}") print(f" Total: {total}") print(f" Pending: {color(str(pending), '33')}") print(f" Completed: {color(str(done), '32')}") if categories: print(f"\n {color('Categories:', '36')}") for c, count in sorted(categories.items()): print(f" {c}: {count}") print() def main(): parser = argparse.ArgumentParser( description="Tasked — terminal todo list manager", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " tasked add 'Fix login bug' -p high -c bug\n" " tasked list\n" " tasked list --category bug\n" " tasked done 1\n" " tasked delete 1\n" " tasked clear --done\n" " tasked stats\n" ), ) parser.add_argument("--version", action="version", version="tasked 1.0.0") sub = parser.add_subparsers(dest="command", required=True) a = sub.add_parser("add", help="Add a new task") a.add_argument("title", nargs="+", help="Task title") a.add_argument("-p", "--priority", choices=["high", "medium", "low"], default="medium", help="Priority level") a.add_argument("-c", "--category", action="append", help="Category tag (can be used multiple times)") a.add_argument("--due", help="Due date (YYYY-MM-DD)") l = sub.add_parser("list", help="List tasks") l.add_argument("--category", help="Filter by category") l.add_argument("--priority", choices=["high", "medium", "low"], help="Filter by priority") l.add_argument("--all", action="store_true", help="Show all tasks") l.add_argument("--completed", action="store_true", help="Show only completed") d = sub.add_parser("done", help="Mark task as complete") d.add_argument("id", type=int, help="Task ID") rm = sub.add_parser("delete", help="Delete a task") rm.add_argument("id", type=int, help="Task ID") c = sub.add_parser("clear", help="Clear tasks") c.add_argument("--done", action="store_true", help="Clear only completed tasks") s = sub.add_parser("stats", help="Show task statistics") args = parser.parse_args() if args.command == "add": cmd_add(args) elif args.command == "list": cmd_list(args) elif args.command == "done": cmd_done(args) elif args.command == "delete": cmd_delete(args) elif args.command == "clear": cmd_clear(args) elif args.command == "stats": cmd_stats(args) if __name__ == "__main__": main()