#!/usr/bin/env python3 import argparse import re from dataclasses import dataclass, field from pathlib import Path base_dir = Path(__file__).absolute().parent @dataclass class LeafTask: completed: bool = False @dataclass class RootTask(LeafTask): tasks: dict[str, LeafTask] = field(default_factory=dict) @dataclass class Command: func: callable read_only: bool = False help: str | None = None @dataclass class CommandList: commands: dict[str, Command] = field(default_factory=dict) def mark(self, read_only: bool = False): def wrap(func): self.commands[func.__name__] = Command(func=func, read_only=read_only, help=func.__doc__) return func return wrap @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] class ToDo: args: argparse.Namespace tasks: dict[str, RootTask] = {} commands: CommandList = CommandList() 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") sub_parsers = parser.add_subparsers(dest="command", help="Command to run", required=True) for command, cmd in self.commands.write_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") 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 for line in self.args.todo_file.read_text().splitlines(): if matches := re.match(r"^-\s+(\[(?P[x ])]\s+)?(?P.+)$", line.rstrip()): task_name = matches.group("task") root_task = RootTask(completed=matches.group("status") == "x") self.tasks[task_name] = root_task elif matches := re.match(r"^\s+-\s+(\[(?P[x ])]\s+)?(?P.+)$", line.rstrip()): root_task.tasks[matches.group("task")] = LeafTask(completed=matches.group("status") == "x") 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 write(self, todo: dict | None = None): self.args.todo_file.write_text(self.render(tasks=todo)) 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 @commands.mark(read_only=True) def list(self): """ 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)) @commands.mark() def add(self): """ 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() @commands.mark() def complete(self): """ Mark a task in todo.md as completed """ print(f"Marking task '{self.args.task}' in '{self.args.root if self.args.root else 'root'}' as completed") self.tasks_root[self.args.task].completed = True @commands.mark() def remove(self): """ Remove a task from todo.md """ del self.tasks_root[self.args.task] if __name__ == "__main__": ToDo().run()