Skip to content

Commit b25b251

Browse files
committed
feat(cli): add simple CLI todo app (add/list/remove)
1 parent 14d4e11 commit b25b251

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed

intermediate/simple_todo.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simple CLI Todo app for Hacktoberfest contribution.
4+
Stores tasks in a JSON file with features: add, list, done, remove, clear.
5+
"""
6+
import json
7+
import argparse
8+
from pathlib import Path
9+
from datetime import datetime
10+
11+
DATA_FILE = Path("tasks.json")
12+
13+
14+
def load_tasks() -> list:
15+
"""
16+
Load and return the list of tasks from the JSON file.
17+
Returns an empty list if the file does not exist or is invalid.
18+
"""
19+
if not DATA_FILE.exists():
20+
return []
21+
try:
22+
return json.loads(DATA_FILE.read_text())
23+
except json.JSONDecodeError:
24+
# Warn the user and reset tasks if JSON is invalid.
25+
print("Warning: tasks.json is corrupted or not valid JSON. Resetting tasks list.")
26+
return []
27+
28+
29+
def save_tasks(tasks: list) -> None:
30+
"""
31+
Save the list of tasks to the JSON file with pretty formatting.
32+
"""
33+
DATA_FILE.write_text(json.dumps(tasks, indent=2, sort_keys=False))
34+
35+
36+
def add_task(text: str) -> None:
37+
"""
38+
Add a new task with the given text to the list.
39+
"""
40+
tasks = load_tasks()
41+
tasks.append({
42+
"id": int(datetime.now().timestamp()),
43+
"text": text,
44+
"done": False,
45+
"created": datetime.now().isoformat()
46+
})
47+
save_tasks(tasks)
48+
print("Added:", text)
49+
50+
51+
def list_tasks() -> None:
52+
"""
53+
List all tasks with their status. Uncompleted tasks show [ ] and done tasks show [✓].
54+
"""
55+
tasks = load_tasks()
56+
if not tasks:
57+
print('No tasks. Add one using: todo.py add "Buy milk"')
58+
return
59+
for i, t in enumerate(tasks, 1):
60+
status = "✓" if t.get("done") else " "
61+
print(f"{i}. [{status}] {t.get('text')} (id:{t.get('id')})")
62+
63+
64+
def mark_done(index: int) -> None:
65+
"""
66+
Mark the task at the given 1-based index as done.
67+
"""
68+
tasks = load_tasks()
69+
if index < 1 or index > len(tasks):
70+
print("Invalid task number.")
71+
return
72+
tasks[index - 1]["done"] = True
73+
save_tasks(tasks)
74+
print("Marked done:", tasks[index - 1]["text"])
75+
76+
77+
def remove_task(index: int) -> None:
78+
"""
79+
Remove the task at the given 1-based index.
80+
"""
81+
tasks = load_tasks()
82+
if index < 1 or index > len(tasks):
83+
print("Invalid task number.")
84+
return
85+
removed = tasks.pop(index - 1)
86+
save_tasks(tasks)
87+
print("Removed:", removed["text"])
88+
89+
90+
def clear_tasks() -> None:
91+
"""
92+
Clear all tasks.
93+
"""
94+
save_tasks([])
95+
print("All tasks cleared.")
96+
97+
98+
def parse_args() -> argparse.Namespace:
99+
"""
100+
Parse command-line arguments and return the parsed namespace.
101+
"""
102+
parser = argparse.ArgumentParser(prog="todo.py", description="Simple CLI todo app")
103+
subparsers = parser.add_subparsers(dest="cmd")
104+
105+
# Add command
106+
sp_add = subparsers.add_parser("add", help="Add a new task")
107+
sp_add.add_argument("text", nargs="+", help="Task text")
108+
# List command
109+
sp_list = subparsers.add_parser("list", help="List all tasks")
110+
# Done command
111+
sp_done = subparsers.add_parser("done", help="Mark a task as done")
112+
sp_done.add_argument("number", type=int, help="Task index to mark as done (from list)")
113+
# Remove command
114+
sp_rm = subparsers.add_parser("remove", help="Remove a task")
115+
sp_rm.add_argument("number", type=int, help="Task index to remove (from list)")
116+
# Clear command
117+
sp_clear = subparsers.add_parser("clear", help="Remove all tasks")
118+
119+
return parser.parse_args()
120+
121+
122+
def main() -> None:
123+
args = parse_args()
124+
if args.cmd == "add":
125+
add_task(" ".join(args.text))
126+
elif args.cmd == "list" or args.cmd is None:
127+
list_tasks()
128+
elif args.cmd == "done":
129+
mark_done(args.number)
130+
elif args.cmd == "remove":
131+
remove_task(args.number)
132+
elif args.cmd == "clear":
133+
clear_tasks()
134+
else:
135+
print("Unknown command. Use add/list/done/remove/clear")
136+
137+
138+
if __name__ == "__main__":
139+
main()

0 commit comments

Comments
 (0)