Compare commits
13 commits
e6d4ba1e25
...
3d9a25d5a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d9a25d5a3 | |||
| c3f6b624d9 | |||
| 35035feba4 | |||
| 70bee705dd | |||
| d0f34de620 | |||
| 9ee5aa41fa | |||
| f8c26abeea | |||
| e4ca23901b | |||
| 0932faba34 | |||
| 9bb2e2f8f1 | |||
| 1a0461eded | |||
| 3021ab1cf0 | |||
| 950aa8920a |
8 changed files with 240 additions and 149 deletions
11
README.md
11
README.md
|
|
@ -1,3 +1,12 @@
|
|||
# Ajal Todo CLI
|
||||
|
||||
A todo utility
|
||||
A todo utility - enhanced
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
pip install ajal-todo-cli
|
||||
ajal-todo --help
|
||||
```
|
||||
|
||||
And move forward from there
|
||||
|
|
|
|||
4
ajal_todo_cli/__init__.py
Normal file
4
ajal_todo_cli/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .app import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
3
ajal_todo_cli/__main__.py
Normal file
3
ajal_todo_cli/__main__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .app import cli
|
||||
|
||||
cli()
|
||||
156
ajal_todo_cli/app.py
Executable file
156
ajal_todo_cli/app.py
Executable file
|
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/env python3
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
cli = typer.Typer()
|
||||
base_dir = Path(__file__).absolute().parent
|
||||
|
||||
|
||||
# == Helper classes and functions ==
|
||||
|
||||
|
||||
@dataclass
|
||||
class LeafTask:
|
||||
completed: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class RootTask(LeafTask):
|
||||
tasks: dict[str, LeafTask] = field(default_factory=dict)
|
||||
|
||||
|
||||
def todo_parse(todo_file: Path) -> dict[str, RootTask]:
|
||||
root_task = None
|
||||
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 todo_render(tasks: dict[str, RootTask]) -> str:
|
||||
lines = ["# A ToDo list", ""]
|
||||
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 todo_write(todo_file: Path, todo: dict[str, RootTask]):
|
||||
todo_file.write_text(todo_render(tasks=todo))
|
||||
|
||||
|
||||
# == CLI helpers ==
|
||||
|
||||
|
||||
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__":
|
||||
cli()
|
||||
18
breakitlive.sh
Normal file
18
breakitlive.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#doitlive speed: 3
|
||||
#doitlive prompt: sorin
|
||||
|
||||
ajal-todo add Test
|
||||
ajal-todo list -r Test
|
||||
ajal-todo list -r Test Test
|
||||
|
||||
ajal-todo add -r Test test1 test2
|
||||
ajal-todo add -r Test test1 test3
|
||||
ajal-todo complete -r Test test1 test3
|
||||
ajal-todo remove -r Test test1 test3
|
||||
|
||||
ajal-todo add Test
|
||||
ajal-todo list -r Test
|
||||
ajal-todo complete -r Test test1
|
||||
ajal-todo add -r Test test1
|
||||
ajal-todo remove -r Test test1
|
||||
ajal-todo remove -r Test test1
|
||||
32
doitlive.sh
Normal file
32
doitlive.sh
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#doitlive speed: 3
|
||||
#doitlive prompt: sorin
|
||||
|
||||
ajal-todo --help
|
||||
ajal-todo -h; echo $?
|
||||
ajal-todo asdf; echo $?
|
||||
|
||||
ajal-todo list --help
|
||||
|
||||
ajal-todo add --help
|
||||
ajal-todo add Test
|
||||
ajal-todo --verbose add Test
|
||||
ajal-todo list
|
||||
ajal-todo list --root Test
|
||||
ajal-todo add -r Test test
|
||||
ajal-todo list -r Test
|
||||
ajal-todo list
|
||||
|
||||
ajal-todo complete --help
|
||||
ajal-todo complete -r Test test
|
||||
ajal-todo --verbose complete -r Test test
|
||||
ajal-todo list -r Test
|
||||
ajal-todo list
|
||||
ajal-todo complete Test
|
||||
ajal-todo list
|
||||
|
||||
ajal-todo remove --help
|
||||
ajal-todo remove -r Test test
|
||||
ajal-todo list -r Test
|
||||
ajal-todo list
|
||||
ajal-todo --verbose remove Test
|
||||
ajal-todo list
|
||||
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[project]
|
||||
name = "ajal-todo-cli"
|
||||
version = "0.1.2"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"typer>=0.13.0",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Console",
|
||||
"Topic :: Education",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
ajal-todo = "ajal_todo_cli:cli"
|
||||
148
todo.py
148
todo.py
|
|
@ -1,148 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
base_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
todo_file = os.path.join(base_dir, "todo.md")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
|
||||
def render_todo(todo: dict[str, dict]) -> 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}")
|
||||
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 main():
|
||||
if len(sys.argv) < 2:
|
||||
print_help()
|
||||
exit(1)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue