Compare commits

...

4 commits

Author SHA1 Message Date
50cf00b245
Enhance ToDo 2024-11-12 00:30:26 -05:00
c0d5ec4a7c
Update doitlive 2024-11-12 00:30:03 -05:00
c72d0773e7
Small improvements 2024-11-12 00:26:48 -05:00
950aa8920a
Add doitlive 2024-11-12 00:26:19 -05:00
4 changed files with 165 additions and 122 deletions

View file

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

11
breakitlive.sh Normal file
View file

@ -0,0 +1,11 @@
#doitlive speed: 3
#doitlive prompt: sorin
python3 todo.py add Test
python3 todo.py list Test
python3 todo.py list Test Test
python3 todo.py add Test test1 test2
python3 todo.py add Test test1 test3
python3 todo.py complete Test test1 test3
python3 todo.py remove Test test1 test3

31
doitlive.sh Normal file
View file

@ -0,0 +1,31 @@
#doitlive speed: 3
#doitlive prompt: sorin
python3 todo.py --help
python3 todo.py help
python3 todo.py -h
python3 todo.py h
echo $?
python3 todo.py asdf
echo $?
python3 todo.py list
python3 todo.py add Test
python3 todo.py list
python3 todo.py list Test
python3 todo.py add Test test
python3 todo.py list Test
python3 todo.py list
python3 todo.py complete Test test
python3 todo.py list Test
python3 todo.py list
python3 todo.py complete Test
python3 todo.py list
python3 todo.py remove Test test
python3 todo.py list Test
python3 todo.py list
python3 todo.py remove Test
python3 todo.py list

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()