#!/bin/python3 import re import logging from pathlib import Path from datetime import datetime from typing import Optional, Type from .TodoObject import TodoObject from .Category import Category from .Note import Note from .Task import Task logger = logging.getLogger("Todo") logger.addHandler(logging.StreamHandler()) class Todo(TodoObject): def __init__(self, data_file : Path = Path.home().joinpath("todo.md"), tab_spacing = 2): """Todo class, reads and parses a todo.md from a file. Default file is 'todo.md' in the home directory. Args: data_file (Path, optional): Location of the markdown file to load from. Defaults to ~/todo.md. tab_spacing (int, optional): How many levels a tab character should represent. Defaults to 2. """ self.data_file = data_file self.tab_spacing = tab_spacing super().__init__(-1, "File") self._load_data() def __str__(self) -> str: return f"File: {self.data_file.name}" def _get_level(self, line: str) -> int: """Calculates the level at which this element lies by parsing the given markdown line. Args: line (str): The string to parse. Returns: int: The level, starting at 0. """ count = 0 if line[0] == "-" or line[0] == " ": for char in line: if char == " ": count += 1 else: break return count // 2 elif line[0] == "#": for char in line: if char == "#": count += 1 else: break return count - 1 else: # catch-all for unlisted notes return 0 def _get_parent(self, level: int, obj: TodoObject, obj_type: Optional[Type[TodoObject]] = None): """Gets the parent for an object at level X. If an object with a lower level cannot be found within the obj, it will return the obj. Args: level (int): The level to find the parent for. obj (TodoObject): Object to grab parents from. obj_type (Type[TodoObject], optional): The type of the object the parent should be. Default is None. Returns: TodoObject: The object found. """ if level > 0: for child in obj.children[::-1]: if obj_type is not None and isinstance(child, obj_type) and child.level < level: return child if obj_type is None and child.level < level: return child return obj else: return obj def _parse_note(self, line: str, parent: TodoObject): """Parses a markdown line representing a note. Args: line (str): The line to parse. category (Category): The category this note belongs to. """ level = self._get_level(line) parent = self._get_parent(level, parent) if re.match(" * -", line): Note(level, line[level*2+2:], parent) else: Note(level, line[level*2:], parent, False) def _parse_task(self, line: str, parent: Category): """Parses the markdown line representing a task. Args: line (str): The line to parse. category (Category): The category this task belongs to. """ level = self._get_level(line) parent = self._get_parent(level, parent) if "[ ]" in line: complete = False elif "[x]" in line: complete = True line = line[level*2+6:] if "|" in line: line = line.replace(" |", "|") name = line.split("|")[0] date = datetime.strptime(line.split("|")[1], '%b %d %Y') else: name = line date = None Task(name, date, complete, level, parent) def _parse_category(self, line: str): """Parses the markdown line representing a category of todos. Args: line (str): The line to parse. """ level = self._get_level(line) parent = self._get_parent(level, self, obj_type=Category) line = line[level+2:] Category(line, level, parent) def _load_data(self): """Load categories and tasks from self.data_file.""" with self.data_file.open("r") as f: for line in f: line = line.replace("\t", self.tab_spacing*" ") line = line.rstrip() if len(line.strip()) == 0: # skip empty lines continue elif line[0] == "#": self._parse_category(line) elif re.match(" *- \\[[x ]\\]", line): self._parse_task(line, self.get_children(obj_type=Category)[-1]) else: self._parse_note(line, self.get_children(obj_type=Category)[-1]) def write_data(self): """Write the Todo data to the datafile.""" self.data_file.write_text(self.get_md())