Compare commits

...

9 commits

Author SHA1 Message Date
d0f34de620
Update doitlive 2024-11-12 14:40:19 -05:00
9ee5aa41fa
Restore todo.md 2024-11-12 00:47:34 -05:00
f8c26abeea
Use uv and typer 2024-11-12 00:47:34 -05:00
e4ca23901b
Update doitlive 2024-11-12 00:41:34 -05:00
0932faba34
Enhance ToDo 2024-11-12 00:40:48 -05:00
9bb2e2f8f1
Update doitlive 2024-11-12 00:40:28 -05:00
1a0461eded
Small improvements 2024-11-12 00:40:27 -05:00
3021ab1cf0
Fix doitlive echo 2024-11-12 00:39:49 -05:00
950aa8920a
Add doitlive 2024-11-12 00:26:19 -05:00
5 changed files with 206 additions and 125 deletions

View file

@ -1,3 +1,17 @@
# Ajal Todo CLI
A todo utility
A todo utility - enhanced
## Usage
You should have `uv` installed or have `virtualenv` or `venv` already set up, and using python 3.12+
Run (assuming you are using `uv`):
```shell
uv venv
source .venv/bin/activate
uv sync
```
Now you can run commands, start by running `uv run todo.py --help` and continue from there

18
breakitlive.sh Normal file
View file

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

32
doitlive.sh Normal file
View file

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

9
pyproject.toml Normal file
View file

@ -0,0 +1,9 @@
[project]
name = "ajal-todo-cli"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"typer>=0.13.0",
]

254
todo.py
View file

@ -1,148 +1,156 @@
#!/usr/bin/env python3
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
base_dir = os.path.abspath(os.path.dirname(__file__))
todo_file = os.path.join(base_dir, "todo.md")
import typer
cli = typer.Typer()
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)
# == Helper classes and functions ==
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 LeafTask:
completed: bool = False
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 RootTask(LeafTask):
tasks: dict[str, LeafTask] = field(default_factory=dict)
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)
def print_help():
print("""Usage: todo.py [add|complete|remove] TASK [PARENT]
todo.py list [PARENT]
Manages todo.md file as a ToDo file""")
def parse_todo() -> dict[str, dict]:
todo = {}
with open(todo_file, "r") as f:
lines = f.readlines()
def todo_parse(todo_file: Path) -> dict[str, RootTask]:
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
tasks = {}
for line in 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")
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")
return tasks
def render_todo(todo: dict[str, dict]) -> str:
def todo_render(tasks: dict[str, RootTask]) -> str:
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.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_todo(todo: dict[str, dict]):
with open(todo_file, "w") as f:
f.write(render_todo(todo))
def todo_write(todo_file: Path, todo: dict[str, RootTask]):
todo_file.write_text(todo_render(tasks=todo))
def main():
if len(sys.argv) < 2:
print_help()
exit(1)
# == CLI helpers ==
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)
def root_callback(ctx: typer.Context, root: str = None) -> str | None:
if root is None:
return root
if root not in ctx.obj["tasks"]:
typer.echo(f"Root task '{root}' does not exist", err=True)
raise typer.Exit(1)
else:
ctx.obj.update({
"tasks_root": ctx.obj["tasks"][root].tasks,
"tasks_class": LeafTask,
})
return root
@cli.callback()
def main_callback(
ctx: typer.Context,
todo_file: Path = lambda: base_dir / "todo.md",
verbose: bool = typer.Option(False, "--verbose", "-v", help="Set verbosity"),
):
ctx.obj = dict(ctx.params)
if not todo_file.exists():
typer.echo(f"--todo-file '{todo_file}' does not exist", err=True)
raise typer.Exit(1)
tasks = todo_parse(todo_file)
ctx.obj.update({
"tasks": tasks,
"tasks_root": tasks,
"task_class": RootTask,
})
# == CLI definitions ==
@cli.command(name="list")
def task_list(
ctx: typer.Context,
root: Optional[str] = typer.Option(None, "--root", "-r", help="Root task", callback=root_callback),
):
"""
List all tasks in todo.md
Returns all tasks if no root task is passed, otherwise only the root task and subtasks
"""
ctx.obj["verbose"] and typer.echo(f"Listing tasks in '{root if root else 'root'}'")
if root is None:
typer.echo(todo_render(ctx.obj["tasks"]))
else:
typer.echo(todo_render({root: ctx.obj["tasks"][root]}))
@cli.command(name="add")
def task_add(
ctx: typer.Context,
task: str = typer.Argument(..., help="Task name"),
root: Optional[str] = typer.Option(None, "--root", "-r", help="Root task", callback=root_callback),
):
"""
Add a task to todo.md
"""
ctx.obj["verbose"] and typer.echo(f"Adding task '{task}' to '{root if root else 'root'}'")
ctx.obj["tasks_root"][task] = ctx.obj["task_class"]()
todo_write(ctx.obj["todo_file"], ctx.obj["tasks"])
typer.echo(todo_render(ctx.obj["tasks"]))
@cli.command(name="complete")
def task_complete(
ctx: typer.Context,
task: str = typer.Argument(..., help="Task name"),
root: Optional[str] = typer.Option(None, "--root", "-r", help="Root task", callback=root_callback),
):
"""
Mark a task in todo.md as completed
"""
ctx.obj["verbose"] and typer.echo(f"Marking task '{task}' in '{root if root else 'root'}' as completed")
ctx.obj["tasks_root"][task].completed = True
todo_write(ctx.obj["todo_file"], ctx.obj["tasks"])
typer.echo(todo_render(ctx.obj["tasks"]))
@cli.command(name="remove")
def task_remove(
ctx: typer.Context,
task: str = typer.Argument(..., help="Task name"),
root: Optional[str] = typer.Option(None, "--root", "-r", help="Root task", callback=root_callback),
):
"""
Remove a task from todo.md
"""
ctx.obj["verbose"] and typer.echo(f"Removing task '{task}' from '{root if root else 'root'}'")
del ctx.obj["tasks_root"][task]
todo_write(ctx.obj["todo_file"], ctx.obj["tasks"])
typer.echo(todo_render(ctx.obj["tasks"]))
if __name__ == "__main__":
main()
cli()