Compare commits

...

2 commits

Author SHA1 Message Date
df0d8c5113
Enhance ToDo 2024-11-11 18:56:47 -05:00
8e187c9790
Small improvements 2024-11-11 16:50:46 -05:00
2 changed files with 123 additions and 122 deletions

View file

@ -1,3 +1,3 @@
# Ajal Todo CLI # Ajal Todo CLI
A todo utility A todo utility - enhanced

243
todo.py
View file

@ -1,148 +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
print(f"Adding task '{task}' to '{parent if parent else 'root'}'") class LeafTask:
todo = parse_todo() completed: bool = False
if parent and parent not in todo:
print(f"Could not find parent task '{parent}' in root task list")
exit(1)
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
print(f"Listing tasks in '{parent if parent else 'root'}'") class RootTask(LeafTask):
todo = parse_todo() tasks: dict[str, LeafTask] = field(default_factory=dict)
if parent and parent not in todo:
print(f"Could not find parent task '{parent}' in root task list")
exit(1)
if parent:
todo = {parent: todo[parent]}
print(render_todo(todo))
def complete_task(task, parent=None): @dataclass
print(f"Marking task '{task}' in '{parent if parent else 'root'}' as completed") class Command:
todo = parse_todo() func: callable
if parent: read_only: bool = False
if parent not in todo: help: str | None = None
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)
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
print(f"Removing task '{task}' from '{parent if parent else 'root'}'") class CommandList:
todo = parse_todo() commands: dict[str, Command] = field(default_factory=dict)
if parent:
if parent not in todo: def mark(self, read_only: bool = False):
print(f"Could not find parent task '{parent}' in root task list") def wrap(func):
exit(1) self.commands[func.__name__] = Command(func=func, read_only=read_only, help=func.__doc__)
if task not in todo[parent]["sub_tasks"]: return func
print(f"Could not find task '{task}' in parent task '{parent}'") return wrap
exit(1)
if task not in todo: @property
print(f"Could not find task '{task}' in root task list") def read_list(self):
exit(1) return [(command, cmd) for command, cmd in self.commands.items() if cmd.read_only]
if parent:
del todo[parent]["sub_tasks"][task] @property
else: def write_list(self):
del todo[task] return [(command, cmd) for command, cmd in self.commands.items() if not cmd.read_only]
write_todo(todo)
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": {}, elif matches := re.match(r"^\s+-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.+)$", line.rstrip()):
} root_task.tasks[matches.group("task")] = LeafTask(completed=matches.group("status") == "x")
todo[root_task_name] = root_task
elif matches := re.match(r"^\s+-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.*)$", line.rstrip()):
leaf_task = matches.group("task")
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 [task, parent]:
globals()[task_func](task, parent)
case _:
print_help()
exit(1)
case _:
print_help()
exit(1)
if __name__ == "__main__": if __name__ == "__main__":
main() ToDo().run()