-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtodo.py
More file actions
274 lines (222 loc) · 8.05 KB
/
todo.py
File metadata and controls
274 lines (222 loc) · 8.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/usr/bin/env python3
"""
todo — Dead-simple CLI task manager. File-backed, zero deps.
3 commands to learn. Tasks stored in .todo.json in current directory.
Perfect for per-project task tracking from the terminal.
Usage:
py todo.py add "Fix the login bug"
py todo.py add "Review PR #42" -p high
py todo.py add "Write tests" -t backend,urgent
py todo.py list # Show pending tasks
py todo.py list --all # Include completed
py todo.py done 1 # Complete task #1
py todo.py done 1 3 5 # Complete multiple
py todo.py edit 2 "Updated description" # Edit task text
py todo.py rm 3 # Delete task
py todo.py clear # Remove completed tasks
py todo.py search "login" # Search tasks
py todo.py stats # Summary statistics
"""
import argparse
import json
import sys
import os
from datetime import datetime
from pathlib import Path
TODO_FILE = ".todo.json"
def load_todos() -> list[dict]:
"""Load tasks from .todo.json"""
path = Path(TODO_FILE)
if not path.exists():
return []
try:
return json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, Exception):
return []
def save_todos(todos: list[dict]):
"""Save tasks to .todo.json"""
Path(TODO_FILE).write_text(
json.dumps(todos, indent=2, default=str) + "\n",
encoding="utf-8"
)
def next_id(todos: list[dict]) -> int:
"""Get next available ID."""
if not todos:
return 1
return max(t.get("id", 0) for t in todos) + 1
def cmd_add(args):
"""Add a new task."""
todos = load_todos()
task = {
"id": next_id(todos),
"text": " ".join(args.text),
"done": False,
"priority": args.priority or "normal",
"tags": [t.strip() for t in args.tags.split(",")] if args.tags else [],
"created": datetime.now().isoformat(),
"completed": None,
}
todos.append(task)
save_todos(todos)
icon = {"high": "!", "normal": " ", "low": "."}
print(f" [{icon.get(task['priority'], ' ')}] #{task['id']} {task['text']}")
def cmd_list(args):
"""List tasks."""
todos = load_todos()
if not todos:
print(" No tasks. Add one: py todo.py add \"Your task here\"")
return
# Filter
if not args.all:
shown = [t for t in todos if not t.get("done")]
else:
shown = todos
if args.tag:
shown = [t for t in shown if args.tag in t.get("tags", [])]
if not shown:
print(" All tasks completed!" if not args.all else " No matching tasks.")
return
# Sort: priority (high first), then by id
priority_order = {"high": 0, "normal": 1, "low": 2}
shown.sort(key=lambda t: (priority_order.get(t.get("priority", "normal"), 1), t.get("id", 0)))
# Display
pending = sum(1 for t in todos if not t.get("done"))
done = sum(1 for t in todos if t.get("done"))
print(f"\n Tasks ({pending} pending, {done} done)\n")
for t in shown:
check = "x" if t.get("done") else " "
pri = {"high": "!", "normal": " ", "low": "."}
icon = pri.get(t.get("priority", "normal"), " ")
tags = ""
if t.get("tags"):
tags = " [" + ", ".join(t["tags"]) + "]"
text = t.get("text", "")
# Strikethrough for done tasks (terminal-safe)
if t.get("done"):
text = f"({text})"
print(f" [{check}] {icon} #{t['id']:<3} {text}{tags}")
print()
def cmd_done(args):
"""Mark tasks as completed."""
todos = load_todos()
ids = set(args.ids)
count = 0
for t in todos:
if t["id"] in ids and not t.get("done"):
t["done"] = True
t["completed"] = datetime.now().isoformat()
print(f" Done: #{t['id']} {t['text']}")
count += 1
if count == 0:
print(" No matching pending tasks found.")
save_todos(todos)
def cmd_edit(args):
"""Edit task text."""
todos = load_todos()
for t in todos:
if t["id"] == args.id:
old = t["text"]
t["text"] = " ".join(args.text)
save_todos(todos)
print(f" Updated #{args.id}: {old} -> {t['text']}")
return
print(f" Task #{args.id} not found.")
def cmd_rm(args):
"""Delete a task."""
todos = load_todos()
new_todos = [t for t in todos if t["id"] not in args.ids]
removed = len(todos) - len(new_todos)
save_todos(new_todos)
print(f" Removed {removed} task(s).")
def cmd_clear(args):
"""Remove all completed tasks."""
todos = load_todos()
pending = [t for t in todos if not t.get("done")]
removed = len(todos) - len(pending)
save_todos(pending)
print(f" Cleared {removed} completed task(s). {len(pending)} remaining.")
def cmd_search(args):
"""Search tasks by text."""
todos = load_todos()
query = args.query.lower()
matches = [t for t in todos if query in t.get("text", "").lower()
or query in " ".join(t.get("tags", [])).lower()]
if not matches:
print(f" No tasks matching '{args.query}'")
return
print(f"\n Search results for '{args.query}':\n")
for t in matches:
check = "x" if t.get("done") else " "
print(f" [{check}] #{t['id']} {t['text']}")
print()
def cmd_stats(args):
"""Show task statistics."""
todos = load_todos()
if not todos:
print(" No tasks.")
return
total = len(todos)
done = sum(1 for t in todos if t.get("done"))
pending = total - done
high = sum(1 for t in todos if not t.get("done") and t.get("priority") == "high")
# Tag frequency
all_tags = []
for t in todos:
all_tags.extend(t.get("tags", []))
tag_counts = {}
for tag in all_tags:
tag_counts[tag] = tag_counts.get(tag, 0) + 1
print(f"\n Task Statistics")
print(f" Total: {total}")
print(f" Pending: {pending}")
print(f" Done: {done}")
print(f" High-pri: {high}")
if total > 0:
print(f" Progress: {done/total*100:.0f}%")
if tag_counts:
print(f" Tags: {', '.join(f'{k}({v})' for k, v in sorted(tag_counts.items(), key=lambda x: -x[1]))}")
print()
def main():
parser = argparse.ArgumentParser(description="todo -- dead-simple CLI task manager")
sub = parser.add_subparsers(dest="command")
# add
p = sub.add_parser("add", help="Add a task")
p.add_argument("text", nargs="+", help="Task description")
p.add_argument("-p", "--priority", choices=["high", "normal", "low"], help="Priority level")
p.add_argument("-t", "--tags", help="Comma-separated tags")
# list
p = sub.add_parser("list", aliases=["ls"], help="List tasks")
p.add_argument("--all", "-a", action="store_true", help="Include completed tasks")
p.add_argument("--tag", help="Filter by tag")
# done
p = sub.add_parser("done", help="Mark task(s) as completed")
p.add_argument("ids", nargs="+", type=int, help="Task ID(s)")
# edit
p = sub.add_parser("edit", help="Edit task text")
p.add_argument("id", type=int, help="Task ID")
p.add_argument("text", nargs="+", help="New text")
# rm
p = sub.add_parser("rm", help="Delete task(s)")
p.add_argument("ids", nargs="+", type=int, help="Task ID(s)")
# clear
sub.add_parser("clear", help="Remove completed tasks")
# search
p = sub.add_parser("search", help="Search tasks")
p.add_argument("query", help="Search text")
# stats
sub.add_parser("stats", help="Show statistics")
args = parser.parse_args()
commands = {
"add": cmd_add, "list": cmd_list, "ls": cmd_list,
"done": cmd_done, "edit": cmd_edit, "rm": cmd_rm,
"clear": cmd_clear, "search": cmd_search, "stats": cmd_stats,
}
if args.command in commands:
commands[args.command](args)
else:
# Default: show list
args.all = False
args.tag = None
cmd_list(args)
if __name__ == "__main__":
main()