Enhance ToDo
This commit is contained in:
parent
8e187c9790
commit
df0d8c5113
2 changed files with 120 additions and 123 deletions
|
|
@ -1,3 +1,3 @@
|
||||||
# Ajal Todo CLI
|
# Ajal Todo CLI
|
||||||
|
|
||||||
A todo utility
|
A todo utility - enhanced
|
||||||
|
|
|
||||||
241
todo.py
241
todo.py
|
|
@ -1,152 +1,149 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import argparse
|
||||||
import re
|
import re
|
||||||
import sys
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
base_dir = os.path.abspath(os.path.dirname(__file__))
|
base_dir = Path(__file__).absolute().parent
|
||||||
todo_file = os.path.join(base_dir, "todo.md")
|
|
||||||
|
|
||||||
|
|
||||||
def add_task(task: str, parent=None):
|
@dataclass
|
||||||
todo = parse_todo()
|
class LeafTask:
|
||||||
if parent and parent not in todo:
|
completed: bool = False
|
||||||
print(f"Could not find parent task '{parent}' in root task list")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
print(f"Adding task '{task}' to '{parent if parent else 'root'}'")
|
|
||||||
if parent:
|
|
||||||
todo[parent]["sub_tasks"][task] = {"completed": False}
|
|
||||||
else:
|
|
||||||
todo[task] = {"completed": False, "sub_tasks": {}}
|
|
||||||
write_todo(todo)
|
|
||||||
|
|
||||||
|
|
||||||
def list_tasks(parent=None):
|
@dataclass
|
||||||
todo = parse_todo()
|
class RootTask(LeafTask):
|
||||||
if parent and parent not in todo:
|
tasks: dict[str, LeafTask] = field(default_factory=dict)
|
||||||
print(f"Could not find parent task '{parent}' in root task list")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
print(f"Listing tasks in '{parent if parent else 'root'}'")
|
|
||||||
if parent:
|
|
||||||
todo = {parent: todo[parent]}
|
|
||||||
print(render_todo(todo))
|
|
||||||
|
|
||||||
|
|
||||||
def complete_task(task, parent=None):
|
@dataclass
|
||||||
todo = parse_todo()
|
class Command:
|
||||||
if parent:
|
func: callable
|
||||||
if parent not in todo:
|
read_only: bool = False
|
||||||
print(f"Could not find parent task '{parent}' in root task list")
|
help: str | None = None
|
||||||
exit(1)
|
|
||||||
if task not in todo[parent]["sub_tasks"]:
|
|
||||||
print(f"Could not find task '{task}' in parent task '{parent}'")
|
|
||||||
exit(1)
|
|
||||||
if task not in todo:
|
|
||||||
print(f"Could not find task '{task}' in root task list")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
print(f"Marking task '{task}' in '{parent if parent else 'root'}' as completed")
|
|
||||||
if parent:
|
|
||||||
task = todo[parent]["sub_tasks"][task]
|
|
||||||
else:
|
|
||||||
task = todo[task]
|
|
||||||
task["completed"] = True
|
|
||||||
write_todo(todo)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_task(task, parent=None):
|
@dataclass
|
||||||
todo = parse_todo()
|
class CommandList:
|
||||||
if parent:
|
commands: dict[str, Command] = field(default_factory=dict)
|
||||||
if parent not in todo:
|
|
||||||
print(f"Could not find parent task '{parent}' in root task list")
|
|
||||||
exit(1)
|
|
||||||
if task not in todo[parent]["sub_tasks"]:
|
|
||||||
print(f"Could not find task '{task}' in parent task '{parent}'")
|
|
||||||
exit(1)
|
|
||||||
if task not in todo:
|
|
||||||
print(f"Could not find task '{task}' in root task list")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
print(f"Removing task '{task}' from '{parent if parent else 'root'}'")
|
def mark(self, read_only: bool = False):
|
||||||
if parent:
|
def wrap(func):
|
||||||
del todo[parent]["sub_tasks"][task]
|
self.commands[func.__name__] = Command(func=func, read_only=read_only, help=func.__doc__)
|
||||||
else:
|
return func
|
||||||
del todo[task]
|
return wrap
|
||||||
write_todo(todo)
|
|
||||||
|
@property
|
||||||
|
def read_list(self):
|
||||||
|
return [(command, cmd) for command, cmd in self.commands.items() if cmd.read_only]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def write_list(self):
|
||||||
|
return [(command, cmd) for command, cmd in self.commands.items() if not cmd.read_only]
|
||||||
|
|
||||||
|
|
||||||
def print_help():
|
class ToDo:
|
||||||
print("""Usage: todo.py [add|complete|remove] TASK [PARENT]
|
args: argparse.Namespace
|
||||||
todo.py list [PARENT]
|
tasks: dict[str, RootTask] = {}
|
||||||
|
commands: CommandList = CommandList()
|
||||||
|
|
||||||
Manages todo.md file as a ToDo file""")
|
def __init__(self):
|
||||||
|
self.args = self.parse_args()
|
||||||
|
self.tasks_root = self.tasks
|
||||||
|
self.task_class = RootTask
|
||||||
|
|
||||||
|
def parse_args(self):
|
||||||
|
parser = argparse.ArgumentParser(description="Manages todo.md file as a ToDo file")
|
||||||
|
parser.add_argument("--todo-file", "-f", help="Path to the todo file.", default=base_dir / "todo.md")
|
||||||
|
|
||||||
def parse_todo() -> dict[str, dict]:
|
sub_parsers = parser.add_subparsers(dest="command", help="Command to run", required=True)
|
||||||
todo = {}
|
for command, cmd in self.commands.write_list:
|
||||||
with open(todo_file, "r") as f:
|
sub_parser = sub_parsers.add_parser(command, help=cmd.help.lstrip().splitlines()[0], description=cmd.help)
|
||||||
lines = f.readlines()
|
sub_parser.add_argument("--root", "-r", help="Root task")
|
||||||
|
sub_parser.add_argument("task", help="Task name")
|
||||||
|
for command, cmd in self.commands.read_list:
|
||||||
|
sub_parser = sub_parsers.add_parser(command, help=cmd.help.lstrip().splitlines()[0], description=cmd.help)
|
||||||
|
sub_parser.add_argument("--root", "-r", help="Root task")
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def command(self) -> Command:
|
||||||
|
return self.commands.commands[self.args.command]
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
self.parse()
|
||||||
|
self.augment_root()
|
||||||
|
self.command.func(self)
|
||||||
|
if not self.command.read_only:
|
||||||
|
self.write()
|
||||||
|
self.list()
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
root_task = None
|
root_task = None
|
||||||
for line in lines:
|
for line in self.args.todo_file.read_text().splitlines():
|
||||||
if matches := re.match(r"^-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.+)$", line.rstrip()):
|
if matches := re.match(r"^-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.+)$", line.rstrip()):
|
||||||
root_task_name = matches.group("task")
|
task_name = matches.group("task")
|
||||||
root_task = {
|
root_task = RootTask(completed=matches.group("status") == "x")
|
||||||
"completed": matches.group("status") == "x",
|
self.tasks[task_name] = root_task
|
||||||
"sub_tasks": {},
|
|
||||||
}
|
|
||||||
todo[root_task_name] = root_task
|
|
||||||
elif matches := re.match(r"^\s+-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.+)$", line.rstrip()):
|
elif matches := re.match(r"^\s+-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.+)$", line.rstrip()):
|
||||||
leaf_task = matches.group("task")
|
root_task.tasks[matches.group("task")] = LeafTask(completed=matches.group("status") == "x")
|
||||||
root_task["sub_tasks"][leaf_task] = {
|
|
||||||
"completed": matches.group("status") == "x",
|
|
||||||
}
|
|
||||||
return todo
|
|
||||||
|
|
||||||
|
def render(self, tasks: dict | None = None):
|
||||||
|
lines = ["# A ToDo list", ""]
|
||||||
|
for root_task_name, root_task in (tasks or self.tasks).items(): # type: str, RootTask
|
||||||
|
lines.append(f"- [{'x' if root_task.completed else ' '}] {root_task_name}")
|
||||||
|
for leaf_task_name, task in root_task.tasks.items():
|
||||||
|
lines.append(f" - [{'x' if task.completed else ' '}] {leaf_task_name}")
|
||||||
|
lines += [""]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
def render_todo(todo: dict[str, dict]) -> str:
|
def write(self, todo: dict | None = None):
|
||||||
lines = ["# A ToDo list", ""]
|
self.args.todo_file.write_text(self.render(tasks=todo))
|
||||||
for task, task_data in todo.items(): # type: str, dict
|
|
||||||
lines.append(f"- [{'x' if task_data["completed"] else ' '}] {task}")
|
|
||||||
for leaf_task, leaf_task_data in task_data["sub_tasks"].items():
|
|
||||||
lines.append(f" - [{'x' if leaf_task_data["completed"] else ' '}] {leaf_task}")
|
|
||||||
lines += [""]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
def augment_root(self):
|
||||||
|
if self.args.root:
|
||||||
|
if self.args.root not in self.tasks:
|
||||||
|
print(f"Root task '{self.args.root}' does not exist")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
self.tasks_root = self.tasks[self.args.root].tasks
|
||||||
|
self.task_class = LeafTask
|
||||||
|
|
||||||
def write_todo(todo: dict[str, dict]):
|
@commands.mark(read_only=True)
|
||||||
with open(todo_file, "w") as f:
|
def list(self):
|
||||||
f.write(render_todo(todo))
|
"""
|
||||||
|
List all tasks in todo.md
|
||||||
|
|
||||||
|
Returns all tasks if no root task is passed, otherwise only the root task and subtasks
|
||||||
|
"""
|
||||||
|
print(f"Listing tasks in '{self.args.root if self.args.root else 'root'}'")
|
||||||
|
print(self.render(tasks={self.args.root: self.tasks[self.args.root]} if self.args.root else None))
|
||||||
|
|
||||||
def main():
|
@commands.mark()
|
||||||
if len(sys.argv) < 2:
|
def add(self):
|
||||||
print_help()
|
"""
|
||||||
exit(1)
|
Add a task to todo.md
|
||||||
|
"""
|
||||||
|
print(f"Adding task '{self.args.task}' to '{self.args.root if self.args.root else 'root'}'")
|
||||||
|
self.tasks_root[self.args.task] = self.task_class()
|
||||||
|
|
||||||
match sys.argv[1]:
|
@commands.mark()
|
||||||
case "help" | "--help" | "h" | "-h":
|
def complete(self):
|
||||||
print_help()
|
"""
|
||||||
case "list":
|
Mark a task in todo.md as completed
|
||||||
match sys.argv[2:]:
|
"""
|
||||||
case [parent, *_]:
|
print(f"Marking task '{self.args.task}' in '{self.args.root if self.args.root else 'root'}' as completed")
|
||||||
list_tasks(parent)
|
self.tasks_root[self.args.task].completed = True
|
||||||
case _:
|
|
||||||
list_tasks()
|
@commands.mark()
|
||||||
case "add" | "complete" | "remove" as task_name:
|
def remove(self):
|
||||||
task_func = f"{task_name}_task"
|
"""
|
||||||
match sys.argv[2:]:
|
Remove a task from todo.md
|
||||||
case [task]:
|
"""
|
||||||
globals()[task_func](task)
|
del self.tasks_root[self.args.task]
|
||||||
case [parent, task, *_]:
|
|
||||||
globals()[task_func](task, parent)
|
|
||||||
case _:
|
|
||||||
print_help()
|
|
||||||
exit(1)
|
|
||||||
case _:
|
|
||||||
print_help()
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
ToDo().run()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue