support checkboxes
This commit is contained in:
parent
b0f81d7f86
commit
d0d0816d79
@ -10,9 +10,9 @@ A suite of tools for dealing with a directory of markdown files.
|
|||||||
- TODO: add config file
|
- TODO: add config file
|
||||||
- [ ] make file types configurable
|
- [ ] make file types configurable
|
||||||
- [ ] look in local dir, then XDG_HOME
|
- [ ] look in local dir, then XDG_HOME
|
||||||
- TODO: support checkboxes
|
- DONE: support checkboxes
|
||||||
- [ ] scan lists
|
- [x] scan lists
|
||||||
- [ ] percent complete
|
- [x] percent complete
|
||||||
- TODO: decide on more permanent name
|
- TODO: decide on more permanent name
|
||||||
- IDEA: recurring?
|
- IDEA: recurring?
|
||||||
- IDEA: track changes
|
- IDEA: track changes
|
||||||
|
@ -2,6 +2,7 @@ import click
|
|||||||
import pathlib
|
import pathlib
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
@ -42,6 +43,51 @@ def scan_contents(file: pathlib.Path) -> dict:
|
|||||||
return {"words": len(words), "todos": len(TODO_TODO_RE.findall(text))}
|
return {"words": len(words), "todos": len(TODO_TODO_RE.findall(text))}
|
||||||
|
|
||||||
|
|
||||||
|
def render_checkbox(done: bool):
|
||||||
|
return "☑" if done else "☐"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TodoItem:
|
||||||
|
file: str
|
||||||
|
status: str
|
||||||
|
description: str
|
||||||
|
tags: list[str]
|
||||||
|
style: str
|
||||||
|
subtasks: list[tuple[bool, str]]
|
||||||
|
|
||||||
|
def fields(self):
|
||||||
|
return [
|
||||||
|
"file",
|
||||||
|
"status",
|
||||||
|
"description",
|
||||||
|
"tags",
|
||||||
|
]
|
||||||
|
|
||||||
|
def to_row(self):
|
||||||
|
return [
|
||||||
|
self.file,
|
||||||
|
self.status + self.subtask_status(),
|
||||||
|
self.description + self.subtask_nested(),
|
||||||
|
" | ".join(self.tags),
|
||||||
|
]
|
||||||
|
|
||||||
|
def subtask_nested(self):
|
||||||
|
if not self.subtasks:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return "\n" + "\n".join(
|
||||||
|
f"- {render_checkbox(s[0])} {s[1]}" for s in self.subtasks
|
||||||
|
)
|
||||||
|
|
||||||
|
def subtask_status(self):
|
||||||
|
total = len(self.subtasks)
|
||||||
|
if not total:
|
||||||
|
return ""
|
||||||
|
done = sum(1 if st[0] else 0 for st in self.subtasks)
|
||||||
|
return f" {done}/{total}"
|
||||||
|
|
||||||
|
|
||||||
def pull_todos(file: pathlib.Path):
|
def pull_todos(file: pathlib.Path):
|
||||||
text = file.read_text().splitlines()
|
text = file.read_text().splitlines()
|
||||||
active_todo = None
|
active_todo = None
|
||||||
@ -61,22 +107,20 @@ def pull_todos(file: pathlib.Path):
|
|||||||
style = "#999999"
|
style = "#999999"
|
||||||
elif status == "IDEA":
|
elif status == "IDEA":
|
||||||
style = "blue"
|
style = "blue"
|
||||||
active_todo = {
|
active_todo = TodoItem(
|
||||||
"file": file.name,
|
file=file.name,
|
||||||
"status": status,
|
status=status,
|
||||||
"description": description,
|
description=description,
|
||||||
"tags": " | ".join(tag_strs),
|
tags=tag_strs,
|
||||||
"style": style,
|
style=style,
|
||||||
"subtasks": [],
|
subtasks=[],
|
||||||
}
|
)
|
||||||
elif active_todo:
|
elif active_todo:
|
||||||
# check for checkbox if we're nested inside a todo
|
# check for checkbox if we're nested inside a todo
|
||||||
checkbox = CHECKBOX_RE.match(line)
|
checkbox = CHECKBOX_RE.match(line)
|
||||||
if checkbox:
|
if checkbox:
|
||||||
checkbox_status, desc = checkbox.groups()
|
checkbox_status, desc = checkbox.groups()
|
||||||
active_todo["subtasks"].append(
|
active_todo.subtasks.append((checkbox_status == "x", desc))
|
||||||
{"status": checkbox_status, "description": desc}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
yield active_todo
|
yield active_todo
|
||||||
active_todo = None
|
active_todo = None
|
||||||
@ -96,19 +140,17 @@ def human_readable_date(dt: datetime.datetime) -> str:
|
|||||||
return f"{int(delta.total_seconds() / 3600 / 24)}d ago"
|
return f"{int(delta.total_seconds() / 3600 / 24)}d ago"
|
||||||
|
|
||||||
|
|
||||||
def lod_table(data: list[dict]) -> Table | str:
|
def lod_table(data: list[TodoItem]) -> Table | str:
|
||||||
"""list of dicts to Table"""
|
"""list of dicts to Table"""
|
||||||
if not data:
|
if not data:
|
||||||
return "no results"
|
return "no results"
|
||||||
|
|
||||||
table = Table()
|
table = Table()
|
||||||
for key in data[0].keys():
|
for key in data[0].fields():
|
||||||
if key != "style":
|
table.add_column(key)
|
||||||
table.add_column(key)
|
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
style = row.pop("style", None)
|
table.add_row(*row.to_row(), style=row.style)
|
||||||
table.add_row(*(str(x) for x in row.values()), style=style)
|
|
||||||
|
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user