support checkboxes
This commit is contained in:
		
							parent
							
								
									b0f81d7f86
								
							
						
					
					
						commit
						d0d0816d79
					
				
					 2 changed files with 62 additions and 20 deletions
				
			
		|  | @ -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 a new issue