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

241
todo.py
View file

@ -1,148 +1,149 @@
#!/usr/bin/env python3
import os
import argparse
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
base_dir = os.path.abspath(os.path.dirname(__file__))
todo_file = os.path.join(base_dir, "todo.md")
base_dir = Path(__file__).absolute().parent
def add_task(task: str, parent=None):
print(f"Adding task '{task}' to '{parent if parent else 'root'}'")
todo = parse_todo()
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)
@dataclass
class LeafTask:
completed: bool = False
def list_tasks(parent=None):
print(f"Listing tasks in '{parent if parent else 'root'}'")
todo = parse_todo()
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))
@dataclass
class RootTask(LeafTask):
tasks: dict[str, LeafTask] = field(default_factory=dict)
def complete_task(task, parent=None):
print(f"Marking task '{task}' in '{parent if parent else 'root'}' as completed")
todo = parse_todo()
if parent:
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)
if parent:
task = todo[parent]["sub_tasks"][task]
else:
task = todo[task]
task["completed"] = True
write_todo(todo)
@dataclass
class Command:
func: callable
read_only: bool = False
help: str | None = None
def remove_task(task, parent=None):
print(f"Removing task '{task}' from '{parent if parent else 'root'}'")
todo = parse_todo()
if parent:
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)
if parent:
del todo[parent]["sub_tasks"][task]
else:
del todo[task]
write_todo(todo)
@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]
def print_help():
print("""Usage: todo.py [add|complete|remove] TASK [PARENT]
todo.py list [PARENT]
class ToDo:
args: argparse.Namespace
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]:
todo = {}
with open(todo_file, "r") as f:
lines = f.readlines()
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 lines:
if matches := re.match(r"^-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.*)$", line.rstrip()):
root_task_name = matches.group("task")
root_task = {
"completed": matches.group("status") == "x",
"sub_tasks": {},
}
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
for line in self.args.todo_file.read_text().splitlines():
if matches := re.match(r"^-\s+(\[(?P<status>[x ])]\s+)?(?P<task>.+)$", 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<status>[x ])]\s+)?(?P<task>.+)$", line.rstrip()):
root_task.tasks[matches.group("task")] = LeafTask(completed=matches.group("status") == "x")
def render_todo(todo: dict[str, dict]) -> str:
def render(self, tasks: dict | None = None):
lines = ["# A ToDo list", ""]
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}")
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 write_todo(todo: dict[str, dict]):
with open(todo_file, "w") as f:
f.write(render_todo(todo))
def main():
if len(sys.argv) < 2:
print_help()
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
match sys.argv[1]:
case "help" | "--help" | "h" | "-h":
print_help()
case "list":
match sys.argv[2:]:
case [parent]:
list_tasks(parent)
case _:
list_tasks()
case "add" | "complete" | "remove" as task_name:
task_func = f"{task_name}_task"
match sys.argv[2:]:
case [task]:
globals()[task_func](task)
case [task, parent]:
globals()[task_func](task, parent)
case _:
print_help()
exit(1)
case _:
print_help()
exit(1)
@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__":
main()
ToDo().run()