diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 7232a825..d6d478c9 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -10,4 +10,3 @@ liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: ['https://p.paytm.me/xCTH/dehy6dea'] diff --git a/.gitignore b/.gitignore index c2f5545c..9076f4c8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ test.py todos +# VSCODE +.vscode/ + # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..45828e62 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 1.0 [Unreleased] + +### Added + +- **A whole new API to interact with dooit** (closes https://github.com/kraanzu/dooit/issues/23) +- **Unlimited brancing of todos and workspaces** (closes https://github.com/kraanzu/dooit/issues/85 and https://github.com/kraanzu/dooit/issues/70 and https://github.com/kraanzu/dooit/issues/58) +- **Smart node addition on ** + - Dooit now will not add an extra item automatically if you are just editing description of some node +- **Support for edit and addition in SEARCH mode** + - Previously users were only allowed to jump to the todo +- **Synchronisation between multiple instances** + - Previously dooit printed a warning and exited with a message indicating that it's already running (closes https://github.com/kraanzu/dooit/issues/49) +- **New parameters for todos!** + - **Recurrence** ==> Adds recurrence to todos for regular reminder! + - **Effort** ==> An Integer Value to determine the effort/time it will take to complete the todo (for https://github.com/kraanzu/dooit/issues/74) + - **Tags** ==> Add tags to todos + - **Time** ==> Previously ony date was supported (closes https://github.com/kraanzu/dooit/issues/66) +- **Better date&time parsing** + - Dooit now uses [dateparser](https://pypi.org/project/dateparser/) module to make it a lot easier to add and edit date&time (for https://github.com/kraanzu/dooit/issues/71) +- **Custiomizable Bar** + - Dooit now has a bar that can be customized to your liking! + - You can now use python scripts to display your desired content in the bar + - You can also use dooit API to display info in status bars +- A better config parser which handles ovveriden case better! (closes https://github.com/kraanzu/dooit/issues/64) + +### Fixed + - Update textual which might fix several issues! (maybe closes https://github.com/kraanzu/dooit/issues/57 and https://github.com/kraanzu/dooit/issues/57) + - Using '/' in todos crashes app (closes https://github.com/kraanzu/dooit/issues/84) + + +### Changed + - Remove mouse support...coz why not? (closes https://github.com/kraanzu/dooit/issues/69) + - Some keybindings + - Unicode by default (remove nerd font icons from default config) + - match-case stmts (Adds support for python>=3.7) + diff --git a/README.md b/README.md index e25b9fd0..4f56fca4 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,14 @@ to make sure that you complete your tasks on time ;) # Installation 🔨 -Dooit can be installed with either Pip or Homebrew. +Dooit can be installed with various package managers! ### With Pip 🐍 You can install dooit easily using a python one-liner: ```bash -python3 -m pip install git+https://github.com/kraanzu/dooit.git -``` - -Or the long way: - -```bash -git clone https://github.com/kraanzu/dooit.git -cd dooit -pip3 install . +pip install dooit ``` ### With AUR helper 📦 @@ -34,7 +26,6 @@ pip3 install . yay -S dooit-git ``` - ### With Homebrew 🍻 You can install the latest stable version of dooit with [Homebrew](https://brew.sh): @@ -49,35 +40,30 @@ Alternatively, you can install the most recent development version of dooit: brew install dooit --HEAD ``` -## Additional Notes 📝 - -Simply type `dooit` in your terminal to launch it. ezy pzy. - -> ⚠️ Note: The config file for `dooit` is located at your $XDG_CONFIG_HOME (or ~/.config/dooit) - -> ⚠️ Note: The default icons used in the application are a part of [nerd fonts](https://www.nerdfonts.com/) \ -              and can be customised by changing the config file - -> ⚠️ Note: you must use python version >=3.10 - # Features 🌟 > Some features that dooit comes with: - An interactive & beautiful UI -- Configurable icons and themes -- Both Mouse and Keyboard support (Vim like keybindings) -- Topicwise seperated Todo Lists (With branching) -- Editable Todo's about, date and urgency +- An API automate stuff with python scripts and integrate with other apps (!plugin support in pipeline!) +- Configurable icons, themes and bar! +- Vim like keybindings +- Topicwise separated Todo Lists (With branching) - Nested todos! +- Support for recurrence todos - Sort options with menu (Name, Date, Urgency, Status) -- Search & jump-to-todo mode on the fly! +- Search & edit on the fly! + +**Note: See [CHANGELOG.md]() to get more details on changes and feature additions!** -> See Demo Video below in order to get a visual :) +# Usage and configuration :gear: +After launching the app, You can press the `?` key to get started with the app :)\ +You can also tweak everything including the UI, keybindings and status bar to your liking\ +Head over to [wiki](https://github.com/kraanzu/dooit/wiki/Configuration) to know more! -# Demo 🎥 -https://user-images.githubusercontent.com/97718086/174479591-5fe4f425-c9f3-4db2-969c-df8aa400e103.mp4 +# Screenshots 🖼️ +![Screenshot](https://user-images.githubusercontent.com/97718086/221467485-fae198f7-51b1-4a71-91d9-88b51897aeeb.png) # Contribution 🤝 - Want to contribute? Feel free to open a PR! 😸 @@ -89,4 +75,3 @@ https://user-images.githubusercontent.com/97718086/174479591-5fe4f425-c9f3-4db2- If you liked dooit then you might wanna try out some of my other TUI projects as well - [termtyper](https://github.com/kraanzu/termtyper) - A typing-test app for terminal - [gupshup](https://github.com/kraanzu/gupshup) - A localhost TUI chat client - diff --git a/dooit/__init__.py b/dooit/__init__.py index 68b63945..8fec625e 100644 --- a/dooit/__init__.py +++ b/dooit/__init__.py @@ -1,18 +1,6 @@ -import pkg_resources import argparse -import psutil -from .ui.tui import Doit - - -def is_running() -> bool: - counter = 0 - try: - for process in psutil.process_iter(): - counter += process.name() in ["dooit", "dooit.exe"] - except psutil.NoSuchProcess: - pass - - return counter > 1 +from importlib.metadata import version +from .ui.tui import Dooit def main(): @@ -21,10 +9,7 @@ def main(): args = parser.parse_args() if args.version: - ver = pkg_resources.get_distribution("dooit").version + ver = version("dooit") print(f"dooit - {ver}") else: - if is_running(): - exit(print("One instance of dooit is already running!\nQuiting...")) - - Doit.run() + Dooit().run() diff --git a/dooit/api/__init__.py b/dooit/api/__init__.py new file mode 100644 index 00000000..89b59068 --- /dev/null +++ b/dooit/api/__init__.py @@ -0,0 +1,6 @@ +from .model import Model, T +from .manager import Manager, manager +from .todo import Todo +from .workspace import Workspace + +__all__ = ["Model", "Manager", "Todo", "Workspace", "T", "manager"] diff --git a/dooit/api/manager.py b/dooit/api/manager.py new file mode 100644 index 00000000..2b3b3444 --- /dev/null +++ b/dooit/api/manager.py @@ -0,0 +1,83 @@ +from time import time +from typing import Any, Dict, Optional +from .model import Model +from ..utils import Parser +from ..api.workspace import Workspace + +WORKSPACE = "workspace" +parser = Parser() + + +class Manager(Model): + """ + Manager top class that manages basically + """ + + _lock = 0 + fields = [] + nomenclature: str = "Workspace" + last_modified = 0 + + def lock(self) -> None: + self._lock += 1 + + def unlock(self) -> None: + self._lock -= 1 + + def is_locked(self) -> bool: + return self._lock != 0 + + def __init__(self, parent: Optional["Model"] = None) -> None: + super().__init__(parent) + + def add_workspace(self) -> Workspace: + return self.add_child(WORKSPACE) + + def _get_commit_data(self): + return { + getattr(child, "description"): child.commit() for child in self.workspaces + } + + def commit(self) -> None: + if self.is_locked(): + return + + self.lock() + self.last_modified = time() + parser.save(self._get_commit_data()) + self.unlock() + + def setup(self, data: Optional[Dict] = None) -> None: + if self.is_locked(): + return + + data = data or parser.load() + if not data: + return + + self.workspaces.clear() + self.todos.clear() + self.last_modified = parser.last_modified + self.from_data(data) + + def from_data(self, data: Any) -> None: + for i, j in data.items(): + child = self.add_child(WORKSPACE, len(self.workspaces)) + child.edit("description", i) + child.from_data(j) + + def refresh_data(self) -> bool: + + if abs(self.last_modified - parser.last_modified) <= 2: + return False + + if self.last_modified > parser.last_modified: + self.commit() + return False + + self.setup() + return True + + +manager = Manager() +manager.setup() diff --git a/dooit/api/model.py b/dooit/api/model.py new file mode 100644 index 00000000..849678c2 --- /dev/null +++ b/dooit/api/model.py @@ -0,0 +1,267 @@ +from typing import Any, Dict, List, Optional, TypeVar +from uuid import uuid4 +from dataclasses import dataclass +from rich.text import Text + +T = TypeVar("T", bound="Model") + + +@dataclass +class Result: + """ + Response class to return result of an operation + """ + + ok: bool + cancel_op: bool + message: Optional[str] = None + color: str = "white" + + @classmethod + def Ok(cls, message: Optional[str] = None): + return cls(True, False, message, "green") + + @classmethod + def Warn(cls, message: Optional[str] = None): + return cls(False, False, message, "yellow") + + @classmethod + def Err(cls, message: str): + return cls(False, True, message, "red") + + def is_ok(self) -> bool: + return self.ok + + def is_err(self) -> bool: + return not self.ok + + def text(self): + def colored(a, b): + return f"[{b}]{a}[/{b}]" + + if self.message: + return colored(self.message, self.color) + + return Text() + + +Ok = Result.Ok +Err = Result.Err +Warn = Result.Warn + + +class Model: + """ + Model class to for the base tree structure + """ + + fields: List + sortable_fields: List + + def __init__( + self, + parent: Optional["Model"] = None, + ) -> None: + from dooit.api.workspace import Workspace + from dooit.api.todo import Todo + + self.name = str(uuid4()) + self.parent = parent + + self.workspaces: List[Workspace] = [] + self.todos: List[Todo] = [] + + @property + def kind(self): + return self.__class__.__name__.lower() + + @property + def path(self): + """ + Uniquie path for model + """ + + return "$" + + def _get_children(self, kind: str) -> List: + """ + Get children list (workspace/todo) + """ + if kind not in ["workspace", "todo"]: + raise TypeError(f"Cannot perform this operation for type {kind}") + + return self.workspaces if kind.lower() == "workspace" else self.todos + + def _get_child_index(self, kind: str, **kwargs) -> int: + """ + Get child index by attr + """ + + key, value = list(kwargs.items())[0] + for i, j in enumerate(self._get_children(kind)): + if getattr(j, key) == value: + return i + + return -1 + + def _get_index(self, kind: str) -> int: + """ + Get items's index among it's siblings + """ + + if not self.parent: + return -1 + + return self.parent._get_child_index(kind, name=self.name) + + def edit(self, key: str, value: str) -> Result: + """ + Edit item's attrs + """ + + var = f"_{key}" + if hasattr(self, var): + return getattr(self, var).set(value) + else: + return Err("Invalid Request!") + + def shift_up(self) -> None: + """ + Shift the item one place up among its siblings + """ + + idx = self._get_index(self.kind) + + if idx in [0, -1]: + return + + if not self.parent: + return + arr = self.parent._get_children(self.kind) + arr[idx], arr[idx - 1] = arr[idx - 1], arr[idx] + + def shift_down(self) -> None: + """ + Shift the item one place down among its siblings + """ + + idx = self._get_index(self.kind) + + if idx == -1 or not self.parent: + return + + arr = self.parent._get_children(self.kind) + if idx == len(arr) - 1: + return + + arr[idx], arr[idx + 1] = arr[idx + 1], arr[idx] + + def prev_sibling(self: T) -> Optional[T]: + """ + Returns previous sibling item, if any, else None + """ + + if not self.parent: + return + + idx = self.parent._get_child_index(self.kind, name=self.name) + + if idx: + return self._get_children(self.kind)[idx - 1] + + def next_sibling(self: T) -> Optional[T]: + """ + Returns next sibling item, if any, else None + """ + + if not self.parent: + return + + idx = self.parent._get_child_index(self.kind, name=self.name) + arr = self.parent._get_children(self.kind) + + if idx < len(arr) - 1: + return arr[idx + 1] + + def add_sibling(self: T, inherit: bool = False) -> T: + """ + Add item sibling + """ + + if self.parent: + idx = self.parent._get_child_index(self.kind, name=self.name) + return self.parent.add_child(self.kind, idx + 1, inherit) + else: + raise TypeError("Cannot add sibling") + + def add_child(self, kind: str, index: int = 0, inherit: bool = False) -> Any: + """ + Adds a child to specified index (Defaults to first position) + """ + from ..api.workspace import Workspace + from ..api.todo import Todo + + if kind == "workspace": + child = Workspace(parent=self) + else: + child = Todo(parent=self) + if inherit and isinstance(self, Todo): + child.fill_from_data(self.to_data()) + child._description.value = "" + child._effort._value = 0 + child._tags.value = "" + child.edit("status", "PENDING") + + children = self._get_children(kind) + children.insert(index, child) + + return child + + def remove_child(self, kind: str, name: str) -> Any: + """ + Remove the child based on attr + """ + + idx = self._get_child_index(kind, name=name) + if idx != -1: + return self._get_children(kind).pop(idx) + + def drop(self) -> None: + """ + Delete the item + """ + + if self.parent: + self.parent.remove_child(self.kind, self.name) + + def sort(self, attr: str) -> None: + """ + Sort the children based on specific attr + """ + + if self.parent: + children = self.parent._get_children(self.kind) + children.sort(key=lambda x: getattr(x, f"_{attr}").get_sortable()) + + def commit(self) -> Dict[str, Any]: + """ + Get a object summary that can be stored + """ + + return { + getattr( + child, + "descrption", + ): child.commit() + for child in self.workspaces + } + + def from_data(self, data: Dict[str, Any]) -> None: + """ + Fill in the attrs from data provided + """ + + for i, j in data.items(): + self.add_child("workspace") + self.workspaces[-1].edit("descrption", i) + self.workspaces[-1].from_data(j) diff --git a/dooit/api/model_items.py b/dooit/api/model_items.py new file mode 100644 index 00000000..c3717176 --- /dev/null +++ b/dooit/api/model_items.py @@ -0,0 +1,409 @@ +import re +from os import environ +from typing import Any, Tuple +from datetime import datetime, timedelta +from dooit.utils.dateparser import parse +from .model import Result, Ok, Warn, Err + +DATE_ORDER = environ.get("DOOIT_DATE_ORDER", "DMY") +DATE_FORMAT = ( + "-".join(list(DATE_ORDER)).replace("D", "%d").replace("M", "%m").replace("Y", "%y") +) +TIME_FORMAT = "@%H:%M" +DURATION_LEGEND = { + "m": "minute", + "h": "hour", + "d": "day", + "w": "week", +} +OPTS = { + "PENDING": "x", + "COMPLETED": "X", + "OVERDUE": "O", +} +CASUAL_FORMAT = "%d %h @ %H:%M" + + +def split_duration(duration: str) -> Tuple[str, str]: + if re.match(r"^(\d+)[mhdw]$", duration): + return duration[-1], duration[:-1] + else: + return tuple() + + +class Item: + """ + A workspace/todo item/param + """ + + value: Any + + def __init__(self, model: Any) -> None: + self.model = model + self.model_kind = model.__class__.__name__.lower() + + def set(self, val: str) -> Result: + """ + Set the value after validation + """ + raise NotImplementedError + + def get_sortable(self) -> Any: + """ + Returns a value for item for sorting + """ + raise NotImplementedError + + def to_txt(self) -> str: + """ + Convert to storable format + """ + raise NotImplementedError + + def from_txt(self, txt: str) -> None: + """ + Parse from stored todo string + """ + raise NotImplementedError + + +class Status(Item): + pending = True + + @property + def value(self): + self.handle_recurrence() + + if not self.pending: + return "COMPLETED" + + due = self.model._due._value + if not due or due == "none": # why? dateparser slowpok + return "PENDING" + + if due.hour or due.minute: + now = parse("now") + if due < now: + return "OVERDUE" + else: + today = parse("today").date() + due = due.date() + if today > due: + return "OVERDUE" + + return "PENDING" + + def toggle_done(self) -> bool: + self.pending = not self.pending + self.update_others() + return not self.pending + + def handle_recurrence(self): + if not self.model.recurrence: + return + + if self.pending: + return + + due = self.model._due._value + if not due or due == "none": + return + + sign, frequency = split_duration(self.model._recurrence.value) + frequency = int(frequency) + time_to_add = timedelta(**{f"{DURATION_LEGEND[sign]}s": frequency}) + new_time = due + time_to_add + + if new_time >= datetime.now(): + return + + self.model.edit("due", new_time.strftime(DATE_FORMAT)) + self.pending = True + + def set(self, val: Any) -> Result: + self.pending = val != "COMPLETED" + self.update_others() + return Ok() + + def update_others(self): + + # Update ancestors + current = self.model + while parent := current.parent: + if hasattr(parent, "status"): + if parent.todos: + is_done = all(i.status == "COMPLETED" for i in parent.todos) + parent._status.pending = not is_done + current = parent + else: + break + + # Update children + def update_children(todo=self.model, status=self.pending): + + todo._status.pending = status + for i in todo.todos: + update_children(i, status) + + update_children() + + def to_txt(self) -> str: + return "X" if self.value == "COMPLETED" else "O" + + def from_txt(self, txt: str) -> None: + status = txt.split()[0] + if status == "X": + self.set("COMPLETED") + else: + self.set("PENDING") + + def get_sortable(self) -> Any: + if self.value == "OVERDUE": + return 1 + elif self.value == "PENDING": + return 2 + else: + return 3 + + +class Description(Item): + value = "" + + def clean(self, s: str): + for i, j in enumerate(s): + if j not in "@+%": # left striping as this messes up with other attrs + return s[i:] + + return s + + def set(self, value: Any) -> Result: + value = self.clean(value) + if value: + new_index = -1 + if self.model: + new_index = self.model.parent._get_child_index( + self.model_kind, description=value + ) + + old_index = self.model._get_index(self.model_kind) + + if new_index != -1 and new_index != old_index: + return Err( + f"A {self.model_kind} with same description is already present" + ) + else: + self.value = value + return Ok() + + return Err("Can't leave description empty!") + + def to_txt(self) -> str: + return self.value + + def from_txt(self, txt: str) -> None: + value = "" + for i in txt.split()[3:]: + if i[0].isalpha(): + value = value + i + " " + + self.value = value.strip() + + def get_sortable(self) -> Any: + return self.value + + +class Due(Item): + _value = None + + @property + def value(self): + if not self._value: + return "none" + + time = self._value.time() + if time.hour == time.minute == 0: + return self._value.strftime("%d %h") + else: + return self._value.strftime("%d %h %H:%M") + + def set(self, val: str) -> Result: + val = val.strip() + + if not val or val == "none": + self._value = None + return Ok("Due removed for the todo") + + if val.strip() == "today": + val = "today 0:0" # remove un-necessary time + + res = parse(val) + if res: + self._value = res + return Ok(f"Due date changed to [b cyan]{self.value}[/b cyan]") + + return Warn("Cannot parse the string!") + + def to_txt(self) -> str: + if self._value: + t = self._value.time() + if t.hour == t.minute == 0: + save = self._value.strftime(DATE_FORMAT) + else: + save = self._value.strftime(DATE_FORMAT + TIME_FORMAT) + else: + save = "none" + return f"due:{save}" + + def from_txt(self, txt: str) -> None: + value = txt.split()[2].lstrip("due:").lower() + if value != "none": + if "@" in value: + self._value = datetime.strptime(value, DATE_FORMAT + TIME_FORMAT) + else: + self._value = datetime.strptime(value, DATE_FORMAT) + + def get_sortable(self) -> Any: + return self._value or datetime.max + + +class Urgency(Item): + value = 1 + + def increase(self) -> Result: + return self.set(self.value + 1) + + def decrease(self) -> Result: + return self.set(self.value - 1) + + def set(self, val: Any) -> Result: + + val = int(val) + if val < 1: + return Warn("Urgency cannot be decreased further!") + if val > 4: + return Warn("Urgency cannot be increased further!") + + self.value = val + return Ok() + + def to_txt(self) -> str: + return f"({self.value})" + + def from_txt(self, txt: str) -> None: + self.set(txt.split()[1][1]) + + def get_sortable(self) -> Any: + return -self.value + + +class Tags(Item): + value = "" + + def set(self, val: str) -> Result: + tags = [i.strip() for i in val.split(",") if i] + self.value = ",".join(set(tags)) + return Ok() + + def add_tag(self, tag: str): + return self.set(self.value + "," + tag) + + def to_txt(self): + return " ".join(f"@{i}" for i in self.value.split(",") if i) + + def from_txt(self, txt: str) -> None: + flag = True + for i in txt.split()[3:]: + if i[0] == "@": + self.add_tag(i[1:]) + flag = False + else: + if not flag: + break + + +class Recurrence(Item): + value = "" + + def set(self, val: str) -> Result: + if not val: + self.value = "" + return Ok("Recurrence removed") + + res = split_duration(val.strip()) + if not res: + return Warn("Cannot parse! Please use format: [b][/b]") + + self.value = val + if self.model.due == "none": + if val[-1] in "dw": + self.model.edit("due", "today") + else: + self.model.edit("due", "now") + + return Ok(f"Recurrence set for {self.value} [i]starting today[/i]") + + return Ok(f"Recurrence set for {self.value}") + + def to_txt(self) -> str: + if self.value: + return f"%{self.value}" + + return "" + + def from_txt(self, txt: str) -> None: + for i in txt.split(): + if i[0] == "%": + self.value = i[1:] + break + + def get_sortable(self) -> Any: + if not self.value: + return timedelta.max + else: + frequency, value = split_duration(self.value) + return timedelta(**{DURATION_LEGEND[frequency] + "s": int(value)}) + + +class Effort(Item): + _value = 0 + + @property + def value(self): + if self._value: + return str(self._value) + + return "" + + def set(self, val: str) -> Result: + if not val: + self._value = "" + return Ok("Effort removed for the todo") + + if not val.isnumeric(): + return Warn("Only numeric values allowed") + + val_int = int(val) + if val_int >= 0: + self._value = val_int + return Ok() + + return Warn("Cannot decrease effort below zero") + + def get_sortable(self) -> Any: + if self._value: + return self._value + else: + # NOTE: If someone opens an issue for this... + # my ans: if its above 100 then probably the other tasks require low effort + return 10**2 + + def to_txt(self) -> str: + if self.value: + return f"+{self.value}" + else: + return "" + + def from_txt(self, txt: str) -> None: + for i in txt.split()[3:]: + if i[0] == "+": + self.set(i[1:]) diff --git a/dooit/api/todo.py b/dooit/api/todo.py new file mode 100644 index 00000000..113cd6df --- /dev/null +++ b/dooit/api/todo.py @@ -0,0 +1,148 @@ +from typing import Any, List, Optional, TypeVar +from .model import Model, Result + + +TODO = "todo" +OPTS = { + "PENDING": "x", + "COMPLETED": "X", + "OVERDUE": "O", +} +T = TypeVar("T", bound="Model") + + +def reversed_dict(d): + return {j: i for i, j in d.items()} + + +class Todo(Model): + fields = ["description", "due", "urgency", "effort", "tags", "status", "recurrence"] + sortable_fields = [ + "description", + "due", + "urgency", + "effort", + "status", + "recurrence", + ] + + def __init__(self, parent: Optional[T] = None) -> None: + from .model_items import ( + Status, + Due, + Urgency, + Recurrence, + Tags, + Description, + Effort, + ) + + super().__init__(parent) + self._status = Status(self) + self._description = Description(self) + self._urgency = Urgency(self) + self._effort = Effort(self) + self._tags = Tags(self) + self._recurrence = Recurrence(self) + self._due = Due(self) + self.todos: List[Todo] = [] + + @property + def path(self): + parent_path = self.parent.path if self.parent else "" + return self.description + "/" + parent_path + + @property + def effort(self): + return self._effort.value + + @property + def urgency(self): + return self._urgency.value + + @property + def description(self): + return self._description.value + + @property + def recurrence(self): + return self._recurrence.value + + @property + def due(self): + return self._due.value + + @property + def status(self): + return self._status.value + + @property + def tags(self): + return self._tags.value + + def add_child( + self, kind: str = "todo", index: int = 0, inherit: bool = False + ) -> Any: + if kind != "todo": + raise TypeError(f"Cannot add child of kind {kind}") + + return super().add_child(kind, index, inherit) + + def add_todo(self, index: int = 0, inherit: bool = False): + return self.add_child(TODO, index, inherit) + + def edit(self, key: str, value: str) -> Result: + res = super().edit(key, value) + self._status.update_others() + return res + + def toggle_complete(self) -> bool: + return self._status.toggle_done() + + def decrease_urgency(self) -> None: + self._urgency.decrease() + + def increase_urgency(self) -> None: + self._urgency.increase() + + def to_data(self) -> str: + """ + Return todo.txt format of the todo + """ + + status = self._status.to_txt() + urgency = self._urgency.to_txt() + due = self._due.to_txt() + effort = self._effort.to_txt() + tags = self._tags.to_txt() + recur = self._recurrence.to_txt() + description = self._description.to_txt() + + arr = [status, urgency, due, effort, tags, recur, description] + arr = [i for i in arr if i] + return " ".join(arr) + + def fill_from_data(self, data: str) -> None: + self._status.from_txt(data) + self._urgency.from_txt(data) + self._due.from_txt(data) + self._description.from_txt(data) + self._recurrence.from_txt(data) + self._effort.from_txt(data) + self._tags.from_txt(data) + + def commit(self) -> List[Any]: + if self.todos: + return [ + self.to_data(), + [child.commit() for child in self.todos], + ] + else: + return [self.to_data()] + + def from_data(self, data: List) -> None: + self.fill_from_data(data[0]) + if len(data) > 1: + for i in data[1]: + child_todo = self.add_child(kind="todo", index=len(self.todos)) + child_todo.from_data(i) diff --git a/dooit/api/workspace.py b/dooit/api/workspace.py new file mode 100644 index 00000000..c9b84ef2 --- /dev/null +++ b/dooit/api/workspace.py @@ -0,0 +1,63 @@ +from typing import Any, Dict, Optional +from ..api.todo import Todo +from .model import Model + +WORKSPACE = "workspace" +TODO = "todo" + + +class Workspace(Model): + fields = ["description"] + sortable_fields = ["description"] + + def __init__(self, parent: Optional["Model"] = None) -> None: + from .model_items import Description + + super().__init__(parent) + self._description = Description(self) + + @property + def path(self): + parent_path = self.parent.path if self.parent else "" + return self.description + "#" + parent_path + + @property + def description(self): + return self._description.value + + def add_workspace(self, index: int = 0): + return super().add_child(WORKSPACE, index) + + def add_todo(self, index: int = 0) -> Todo: + return super().add_child(TODO, index) + + def commit(self) -> Dict[str, Any]: + child_workspaces = { + workspace.description: workspace.commit() + for workspace in self.workspaces + if workspace.description + } + + todos = {"common": [todo.commit() for todo in self.todos if todo.description]} + + return { + **todos, + **child_workspaces, + } + + def from_data(self, data: Any) -> None: + if isinstance(data, dict): + for i, j in data.items(): + if i == "common": + for data in j: + todo = self.add_todo(index=len(self.todos)) + todo.from_data(data) + continue + + workspace = self.add_child("workspace", index=len(self.workspaces)) + workspace.edit("description", i) + workspace.from_data(j) + + elif isinstance(data, list): + todo = self.add_todo(index=len(self.todos)) + todo.from_data(data) diff --git a/dooit/ui/css/screen.py b/dooit/ui/css/screen.py new file mode 100644 index 00000000..a3e1baf2 --- /dev/null +++ b/dooit/ui/css/screen.py @@ -0,0 +1,28 @@ +""" +Main CSS File for App +""" + +from dooit.utils import Config + + +screen_CSS = f""" +Screen {{ + background: {Config().get("BACKGROUND")}; + layout: grid; + grid-size: 2 2; + grid-columns: 2fr 8fr; + grid-rows: 1fr 1; +}} + +StatusBar {{ + column-span: 2; +}} + +Vertical {{ + height: 100%; + width: 100%; + column-span: 2; + row-span: 2; + scrollbar-size: 1 1; +}} +""" diff --git a/dooit/ui/events/__init__.py b/dooit/ui/events/__init__.py index e777b57d..8fbe0614 100644 --- a/dooit/ui/events/__init__.py +++ b/dooit/ui/events/__init__.py @@ -1,27 +1,21 @@ from .events import ( - MenuOptionChange, ChangeStatus, Notify, - ModifyTopic, ApplySortMethod, - HighlightNode, StatusType, SortMethodType, - ListItemSelected, + TopicSelect, SwitchTab, - RemoveTopic, + SpawnHelp, ) __all__ = [ - "MenuOptionChange", + "SpawnHelp", "ChangeStatus", "Notify", - "ModifyTopic", "ApplySortMethod", - "HighlightNode", "StatusType", "SortMethodType", - "ListItemSelected", + "TopicSelect", "SwitchTab", - "RemoveTopic", ] diff --git a/dooit/ui/events/events.py b/dooit/ui/events/events.py index 8590797b..a43bc43f 100644 --- a/dooit/ui/events/events.py +++ b/dooit/ui/events/events.py @@ -1,30 +1,31 @@ from typing import Literal -from textual.widgets import NodeID +from rich.text import TextType, Text from textual.message import Message, MessageTarget -StatusType = Literal["NORMAL", "INSERT", "DATE", "SEARCH", "SORT"] -SortMethodType = Literal["name", "status", "date", "urgency"] +StatusType = Literal["NORMAL", "INSERT", "DATE", "SEARCH", "SORT", "K PENDING"] +SortMethodType = Literal["description", "status", "date", "urgency"] -class MenuOptionChange(Message, bubble=True): +class SwitchTab(Message, bubble=True): """ - Emitted when user moves through nav menu + Emitted when user needs to focus other pane """ - def __init__(self, sender: MessageTarget, option: str) -> None: - super().__init__(sender) - self.option = option + +class SpawnHelp(Message, bubble=True): + """ + Emitted when user presses `?` in NORMAL mode + """ class ChangeStatus(Message, bubble=True): """ Emitted when there is a change in the `status` - See: StatusType """ def __init__(self, sender: MessageTarget, status: StatusType) -> None: super().__init__(sender) - self.status = status + self.status: StatusType = status class Notify(Message, bubble=True): @@ -32,66 +33,28 @@ class Notify(Message, bubble=True): Emitted when A notification message on status bar is to be shown """ - def __init__(self, sender: MessageTarget, message: str) -> None: + def __init__(self, sender: MessageTarget, message: TextType) -> None: super().__init__(sender) + if isinstance(message, Text): + message = message.markup self.message = message -class ModifyTopic(Message, bubble=True): - """ - Emitted when a nav top is renamed - """ - - def __init__(self, sender: MessageTarget, old: str, new: str) -> None: - super().__init__(sender) - self.old = old - self.new = new - - class ApplySortMethod(Message, bubble=True): """ Emitted when the user selects a sort method from sort-menu """ - def __init__(self, sender: MessageTarget, method: SortMethodType) -> None: + def __init__(self, sender: MessageTarget, method: str) -> None: super().__init__(sender) self.method = method -class HighlightNode(Message, bubble=True): - """ - Emitted when the user selects a todo from search list - """ - - def __init__(self, sender: MessageTarget, id: NodeID) -> None: - super().__init__(sender) - self.id = id - - -class ListItemSelected(Message, bubble=True): +class TopicSelect(Message, bubble=True): """ Emitted when the user selects a todo from search list """ - def __init__(self, sender: MessageTarget, selected: str, focus: bool) -> None: - super().__init__(sender) - self.selected = selected - self.focus = focus - - -class SwitchTab(Message, bubble=True): - """ - Emitted when user presses `Esc` while in normal mode in todo - """ - - pass - - -class RemoveTopic(Message, bubble=True): - """ - Emitted when user presses `Esc` while in normal mode in todo - """ - - def __init__(self, sender: MessageTarget, selected: str) -> None: + def __init__(self, sender: MessageTarget, item) -> None: super().__init__(sender) - self.selected = selected + self.item = item diff --git a/dooit/ui/formatters/__init__.py b/dooit/ui/formatters/__init__.py new file mode 100644 index 00000000..91fa97ec --- /dev/null +++ b/dooit/ui/formatters/__init__.py @@ -0,0 +1,4 @@ +from .todo_tree_formatter import TodoFormatter, Formatter +from .workspace_tree_formatter import WorkspaceFormatter + +__all__ = ["TodoFormatter", "WorkspaceFormatter", "Formatter"] diff --git a/dooit/ui/formatters/formatter.py b/dooit/ui/formatters/formatter.py new file mode 100644 index 00000000..b691e8d5 --- /dev/null +++ b/dooit/ui/formatters/formatter.py @@ -0,0 +1,45 @@ +from typing import Type, Dict +from rich.text import Text +from dooit.api import Model + + +class Formatter: + model_type: Type[Model] = Model + + def __init__( + self, + format: Dict[str, str], + ) -> None: + self.format = format + self.STYLE_DIM = format["dim"] + self.STYLE_HIGHLIGHT = format["highlight"] + self.STYLE_EDITING = format["editing"] + + def cursor_highlight(self, text: str, is_highlighted: bool, editing: str): + if is_highlighted: + return ( + self.colored(text, "b " + self.STYLE_EDITING) + if editing != "none" + else self.colored(text, "b " + self.STYLE_HIGHLIGHT) + ) + + return self.colored(text, "d " + self.STYLE_DIM) + + def color_combo(self, icon: str, text: str, color: str): + return self.colored(f"[b] {icon}[/b]{text}", color) + + def colored(self, text: str, color: str): + return f"[{color}]{text}[/{color}]" + + def style( + self, + column: str, # column name + item: model_type, # workspace obj + is_highlighted: bool, + editing: str, + kwargs: Dict[str, str], # display items, + ) -> Text: + func_name = f"style_{column}" + func = getattr(self, func_name) + res = func(item, is_highlighted, editing, kwargs) + return Text.from_markup(res) diff --git a/dooit/ui/formatters/todo_tree_formatter.py b/dooit/ui/formatters/todo_tree_formatter.py new file mode 100644 index 00000000..7e26d75f --- /dev/null +++ b/dooit/ui/formatters/todo_tree_formatter.py @@ -0,0 +1,148 @@ +from typing import Dict +from dooit.api import Todo +from dooit.utils.conf_reader import Config +from .formatter import Formatter + +c = Config() +RED = c.get("red") +GREEN = c.get("green") +YELLOW = c.get("yellow") +ORANGE = c.get("orange") +DURATION_LEGEND = { + "m": "minute", + "h": "hour", + "d": "day", + "w": "week", +} + + +class TodoFormatter(Formatter): + model_type = Todo + + def todo_highlight(self, text: str, is_highlighted: bool, todo: Todo): + color = self.status_color(todo) + if is_highlighted: + return self.colored(text, "b " + color) + else: + return self.colored(text, "d " + color) + + def status_color(self, todo: Todo): + status = todo.status + if status == "COMPLETED": + return GREEN + elif status == "PENDING": + return YELLOW + else: + return RED + + def style_description( + self, + item: model_type, + is_highlighted: bool, + editing: str, + kwargs: Dict[str, str], + ) -> str: + text = kwargs["description"] + + if item.status == "COMPLETED": + text = self.colored(text, "strike") + + # STATUS ICON + status_icon = item.status.lower() + "_icon" + status_icon = self.format[status_icon] + text = self.colored(status_icon, self.status_color(item)) + text + + # DESCRIPTION + if children := item.todos: + d = { + "total": len(children), + "done": sum(i.status == "COMPLETED" for i in children), + "remaining": sum(i.status != "COMPLETED" for i in children), + } + text += self.format["children_hint"].format(**d) + + # EFFORT + if effort := kwargs["effort"]: + icon = self.format["effort_icon"] + color = self.format["effort_color"] + text += self.color_combo(icon, effort, color) + + # TAGS + if tags := kwargs["tags"]: + tags = [i.strip() for i in kwargs["tags"].split(",")] + icon = self.format["tags_icon"] + seperator = self.format["tags_seperator"] + color = self.format["tags_color"] + t = f" {icon}" + + if seperator == "comma": + t += ", ".join(tags) + elif seperator == "pipe": + t += " | ".join(tags) + else: + t += f" {icon}".join(tags) + + text += self.colored(t, color) + + # RECURRENCE + if recurrence := kwargs["recurrence"]: + + if recurrence: + if editing != "recurrence" or not is_highlighted: + frequency, value = recurrence[:-1], recurrence[-1] + recurrence = f"{frequency} {DURATION_LEGEND.get(value)}" + if frequency != "1": + recurrence += "s" + + color = self.format["recurrence_color"] + icon = self.format["recurrence_icon"] + text += self.color_combo(icon, recurrence, color) + + if self.format["color_todos"]: + return self.todo_highlight(text, is_highlighted, item) + else: + return self.cursor_highlight(text, is_highlighted, editing) + + def style_due( + self, + item: model_type, + is_highlighted: bool, + editing: str, + kwargs: Dict[str, str], + ) -> str: + icon_color = self.status_color(item) + text = self.colored(self.format["due_icon"], icon_color) + if item.status == "COMPLETED": + text += self.colored(kwargs["due"], "strike") + else: + text += kwargs["due"] + + if self.format["color_todos"]: + return self.todo_highlight(text, is_highlighted, item) + else: + return self.cursor_highlight(text, is_highlighted, editing) + + def style_urgency( + self, + item: model_type, + is_highlighted: bool, + editing: str, + kwargs: Dict[str, str], + ) -> str: + val = item.urgency + if val == 3: + color = ORANGE + elif val == 2: + color = YELLOW + elif val == 1: + color = GREEN + else: + color = RED + + if item.status == "COMPLETED": + color = "strike " + color + + icon = f"urgency{val}_icon" + icon = self.format[icon] + + return self.colored(icon, color) diff --git a/dooit/ui/formatters/workspace_tree_formatter.py b/dooit/ui/formatters/workspace_tree_formatter.py new file mode 100644 index 00000000..37c704f1 --- /dev/null +++ b/dooit/ui/formatters/workspace_tree_formatter.py @@ -0,0 +1,22 @@ +from typing import Dict +from dooit.api import Workspace +from .formatter import Formatter + + +class WorkspaceFormatter(Formatter): + model_type = Workspace + + def style_description( + self, + item: Workspace, + is_highlighted: bool, + is_editing: bool, + kwargs: Dict[str, str], + ) -> str: + pass + text = kwargs["description"] + + if children := item.workspaces: + text += self.format["children_hint"].format(count=len(children)) + + return self.cursor_highlight(text, is_highlighted, is_editing) diff --git a/dooit/ui/tui.py b/dooit/ui/tui.py index 63514fcc..29fff078 100644 --- a/dooit/ui/tui.py +++ b/dooit/ui/tui.py @@ -1,434 +1,67 @@ -# PARSE DATA -from ..utils.parser import Parser - -parser = Parser() -from ..utils.config import conf - -keys = conf.keys - - -from os import get_terminal_size -from threading import Thread -from rich.align import Align -from rich.console import Group -from rich.text import Text -from textual import events from textual.app import App -from textual.layouts.dock import DockLayout -from textual.layouts.grid import GridLayout -from textual.widget import Widget - - -from .events import * # NOQA -from ..ui.widgets import * # NOQA - - -message: str = conf.load_config(sub="welcome_message") -ascii_art: str = conf.load_config(sub="ascii_art") - -BANNER = Text( - ascii_art, - style="green", -) - -WELCOME = Text.from_markup( - message, - style="magenta", -) +from textual import events +from dooit.utils.watcher import Watcher +from dooit.ui.events import * # noqa +from dooit.ui.widgets import WorkspaceTree, TodoTree, StatusBar, HelpScreen +from dooit.api.manager import manager +from dooit.ui.css.screen import screen_CSS -HELP = Text.from_markup( - f""" - Press [bold yellow]`{keys.show_help[0]}`[/bold yellow] to show help menu -""", - style="cyan", -) +class Dooit(App): -class Doit(App): - async def on_mount(self) -> None: - self.current_menu = "" - self.main_area_scroll = MinimalScrollView() - await self.init_vars() + CSS = screen_CSS + SCREENS = {"help": HelpScreen(name="help")} + BINDINGS = [("ctrl+q", "quit", "Quit App")] - await self.setup_screen() + async def on_load(self): + self.navbar = WorkspaceTree() + self.todos = TodoTree() + self.bar = StatusBar() - self.help = False + async def on_mount(self): + self.watcher = Watcher() + self.current_focus = "navbar" + self.navbar.toggle_highlight() + self.set_interval(1, self.poll) - for widget in self.navbar_box: - widget.toggle_highlight() + async def poll(self): + if not manager.is_locked() and self.watcher.has_modified(): + if manager.refresh_data(): + await self.navbar._refresh_data() - async def on_load(self) -> None: - await self.bind("ctrl+q", "quit", "Quit") - self.show_header = conf.load_config(sub="show_headers") - self.working_thread = Thread() - self.working_thread.start() + def compose(self): + yield self.navbar + yield self.todos + yield self.bar async def action_quit(self) -> None: - await self.on_key(events.Key(self, "escape")) # incase of empty todo - self.working_thread.join() - await super().action_quit() - - async def toggle_help(self): - self.help = not self.help - - await self._clear_screen() - if self.help: - self.help_menu = MinimalScrollView(HelpMenu()) - await self.view.dock(self.help_menu) - else: - await self.setup_screen() - - async def setup_screen(self): - await self.setup_grid() - self.setup_areas() - self.place_widgets() - await self.reset_screen() - - async def _clear_screen(self) -> None: - """ - Removes all the widgets and clears the window - """ - - if isinstance(self.view.layout, DockLayout): - self.view.layout.docks.clear() - self.view.widgets.clear() - - async def init_vars(self) -> None: - """ - Init class Vars - """ - - self.navbar_heading = Box(name="navbar", options=[" Menu"]) - self.todos_heading = Box(name="todos", options=[" Todos"]) - - self.navbar, self.todo_lists = await parser.load() - - self.navbar_scroll = MinimalScrollView(self.navbar) - - self.status_bar = StatusBar() - self.search_tree = SearchTree() - self.sort_menu = SortOptions(options=["name", "date", "urgency", "status"]) - - self.current_status = "NORMAL" - self.navbar_heading.highlight() - self.current_tab = self.navbar_heading - - async def reset_screen(self) -> None: - """ - Reloads the screen - """ - await self.refresh_screen() - - async def _make_grid(self, grid: GridLayout) -> None: - grid.add_row("sep", size=1) - grid.add_row("a", size=3) - grid.add_row("sep0", size=1) - grid.add_row("b") - grid.add_row("sep1", size=1) - grid.add_row("bar", size=1) - - grid.add_column("sep0", size=1) - grid.add_column("0", fraction=20) - grid.add_column("sep1", size=1) - grid.add_column("padding", size=1) - grid.add_column("sep2", size=1) - grid.add_column("1", fraction=80) - grid.add_column("sep3", size=1) - - async def setup_grid(self) -> None: - """ - Handle grid placing - """ - - self.grid = await self.view.dock_grid() - await self._make_grid(self.grid) - - def place_widgets(self): - if self.show_header: - placements = { - "nav": self.navbar_heading, - "todo": self.todos_heading, - } - - self.grid.place(**placements) - - borders = [] - for i in range(2): - borders.append( - [ - f"middle{2 * i}", - f"top_connector{2 * i}", - f"top{i}", - f"top_connector{2 * i + 1}", - f"middle{2 * i + 1}", - f"bottom_connector{2 * i + 1}", - f"bottom{i}", - f"bottom_connector{2 * i}", - ] - ) - - self.navbar_box = self._make_box(borders[0]) - self.todos_box = self._make_box(borders[1]) - - self.grid.place(bar=self.status_bar) - self.grid.place( - **{ - "0b": (self.navbar_scroll), - } - ) - - placements = {"1b": self.main_area_scroll} - self.grid.place(**placements) - - def setup_areas(self) -> None: - """ - Place widgets - """ - - if self.show_header: - areas = { - "nav": "sep0-start|sep1-end,a", - "todo": "sep2-start|sep3-end,a", - } - - self.grid.add_areas(**areas) - - # WIDGET SPACES - middle_areas = dict() - if not self.show_header: - middle_areas["0b"] = "0,a-start|b-end" - middle_areas["1b"] = "1,a-start|b-end" - else: - middle_areas["0b"] = "0,b" - middle_areas["1b"] = "1,b" - - self.grid.add_areas(**middle_areas) - - sep = "sep0" if self.show_header else "sep" - middle_row = "b" if self.show_header else "a-start|b-end" - - # MIDDLE SEPERATORS - middle_areas = {f"middle{i}": f"sep{i},{middle_row}" for i in range(4)} - self.grid.add_areas(**middle_areas) + manager.commit() + return await super().action_quit() - # TOP SEPERATORS - top_areas = {f"top{i}": f"{i},{sep}" for i in range(2)} - self.grid.add_areas(**top_areas) + def toggle_highlight(self): + self.navbar.toggle_highlight() + self.todos.toggle_highlight() - # BOTTOM SEPERATORS - bottom_areas = {f"bottom{i}": f"{i},sep1" for i in range(2)} - self.grid.add_areas(**bottom_areas) - - # TOP CONNECTORS - top_connector_areas = {f"top_connector{i}": f"sep{i},{sep}" for i in range(4)} - self.grid.add_areas(**top_connector_areas) - - # BOTTOM CONNECTORS - bottom_connector_areas = { - f"bottom_connector{i}": f"sep{i},sep1" for i in range(4) - } - self.grid.add_areas(**bottom_connector_areas) - - self.grid.add_areas(**{"bar": "0-start|1-end,bar"}) - - def _make_box(self, areas: dict[str, str]) -> list[Widget]: - """ - Make border for trees - """ - - box = [ - VerticalLine(), - Connector1(), - HorizontalLine(), - Connector2(), - VerticalLine(), - Connector4(), - HorizontalLine(), - Connector3(), - ] - - for area, widget in zip(areas, box): - self.grid.place(**{area: widget}) - - return box - - async def on_resize(self, event: events.Resize) -> None: - await self.refresh_screen() - return await super().on_resize(event) - - async def refresh_screen(self) -> None: - """ - Re-place all the widgets - """ - - if self.current_menu not in self.todo_lists.keys(): - self.todo_lists[self.current_menu] = TodoList() - - self.todo_list = self.todo_lists[self.current_menu] - - if self.current_menu == "": - main_area_widget = Align.center( - Group(*[Align.center(i) for i in (BANNER, WELCOME, HELP)]), - vertical="middle", - height=round(get_terminal_size()[1] * 0.8), - ) - else: - match self.current_status: - case "SEARCH": - main_area_widget = self.search_tree - case "SORT": - main_area_widget = self.sort_menu - case _: - main_area_widget = self.todo_lists[self.current_menu] - - await self.main_area_scroll.update(main_area_widget) - - def change_current_tab(self, new_tab: str) -> None: - """ - Changes the current tab - """ - - self.current_tab.lowlight() - - def dim(var): - for i in var: - i.dim() - - def illuminate(var): - for i in var: - i.illuminate() - - match new_tab: - case "navbar": - self.current_tab = self.navbar_heading - illuminate(self.navbar_box) - dim(self.todos_box) - case "todos": - self.current_tab = self.todos_heading - dim(self.navbar_box) - illuminate(self.todos_box) - - self.current_tab.highlight() - - async def popup_sort(self): - await self.handle_change_status(ChangeStatus(self, "SORT")) - - def switch_tabs(self): - if self.current_tab == self.navbar_heading: - self.change_current_tab("todos") + async def on_key(self, event: events.Key) -> None: + if self.navbar.has_focus: + await self.navbar.handle_key(event) else: - self.change_current_tab("navbar") - - async def handle_help_key(self, event: events.Key): - match event.key: - case i if i in keys.move_down: - await self.help_menu.key_down() - case i if i in keys.move_up: - await self.help_menu.key_up() - case i if i in keys.move_to_top: - await self.help_menu.key_home() - case i if i in keys.move_to_bottom: - await self.help_menu.key_end() - - async def key_press(self, event: events.Key) -> None: - if (event.key in keys.show_help and self.current_status == "NORMAL") or ( - event.key == "escape" and self.help - ): - await self.toggle_help() - return - - if self.help: - await self.handle_help_key(event) - return - - self.status_bar.clear_message() - - if self.current_tab == self.navbar_heading: - await self.navbar.key_press(event) - else: - match self.current_status: - case "SEARCH": - await self.search_tree.key_press(event) - self.status_bar.set_message(self.search_tree.search.render()) - - case "SORT": - await self.sort_menu.key_press(event) - - case "NORMAL": - if event.key in keys.start_search: - await self.search_tree.set_values(self.todo_list.nodes) - await self.post_message(ChangeStatus(self, "SEARCH")) - - elif event.key in keys.spawn_sort_menu: - await self.popup_sort() - - else: - await self.todo_list.key_press(event) - - case _: - await self.todo_list.key_press(event) - - self.refresh(layout=True) - - async def on_key(self, e: events.Key): - await self.key_press(e) - await self.save_data() - - async def save_data(self): - self.working_thread.join() - self.working_thread = Thread(target=parser.save, args=(self.todo_lists,)) - self.working_thread.start() - - # HANDLING EVENTS - # ---------------------------- - async def handle_change_status(self, event: ChangeStatus) -> None: - status = event.status - reset = (self.current_status in ["SEARCH", "SORT"]) or ( - status - in [ - "SEARCH", - "SORT", - ] - ) - self.current_status = status - self.status_bar.set_status(status) - - if status in ["NORMAL"]: - self.change_current_tab(self.current_tab.name) - - if reset: - await self.reset_screen() - - async def handle_notify(self, event: Notify) -> None: - self.status_bar.set_message(event.message) - - async def handle_list_item_selected(self, event: ListItemSelected) -> None: - self.current_menu = event.selected - await self.reset_screen() - self.change_current_tab("todos" if event.focus else "navbar") - - async def handle_modify_topic(self, event: ModifyTopic) -> None: - if event.old == event.new: - return - - if event.old == "/" or event.old.endswith("//"): - return + await self.todos.handle_key(event) - self.todo_lists[event.new] = self.todo_lists.get(event.old, TodoList()) + async def on_topic_select(self, event: TopicSelect): + await self.todos.update_table(event.item) - if event.old in self.todo_lists: - del self.todo_lists[event.old] + async def on_switch_tab(self, _: SwitchTab): + self.toggle_highlight() - async def handle_apply_sort_method(self, event: ApplySortMethod) -> None: - await self.todo_list.sort_by(event.method) + async def on_apply_sort_method(self, event: ApplySortMethod): + event.sender.sort(attr=event.method) - async def handle_highlight_node(self, event: HighlightNode) -> None: - await self.todo_list.reach_to_node(event.id) + async def on_change_status(self, event: ChangeStatus): + self.bar.set_status(event.status) - async def handle_switch_tab(self, _: SwitchTab) -> None: - self.switch_tabs() + async def on_notify(self, event: Notify): + self.bar.set_message(event.message) - async def handle_remove_topic(self, event: RemoveTopic) -> None: - for topic in list(self.todo_lists.keys()): - if topic.startswith(event.selected): - self.todo_lists.pop(topic, None) + async def on_spawn_help(self, event: SpawnHelp): + self.push_screen("help") diff --git a/dooit/ui/widgets/__init__.py b/dooit/ui/widgets/__init__.py index aa2f6279..8423a00b 100644 --- a/dooit/ui/widgets/__init__.py +++ b/dooit/ui/widgets/__init__.py @@ -1,41 +1,17 @@ -from .nested_list_edit import NestedListEdit -from .todo_list import TodoList -from .entry import Entry -from .navbar import Navbar -from .box import Box from .status_bar import StatusBar -from .minimal_scrollview import MinimalScrollView from .sort_options import SortOptions -from .search_tree import SearchTree -from .simple_input import View, SimpleInput -from .help_menu import HelpMenu +from .simple_input import SimpleInput +from .help_menu import HelpMenu, HelpScreen +from .workspace_tree import WorkspaceTree +from .todo_tree import TodoTree -from .border import ( - HorizontalLine, - VerticalLine, - Connector1, - Connector2, - Connector3, - Connector4, -) __all__ = [ - "NestedListEdit", - "TodoList", - "Entry", - "Navbar", - "Box", - "HorizontalLine", - "VerticalLine", - "Connector1", - "Connector2", - "Connector3", - "Connector4", "StatusBar", - "MinimalScrollView", "SortOptions", - "SearchTree", - "View", "SimpleInput", "HelpMenu", + "HelpScreen", + "TodoTree", + "WorkspaceTree", ] diff --git a/dooit/ui/widgets/border.py b/dooit/ui/widgets/border.py deleted file mode 100644 index 8064845a..00000000 --- a/dooit/ui/widgets/border.py +++ /dev/null @@ -1,138 +0,0 @@ -from rich.console import RenderableType -from rich.text import Text -from textual.widget import Widget - -from ...utils.config import conf - - -class Border(Widget): - """ - Widget to serve as borders - """ - - def __init__( - self, name: str | None = None, color: str = "blue", item="", measure="width" - ) -> None: - super().__init__(name) - self.highlight = False - self.color = color - self.item = item - self.measure = measure - config = conf.load_config() - self.style_highlight = config["body_highlight"] - self.style_dim = config["body_dim"] - - def toggle_highlight(self) -> None: - self.highlight = not self.highlight - self.refresh() - - def illuminate(self) -> None: - self.highlight = True - self.refresh() - - def dim(self) -> None: - self.highlight = False - self.refresh() - - def render(self) -> RenderableType: - count = self.size.width if self.measure == "width" else self.size.height - style = self.style_highlight if self.highlight else self.style_dim - return Text(self.item * count, style=style) - - -class HorizontalLine(Border): - """ - Draws a horizontal line w.r.t to its width - """ - - def __init__(self) -> None: - super().__init__(item="━") - - -class VerticalLine(Border): - """ - Draws a vertical line w.r.t to its height - """ - - def __init__(self) -> None: - super().__init__(item="┃\n", measure="height") - - -class Connector1(Border): - """ - Connects left and top border - """ - - def __init__(self) -> None: - super().__init__(item="┏") - - def render(self) -> RenderableType: - width = self.size.width - 1 - height = self.size.height - 1 - if width: - self.item += "━" * width - if height: - self.item += "\n┃" * height - - style = self.style_highlight if self.highlight else self.style_dim - return Text(self.item, style=style) - - -class Connector2(Border): - """ - Connects right and top border - """ - - def __init__(self) -> None: - super().__init__(item="┓") - - def render(self) -> RenderableType: - width = self.size.width - 2 - height = self.size.height - 1 - if width: - self.item = "━" * width + self.item - if height: - self.item += "\n┃" * height - - style = self.style_highlight if self.highlight else self.style_dim - return Text(self.item, style=style) - - -class Connector3(Border): - """ - Connects left and bottom border - """ - - def __init__(self) -> None: - super().__init__(item="┗") - - def render(self) -> RenderableType: - width = self.size.width - 1 - height = self.size.height - 1 - if width: - self.item += "━" * width - if height: - self.item += "\n┃" * height - - style = self.style_highlight if self.highlight else self.style_dim - return Text(self.item, style=style) - - -class Connector4(Border): - """ - Connects right and bottom border - """ - - def __init__(self) -> None: - super().__init__(item="┛") - - def render(self) -> RenderableType: - width = self.size.width - 2 - height = self.size.height - 1 - if width: - self.item = "━" * width + self.item - if height: - self.item += "\n┃" * height - - style = self.style_highlight if self.highlight else self.style_dim - return Text(self.item, style=style) diff --git a/dooit/ui/widgets/box.py b/dooit/ui/widgets/box.py deleted file mode 100644 index d1449cce..00000000 --- a/dooit/ui/widgets/box.py +++ /dev/null @@ -1,48 +0,0 @@ -from rich.box import SQUARE_DOUBLE_HEAD -from rich.console import RenderableType -from rich.panel import Panel -from rich.style import StyleType -from rich.table import Table -from rich.text import Text -from textual.widget import Widget - -from ...utils.config import conf - - -class Box(Widget): - """ - A simple widget to render text with a panel - """ - - def __init__( - self, - name: str | None = None, - options: list[str] = [], - color: StyleType = "blue", - ) -> None: - super().__init__(name=name) - self.options = options - self.color = color - self.highlighted = False - config = conf.load_config() - self.style_highlighted = config["header_highlight"] - self.style_dim = config["header_dim"] - - def render(self) -> RenderableType: - table = Table.grid(padding=(0, 1), expand=True) - style = self.style_highlighted if self.highlighted else self.style_dim - - for i in self.options: - table.add_column(i, justify="center", ratio=1) - - table.add_row(*[Text(name, style=style) for name in self.options]) - - return Panel(table, border_style=style, height=3, box=SQUARE_DOUBLE_HEAD) - - def highlight(self) -> None: - self.highlighted = True - self.refresh() - - def lowlight(self) -> None: - self.highlighted = False - self.refresh() diff --git a/dooit/ui/widgets/entry.py b/dooit/ui/widgets/entry.py deleted file mode 100644 index 6c484dc7..00000000 --- a/dooit/ui/widgets/entry.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Any, Literal -from textual import events -from rich.text import TextType -from string import printable as chars -from random import choice -from .simple_input import SimpleInput - - -def generate_uuid() -> str: - """ - Generates a unique id for entries - """ - uuid = "" - for _ in range(32): - uuid += choice(chars) - return uuid - - -class Entry(SimpleInput): - """ - A Simple subclass of TextInput widget with no borders - """ - - def __init__(self, name: str | None = None) -> None: - super().__init__(name, placeholder="") - self.name = name - self.about = SimpleInput() - self.urgency = 1 - self.status = "PENDING" - self.due = SimpleInput() - self.focused = None - self.uuid = generate_uuid() - - async def make_focus(self, part: Literal["about", "due"]) -> None: - eval(f"self.{part}.on_focus()") - await eval(f"self.{part}.handle_keypress('end')") - self.focused = part - - async def remove_focus(self) -> None: - if self.focused: - eval(f"self.{self.focused}.on_blur()") - await eval(f"self.{self.focused}.handle_keypress('home')") - - async def send_key(self, event: events.Key) -> None: - if self.focused: - await eval(f"self.{self.focused}.on_key(event)") - - def increase_urgency(self) -> None: - self.urgency = min(self.urgency + 1, 4) - - def decrease_urgency(self) -> None: - self.urgency = max(self.urgency - 1, 1) - - def _format_text(self, text: str) -> str: - return text - - def render_panel(self, text: TextType) -> TextType: - return text - - async def handle_keypress(self, key: str) -> None: - await super().handle_keypress(key) - self.refresh() - - # DEPRECATED: will be removed in v0.3.0 - def encode(self) -> dict[str, Any]: - return { - "about": self.about.value, - "urgency": self.urgency, - "due": self.due.value, - "status": self.status, - } - - # DEPRECATED: will be removed in v0.3.0 - @classmethod - def from_encoded(cls, data: dict[str, Any]) -> "Entry": - entry = cls() - entry.about.value = data["about"] - entry.urgency = data["urgency"] - entry.due.value = data["due"] - entry.status = data["status"] - return entry - - def to_txt(self) -> str: - match self.status: - case "PENDING": - status = "x" - case "COMPLETED": - status = "X" - case _: - status = "O" # OVERDUE - - return f"{status} ({self.urgency}) due:{self.due.value or 'None'} {self.about.value}" - - @classmethod - def from_txt(cls, txt: str): - status, urgency, due, *about = txt.split() - - match status: - case "x": - status = "PENDING" - case "X": - status = "COMPLETED" - case _: - status = "OVERDUE" - - about = " ".join(about) - - due = due[4:] - if due == "None": - due = "" - - urgency = int(urgency[1:-1]) - - entry = cls() - entry.about.value = about - entry.urgency = urgency - entry.due.value = due - entry.status = status - return entry diff --git a/dooit/ui/widgets/help_menu.py b/dooit/ui/widgets/help_menu.py index 0f4c5d5f..02f2fd85 100644 --- a/dooit/ui/widgets/help_menu.py +++ b/dooit/ui/widgets/help_menu.py @@ -1,55 +1,92 @@ +from typing import Dict, List +from textual.containers import Vertical +from textual.screen import Screen +from textual.widgets import Static from rich.align import Align -from rich.box import MINIMAL from rich.console import Group, RenderableType -from rich.panel import Panel from rich.style import StyleType from rich.table import Table from rich.text import Text -from rich.tree import Tree -from textual.widget import Widget - -from ...utils.config import conf, Key - -keys = Key({i: "/".join(j).replace("ctrl+i", "tab") for i, j in conf.keybinds.items()}) -NL = "\n" +from textual import events +from dooit.utils import KeyBinder # UTILS # --------------------------------------------------- +NL = Text("\n") +kb = KeyBinder() def colored(text: str, color: StyleType) -> str: - return f"[{color}]{text}[/{color}]" + return f"[{color}]{text}[/]" + + +def convert_to_row(bindings: Dict): + arr = [] + methods = kb.raw + + for i, j in bindings.items(): + if i in methods: + key = methods[i] + if isinstance(key, list): + key = "/".join(key) + + arr.append( + [ + Text.from_markup( + colored( + " or ", + "i cyan", + ).join(methods[i]), + style="b green", + ), + Text(i, style="magenta"), + Text.from_markup(j, style="b blue"), + ] + ) + else: + arr.append( + [ + Text(i, style="b green"), + Text("N/A", style="d white", justify="center"), + Text.from_markup(j, style="b blue"), + ] + ) + + return arr def generate_kb_table( kb: dict[str, str], topic: str, notes: list[str] = [] ) -> RenderableType: - table = Table.grid(expand=False, padding=(0, 2)) - table.add_column("mode") - table.add_column("cmd") - table.add_column("colon") - table.add_column("help") + """ + Generate Table for modes + """ + + arr = convert_to_row(kb) + + table = Table.grid(expand=True, padding=(0, 3)) + table.add_column("mode", width=20) + table.add_column("keybind", width=15) + table.add_column("cmd", width=20) + table.add_column("colon", width=2) + table.add_column("help", width=50) table.add_row(Text.from_markup(f" [r green] {topic} [/r green]"), "", "", "") - for cmd, help in kb.items(): - table.add_row( - "", - (Text.from_markup(colored(cmd, "blue"))), - "", - (Text.from_markup(colored(" " + help, "magenta")) + NL), - ) + + for i in arr: + table.add_row("", i[0], i[1], Text("->", style="d white"), i[2]) if notes: - notes = [f"{colored('', 'd yellow')} {i}" for i in notes] + notes = [f"{colored('!', 'd yellow')} {i}" for i in notes] notes = [colored(" Note:", "d white")] + notes return Align.center( - Group(table, *[Text.from_markup(i) for i in notes], NL + seperator + NL) + Group(table, *[Text.from_markup(i) for i in notes], NL, seperator, NL) ) -seperator = f"{colored('─' * 60, 'bold dim black')}" +seperator = Text.from_markup(f"{colored('─' * 60, 'b d black')}", justify="center") # ---------------- X ------------------------- @@ -57,27 +94,29 @@ def generate_kb_table( # KEYBINDINGS # -------------------------------------------- NORMAL_KB = { - f"{keys.move_down}": "Move down in list", - f"{keys.shift_down}": "Shift todo down in list", - f"{keys.move_up}": "Move up in list", - f"{keys.shift_up}": "Shift todo up in list", - f"{keys.edit_node}": "Edit todo/topic", - f"{keys.toggle_complete}": "Toggle todo status as complete/incomplete**", - f"{keys.yank_todo}": "Copy todo's text", - f"{keys.edit_date}": "Edit date**", - f"{keys.increase_urgency}": "Increase urgency**", - f"{keys.decrease_urgency}": "Decrease urgency**", - f"{keys.move_to_top}": "Move to top of list", - f"{keys.move_to_bottom}": "Move to bottom of list", - f"{keys.toggle_expand}": "Toggle-expand highlighted item", - f"{keys.toggle_expand_parent}": "Toggle-expand parent item", - f"{keys.remove_node}": "Remove highlighted node", - f"{keys.add_sibling}": "Add sibling todo/topic", - f"{keys.add_child}": "Add child todo/topic", - f"{keys.spawn_sort_menu}": "Launch sort menu", - f"{keys.start_search}": "Start Search Mode ⃰ ⃰ ", - f"{keys.select_node}": "Select topic ⃰ ", - f"{keys.move_focus_to_menu}": "Move focus to Menu ⃰ ⃰ ", + "move down": "Move down in list", + "shift down": "Shift todo down in list", + "move up": "Move up in list", + "shift up": "Shift todo up in list", + "toggle complete": "Toggle todo status as complete/incomplete**", + "copy text": "Copy todo's text", + "move to top": "Move to top of list", + "move to bottom": "Move to bottom of list", + "toggle expand": "Toggle-expand highlighted item", + "toggle expand parent": "Toggle-expand parent item", + "remove item": "Remove highlighted node", + "add sibling": "Add sibling todo/workspace", + "add child": "Add child todo/workspace", + "sort menu toggle": "Launch sort menu", + "start search": "Start Search Mode", + "switch pane": "Change focused pane", + "edit due": "Edit date**", + "edit description": "Edit description**", + "edit effort": "Edit effort for todo**", + "edit recurrence": "Edit recurrence for todo**", + "edit tags": "Edit tags for todo**", + "increase urgency": "Increase urgency**", + "decrease urgency": "Decrease urgency**", "ctrl+q": "Quit the Application", } @@ -105,14 +144,15 @@ def generate_kb_table( "escape": "Navigate in the searched items" + "\n" + "Goes back to normal mode [i u]if navigating[/i u]", - f"{keys.start_search}": "Go back to search input [i u]if navigating[/i u]", + "start search": "Go back to search input [i u]if navigating[/i u]", "any": "Press key to search input", } SORT_KB = { - f"{keys.move_down}": "Move down", - f"{keys.move_up}": "Move up", - f"{keys.select_node}": "Select the sorting method", + "move down": "Move to next sort option", + "move up": "Move to previous sort option", + "sort menu toggle": "Cancel sort operation", + "enter": "Select the sorting method", } # ---------------- X ------------------------- @@ -121,24 +161,26 @@ def generate_kb_table( # -------------------------------------------- HEADER = f""" {colored("Welcome to the help menu!", 'yellow')} -{seperator} +{seperator.markup} """ -BODY = f""" {colored(f'Dooit is build to be used from the keyboard,{NL} but mouse can also be used to navigate', 'green')} +BODY = f""" {colored(f'Dooit is built to be used from the keyboard!', 'green')} Documentation below will walk you through the controls: -{seperator} +{seperator.markup} """ THANKS = f"{colored('Thanks for using dooit :heart:', 'yellow')}" -AUTHOR = f"{colored('--kraanzu', 'orchid')}{NL * 2}{seperator}{NL}" +AUTHOR = f"{colored('--kraanzu', 'orchid')}{NL.plain * 2}{seperator.markup}{NL}" -OUTRO = f"Press {colored('escape', 'green')} or {colored(f'{keys.show_help}', 'green')} to exit help menu" +OUTRO = ( + f"Press {colored('escape', 'green')} or {colored('?', 'green')} to exit help menu" +) # ---------------- X ------------------------- -class HelpMenu(Widget): +class HelpMenu: """ A Help Menu Widget """ @@ -149,20 +191,43 @@ class HelpMenu(Widget): author = Text.from_markup(AUTHOR, justify="center") outro = Text.from_markup(OUTRO, justify="center") - def render(self) -> RenderableType: - tree = Tree("") - tree.hide_root = True - tree.expanded = True - - tree.add(self.header) - tree.add(self.body) - tree.add(generate_kb_table(NORMAL_KB, "NORMAL", NORMAL_NB)) - tree.add(generate_kb_table(INSERT_KB, "INSERT")) - tree.add(generate_kb_table(DATE_KB, "DATE", DATE_NB)) - tree.add(generate_kb_table(SEARCH_KB, "SEARCH")) - tree.add(generate_kb_table(SORT_KB, "SORT")) - tree.add(self.thanks) - tree.add(self.author) - tree.add(self.outro) - - return Panel(tree, box=MINIMAL) + def items(self) -> List[RenderableType]: + arr = [] + arr.append(self.header) + arr.append(self.body) + arr.append(generate_kb_table(NORMAL_KB, "NORMAL", NORMAL_NB)) + arr.append(generate_kb_table(INSERT_KB, "INSERT")) + arr.append(generate_kb_table(DATE_KB, "DATE", DATE_NB)) + arr.append(generate_kb_table(SEARCH_KB, "SEARCH")) + arr.append(generate_kb_table(SORT_KB, "SORT")) + arr.append(self.thanks) + arr.append(self.author) + arr.append(self.outro) + + return arr + + +class HelpScreen(Screen): + """ + Help Screen to view Help Menu + """ + + BINDINGS = [ + ("escape", "app.pop_screen", "Pop screen"), + ("question_mark", "app.pop_screen", "Pop screen"), + ] + view = Vertical(*[Static(i) for i in HelpMenu().items()]) + + def compose(self): + yield self.view + + async def on_key(self, event: events.Key): + key = event.character + if key in ["j", "down"]: + self.view.scroll_down() + elif key in ["k", "up"]: + self.view.scroll_up() + elif key in ["home", "g"]: + self.view.scroll_home() + elif key in ["end", "G"]: + self.view.scroll_end() diff --git a/dooit/ui/widgets/minimal_scrollview.py b/dooit/ui/widgets/minimal_scrollview.py deleted file mode 100644 index 2114a6a7..00000000 --- a/dooit/ui/widgets/minimal_scrollview.py +++ /dev/null @@ -1,29 +0,0 @@ -from rich.console import RenderableType -from textual.layouts.grid import GridLayout -from textual.message import Message -from textual.widgets import ScrollView - - -class MinimalScrollView(ScrollView): - """ - Just a ScrollView without bars - """ - - async def update(self, renderable: RenderableType, home: bool = False) -> None: - return await super().update(renderable, home) - - async def handle_window_change(self, message: Message) -> None: - message.stop() - - virtual_width, virtual_height = self.window.virtual_size - width, height = self.size - - self.x = self.validate_x(self.x) - self.y = self.validate_y(self.y) - - self.hscroll.virtual_size = virtual_width - self.hscroll.window_size = width - self.vscroll.virtual_size = virtual_height - self.vscroll.window_size = height - - assert isinstance(self.layout, GridLayout) diff --git a/dooit/ui/widgets/navbar.py b/dooit/ui/widgets/navbar.py deleted file mode 100644 index 76f74fdc..00000000 --- a/dooit/ui/widgets/navbar.py +++ /dev/null @@ -1,269 +0,0 @@ -from os import get_terminal_size -from rich.align import Align -from rich.console import RenderableType -from rich.text import Text -from textual import events -from textual.widgets import TreeNode, NodeID - -from ...ui.widgets.simple_input import SimpleInput, View -from ...ui.events import ( - ModifyTopic, - ListItemSelected, - ChangeStatus, - Notify, - RemoveTopic, -) -from ...ui.widgets import NestedListEdit - -EMPTY_TOPIC = Text.from_markup( - """ -Nothing yet? -Press [b yellow]'a'[/b yellow] to add a topic -""", - style="dim white", -) -WARNING = "[b yellow]WARNING[/b yellow]" - - -class Navbar(NestedListEdit): - """ - A widget to show the todo menu - """ - - def __init__(self): - super().__init__("", SimpleInput()) - from dooit.utils.config import conf - - self.config = conf.load_config("menu") - self.select_key = conf.keys.select_node - - def render(self) -> RenderableType: - if self.root.tree.children: - return self._tree - else: - return Align.center( - EMPTY_TOPIC, - vertical="middle", - height=round(0.8 * get_terminal_size()[1]), - ) - - def _get_node_path(self): - - path = "" - node = self.highlighted_node - while node.parent: - path = f"{node.data.value}/{path}" - node = node.parent - - return path - - def render_node(self, node: TreeNode) -> RenderableType: - - width = self._get_width(node.parent != self.root) - if ( - not hasattr(node.data, "view") - or node.data.view.end - node.data.view.start != width - ): - node.data.view = View(0, width) - - return self.render_custom_node(node) - - async def focus_node(self) -> None: - self.prev_value = self.highlighted_node.data.value - self._last_path = self._get_node_path() - self.highlighted_node.data.on_focus() - self.warn = False - await self.highlighted_node.data.handle_keypress("end") - await self.post_message(ChangeStatus(self, "INSERT")) - self.editing = True - - async def remove_node(self, id: NodeID | None = None) -> None: - await self.post_message(RemoveTopic(self, self._get_node_path())) - await super().remove_node(id) - - async def check_node(self) -> bool: - val = self.highlighted_node.data.value.strip() - if not val: - if not self.prev_value: - await self.remove_node() - await self.post_message( - Notify(self, f"{WARNING}: Empty topic! Deleted") - ) - return True - else: - self.highlighted_node.data.value = self.prev_value - await self.post_message( - Notify(self, f"{WARNING}: Can't leave topic name empty") - ) - return True - - if ( - sum( - i.data.value == val - for i in (self.highlighted_node.parent or self.root).children - ) - > 1 - ): - - await self.post_message( - Notify( - self, - f"{WARNING}: Duplicate sibling topic!" - if not self.warn - else "Topic Deleted!", - ) - ) - return False - - return True - - async def unfocus_node(self) -> None: - await self.post_message( - ModifyTopic(self, self._last_path, self._get_node_path()), - ) - - ok = await self.check_node() - if not ok: - if self.warn: - await self.remove_node() - else: - self.warn = True - return - - await self.post_message(ChangeStatus(self, "NORMAL")) - await self.highlighted_node.data.handle_keypress("home") - self.highlighted_node.data.on_blur() - self.editing = False - - async def send_key_to_selected(self, event: events.Key) -> None: - await self.highlighted_node.data.on_key(event) - - async def key_press(self, event: events.Key): - if ( - not self.editing - and self.highlighted_node != self.root - and event.key in self.select_key - ): - await self.post_message( - ListItemSelected( - self, - self._get_node_path(), - focus=True, - ) - ) - self.refresh() - return - - await super().key_press(event) - - if self.highlighted != self.root.id and not self.editing: - await self.emit( - ListItemSelected( - self, - self._get_node_path(), - focus=False, - ) - ) - self.refresh() - - def _get_width(self, child: bool): - width = self.size.width - 6 - if child: - width -= 3 - return width - - def get_ibox(self, child: bool = False): - ibox = SimpleInput() - width = self._get_width(child) - ibox.view = View(0, width) - return ibox - - async def add_child(self) -> None: - """ - Adds child to current selected node - """ - - node = self.highlighted_node - if node == self.root or node.parent == self.root: - node = self.highlighted_node - await node.add( - "child", - self.get_ibox(child=node != self.root), - ) - await node.expand() - await self.reach_to_last_child() - await self.focus_node() - - self.refresh() - - async def add_sibling(self) -> None: - """ - Adds sibling for the currently selected node - """ - - parent = self.highlighted_node.parent - - if not parent: - await self.add_child() - return - else: - children = parent.children - tree = parent.tree.children - - await parent.add("", self.get_ibox(child=parent != self.root)) - i = children.index(self.highlighted_node) - id = children[-1].id - children.insert(i + 1, children.pop()) - tree.insert(i + 1, tree.pop()) - - while self.highlighted != id: - await self.cursor_down() - - await self.focus_node() - self.refresh() - - def render_custom_node(self, node: TreeNode) -> RenderableType: - icons = self.config["icons"] - colors = self.config["theme"] - - label = Text.from_markup(str(node.data.render()) or "") - label.plain = label.plain[: self.size.width - 2] - - # Setup pre-icons - if node.children: - if not node.expanded: - icon = icons["nested_close"] - else: - icon = icons["nested_open"] - else: - icon = icons["single_topic"] - - # Padding adjustment - label.plain = f" {icon} " + label.plain + " " - label.pad_right(self.size.width) - - if node.id == self.highlighted: - if self.editing: - label.stylize(colors["style_editing"]) - else: - label.stylize(colors["style_focused"]) - else: - label.stylize(colors["style_unfocused"]) - - meta = { - "@click": f"click_label({node.id})", - "tree_node": node.id, - "cursor": node.is_cursor, - } - - label.apply_meta(meta) - return label - - async def handle_tree_click(self, *_) -> None: - await self.post_message( - ListItemSelected( - self, - self._get_node_path(), - focus=True, - ) - ) diff --git a/dooit/ui/widgets/nested_list_edit.py b/dooit/ui/widgets/nested_list_edit.py deleted file mode 100644 index 99965349..00000000 --- a/dooit/ui/widgets/nested_list_edit.py +++ /dev/null @@ -1,266 +0,0 @@ -from rich.console import RenderableType -from rich.padding import PaddingDimensions -from rich.style import StyleType -from rich.text import Text, TextType -from textual import events -from textual.widgets import TreeControl, TreeNode, NodeID -from textual.messages import CursorMove - -from ...ui.widgets.simple_input import SimpleInput, View - - -class NestedListEdit(TreeControl): - """ - A editable & nested list - """ - - def __init__( - self, - label: TextType, - data: RenderableType = SimpleInput(), - name: str | None = None, - padding: PaddingDimensions = (1, 1), - style_unfocus: StyleType = "d white", - style_focus: StyleType = "b blue", - style_editing: StyleType = "b cyan", - ) -> None: - super().__init__(label, data, name=name, padding=padding) - self._tree.hide_root = True - self._tree.expanded = True - self.style_focus = style_focus - self.style_unfocus = style_unfocus - self.style_editing = style_editing - self.editing = False - self.highlight(self.root.id) - - from ...utils import conf - - self.keys = conf.keys - - async def watch_cursor_line(self, value: int) -> None: - await self.post_message(CursorMove(self, value + self.gutter.top)) - - def highlight(self, id: NodeID) -> None: - self.highlighted = id - self.highlighted_node = self.nodes[self.highlighted] - self.refresh() - - async def focus_node(self, part: str = "about") -> None: - await self.highlighted_node.data.make_focus(part) - self.editing = True - - async def unfocus_node(self) -> None: - await self.highlighted_node.data.remove_focus() - self.editing = False - - async def remove_node(self, id: NodeID | None = None) -> None: - if id == self.root.id: - return - - node = self.nodes[id or self.highlighted] - - if node.expanded: - await node.toggle() - - if node.next_node: - await self.cursor_down() - elif prev_node := node.previous_node: - if prev_node == self.root: - self.highlight(self.root.id) - else: - await self.cursor_up() - - parent = node.parent or self.root - for index, child in enumerate(parent.children): - if child == node: - parent.children.pop(index) - parent.tree.children.pop(index) - del self.nodes[node.id] - - self.refresh(layout=True) - - async def key_down(self, _: events.Key) -> None: - pass - - async def key_up(self, _: events.Key) -> None: - pass - - async def cursor_down(self) -> None: - node = self.highlighted_node - - if next_node := node.next_node: - self.cursor_line += 1 - self.highlight(next_node.id) - elif node == self.root: - if node.children: - self.cursor_line += 1 - self.highlight(node.children[0].id) - - async def cursor_up(self) -> None: - node = self.highlighted_node - - if prev_node := node.previous_node: - if prev_node != self.root: - self.cursor_line -= 1 - self.highlight(prev_node.id) - - async def shift_up(self): - if prev := self.highlighted_node.previous_node: - if prev != self.highlighted_node.parent: - parent = self.highlighted_node.parent or self.root - children = parent.children - tree = parent.tree.children - pos = children.index(self.highlighted_node) - children[pos], children[pos - 1] = children[pos - 1], children[pos] - tree[pos], tree[pos - 1] = tree[pos - 1], tree[pos] - - async def shift_down(self): - if node := self.highlighted_node.next_node: - if node.parent == self.highlighted_node.parent: - parent = self.highlighted_node.parent or self.root - children = parent.children - tree = parent.tree.children - pos = children.index(self.highlighted_node) - children[pos], children[pos + 1] = children[pos + 1], children[pos] - tree[pos], tree[pos + 1] = tree[pos + 1], tree[pos] - - async def move_to_top(self) -> None: - if children := self.root.children: - self.highlight(children[0].id) - self.cursor_line = 0 - - async def move_to_bottom(self) -> None: - if children := self.root.children: - while self.highlighted != children[-1].id: - await self.cursor_down() - - async def toggle_expand(self) -> None: - if self.highlighted != self.root.id: - await self.highlighted_node.toggle() - - async def toggle_expand_parent(self) -> None: - if ( - self.highlighted != self.root.id - and self.highlighted_node.parent != self.root - ): - await self.reach_to_parent() - await self.highlighted_node.toggle() - - async def reach_to_parent(self) -> None: - node = self.highlighted_node - if parent := node.parent: - index = parent.children.index(node) + 1 - self.cursor_line -= index - self.highlight(parent.id) - - async def reach_to_last_child(self) -> None: - if children := self.highlighted_node.children: - self.cursor_line += len(children) - self.highlight(children[-1].id) - - async def add_child(self) -> None: - node = self.highlighted_node - await node.add("child", SimpleInput()) - await node.expand() - await self.reach_to_last_child() - await self.focus_node() - - async def add_sibling(self) -> None: - if self.highlighted_node.parent == self.root: - await self.root.add("child", SimpleInput()) - await self.move_to_bottom() - else: - await self.reach_to_parent() - await self.add_child() - await self.focus_node() - - async def send_key_to_selected(self, event: events.Key) -> None: - await self.highlighted_node.data.send_key(event) - - async def key_press(self, event: events.Key): - if self.editing: - match event.key: - case "escape": - await self.unfocus_node() - case "enter": - if self.highlighted_node.data.value: - await self.unfocus_node() - if not self.editing: - await self.add_sibling() - case _: - await self.send_key_to_selected(event) - - else: - keys = self.keys - match event.key: - case i if i in keys.move_down: - await self.cursor_down() - case i if i in keys.shift_down: - await self.shift_down() - case i if i in keys.move_up: - await self.cursor_up() - case i if i in keys.shift_up: - await self.shift_up() - case i if i in keys.move_to_top: - await self.move_to_top() - case i if i in keys.move_to_bottom: - await self.move_to_bottom() - case i if i in keys.toggle_expand: - await self.toggle_expand() - case i if i in keys.toggle_expand_parent: - await self.toggle_expand_parent() - case i if i in keys.add_child: - await self.add_child() - case i if i in keys.add_sibling: - await self.add_sibling() - case i if i in keys.edit_node: - if self.highlighted != self.root.id: - await self.focus_node() - case i if i in keys.remove_node: - if self.highlighted != self.root.id: - await self.remove_node() - - self.refresh() - - async def on_mouse_move(self, event: events.MouseMove) -> None: - """ - Move the highlight along with mouse hover - """ - - if not self.editing: - if id := event.style.meta.get("tree_node"): - self.highlight(id) - - def render_node(self, node: TreeNode) -> RenderableType: - - if not hasattr(node.data.about, "view"): - node.data.about.view = View(0, self.size.width - 6) - - return self.render_custom_node(node) - - def render_custom_node(self, node) -> Text: - - label = ( - Text(str(node.data.about.render()), no_wrap=True) - if isinstance(node.label, str) - else node.label - ) - label.pad_right(self.size.width) - - if node.id == self.highlighted: - label.stylize(self.style_focus) - else: - label.stylize(self.style_unfocus) - - meta = { - "@click": f"click_label({node.id})", - "tree_node": node.id, - } - - label.apply_meta(meta) - return label - - async def handle_tree_click(self, *_) -> None: - if not self.editing: - await self.focus_node() - self.refresh() diff --git a/dooit/ui/widgets/search_tree.py b/dooit/ui/widgets/search_tree.py deleted file mode 100644 index 1d22018e..00000000 --- a/dooit/ui/widgets/search_tree.py +++ /dev/null @@ -1,120 +0,0 @@ -from os import get_terminal_size -from rich.align import Align -from rich.console import RenderableType -from rich.text import Text -from textual import events -from textual.widgets import NodeID, TreeNode - -from ...ui.events.events import ChangeStatus, HighlightNode -from ...ui.widgets.simple_input import SimpleInput, View -from ...ui.widgets.todo_list import TodoList - -NO_MATCH = """ - [blue][/blue] - [d white]0 matches -Nothing showed up?! Maybe try something different?[/d white] -""" - - -class SearchTree(TodoList): - """ - A tree with built in searching - """ - - def render(self) -> RenderableType: - if self.any: - return self._tree - else: - return Align.center( - NO_MATCH, vertical="middle", height=round(get_terminal_size()[1] * 0.8) - ) - - def render_about(self, node, _) -> RenderableType: - label: Text = super().render_about(node, _) - if self.highlighted_node == node: - label.append(" <=") - - if val := self.search.value: - label.highlight_regex(val, style="red") - - return label - - async def set_values(self, nodes): - """ - Initialize with all the values - """ - - self.all_nodes: dict[NodeID, TreeNode] = nodes - self.search = SimpleInput() - self.search.view = View(0, 100) - self.search.on_focus() - self.searching = True - await self.refresh_search() - - async def refresh_search(self) -> None: - """ - Refresh tree on search value change - """ - - self.any = False - self.root.children = [] - - for i in list(self.nodes.keys()): - if i != self.root.id: - self.nodes.pop(i) - - self.root.tree.children = [] - self.cursor_line = 0 - self.highlighted = self.root.id - - for id, i in self.all_nodes.items(): - if i.data.about.value and self.search.value in i.data.about.value: - i.data.id = id - await self.root.add("", i.data) - self.any = True - - self.refresh(layout=True) - - async def find_id(self) -> NodeID: - uuid = self.nodes[self.highlighted].data.uuid - for id, i in self.all_nodes.items(): - if i.data.uuid == uuid: - return id - - return NodeID(0) - - async def key_press(self, event: events.Key) -> None: - if self.searching: - match event.key: - case "escape": - if self.searching: - self.searching = False - self.search.on_blur() - self.highlight(self.root.id) - await self.cursor_down() - - case _: - await self.search.handle_keypress(event.key) - await self.refresh_search() - - else: - keys = self.keys - match event.key: - case i if i in keys.start_search: - self.searching = True - self.search.on_focus() - case "escape": - await self.post_message(ChangeStatus(self, "NORMAL")) - case i if i in keys.move_down: - await self.cursor_down() - case i if i in keys.move_up: - await self.cursor_up() - case i if i in keys.move_to_top: - await self.move_to_top() - case i if i in keys.move_to_bottom: - await self.move_to_bottom() - case "enter": - await self.post_message(ChangeStatus(self, "NORMAL")) - await self.post_message(HighlightNode(self, await self.find_id())) - - self.refresh() diff --git a/dooit/ui/widgets/simple_input.py b/dooit/ui/widgets/simple_input.py index 7fd47e91..8883e884 100644 --- a/dooit/ui/widgets/simple_input.py +++ b/dooit/ui/widgets/simple_input.py @@ -1,7 +1,5 @@ import pyperclip -from typing import Literal -from rich.console import RenderableType -from rich.panel import Panel +from typing import Any, Literal from rich.style import StyleType from rich.text import Text, TextType from rich.box import Box @@ -11,43 +9,11 @@ from textual.reactive import Reactive -class View: - """ - A class to manage current viewing portion of text input - """ - - def __init__(self, start: int = 0, end: int = 0) -> None: - self.start = start - self.end = end - - def shift_left(self, delta: int) -> None: - """ - Shift the view to the left by provided delta - """ - - delta = min(delta, self.start) - self.start -= delta - self.end -= delta - - def shift_right(self, delta: int, max_val: int) -> None: - """ - Shift the view to the left by provided delta - """ - - delta = min(delta, max_val - self.end) - self.start += delta - self.end += delta - - def __str__(self) -> str: - return f"View({self.start}, {self.end})" - - class SimpleInput(Widget): """ A simple single line Text Input widget """ - value: str = "" cursor: str = "|" _cursor_position: int = 0 _has_focus: Reactive[bool] = Reactive(False) @@ -55,16 +21,18 @@ class SimpleInput(Widget): def __init__( self, name: str | None = None, + value: Any = "", title: TextType = "", title_align: AlignMethod = "center", border_style: StyleType = "blue", box: Box | None = None, - placeholder: TextType = Text("", style="dim white"), + placeholder: Text = Text("", style="dim white"), password: bool = False, list: tuple[Literal["blacklist", "whitelist"], list[str]] = ("blacklist", []), ) -> None: - super().__init__(name) + super().__init__(name=name) self.title = title + self.value = str(value) self.title_align: AlignMethod = title_align # Silence compiler warning self.border_style: StyleType = border_style self.placeholder = placeholder @@ -79,63 +47,21 @@ def __init__( def has_focus(self) -> bool: return self._has_focus - async def on_resize(self, _: events.Resize) -> None: - self._set_view() - self.update_view(self._cursor_position, 0) - self._cursor_position = 0 - self.refresh() - - def _format_text(self, text: str) -> str: - """ - Trims the non-visible part of the widget - """ - return text[self.view.start : self.view.end] - - def _set_view(self): - if self.box: - self.width = self.size.width - 4 - else: - self.width = self.size.width - - # self.width = self.size.width - 4 - self.view = View(0, self.width) - - def render(self) -> RenderableType: + def render(self) -> Text: """ Renders a Panel for the Text Input Box """ - if not hasattr(self, "view"): - self._set_view() - if self.has_focus: text = self._render_text_with_cursor() else: if len(self.value) == 0: - return self.render_panel(self.placeholder) + return self.placeholder else: text = self.value - formatted_text = Text.from_markup(self._format_text(text)) - return self.render_panel(formatted_text) - - def render_panel(self, text: TextType) -> RenderableType: - """ - Builds a panel for the Inpux Box - """ - - if self.box: - return Panel( - text, - title=self.title, - title_align=self.title_align, - height=3, - border_style=("bold " if self.has_focus else "dim ") - + str(self.border_style), - box=self.box, - ) - else: - return text + formatted_text = Text.from_markup(text) + return formatted_text def _render_text_with_cursor(self) -> str: """ @@ -190,7 +116,7 @@ async def _insert_text(self, text: str | None = None) -> None: # should work just fine on windows and mac if text is None: - text = pyperclip.paste() + text = str(pyperclip.paste()) self.value = ( self.value[: self._cursor_position] @@ -230,7 +156,6 @@ async def _move_cursor_backward(self, word=False, delete=False) -> None: if delete: self.value = self.value[: self._cursor_position] + self.value[prev:] - self.view.shift_left(prev - self._cursor_position) async def _move_cursor_forward(self, word=False, delete=False) -> None: """ @@ -262,80 +187,72 @@ async def _move_cursor_forward(self, word=False, delete=False) -> None: self.value = self.value[:prev] + self.value[self._cursor_position :] self._cursor_position = prev # Because the cursor never actually moved :) - def update_view(self, prev: int, curr: int) -> None: - """ - Updates the current view-able part of the text if there is an overflow - """ - if not hasattr(self, "view"): - self._set_view() - - if prev >= self.view.start and curr < self.view.start: - self.view.shift_left(prev - curr) - - elif prev <= self.view.end and curr >= self.view.end: - self.view.shift_right(curr - prev, len(self.value) + 1) - async def clear_input(self): - await self.handle_keypress("end") + self.move_cursor_to_end() while self.value: - await self.handle_keypress("ctrl+h") + await self.handle_keypress("backspace") + + def move_cursor_to_end(self): + self._cursor_position = len(self.value) async def handle_keypress(self, key: str) -> None: """ Handles Keypresses """ - prev = self._cursor_position - match key: + if key == "space": + key = " " + + if key == "enter": + self.on_blur() - # Moving backward - case "left": - await self._move_cursor_backward() + # Moving backward + elif key == "left": + await self._move_cursor_backward() - case "ctrl+left": - await self._move_cursor_backward(word=True) + elif key == "ctrl+left": + await self._move_cursor_backward(word=True) - case "ctrl+h": # Backspace - await self._move_cursor_backward(delete=True) + elif key == "backspace": # Backspace + await self._move_cursor_backward(delete=True) - case "ctrl+w": - await self._move_cursor_backward(word=True, delete=True) + elif key == "ctrl+w": + await self._move_cursor_backward(word=True, delete=True) - # Moving forward - case "right": - await self._move_cursor_forward() + # Moving forward + elif key == "right": + await self._move_cursor_forward() - case "ctrl+right": - await self._move_cursor_forward(word=True) + elif key == "ctrl+right": + await self._move_cursor_forward(word=True) - case "delete": - await self._move_cursor_forward(delete=True) + elif key == "delete": + await self._move_cursor_forward(delete=True) - case "ctrl+delete": - await self._move_cursor_forward(word=True, delete=True) + elif key == "ctrl+delete": + await self._move_cursor_forward(word=True, delete=True) - case "ctrl+l": - await self.clear_input() + elif key == "ctrl+l": + await self.clear_input() - # EXTRAS - case "home": - self._cursor_position = 0 + # EXTRAS + elif key == "home": + self._cursor_position = 0 - case "end": - self._cursor_position = len(self.value) + elif key == "end": + self.move_cursor_to_end() - case "ctrl+i": - await self._insert_text("\t") + elif key == "tab": + await self._insert_text("\t") - # COPY-PASTA - case "ctrl+v": - try: - await self._insert_text() - except: - return + # COPY-PASTA + elif key == "ctrl+v": + try: + await self._insert_text() + except: + return if len(key) == 1: await self._insert_text(key) - self.update_view(prev, self._cursor_position) self.refresh() diff --git a/dooit/ui/widgets/sort_options.py b/dooit/ui/widgets/sort_options.py index 550ad23b..4cd83d10 100644 --- a/dooit/ui/widgets/sort_options.py +++ b/dooit/ui/widgets/sort_options.py @@ -1,4 +1,4 @@ -from os import get_terminal_size +from typing import Optional, Type from rich.align import Align from rich.box import HEAVY from rich.console import RenderableType @@ -7,10 +7,8 @@ from rich.text import Text from rich.style import StyleType from textual.widget import Widget -from textual import events - -from ...ui.events.events import ApplySortMethod, ChangeStatus -from ...utils.config import conf +from dooit.ui.events import ApplySortMethod, ChangeStatus, Notify +from dooit.utils import KeyBinder class SortOptions(Widget): @@ -18,88 +16,97 @@ class SortOptions(Widget): A list class to show and select the items in a list """ + key_manager = KeyBinder() + def __init__( self, name: str | None = None, options: list[str] = [], + parent_widget: Optional[Widget] = None, style_unfocused: StyleType = "white", style_focused: StyleType = "bold reverse green ", pad: bool = True, - rotate: bool = False, wrap: bool = True, ) -> None: - super().__init__(name) + super().__init__(name=name) self.options = options self.style_unfocused = style_unfocused self.style_focused = style_focused self.pad = pad - self.rotate = rotate self.wrap = wrap + self.parent_widget = parent_widget self.highlighted = 0 - self.keys = conf.keys def highlight(self, id: int) -> None: self.highlighted = id self.refresh(layout=True) - async def hide(self): - await self.key_press(events.Key(self, "escape")) + def hide(self) -> None: + self.visible = False - def move_cursor_down(self) -> None: + async def move_down(self) -> None: """ Moves the highlight down """ - if self.rotate: - self.highlight((self.highlighted + 1) % len(self.options)) - else: - self.highlight(min(self.highlighted + 1, len(self.options) - 1)) + self.highlight(min(self.highlighted + 1, len(self.options) - 1)) - def move_cursor_up(self) -> None: + async def move_up(self) -> None: """ Moves the highlight up """ - if self.rotate: - self.highlight( - (self.highlighted - 1 + len(self.options)) % len(self.options) - ) - else: - self.highlight(max(self.highlighted - 1, 0)) + self.highlight(max(self.highlighted - 1, 0)) - def move_cursor_to_top(self) -> None: + async def move_to_top(self) -> None: """ Moves the cursor to the top """ self.highlight(0) - def move_cursor_to_bottom(self) -> None: + async def move_to_bottom(self) -> None: """ Moves the cursor to the bottom """ self.highlight(len(self.options) - 1) - async def key_press(self, event: events.Key) -> None: - event.stop() - - key = self.keys - match event.key: - case "escape": - # self.visible = False - # self.refresh() - await self.post_message(ChangeStatus(self, "NORMAL")) - case i if i in key.move_down: - self.move_cursor_down() - case i if i in key.move_up: - self.move_cursor_up() - case i if i in key.move_to_top: - self.move_cursor_to_top() - case i if i in key.move_to_bottom: - self.move_cursor_to_bottom() - case "enter": - await self.emit(ApplySortMethod(self, self.options[self.highlighted])) - await self.hide() + async def sort_menu_toggle(self): + await self.send_message(ChangeStatus, "NORMAL") + self.visible = False + + async def send_message(self, event: Type, *args): + if self.parent_widget: + await self.parent_widget.post_message( + event( + self.parent_widget, + *args, + ) + ) + + async def handle_key(self, key: str) -> None: + + if key == "escape": + await self.sort_menu_toggle() + return + + if key == "enter": + await self.send_message(ApplySortMethod, self.options[self.highlighted]) + await self.sort_menu_toggle() + return + + self.key_manager.attach_key(key) + bind = self.key_manager.get_method() + if bind: + if hasattr(self, bind.func_name): + func = getattr(self, bind.func_name) + await func(*bind.params) + else: + await self.post_message( + Notify(self, "[yellow]No such operation for sort menu![/yellow]") + ) + + self.refresh() def add_option(self, option: str) -> None: self.options.append(option) @@ -107,22 +114,13 @@ def add_option(self, option: str) -> None: def render(self) -> RenderableType: - # 1 borders + 1 space padding on each side tree = Tree("") tree.hide_root = True tree.expanded = True for index, option in enumerate(self.options): label = Text(option) - match option: - case "name": - label = Text("  ") + label - case "date": - label = Text("  ") + label - case "status": - label = Text("  ") + label - case "urgency": - label = Text("  ") + label + label = Text(" ") + label label.pad_right(self.size.width) label.plain = label.plain.ljust(20) @@ -132,7 +130,6 @@ def render(self) -> RenderableType: label.stylize(self.style_focused) meta = { - "@click": f"click_label({index})", "selected": index, } label.apply_meta(meta) @@ -149,10 +146,5 @@ def render_panel(self, tree) -> RenderableType: box=HEAVY, ), vertical="middle", - height=round(get_terminal_size()[1] * 0.8), + height=self._size.height, ) - - async def action_click_label(self, id): - self.highlight(id) - await self.emit(ApplySortMethod(self, self.options[self.highlighted])) - await self.hide() diff --git a/dooit/ui/widgets/status_bar.py b/dooit/ui/widgets/status_bar.py index 872e2d7e..a9de4ea8 100644 --- a/dooit/ui/widgets/status_bar.py +++ b/dooit/ui/widgets/status_bar.py @@ -1,12 +1,15 @@ -from datetime import datetime +from inspect import getfullargspec as get_args +from typing import Callable from rich.console import RenderableType -from rich.text import Text from rich.table import Table +from rich.text import Text, TextType from textual.widget import Widget - -from ...utils.config import conf +from dooit.utils.conf_reader import Config +from dooit.api import manager from ..events import StatusType +bar = Config().get("bar") + class StatusBar(Widget): """ @@ -17,83 +20,55 @@ def __init__(self) -> None: super().__init__() self.message = "" self.status = "NORMAL" - self.color = "blue" self.set_interval(1, self.refresh) - config = conf.load_config("status_bar") - self.theme = config["theme"] - self.clock_icon = config["icons"]["clock"] - self.calendar_icon = config["icons"]["calendar"] - def set_message(self, message) -> None: + def set_message(self, message: TextType = "") -> None: + + if isinstance(message, Text): + message = message.markup + self.message = message self.refresh() def clear_message(self) -> None: - self.set_message("") - - def get_clock(self) -> str: - """ - Returns current time - """ - - return f"{datetime.now().time().strftime(' {0} %X ')}".format(self.clock_icon) - - def get_date(self) -> str: - """ - Returns current time - """ - return f"{self.calendar_icon} {datetime.today().strftime('%d/%m/%Y')}" + self.set_message() def set_status(self, status: StatusType) -> None: self.status = status - match status: - case "NORMAL": - self.color = self.theme["normal"] - case "INSERT": - self.color = self.theme["insert"] - case "DATE": - self.color = self.theme["date"] - case "SEARCH": - self.color = self.theme["search"] - case "SORT": - self.color = self.theme["sort"] self.refresh() + def get_params(self): + return { + "status": self.status, + "message": self.message, + "manager": manager, + } + def render(self) -> RenderableType: - style_clock = self.theme["clock"] - style_date = self.theme["date"] - - bar = Table.grid(padding=(0, 1), expand=True) - bar.add_column("status", justify="center", width=len(self.status) + 1) - bar.add_column("message", justify="left", ratio=1) - bar.add_column("date", justify="center", width=13) - bar.add_column("clock", justify="center", width=12) - - status = Text(f" {self.status}", style=f"reverse {self.color}") - message = Text.from_markup(f" {self.message}", style="magenta on black") - message.pad_right(self.size.width) - - bar.add_row( - status, - message, - Text( - self.get_date(), - style=style_date, - ), - Text( - self.get_clock(), - style=style_clock, - ), - ) - return bar - - -if __name__ == "__main__": - from textual.app import App - - class MyApp(App): - async def on_mount(self): - await self.view.dock(StatusBar()) - - MyApp.run() + table = Table.grid(expand=True, padding=(0, 0)) + table.add_column("A") + table.add_column("B", ratio=1) + table.add_column("C") + + params = self.get_params() + renderables = [] + + for col in "ABC": + temp = Text() + for func in bar[col]: + if isinstance(func, Callable): + args = get_args(func).args + text = func(**{i: params[i] for i in args}) + else: + text = func + + if isinstance(text, Text): + temp += text + else: + temp += Text.from_markup(str(text)) + + renderables.append(temp) + + table.add_row(*renderables) + return table diff --git a/dooit/ui/widgets/todo_list.py b/dooit/ui/widgets/todo_list.py deleted file mode 100644 index 5cead22c..00000000 --- a/dooit/ui/widgets/todo_list.py +++ /dev/null @@ -1,574 +0,0 @@ -import re -from os import get_terminal_size -from datetime import datetime -from typing import Callable -import pyperclip -from rich.align import Align -from rich.console import RenderableType -from rich.text import Text -from textual import events -from textual.widgets import TreeNode, NodeID - -from dooit.ui.widgets.simple_input import View - -from ...ui.widgets import NestedListEdit -from ...ui.widgets.entry import Entry -from ...ui.events import * # NOQA - -NodeDataTye = Entry - -EMPTY_TODO = """ - [b blue][/b blue] - [d white]Wow! so empty? -You can add todo by pressing '[b green]a[/b green]'[/d white] -""" -WARNING = "[b yellow]WARNING[/b yellow]" - -colors = { - 1: "green", - 2: "yellow3", - 3: "orange1", - 4: "indian_red", -} - -urgency_icons = { - 1: "🅓", - 2: "🅒", - 3: "🅑", - 4: "🅐", -} - - -def percentage(percent, total) -> int: - return round(percent * total / 100) - - -class TodoList(NestedListEdit): - """ - A Class that allows editing while displaying trees - """ - - def __init__(self, name: str | None = None): - super().__init__( - "", - Entry(), - name=name, - style_focus="bold grey85", - style_editing="bold cyan", - style_unfocus="bold grey50", - ) - self.focused = None - - from dooit.utils.config import conf - - self.config = conf.load_config("todos") - self.icons = self.config["icons"] - self.keys = conf.keys - - async def _sort_by_arrangement(self, seq: list[int]) -> None: - - parent = self.highlighted_node.parent - if not parent: - return - - tree = parent.tree.children - - dup_tree = [] - node_tree = [] - for i in seq: - dup_tree += (tree[i],) - node_tree += (parent.children[i],) - - parent.tree.children = dup_tree.copy() - parent.children = node_tree.copy() - self.refresh() - - async def _sort(self, func: Callable, reverse: bool = False) -> None: - - parent = self.highlighted_node.parent - if not parent: - return - - dup = list(enumerate(parent.children)) - dup.sort(key=lambda x: func(x[1]), reverse=reverse) - arrangemnt = [i for i, _ in dup] - await self._sort_by_arrangement(arrangemnt) - - async def sort_by_urgency(self) -> None: - await self._sort( - func=lambda node: node.data.urgency, - reverse=True, - ) - - async def sort_by_status(self) -> None: - def f(status: str) -> int: - match status: - case "OVERDUE": - return 1 - case "PENDING": - return 2 - case "COMPLETED": - return 3 - return 0 - - await self._sort( - func=lambda node: f(node.data.status), - ) - - async def sort_by_name(self) -> None: - await self._sort( - func=lambda node: node.data.about.value, - ) - - # TODO - async def sort_by_date(self) -> None: - def f(date): - if not date: - return datetime.max - else: - return datetime(*self._parse_date(date)) - - await self._sort(func=lambda node: f(node.data.due.value)) - - async def sort_by(self, method: str) -> None: - await eval(f"self.sort_by_{method}()") - - def _parse_date(self, date: str) -> tuple: - day = int(date[:2]) - month = int(date[3:5]) - year = int(date[6:]) - - return year, month, day - - def render(self) -> RenderableType: - if self.root.tree.children: - return self._tree - else: - return Align.center( - EMPTY_TODO, - vertical="middle", - height=round(get_terminal_size()[1] * 0.8), - ) - - async def focus_node(self, part="about", status="INSERT") -> None: - if self.highlighted == self.root.id: - return - - self.warn = False - self.focused = part - self.prev_about = self.highlighted_node.data.about.value - self.prev_date = self.highlighted_node.data.due.value - - await self.post_message(ChangeStatus(self, status)) - await super().focus_node(part) - - await self.post_message( - events.Key(self, "right") - ) # handle scrollview late update - - async def unfocus_node(self) -> None: - - ok = await self.check_node() - if not ok: - if self.warn: - await self.remove_node() - await self.post_message(ChangeStatus(self, "NORMAL")) - await super().unfocus_node() - else: - self.warn = True - return - else: - await self.post_message(ChangeStatus(self, "NORMAL")) - await super().unfocus_node() - - async def modify_due_status(self, status: str) -> None: - node = self.highlighted_node - node.data.status = status - - parent = node.parent - if parent and parent != self.root: - if all(child.data.status == "COMPLETED" for child in parent.children): - parent.data.status = "COMPLETED" - else: - parent.data.status = "PENDING" - - elif parent == self.root: - if status == "COMPLETED": - for i in node.children: - i.data.status = "COMPLETED" - - self.refresh() - - async def key_press(self, event: events.Key) -> None: - if self.editing: - match event.key: - case "escape": - await self.unfocus_node() - case "enter": - if ( - self.focused == "about" - and self.highlighted_node.data.about.value - ): - await self.unfocus_node() - if not self.editing: - await self.add_sibling() - - case _: - await self.send_key_to_selected(event) - - else: - keys = self.keys - match event.key: - case i if i in keys.move_down: - await self.cursor_down() - case i if i in keys.shift_down: - await self.shift_down() - case i if i in keys.move_up: - await self.cursor_up() - case i if i in keys.shift_up: - await self.shift_up() - case i if i in keys.move_to_top: - await self.move_to_top() - case i if i in keys.move_to_bottom: - await self.move_to_bottom() - case i if i in keys.toggle_expand: - await self.toggle_expand() - case i if i in keys.toggle_expand_parent: - await self.toggle_expand_parent() - case i if i in keys.add_child: - await self.add_child() - case i if i in keys.add_sibling: - await self.add_sibling() - case i if i in keys.edit_node: - await self.focus_node("about", "INSERT") - case i if i in keys.edit_date: - await self.focus_node("due", "DATE") - case i if i in keys.remove_node: - await self.remove_node() - case i if i in keys.toggle_complete: - await self.mark_complete() - case i if i in keys.increase_urgency: - self.highlighted_node.data.increase_urgency() - case i if i in keys.decrease_urgency: - self.highlighted_node.data.decrease_urgency() - case i if i in keys.yank_todo: - try: - pyperclip.copy(self.highlighted_node.data.about.value) - await self.post_message(Notify(self, "Copied to Clipboard!")) - except: - await self.post_message( - Notify(self, "Cannot copy to Clipboard :(") - ) - - case i if i in keys.move_focus_to_menu: - if not self.editing: - await self.post_message(SwitchTab(self)) - - self.refresh() - - async def mark_complete(self) -> None: - if self.highlighted_node.data.status != "COMPLETED": - await self.modify_due_status("COMPLETED") - else: - await self.modify_due_status("PENDING") - await self.update_due_status() - - async def check_node(self) -> bool: - match self.focused: - case "about": - val = self.highlighted_node.data.about.value.strip() - if not val: - if not self.prev_about: - await self.remove_node() - return True - else: - self.highlighted_node.data.about.value = self.prev_about - await self.post_message( - Notify( - self, f"{WARNING}: Empty todo! Reverting to original" - ) - ) - return True - - if ( - sum( - i.data.about.value == val - for i in (self.highlighted_node.parent or self.root).children - ) - > 1 - ): - await self.post_message( - Notify( - self, - f"{WARNING}: Duplicate todo sibling !" - if not self.warn - else "Todo deleted!", - ) - ) - return False - - case "due": - date = self.highlighted_node.data.due.value.strip() - today = datetime.today() - - if len(date) == 0: - pass - elif re.findall(r"^\d(?:\d?)$", date): - # just day - date = f"{int(date):02}-{today.strftime('%m-%Y')}" - elif re.findall(r"^\d(?:\d?)-\d(?:\d?)$", date): - # day and month - date = date.split("-") - date = f"{int(date[0]):02}-{int(date[1]):02}-{today.strftime('%Y')}" - elif re.findall(r"^\d(?:\d?)-\d(?:\d?)-\d(?:\d?)(?:\d?)(?:\d?)$", date): - date = date.split("-") - year = str(date[2]) - y2k = "2000" # assuming this code is not used after the year 2999 - date = f"{int(date[0]):02}-{int(date[1]):02}-{y2k[:-len(year)] + year}" - else: - await self.post_message( - Notify( - self, - message="Invalid date format! Enter in format: dd-mm-yyyy", - ) - ) - - if len(date) != 0 and not self._is_valid_date(date): - date = self.prev_date - await self.post_message( - Notify(self, message="Please enter a valid date") - ) - else: - await self.post_message( - Notify(self, message="Your due date was updated") - ) - - self.highlighted_node.data.due.value = date - await self.update_due_status() - - self.focused = None - self.refresh() - return True - - def _is_valid_date(self, date: str) -> bool: - try: - datetime.strptime(date, "%d-%m-%Y") - return True - except ValueError: - return False - - def _is_expired(self, date) -> bool: - present = datetime.now() - due = datetime(*self._parse_date(date)) - - return due < present - - async def update_due_status(self) -> None: - date = self.highlighted_node.data.due.value - status = self.highlighted_node.data.status - - if status == "COMPLETED": - return - - if date and self._is_expired(date): - await self.modify_due_status("OVERDUE") - else: - await self.modify_due_status("PENDING") - - def _about_width(self, child: bool): - return percentage(70, self.size.width - 2) - 6 - (child * 3) - - def _due_width(self, child: bool): - return percentage(25, self.size.width - 2) - 6 - (child * 3) - - def _get_entry(self, child: bool) -> Entry: - entry = NodeDataTye() - entry.about.view = View( - 0, percentage(70, self.size.width - 2) - 6 - (child * 3) - ) - entry.due.view = View(0, percentage(25, self.size.width - 2) - 6 - (child * 3)) - return entry - - async def reach_to_node(self, id: TreeNode | NodeID) -> None: - - if isinstance(id, TreeNode): - id = id.id - - if self.nodes[id] in self.root.children: - await self.move_to_top() - while self.highlighted != id: - await self.cursor_down() - else: - await self.reach_to_node(self.nodes[id].parent) - await self.highlighted_node.expand() - while self.highlighted != id: - await self.cursor_down() - - async def add_child(self) -> None: - node = self.highlighted_node - if node == self.root or node.parent == self.root: - await node.add("child", self._get_entry(node != self.root)) - await node.expand() - await self.reach_to_last_child() - await self.focus_node() - self.refresh(layout=True) - - async def add_sibling(self) -> None: - parent = self.highlighted_node.parent - - if not parent: - await self.add_child() - return - else: - children = parent.children - tree = parent.tree.children - - await parent.add("", self._get_entry(child=parent != self.root)) - - i = children.index(self.highlighted_node) - id = children[-1].id - children.insert(i + 1, children.pop()) - tree.insert(i + 1, tree.pop()) - - while self.highlighted != id: - await self.cursor_down() - - await self.focus_node() - self.refresh() - - def render_node(self, node: TreeNode) -> RenderableType: - """ - Renders styled node - """ - - from rich.table import Table - - table = Table.grid(padding=(0, 1), expand=True) - table.add_column("about", justify="left", ratio=70) - table.add_column("due", justify="left", ratio=25) - table.add_column("urgency", justify="left", width=2) - - color = "yellow" - match node.data.status: - case "COMPLETED": - color = "green" - case "OVERDUE": - color = "red" - - table.add_row( - self.render_about(node, color), - self.render_date(node, color), - self.render_urgency(node, color), - ) - - return table - - def _highlight_node(self, node: TreeNode, label: Text) -> Text: - # setup highlight - style_editing = self.config["theme"]["style_editing"] - style_focused = self.config["theme"]["style_focused"] - style_unfocused = self.config["theme"]["style_unfocused"] - - if node.id == self.highlighted: - if self.editing: - label.stylize(style_editing) - else: - label.stylize(style_focused) - else: - label.stylize(style_unfocused) - - if node.data.status == "COMPLETED": - label.stylize("strike") - - return label - - def render_about(self, node, _) -> Text: - # Setting up text - - width = self._about_width(node.parent != self.root) - if ( - not hasattr(node.data.about, "view") - ) or node.data.about.view.end - node.data.about.view.start != width: - node.data.about.view = View(0, width) - - label = Text.from_markup(str(node.data.about.render())) or Text() - label = self._highlight_node(node, label) - - # setup milestone - if children := node.children: - total = len(children) - done = sum(child.data.status == "COMPLETED" for child in children) - - if not (self.highlighted_node == node and self.editing): - label += Text.from_markup(f" ( [green][/green] {done}/{total} )") - - # setup pre-icons - if node != self.root: - match node.data.status: - case "COMPLETED": - label = ( - Text.from_markup( - f"[b green]{self.icons['todo_completed']} [/b green]" - ) - + label - ) - case "PENDING": - label = ( - Text.from_markup( - f"[b yellow]{self.icons['todo_pending']} [/b yellow]" - ) - + label - ) - case "OVERDUE": - label = ( - Text.from_markup( - f"[b red]{self.icons['todo_overdue']} [/b red]" - ) - + label - ) - - meta = { - "@click": "click_about()", - "tree_node": node.id, - "cursor": node.is_cursor, - } - - label.apply_meta(meta) - return label - - def render_date(self, node: TreeNode, color) -> Text: - - icon = self.icons["due_date"] - - width = self._due_width(node.parent != self.root) - if ( - not hasattr(node.data.due, "view") - ) or node.data.due.view.end - node.data.due.view.start != width: - node.data.due.view = View(0, width) - - label = Text.from_markup(str(node.data.due.render())) or Text("No Due Date") - label = self._highlight_node(node, label) - label = Text.from_markup(f"[{color}] {icon} [/{color}]") + label - - meta = { - "@click": "click_date()", - "tree_node": node.id, - "cursor": node.is_cursor, - } - - label.apply_meta(meta) - return label - - def render_urgency(self, node: TreeNode, _) -> Text: - urgency = max(1, min(node.data.urgency, 4)) - node.data.urgency = urgency # for older versions which has >7 support - color = colors.get(urgency) - icon = urgency_icons.get(urgency) - label = Text.from_markup(f"[{color}]{icon}[/{color}]") - return label - - async def action_click_date(self) -> None: - await self.focus_node("due", "DATE") - - async def action_click_about(self) -> None: - await self.focus_node("about", "INSERT") diff --git a/dooit/ui/widgets/todo_tree.py b/dooit/ui/widgets/todo_tree.py new file mode 100644 index 00000000..220e26b9 --- /dev/null +++ b/dooit/ui/widgets/todo_tree.py @@ -0,0 +1,78 @@ +from typing import Optional +from dooit.ui.formatters import TodoFormatter +from dooit.utils import KeyBinder, Config +from dooit.ui.events.events import SwitchTab +from dooit.api import Workspace, Todo +from .tree import TreeList + +conf = Config() +EMPTY_TODO = conf.get("EMPTY_TODO") +dashboard = conf.get("dashboard") +PRINTABLE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " +COLUMN_ORDER = conf.get("COLUMN_ORDER") +format = conf.get("TODO") + + +class TodoTree(TreeList): + """ + Tree structured Class to manage todos + """ + + options = Todo.sortable_fields + EMPTY = dashboard + model_kind = "todo" + model_type = Todo + styler = TodoFormatter(format) + COLS = COLUMN_ORDER + key_manager = KeyBinder() + + def _get_children(self, model: Workspace): + if model: + return model.todos + return [] + + async def switch_pane(self): + if self.filter.value: + await self.stop_search() + + await self.post_message(SwitchTab(self)) + + async def update_table(self, model: Optional[Workspace] = None): + self.EMPTY = EMPTY_TODO if model else dashboard + self.model = model + await self.rearrange() + self.refresh() + + @property + def item(self) -> Todo: + return super().item + + def _setup_table(self) -> None: + super()._setup_table(format["pointer"]) + for col in COLUMN_ORDER: + if col == "description": + d = {"ratio": 1} + elif col == "due": + # 12 -> size of formatted date + # 02 -> padding + d = {"width": 12 + 2 + len(format["due_icon"])} + elif col == "urgency": + d = {"width": 1} + else: + raise TypeError + + self.table.add_column(col, **d) + + # ########################################## + + async def increase_urgency(self): + self.item.increase_urgency() + self.commit() + + async def decrease_urgency(self): + self.item.decrease_urgency() + self.commit() + + async def toggle_complete(self): + self.item.toggle_complete() + self.commit() diff --git a/dooit/ui/widgets/tree.py b/dooit/ui/widgets/tree.py new file mode 100644 index 00000000..f5b2ad9d --- /dev/null +++ b/dooit/ui/widgets/tree.py @@ -0,0 +1,607 @@ +import re +import pyperclip +from textual.geometry import Size +from typing import Any, List, Literal, Optional, Type +from rich.align import Align +from rich.console import Group, RenderableType +from rich.panel import Panel +from rich.text import Text, TextType +from rich.table import Table, box +from textual import events +from textual.reactive import reactive +from textual.widget import Widget +from dooit.ui.formatters import Formatter +from dooit.utils.keybinder import KeyBinder +from dooit.api import Manager, manager, Model +from dooit.ui.widgets.sort_options import SortOptions +from dooit.ui.events.events import ChangeStatus, Notify, SpawnHelp, StatusType +from dooit.utils.conf_reader import Config +from .simple_input import SimpleInput +from .utils import Component, VerticalView + +PRINTABLE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " +conf = Config() +DIM = conf.get("BORDER_DIM") +LIT = conf.get("BORDER_LIT") +RED = conf.get("red") +EMPTY_SEARCH = [f"[{RED}]No items found![/{RED}]"] + + +class SearchEnabledError(Exception): + pass + + +class TreeList(Widget): + """ + An editable tree widget + """ + + _has_focus = False + _rows = {} + current = reactive(-1) + options = [] + EMPTY: List + model_type: Type[Model] = Model + model_kind: Literal["workspace", "todo"] + COLS: List + styler: Formatter + key_manager: KeyBinder + + def __init__( + self, + name: str | None = None, + model: Manager = manager, + ) -> None: + super().__init__(name=name) + self.model = model + + async def on_mount(self) -> None: + self.sort_menu = SortOptions() + self.filter = SimpleInput() + self.sort_menu = SortOptions( + name=f"Sort_{self.name}", + options=self.options, + parent_widget=self, + ) + self.editing = "none" + self.sort_menu.visible = False + self._set_screen() + self._refresh_rows() + + def commit(self) -> None: + manager.commit() + + async def _current_change_callback(self) -> None: + pass + + # ------------ INTERNALS ---------------- + + def validate_current(self, current: int): + if current < 0: + if self.row_vals: + return 0 + else: + return -1 + else: + return min(max(0, current), len(self.row_vals) - 1) + + async def watch_current(self, _old: int, _new: int) -> None: + await self._current_change_callback() + self._fix_view() + self.refresh() + + async def notify(self, message: TextType): + await self.post_message(Notify(self, message)) + + def toggle_highlight(self) -> None: + self._has_focus = not self._has_focus + self.refresh() + + @property + def has_focus(self) -> bool: + return self._has_focus + + @property + def component(self) -> Component: + try: + return self.row_vals[self.current] + except: + raise TypeError(self.row_vals, len(self.row_vals), self.current) + + @property + def item(self) -> Any: + return self.component.item + + # -------------------------------------- + + def _size_updated( + self, size: Size, virtual_size: Size, container_size: Size + ) -> None: + super()._size_updated(size, virtual_size, container_size) + self._set_view() + self.refresh() + + def _fix_view(self) -> None: + return self.view.fix_view(self.current) + + def _set_screen(self) -> None: + y = self._size.height - 3 # Panel + self.view = VerticalView(0, y) + + def _set_view(self) -> None: + prev_size = self.view.height() + curr_size = self._size.height - 3 # Panel + diff = prev_size - curr_size + + if diff <= 0: + self.view.shift_upper(diff) + else: + self.view.shift_lower(-diff) + bottom = max(self.current + 1, self.view.b) + self.view.a = bottom - curr_size + self.view.b = bottom + + self._fix_view() + + def _style_empty(self, empty_values: List): + def aligned(original: List) -> List[Text]: + texts: List[Text] = [] + for text in original: + if not isinstance(text, Text): + text = Text.from_markup(str(text)) + + texts.append(text) + + max_len = max(len(i) for i in texts) + for text in texts: + text.pad_right(max_len - len(text)) + + return texts + + formatted = [] + for text in empty_values: + if not isinstance(text, List): + text = [text] + + formatted.extend(aligned(text)) + + return formatted + + def _get_children(self, model: model_type) -> List[model_type]: + raise NotImplementedError + + def _refresh_rows(self) -> None: + _rows_copy = {item.item.path: item.expanded for item in self._rows.values()} + self._rows = {} + + def add_rows(item: Model, nest_level=0): + + path = item.path + + def push_item(item: Model): + expanded = _rows_copy.get(path, False) + + self._rows[path] = Component( + item, nest_level, len(self._rows), expanded + ) + self._rows[path].index = len(self._rows) - 1 + + if pattern := self.filter.value: + description = getattr(item, "description") + if re.findall(pattern, description): + push_item(item) + for i in self._get_children(item): + add_rows(i, nest_level + 1) + else: + push_item(item) + if self._rows[path].expanded: + for i in self._get_children(item): + add_rows(i, nest_level + 1) + + if self.model: + for i in self._get_children(self.model): + add_rows(i) + + self.row_vals: List[Component] = list(self._rows.values()) + self.refresh() + + async def rearrange(self): + if self.current == -1: + self._refresh_rows() + return + + editing = self.editing + path = self.item.path + old_ibox = SimpleInput() + + if editing != "none": + old_ibox = self.component.fields[editing] + + self._refresh_rows() + + def get_index(path): + for i, j in enumerate(self.row_vals): + if j.item.path == path: + return i + + return -2 + + idx = get_index(path) + if idx == -2: + if editing != "none": + await self._cancel_edit() + + self.current = -2 + else: + self.current = idx + if editing != "none": + self.component.fields[editing] = old_ibox + + self.refresh() + + async def change_status(self, status: StatusType): + await self.post_message(ChangeStatus(self, status)) + + async def start_search(self) -> None: + self.filter.on_focus() + await self.notify(self.filter.render()) + await self.change_status("SEARCH") + + async def stop_search(self, clear: bool = True) -> None: + if clear: + self.filter.clear() + + self.filter.on_blur() + self._refresh_rows() + await self.notify(self.filter.render()) + await self.change_status("NORMAL") + + async def start_edit(self, field: Optional[str]) -> None: + if not field or field == "none": + return + + if field not in self.component.fields.keys(): + await self.notify( + f"[yellow]Can't change [b orange1]`{field}`[/b orange1] here![/yellow]" + ) + return + + if field == "description": + await self.change_status("INSERT") + elif field == "due": + await self.change_status("DATE") + + ibox = self.component.fields[field] + ibox.value = getattr(self.item, f"{field}") + + ibox.move_cursor_to_end() # starting a new edit + self.component.fields[field].on_focus() + self.editing = field + + async def _cancel_edit(self): + await self.stop_edit(edit=False) + + async def _move_to_item(self, item: Model, edit: Optional[str] = None) -> None: + ancestors = [item] + while parent := ancestors[-1].parent: + if not isinstance(parent, self.model_type): + break + + ancestors.append(parent) + + while len(ancestors) > 1: + item = ancestors.pop() + component = self._rows[item.path] + if component.expanded: + break + + component.expand() + self._refresh_rows() + + self.current = self._rows[ancestors[0].path].index + await self.start_edit(edit) + + async def move_up(self) -> None: + self.current -= 1 + + async def move_down(self) -> None: + self.current += 1 + + async def move_to_top(self) -> None: + self.current = 0 + + async def move_to_bottom(self) -> None: + self.current = len(self.row_vals) + + async def sort_menu_toggle(self) -> None: + await self.change_status("SORT") + self.sort_menu.visible = True + + async def switch_pane(self) -> None: + pass + + async def handle_key(self, event: events.Key) -> None: + + event.stop() + key = ( + event.character + if (event.character and (event.character in PRINTABLE)) + else event.key + ) + + if self.editing != "none": + field = self.row_vals[self.current].fields[self.editing] + + if key == "escape": + await self._cancel_edit() + elif key == "enter": + await self.stop_edit() + else: + await field.handle_keypress(key) + + else: + + if self.sort_menu.visible: + await self.sort_menu.handle_key(key) + + elif self.filter.has_focus: + if key == "escape": + await self.stop_search() + elif key == "enter": + await self.stop_search(clear=False) + if not self.row_vals: + await self.stop_search() + await self.notify(f"[{RED}]No item found![/]") + else: + await self.filter.handle_keypress(key) + await self.notify(self.filter.render()) + self._refresh_rows() + + elif self.filter.value and key == "enter": + await self.move_to_filter_item() + + else: + + self.key_manager.attach_key(key) + bind = self.key_manager.get_method() + if bind: + await self.change_status("NORMAL") + if hasattr(self, bind.func_name): + func = getattr(self, bind.func_name) + if bind.check_for_cursor and self.current == -1: + return + + try: + await func(*bind.params) + self.current = self.current + except SearchEnabledError: + if self.current != -1: + await self.move_to_filter_item() + await func(*bind.params) + + else: + await self.notify( + "[yellow]Cannot perform this operation here![/yellow]" + ) + + self.refresh() + + async def move_to_filter_item(self): + if self.current != -1: + item = self.item + await self.stop_search() + await self._move_to_item(item) + + async def spawn_help(self): + if self.app.screen.name != "help": + await self.post_message(SpawnHelp(self)) + + def add_row(self, row: Component, highlight: bool) -> None: # noqa + + entry = [] + kwargs = {i: str(j.render()) for i, j in row.fields.items()} + + for column in self.COLS: + res = self.styler.style(column, row.item, highlight, self.editing, kwargs) + entry.append(res) + + return self.push_row(entry, row.depth, highlight) + + def _setup_table(self, pointer: TextType = "") -> None: + if isinstance(pointer, str): + pointer = Text.from_markup(pointer) + + self.pointer = pointer + self.table = Table.grid(expand=True) + if width := len(pointer.plain): + self.table.add_column("pointer", width=width) + + def make_table(self) -> None: + self._setup_table() + + for i in self.view.range(): + try: + self.add_row(self.row_vals[i], i == self.current) + except: + pass + + def push_row(self, row: List[Text], padding: int, pointer: bool) -> None: + if row: + if pointer: + row.insert(0, self.pointer) + else: + row.insert(0, Text(len(self.pointer) * " ")) + + if not hasattr(self, "pad_index"): + self.pad_index = 0 + + for i, j in enumerate(self.table.columns): + if j.header == "description": + self.pad_index = i + break + + if row: + hint = Text(" " * padding) + row[self.pad_index] = hint + row[self.pad_index] + row[self.pad_index].highlight_regex(self.filter.value, style="b red") + + self.table.add_row(*row) + + def render(self) -> RenderableType: + + if self.sort_menu.visible: + return self.render_panel(self.sort_menu.render()) + + if self.row_vals: + self.make_table() + return self.render_panel(self.table) + + if self.filter.value and not self.row_vals: + EMPTY = EMPTY_SEARCH + else: + EMPTY = self.EMPTY + + EMPTY = self._style_empty(EMPTY) + to_render = Align.center( + Group( + *[Align.center(i) for i in EMPTY], + ), + vertical="middle", + ) + return self.render_panel(to_render) + + def render_panel(self, renderable: RenderableType): + height = self._size.height + return Panel( + renderable, + expand=True, + height=height, + box=box.HEAVY, + border_style="b " + LIT if self._has_focus else "d " + DIM, + ) + + async def copy_text(self) -> None: + if self.current != -1: + pyperclip.copy(self.item.description) + await self.notify("[green]Description copied to clipboard![/]") + else: + await self.notify("[red]No item selected![/]") + + # COMMANDS TO INTERACT WITH API + async def stop_edit(self, edit: bool = True) -> None: + if self.editing == "none" or self.current == -1: + return + + editing = self.editing + simple_input = self.component.fields[editing] + old_val = getattr(self.component.item, self.editing) + + if not edit: + simple_input.value = old_val + + res = self.component.item.edit(self.editing, simple_input.value) + + await self.notify(res.text()) + if not res.ok: + if res.cancel_op: + await self.remove_item(move_cursor_up=True) + await self._current_change_callback() + else: + self.commit() + + simple_input.on_blur() + if self.current != -1: + self.component.refresh() + + self.editing = "none" + await self.change_status("NORMAL") + + if edit and not old_val and editing == "description": + await self.add_sibling() + + def _drop(self) -> None: + self.item.drop() + + def _add_child(self) -> model_type: + model = self.item if self.current != -1 else self.model + return model.add_child(self.model_kind, inherit=True) + + def _add_sibling(self) -> model_type: + if self.current > -1: + return self.item.add_sibling(True) + else: + return self._add_child() + + def _shift_down(self) -> None: + return self.item.shift_down() + + def _shift_up(self) -> None: + return self.item.shift_up() + + async def remove_item(self, move_cursor_up: bool = False) -> None: + commit = self.item.description != "" + self._drop() + self._refresh_rows() + self.current -= move_cursor_up + if commit: + self.commit() + + await self._current_change_callback() + + async def add_child(self) -> None: + if self.filter.value: + raise SearchEnabledError + + if self.current != -1: + self.component.expand() + + child = self._add_child() + self._refresh_rows() + await self._move_to_item(child, "description") + + async def add_sibling(self) -> None: + + if self.filter.value: + raise SearchEnabledError + + if self.current == -1: + sibling = self._add_child() + else: + sibling = self._add_sibling() + + self._refresh_rows() + await self._move_to_item(sibling, "description") + + async def shift_up(self) -> None: + self._shift_up() + await self.move_up() + self._refresh_rows() + self.commit() + + async def shift_down(self) -> None: + self._shift_down() + await self.move_down() + self._refresh_rows() + self.commit() + + async def toggle_expand(self) -> None: + self.component.toggle_expand() + self._refresh_rows() + + async def toggle_expand_parent(self) -> None: + parent = self.item.parent + if not parent: + return + + if parent.path in self._rows: + index = self._rows[parent.path].index + self.current = index + + await self.toggle_expand() + + def sort(self, attr: str) -> None: + curr = self.item.path + self.item.sort(attr) + self._refresh_rows() + self.current = self._rows[curr].index + self.commit() diff --git a/dooit/ui/widgets/utils.py b/dooit/ui/widgets/utils.py new file mode 100644 index 00000000..8200e9b0 --- /dev/null +++ b/dooit/ui/widgets/utils.py @@ -0,0 +1,83 @@ +from typing import Iterable +from .simple_input import SimpleInput + + +class Component: + """ + Component class to maintain each row's data + """ + + def __init__( + self, + item, + depth: int = 0, + index: int = 0, + expanded: bool = False, + ) -> None: + self.item = item + self.expanded = expanded + self.depth = depth + self.index = index + self.fields = { + field: SimpleInput( + value=getattr(item, field), + ) + for field in item.fields + } + + def refresh(self) -> None: + for field in self.fields.keys(): + self.fields[field] = SimpleInput( + value=getattr( + self.item, + field, + ) + ) + + def get_field_values(self) -> Iterable[SimpleInput]: + return self.fields.values() + + def toggle_expand(self) -> None: + self.expanded = not self.expanded + + def expand(self, expand: bool = True) -> None: + self.expanded = expand + + +class VerticalView: + """ + Vertical view to manage scrolling + """ + + def __init__(self, a: int, b: int) -> None: + self.a = a + self.b = b + + def fix_view(self, current: int) -> None: + if self.a < 0: + self.shift(abs(self.a)) + + if current <= self.a: + self.shift(current - self.a) + + if self.b <= current: + self.shift(current - self.b) + + def shift_upper(self, delta) -> None: + self.a += delta + + def shift_lower(self, delta) -> None: + self.b += delta + + def shift(self, delta: int) -> None: + self.shift_lower(delta) + self.shift_upper(delta) + + def height(self) -> int: + return self.b - self.a + + def range(self) -> Iterable[int]: + if self.a < 0: + self.shift(abs(self.a)) + + return range(self.a, self.b + 1) diff --git a/dooit/ui/widgets/workspace_tree.py b/dooit/ui/widgets/workspace_tree.py new file mode 100644 index 00000000..1629fadc --- /dev/null +++ b/dooit/ui/widgets/workspace_tree.py @@ -0,0 +1,55 @@ +from typing import List +from dooit.ui.formatters import WorkspaceFormatter +from dooit.utils.keybinder import KeyBinder +from dooit.api import Manager, Workspace +from dooit.ui.events import TopicSelect, SwitchTab +from dooit.utils.conf_reader import Config +from .tree import TreeList + +conf = Config() +EMPTY_WORKSPACE = conf.get("EMPTY_WORKSPACE") +format = conf.get("WORKSPACE") + + +class WorkspaceTree(TreeList): + """ + NavBar class to manage UI's navbar + """ + + options = Workspace.sortable_fields + EMPTY = EMPTY_WORKSPACE + model_kind = "workspace" + model_type = Workspace + styler = WorkspaceFormatter(format) + COLS = ["description"] + key_manager = KeyBinder() + + async def _current_change_callback(self) -> None: + if self.current == -1: + await self.post_message(TopicSelect(self, None)) + else: + await self.post_message(TopicSelect(self, self.item)) + + async def _refresh_data(self): + await self.rearrange() + await self._current_change_callback() + + def _setup_table(self) -> None: + super()._setup_table(format["pointer"]) + self.table.add_column("description", ratio=1) + + async def switch_pane(self) -> None: + if self.current == -1: + return + + if self.filter.value: + if self.current != -1: + await self._current_change_callback() + + await self.stop_search() + self.current = -1 + + await self.post_message(SwitchTab(self)) + + def _get_children(self, model: Manager) -> List[Workspace]: + return model.workspaces diff --git a/dooit/utils/__init__.py b/dooit/utils/__init__.py index 6c006eda..de98c432 100644 --- a/dooit/utils/__init__.py +++ b/dooit/utils/__init__.py @@ -1,8 +1,7 @@ from .parser import Parser -from .config import Config, conf +from .conf_reader import Config +from .watcher import Watcher +from .keybinder import KeyBinder +from .dateparser import parse -__all__ = [ - "Parser", - "Config", - "conf", -] +__all__ = ["Parser", "Config", "Watcher", "KeyBinder", "parse"] diff --git a/dooit/utils/conf_reader.py b/dooit/utils/conf_reader.py new file mode 100644 index 00000000..348eff61 --- /dev/null +++ b/dooit/utils/conf_reader.py @@ -0,0 +1,46 @@ +from importlib.machinery import ModuleSpec +import importlib.util +from os import path +from typing import Any, Dict, Optional +import appdirs +import sys + +sys.path.append(appdirs.user_config_dir("dooit")) +user_config = path.join(appdirs.user_config_dir("dooit"), "config.py") +default_config = path.join(path.dirname(__file__), "default_config.py") + +default_spec = importlib.util.spec_from_file_location("default_config", default_config) +if path.isfile(user_config): + user_spec = importlib.util.spec_from_file_location("user_config", user_config) +else: + user_spec = default_spec + + +def get_vars(spec: Optional[ModuleSpec]) -> Dict[str, Any]: + if spec and spec.loader: + foo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(foo) + return vars(foo) + + return {} + + +def combine_into(d: dict, to: dict) -> None: + for k, v in d.items(): + if isinstance(v, dict): + combine_into(v, to.setdefault(k, {})) + else: + to[k] = v + + +class Config: + def __init__(self) -> None: + self._d = {} + self.update() + + def update(self): + for i in [default_spec, user_spec]: + combine_into(get_vars(i), self._d) + + def get(self, var: str) -> Any: + return self._d[var] diff --git a/dooit/utils/config.py b/dooit/utils/config.py deleted file mode 100644 index 05059d42..00000000 --- a/dooit/utils/config.py +++ /dev/null @@ -1,92 +0,0 @@ -import os -import yaml -from pathlib import Path -from os import environ, path - -HOME = Path.home() -XDG_CONFIG = Path(path.expanduser(environ.get("XDG_CONFIG_HOME") or (HOME / ".config"))) -DOOIT = XDG_CONFIG / "dooit" -CONFIG = DOOIT / "config.yaml" - -SAMPLE = Path(__file__).parent.absolute() / "example_config.yaml" - - -class Key: - def __init__(self, keybinds: dict[str, list[str]]): - # for i, j in keybinds.items(): - # setattr(self, i, j) - - self.move_to_top = keybinds["move_to_top"] - self.move_to_bottom = keybinds["move_to_bottom"] - self.move_down = keybinds["move_down"] - self.shift_down = keybinds["shift_down"] - self.move_up = keybinds["move_up"] - self.shift_up = keybinds["shift_up"] - self.edit_node = keybinds["edit_node"] - self.edit_date = keybinds["edit_date"] - self.toggle_complete = keybinds["toggle_complete"] - self.yank_todo = keybinds["yank_todo"] - self.toggle_expand = keybinds["toggle_expand"] - self.toggle_expand_parent = keybinds["toggle_expand_parent"] - self.remove_node = keybinds["remove_node"] - self.add_sibling = keybinds["add_sibling"] - self.add_child = keybinds["add_child"] - self.spawn_sort_menu = keybinds["spawn_sort_menu"] - self.start_search = keybinds["start_search"] - self.select_node = keybinds["select_node"] - self.move_focus_to_menu = keybinds["move_focus_to_menu"] - self.show_help = keybinds["show_help"] - self.increase_urgency = keybinds["increase_urgency"] - self.decrease_urgency = keybinds["decrease_urgency"] - - -class Config: - def __init__(self) -> None: - self.check_files() - - def make_new_config(self): - with open(SAMPLE, "r", encoding="utf8") as f: - with open(CONFIG, "w", encoding="utf8") as stream: - stream.write(f.read()) - - def check_files(self): - def check_folder(f): - if not Path.is_dir(f): - os.mkdir(f) - - check_folder(XDG_CONFIG) - check_folder(DOOIT) - - if not CONFIG.is_file(): - self.make_new_config() - - try: - self.keybinds = self.load_keybindings() - self.keys = Key(self.keybinds) - except: - self.make_new_config() - self.keybinds = self.load_keybindings() - self.keys = Key(self.keybinds) - - def load_config(self, part: str = "main", sub: str | None = None) -> dict: - with open(CONFIG, "r", encoding="utf8") as stream: - try: - if sub: - return yaml.safe_load(stream)[part][sub] - else: - return yaml.safe_load(stream)[part] - except: - self.make_new_config() - return self.load_config(part, sub) - - def load_keybindings(self) -> dict[str, list[str]]: - keybinds = self.load_config("keybindings") - for key in list(keybinds.keys()): - bind = keybinds[key] - if isinstance(bind, str): - keybinds[key] = [bind] - - return keybinds - - -conf = Config() diff --git a/dooit/utils/dateparser.py b/dooit/utils/dateparser.py new file mode 100644 index 00000000..c99681db --- /dev/null +++ b/dooit/utils/dateparser.py @@ -0,0 +1,18 @@ +from dateparser import parse as dateparse +from threading import Thread +from os import environ + +# from dooit.utils.conf_reader import Config + +DATE_ORDER = environ.get("DOOIT_DATE_ORDER", "DMY") + + +def parse(value: str): + if value == "none": + return None + + return dateparse(value, settings={"DATE_ORDER": DATE_ORDER}) + + +# HACK: Deal with dateparser slowness +Thread(target=parse, args=("",), daemon=True).start() diff --git a/dooit/utils/default_config.py b/dooit/utils/default_config.py new file mode 100644 index 00000000..d2a2cb28 --- /dev/null +++ b/dooit/utils/default_config.py @@ -0,0 +1,150 @@ +from rich.text import Text +from datetime import datetime +import os + +# NOTE: See rich style documentation for details + +################################# +# UTILS # +################################# + + +def colored(text: str, color: str): + return f"[{color}]{text}[/]" + + +def get_status(status): + return colored(f" {status} ", "r " + blue) + + +def get_message(message): + return " " + message + + +def get_clock() -> Text: + return Text(f"{datetime.now().time().strftime(' %X ')}", "r " + cyan) + + +def get_username(): + return Text(f" {os.getlogin()} ", "r " + blue) + + +################################# +# COLORS # +################################# +black = "#2e3440" +white = "#e5e9f0" +grey = "#d8dee9" +red = "#bf616a" +frost_green = "#8fbcbb" +cyan = "#88c0d0" +green = "#a3be8c" +yellow = "#ebcb8b" +blue = "#81a1c1" +magenta = "#b48ead" +orange = "#d08770" + + +################################# +# GENERAL # +################################# +BACKGROUND = black +BORDER_DIM = white +BORDER_LIT = cyan + +################################# +# DASHBOARD # +################################# + +ART = """ +██████╗ █████╗ ███████╗██╗ ██╗██████╗ ██████╗ █████╗ ██████╗ ██████╗ +██╔══██╗██╔══██╗██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔══██╗██╔══██╗██╔══██╗ +██║ ██║███████║███████╗███████║██████╔╝██║ ██║███████║██████╔╝██║ ██║ +██║ ██║██╔══██║╚════██║██╔══██║██╔══██╗██║ ██║██╔══██║██╔══██╗██║ ██║ +██████╔╝██║ ██║███████║██║ ██║██████╔╝╚██████╔╝██║ ██║██║ ██║██████╔╝ +╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝\ +""" + +ART = colored(ART, frost_green) +NL = " \n" +SEP = colored("─" * 60, "d " + grey) +help_message = f"Press {colored('?', magenta)} to spawn help menu" +dashboard = [ART, NL, SEP, NL, NL, NL, help_message] + + +################################# +# WORKSPACE # +################################# +WORKSPACE = { + "dim": grey, + "highlight": white, + "editing": cyan, + "pointer": "> ", + "children_hint": "", # "[{count}]", # vars: count +} +EMPTY_WORKSPACE = [ + "🍻", + "No workspaces yet?", + f"Press {colored('a', cyan)} to add some!", +] + +################################# +# TODOS # +################################# + + +COLUMN_ORDER = ["description", "due", "urgency"] # order of columns +TODO = { + "color_todos": False, + "dim": grey, + "highlight": white, + "editing": cyan, + "pointer": "> ", + "children_hint": colored( + " ({done}/{total})", green + ), # vars: remaining, done, total + # "children_hint": "[b magenta]({remaining}!)[/b magenta]", # vars: remaining, done, total + "due_icon": "🕑", + "effort_icon": "🗲 ", + "effort_color": yellow, + "recurrence_icon": " ⟲ ", + "recurrence_color": blue, + "tags_icon": "🖈 ", + "tags_seperator": "icon", # icon, pipe, comma + "tags_color": red, + "completed_icon": "✓ ", + "pending_icon": "● ", + "overdue_icon": "! ", + "urgency1_icon": "🅐", + "urgency2_icon": "🅑", + "urgency3_icon": "🅒", + "urgency4_icon": "🅓", +} + +EMPTY_TODO = [ + "🤘", + "Wow so Empty!?", + "Add some todos to get started!", +] + +################################# +# STATUS BAR # +################################# +bar = { + "A": [get_status], + "B": [get_message], + "C": [get_clock, get_username], +} + +################################# +# KEYBINDING # +################################# +keybindings = { + "switch pane": "", + "sort menu toggle": "", + "start search": ["/", "S"], + "remove item": "xx", + "edit tags": "t", + "edit effort": "e", + "edit recurrence": "r", +} diff --git a/dooit/utils/example_config.yaml b/dooit/utils/example_config.yaml deleted file mode 100644 index 1677eaf4..00000000 --- a/dooit/utils/example_config.yaml +++ /dev/null @@ -1,97 +0,0 @@ -# rich colors: https://rich.readthedocs.io/en/stable/appendix/colors.html -# nerd fonts: https://www.nerdfonts.com/ - -main: - # To show or not show top bars - show_headers: false - - # Colors for the border on menu and todo lists! - body_dim: dim white - body_highlight: blue - - # Colors for the headings - header_dim: dim white - header_highlight: blue - - welcome_message: |+ - Welcome to doit ! :wave: - - ascii_art: |+ - ██████╗ ██████╗ ██████╗ ██╗████████╗ - ██╔══██╗██╔═══██╗██╔═══██╗██║╚══██╔══╝ - ██║ ██║██║ ██║██║ ██║██║ ██║ - ██║ ██║██║ ██║██║ ██║██║ ██║ - ██████╔╝╚██████╔╝╚██████╔╝██║ ██║ - ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ - -menu: - # Config for the side menu! - icons: - nested_close:  - nested_open: ﱮ - single_topic:  - - theme: - style_editing: reverse red # style while editing the node - style_focused: reverse red # style on hover/highlight - style_unfocused: white # style for the others - -status_bar: - # Config for the status bar at bottom - theme: - clock: reverse yellow - date: reverse green - - # MODES - insert: cyan - message: purple1 - normal: blue - search: magenta - sort: green - - icons: - calendar:  - clock:  - -todos: - icons: - due_date:  - todo_completed:  - todo_overdue:  - todo_pending:  - urgency:  - - theme: - style_editing: bold cyan - style_focused: bold grey85 - style_unfocused: bold grey50 - -keybindings: - # Multiple keybindings goes in list like [g, home] and single with only the charater or combo - # eg: X, ctrl+X - # NOTE: for tab use 'ctrl+i' but shift-tab use "shift+tab" only - # Reference: https://unix.stackexchange.com/questions/563469/conflict-ctrl-i-with-tab-in-normal-mode/563480#563480 - # NOTE: Also, if changing something throws an error, maybe surround the keybind with "" - - move_to_top: [g, home] - move_to_bottom: [G, end] - move_down: [j, down] - shift_down: [J, shift+down] - move_up: [k, up] - shift_up: [K, shift+up] - edit_node: i - edit_date: d - toggle_complete: c - yank_todo: y # copy todo's about - toggle_expand: z - toggle_expand_parent: Z - remove_node: x - add_sibling: a - add_child: A - spawn_sort_menu: ctrl+s - start_search: / - select_node: enter - move_focus_to_menu: h - show_help: "?" - increase_urgency: ["+", "="] - decrease_urgency: ["-", "_"] diff --git a/dooit/utils/keybinder.py b/dooit/utils/keybinder.py new file mode 100644 index 00000000..d5a20240 --- /dev/null +++ b/dooit/utils/keybinder.py @@ -0,0 +1,118 @@ +from collections import defaultdict +from typing import DefaultDict, Dict, List, Optional, Union +from dooit.utils.conf_reader import Config + +configured_keys = Config().get("keybindings") + + +class Bind: + exclude_cursor_check = [ + "add_sibling", + "change_status", + "move_down", + "move_up", + "switch_pane", + "spawn_help", + "start_search", + "stop_search", + ] + + def __init__(self, func_name: str, params: List[str]) -> None: + self.func_name = func_name + self.params = params + self.check_for_cursor = func_name not in self.exclude_cursor_check + + +KeyList = Dict[str, Union[str, List]] +PRINTABLE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " +DEFAULTS = { + "stop search": "", + "switch pane": "", + "move up": ["k", ""], + "shift up": ["K", ""], + "move down": ["j", ""], + "shift down": ["J", ""], + "edit description": "i", + "toggle expand": "z", + "toggle expand parent": "Z", + "add child": "A", + "add sibling": "a", + "remove item": "x", + "move to top": ["g", ""], + "move to bottom": ["G", ""], + "sort menu toggle": "s", + "start search": "/", + "spawn help": "?", + "copy text": "y", + "toggle complete": "c", + "edit due": "d", + "edit tags": "t", + "edit recurrence": "r", + "increase urgency": ["+", "="], + "decrease urgency": ["-", "_"], +} + +configured_keys = DEFAULTS | configured_keys + + +class KeyBinder: + # KEYBIND MANAGER FOR NORMAL MODE + + def __init__(self) -> None: + self.pressed = "" + self.methods: Dict[str, Bind] = {} + self.raw: DefaultDict[str, List[str]] = defaultdict(list) + self.add_keys(configured_keys) + + def convert_to_bind(self, cmd: str): + func_split = cmd.split() + if func_split[0] == "edit": + return Bind("start_edit", [func_split[1]]) + else: + return Bind("_".join(func_split), []) + + def add_keys(self, keys: KeyList) -> None: + for cmd, key in keys.items(): + + if isinstance(key, str): + key = [key] + + for k in key: + if k not in self.raw[cmd]: + self.raw[cmd].append(k) + + self.methods[k] = self.convert_to_bind(cmd) + + def attach_key(self, key: str) -> None: + if key == "escape" and self.pressed: + return self.clear() + + if len(key) > 1: + key = f"<{key}>" + + self.pressed += key + + def clear(self) -> None: + self.pressed = "" + + def find_keys(self) -> List: + + possible_bindings = filter( + lambda keybind: keybind.startswith(self.pressed), + self.methods.keys(), + ) + return list(possible_bindings) + + def get_method(self) -> Optional[Bind]: + + possible_keys = self.find_keys() + if self.pressed and possible_keys: + if len(possible_keys) == 1 and possible_keys[0] == self.pressed: + method = self.methods.get(possible_keys[0]) + self.clear() + return method + else: + return Bind("change_status", ["K PENDING"]) + else: + self.clear() + return Bind("change_status", ["NORMAL"]) diff --git a/dooit/utils/parser.py b/dooit/utils/parser.py index 23007bce..c17ba2d2 100644 --- a/dooit/utils/parser.py +++ b/dooit/utils/parser.py @@ -1,189 +1,64 @@ +import appdirs import yaml +import os +from typing import Dict from pathlib import Path -from os import mkdir, remove, environ -from pickle import load +from os import mkdir -from ..ui.widgets import Entry, Navbar, SimpleInput, TodoList -from ..utils.config import HOME, XDG_CONFIG +XDG_CONFIG = Path(appdirs.user_config_dir("dooit")) +XDG_DATA = Path(appdirs.user_data_dir("dooit")) class Parser: - def __init__(self) -> None: - self.check_files() - - def fix_deprecated(self): - remove(self.old_topic_path) - remove(self.old_todo_path) - - # -------------------------------- - - def save(self, todo: dict[str, TodoList]): - def make_yaml(todolist: TodoList): - arr = [] - for parent in todolist.root.children: - txt = parent.data.to_txt() - arr.append([txt]) - if parent.children: - arr[-1].append([child.data.to_txt() for child in parent.children]) - - return arr - - todolist = {} - for topic, task in todo.items(): - if not topic or topic == "/": - continue - - if topic.count("/") == 1: - todolist[topic[:-1]] = {"common": make_yaml(task)} - else: - idx = topic.index("/") - super_topic = topic[:idx] - sub = topic[idx + 1 : -1] - if sub != "common": - todolist[super_topic] |= {sub: make_yaml(task)} - - with open(self.todo_yaml, "w") as f: - yaml.safe_dump(todolist, f) - - async def load(self): - if self.old_todo_path.is_file() and self.old_topic_path.is_file(): - x = (await self.load_topic(), await self.load_todo()) - self.fix_deprecated() - return x - - with open(self.todo_yaml, "r") as f: - todos = yaml.safe_load(f) or dict() - - navbar = Navbar() - todo_tree = {} - - for topic, subtopics in todos.items(): - s = SimpleInput() - s.value = topic - topic += "/" - await navbar.root.add("", s) - - todo_tree[topic] = TodoList() - - for subtopic, parents in subtopics.items(): - - if subtopic != "common": - s = SimpleInput() - s.value = subtopic - await navbar.root.children[-1].add("", s) - - name = topic + subtopic + "/" - if name not in todo_tree: - todo_tree[name] = TodoList() - - for parent in parents: - - children = [] - if len(parent) > 1: - children = parent[1] - - parent = parent[0] - if subtopic == "common": - tree = todo_tree[topic] - else: - tree = todo_tree[name] - - tree = tree.root - await tree.add("", Entry.from_txt(parent)) - - for child in children: - await tree.children[-1].add("", Entry.from_txt(child)) + """ + Parser class to manage and parse dooit's config and data + """ - return navbar, todo_tree + @property + def last_modified(self) -> float: + return os.stat(self.todo_yaml).st_mtime - # -------------------------------- - - # DEPRECATED: will be removed in v0.3.0 - async def load_topic(self) -> Navbar: - with open(self.old_topic_path, "rb") as f: - return await self.convert_topic(load(f)) - - # DEPRECATED: will be removed in v0.3.0 - async def load_todo(self) -> dict[str, TodoList]: - with open(self.old_todo_path, "rb") as f: - return {i: await self.convert_todo(j) for i, j in load(f).items()} - - # -------------------------------- - - # DEPRECATED: will be removed in v0.3.0 - async def convert_todo(self, e) -> TodoList: - x = TodoList() - for i, j in e: - s = Entry.from_encoded(i) - await x.root.add("", s) - for k in j: - s = Entry.from_encoded(k) - await x.root.children[-1].add("", s) - - return x - - # DEPRECATED: will be removed in v0.3.0 - async def convert_topic(self, e) -> Navbar: - x = Navbar() - for i, j in e: - s = SimpleInput() - s.value = i - await x.root.add("", s) - for k in j: - s = SimpleInput() - s.value = k - await x.root.children[-1].add("", s) - - return x - - # -------------------------------- + def __init__(self) -> None: + self.check_files() - # DEPRECATED: will be removed in v0.3.0 - def fetch_usable_info_todo(self, todo: TodoList) -> list: - x = [] - for i in todo.root.children: - x.append([i.data.encode(), [j.data.encode() for j in i.children]]) + def save(self, data): + """ + Save the todos to data file + """ - return x + with open(self.todo_yaml, "w") as stream: + yaml.safe_dump(data, stream, sort_keys=False) - # DEPRECATED: will be removed in v0.3.0 - def fetch_usable_info_topic(self, topic: Navbar) -> list: - x = [] - for i in topic.root.children: - x.append([i.data.value, [j.data.value for j in i.children]]) + def load(self) -> Dict: + """ + Retrieves the todos from data file + """ - return x + with open(self.todo_yaml, "r") as stream: + data = yaml.safe_load(stream) - # -------------------------------- + return data def check_files(self) -> None: - def check_folder(f): + """ + Checks if all the files and folders are present + to avoid any errors + """ + + def check_folder(f: Path): if not Path.is_dir(f): mkdir(f) check_folder(XDG_CONFIG) + check_folder(XDG_DATA) - dooit = XDG_CONFIG / "dooit" - check_folder(dooit) - - if data := environ.get("XDG_DATA_HOME"): - data_path = Path(data) - else: - local = HOME / ".local" - check_folder(local) - data_path = local / "share" - check_folder(data_path) + self.todo_yaml = XDG_DATA / "todo.yaml" + self.config_file = XDG_CONFIG / "config.py" - dooit_data = data_path / "dooit" - check_folder(dooit_data) - - self.old_todo_path = dooit / "todos.pkl" - self.old_topic_path = dooit / "topics.pkl" - - self.todo_yaml = dooit_data / "todo.yaml" if not Path.is_file(self.todo_yaml): with open(self.todo_yaml, "w") as f: - yaml.safe_dump( - dict(), - f, - ) + yaml.safe_dump(dict(), f) + + if not Path.is_file(self.config_file): + with open(self.config_file, "w") as f: + pass diff --git a/dooit/utils/watcher.py b/dooit/utils/watcher.py new file mode 100644 index 00000000..8975df42 --- /dev/null +++ b/dooit/utils/watcher.py @@ -0,0 +1,28 @@ +import os +import appdirs + +DIR = appdirs.user_data_dir("dooit") +filename = os.path.join(DIR, "todo.yaml") + + +class Watcher: + """ + Watcher class for detecting todo data file changes + """ + + def __init__(self): + self._cached_stamp = -1 + self.filename = filename + + def has_modified(self) -> bool: + """ + Checks if the file has modified since last cached time + """ + + stamp = os.stat(self.filename).st_mtime + if abs(stamp - self._cached_stamp) >= 10**-6: + res = self._cached_stamp != -1 + self._cached_stamp = stamp + return res + + return False diff --git a/main.py b/main.py new file mode 100644 index 00000000..0e482c64 --- /dev/null +++ b/main.py @@ -0,0 +1,3 @@ +from dooit.__init__ import main + +main() diff --git a/poetry.lock b/poetry.lock index 4abb040b..ecf9a538 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,38 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + [[package]] name = "black" -version = "22.6.0" +version = "22.12.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] [package.dependencies] click = ">=8.0.0" @@ -19,55 +47,6 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "cachecontrol" -version = "0.12.11" -description = "httplib2 caching for requests" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""} -msgpack = ">=0.5.2" -requests = "*" - -[package.extras] -filecache = ["lockfile (>=0.9)"] -redis = ["redis (>=2.10.5)"] - -[[package]] -name = "cachy" -version = "0.3.0" -description = "Cachy provides a simple yet effective caching library." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -redis = ["redis (>=3.3.6,<4.0.0)"] -memcached = ["python-memcached (>=1.59,<2.0)"] -msgpack = ["msgpack-python (>=0.5,<0.6)"] - -[[package]] -name = "certifi" -version = "2022.6.15" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - [[package]] name = "cfgv" version = "3.3.1" @@ -75,118 +54,88 @@ description = "Validate configuration and produce human readable error messages. category = "dev" optional = false python-versions = ">=3.6.1" - -[[package]] -name = "charset-normalizer" -version = "2.1.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] - -[[package]] -name = "cleo" -version = "0.8.1" -description = "Cleo allows you to create beautiful and testable command-line interfaces." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -clikit = ">=0.6.0,<0.7.0" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] [[package]] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -[[package]] -name = "clikit" -version = "0.6.2" -description = "CliKit is a group of utilities to build beautiful and testable command line interfaces." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -pastel = ">=0.2.0,<0.3.0" -pylev = ">=1.3,<2.0" - [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" category = "main" optional = false -python-versions = "*" - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[[package]] -name = "crashtest" -version = "0.3.1" -description = "Manage Python errors with ease" -category = "main" -optional = false -python-versions = ">=3.6,<4.0" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] -name = "cryptography" -version = "37.0.4" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +name = "dateparser" +version = "1.1.7" +description = "Date parsing library designed to parse dates from HTML pages" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "dateparser-1.1.7-py2.py3-none-any.whl", hash = "sha256:fbed8b738a24c9cd7f47c4f2089527926566fe539e1a06125eddba75917b1eef"}, + {file = "dateparser-1.1.7.tar.gz", hash = "sha256:ff047d9cffad4d3113ead8ec0faf8a7fc43bab7d853ac8715e071312b53c465a"}, +] [package.dependencies] -cffi = ">=1.12" +python-dateutil = "*" +pytz = "*" +regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27" +tzlocal = "*" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +calendars = ["convertdate", "hijri-converter"] +fasttext = ["fasttext"] +langdetect = ["langdetect"] [[package]] name = "distlib" -version = "0.3.4" +version = "0.3.6" description = "Distribution utilities" -category = "main" +category = "dev" optional = false python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] [[package]] name = "filelock" -version = "3.7.1" +version = "3.9.0" description = "A platform independent file lock." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, +] [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -195,6 +144,10 @@ description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] [package.dependencies] mccabe = ">=0.6.0,<0.7.0" @@ -202,78 +155,198 @@ pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" [[package]] -name = "html5lib" -version = "1.1" -description = "HTML parser based on the WHATWG HTML specification" +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] [package.dependencies] -six = ">=1.9" -webencodings = "*" +python-dateutil = ">=2.8.1" [package.extras] -all = ["genshi", "chardet (>=2.2)", "lxml"] -chardet = ["chardet (>=2.2)"] -genshi = ["genshi"] -lxml = ["lxml"] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "identify" -version = "2.5.1" +version = "2.5.18" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"}, + {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"}, +] [package.extras] license = ["ukkonen"] [[package]] -name = "idna" -version = "3.3" -description = "Internationalized Domain Names in Applications (IDNA)" +name = "importlib-metadata" +version = "4.13.0" +description = "Read metadata from Python packages" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, + {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] -name = "jeepney" -version = "0.8.0" -description = "Low-level, pure Python DBus protocol wrapper." +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.0" +description = "Links recognition library with FULL unicode support." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "linkify-it-py-2.0.0.tar.gz", hash = "sha256:476464480906bed8b2fa3813bf55566282e55214ad7e41b7d1c2b564666caf2f"}, + {file = "linkify_it_py-2.0.0-py3-none-any.whl", hash = "sha256:1bff43823e24e507a099e328fc54696124423dd6320c75a9da45b4b754b748ad"}, +] + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "markdown" +version = "3.3.7" +description = "Python implementation of Markdown." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, + {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, +] [package.extras] -test = ["pytest", "pytest-trio", "pytest-asyncio (>=0.17)", "testpath", "trio", "async-timeout"] -trio = ["trio", "async-generator"] +testing = ["coverage", "pyyaml"] [[package]] -name = "keyring" -version = "23.6.0" -description = "Store and access your passwords safely." +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] [package.dependencies] -jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} -pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} -SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} +mdurl = ">=0.1,<1.0" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] -name = "lockfile" -version = "0.12.2" -description = "Platform-independent file locking module" +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] [[package]] name = "mccabe" @@ -282,160 +355,182 @@ description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = "*" +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] [[package]] -name = "msgpack" -version = "1.0.4" -description = "MessagePack serializer" +name = "mdit-py-plugins" +version = "0.3.4" +description = "Collection of plugins for markdown-it-py" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "mdit-py-plugins-0.3.4.tar.gz", hash = "sha256:3278aab2e2b692539082f05e1243f24742194ffd92481f48844f057b51971283"}, + {file = "mdit_py_plugins-0.3.4-py3-none-any.whl", hash = "sha256:4f1441264ac5cb39fa40a5901921c2acf314ea098d75629750c138f80d552cdf"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<3.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] [[package]] -name = "nodeenv" -version = "1.7.0" -description = "Node.js virtual environment builder" -category = "dev" +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] [[package]] -name = "packaging" -version = "20.9" -description = "Core utilities for Python packages" +name = "mkdocs" +version = "1.4.2" +description = "Project documentation with Markdown." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" +files = [ + {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, + {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, +] [package.dependencies] -pyparsing = ">=2.0.2" +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.2.1,<3.4" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] [[package]] -name = "pastel" -version = "0.2.1" -description = "Bring colors to your terminal." +name = "mkdocs-exclude" +version = "1.0.2" +description = "A mkdocs plugin that lets you exclude files or trees." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" +files = [ + {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, +] + +[package.dependencies] +mkdocs = "*" [[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] [[package]] -name = "pexpect" -version = "4.8.0" -description = "Pexpect allows easy control of interactive console applications." -category = "main" +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] [package.dependencies] -ptyprocess = ">=0.5" +setuptools = "*" [[package]] -name = "pkginfo" -version = "1.8.3" -description = "Query metadatdata from sdists / bdists / installed packages." +name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.extras] -testing = ["nose", "coverage"] +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] -name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" +name = "pathspec" +version = "0.11.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +files = [ + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, +] [[package]] -name = "poetry" -version = "1.1.14" -description = "Python dependency management and packaging made easy." -category = "main" +name = "platformdirs" +version = "3.0.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, + {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, +] -[package.dependencies] -cachecontrol = {version = ">=0.12.9,<0.13.0", extras = ["filecache"], markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -cachy = ">=0.3.0,<0.4.0" -cleo = ">=0.8.1,<0.9.0" -clikit = ">=0.6.2,<0.7.0" -crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -html5lib = ">=1.0,<2.0" -keyring = {version = ">=21.2.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -packaging = ">=20.4,<21.0" -pexpect = ">=4.7.0,<5.0.0" -pkginfo = ">=1.4,<2.0" -poetry-core = ">=1.0.7,<1.1.0" -requests = ">=2.18,<3.0" -requests-toolbelt = ">=0.9.1,<0.10.0" -shellingham = ">=1.1,<2.0" -tomlkit = ">=0.7.0,<1.0.0" -virtualenv = ">=20.0.26,<21.0.0" - -[[package]] -name = "poetry-core" -version = "1.0.8" -description = "Poetry PEP 517 Build Backend" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pre-commit" -version = "2.19.0" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" - -[[package]] -name = "psutil" -version = "5.9.1" -description = "Cross-platform lib for process and system monitoring in Python." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -category = "main" -optional = false -python-versions = "*" +virtualenv = ">=20.10.0" [[package]] name = "pycodestyle" @@ -444,14 +539,10 @@ description = "Python style guide checker" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] [[package]] name = "pyflakes" @@ -460,33 +551,25 @@ description = "passive checker of Python programs" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] [[package]] name = "pygments" -version = "2.12.0" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false python-versions = ">=3.6" - -[[package]] -name = "pylev" -version = "1.4.0" -description = "A pure Python Levenshtein implementation that's not freaking GPL'd." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +plugins = ["importlib-metadata"] [[package]] name = "pyperclip" @@ -495,14 +578,36 @@ description = "A cross-platform clipboard module for Python. (Only handles plain category = "main" optional = false python-versions = "*" +files = [ + {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, +] [[package]] -name = "pywin32-ctypes" -version = "0.2.0" -description = "" +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" category = "main" optional = false python-versions = "*" +files = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] [[package]] name = "pyyaml" @@ -511,70 +616,197 @@ description = "YAML parser and emitter for Python" category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] [[package]] -name = "requests" -version = "2.28.1" -description = "Python HTTP for Humans." +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " category = "main" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] [package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +pyyaml = "*" [[package]] -name = "requests-toolbelt" -version = "0.9.1" -description = "A utility belt for advanced users of python-requests" +name = "regex" +version = "2022.10.31" +description = "Alternative regular expression module, to replace re." category = "main" optional = false -python-versions = "*" - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" +python-versions = ">=3.6" +files = [ + {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, + {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, + {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, + {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, + {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, + {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, + {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, + {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, + {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, + {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, + {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, + {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, + {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, + {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, + {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, + {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, +] [[package]] name = "rich" -version = "12.4.4" +version = "13.3.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false -python-versions = ">=3.6.3,<4.0.0" +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"}, + {file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"}, +] [package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" +markdown-it-py = ">=2.1.0,<3.0.0" +pygments = ">=2.14.0,<3.0.0" [package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] +jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] -name = "secretstorage" -version = "3.3.2" -description = "Python bindings to FreeDesktop.org Secret Service API" -category = "main" +name = "setuptools" +version = "67.4.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -cryptography = ">=2.0" -jeepney = ">=0.6" +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, + {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, +] -[[package]] -name = "shellingham" -version = "1.4.0" -description = "Tool to Detect Surrounding Shell" -category = "main" -optional = false -python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.6" +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -583,25 +815,32 @@ description = "Python 2 and 3 compatibility utilities" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "textual" -version = "0.1.18" -description = "Text User Interface using Rich" +version = "0.12.1" +description = "Modern Text User Interface framework" category = "main" optional = false python-versions = ">=3.7,<4.0" +files = [ + {file = "textual-0.12.1-py3-none-any.whl", hash = "sha256:976226eb7e56e31e70acff0d4146d8c96a0cd35613769c7f9e0b63df28ee6902"}, + {file = "textual-0.12.1.tar.gz", hash = "sha256:f826b422e39e4ca188307336f6a4f4b0a89834dab2628430b613084c70799dfd"}, +] [package.dependencies] -rich = ">=12.3.0,<13.0.0" +importlib-metadata = ">=4.11.3,<5.0.0" +markdown-it-py = {version = ">=2.1.0,<3.0.0", extras = ["linkify", "plugins"]} +mkdocs-exclude = ">=1.0.2,<2.0.0" +rich = ">12.6.0" +typing-extensions = ">=4.0.0,<5.0.0" -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[package.extras] +dev = ["aiohttp (>=3.8.1)", "click (>=8.1.2)", "msgpack (>=1.0.3)"] [[package]] name = "tomli" @@ -610,377 +849,132 @@ description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] -name = "tomlkit" -version = "0.11.1" -description = "Style preserving TOML library" +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] [[package]] -name = "urllib3" -version = "1.26.10" -description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "tzlocal" +version = "2.1" +description = "tzinfo object for the local timezone" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = "*" +files = [ + {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, + {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, +] + +[package.dependencies] +pytz = "*" + +[[package]] +name = "uc-micro-py" +version = "1.0.1" +description = "Micro subset of unicode data files for linkify-it-py projects." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uc-micro-py-1.0.1.tar.gz", hash = "sha256:b7cdf4ea79433043ddfe2c82210208f26f7962c0cfbe3bacb05ee879a7fdb596"}, + {file = "uc_micro_py-1.0.1-py3-none-any.whl", hash = "sha256:316cfb8b6862a0f1d03540f0ae6e7b033ff1fa0ddbe60c12cbe0d4cec846a69f"}, +] [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "virtualenv" -version = "20.15.1" +version = "20.19.0" description = "Virtual Python Environment builder" -category = "main" +category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, + {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, +] [package.dependencies] -distlib = ">=0.3.1,<1" -filelock = ">=3.2,<4" -platformdirs = ">=2,<3" -six = ">=1.9.0,<2" +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<4" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] [[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" +name = "watchdog" +version = "2.3.0" +description = "Filesystem events monitoring" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" +files = [ + {file = "watchdog-2.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c1b3962e5463a848ba2a342cb66c80251dca27a102933b8f38d231d2a9e5a543"}, + {file = "watchdog-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e651b4874477c1bf239417d43818bbfd047aaf641b029fa60d6f5109ede0db0"}, + {file = "watchdog-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d04662017efd00a014cff9068708e085d67f2fac43f48bbbb95a7f97490487f3"}, + {file = "watchdog-2.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f7d759299ce21a3d2a77e18d430c24811369c3432453701790acc6ff45a7101"}, + {file = "watchdog-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4b9bece40d46bf6fb8621817ea7d903eae2b9b3ebac55a51ed50354a79061a8"}, + {file = "watchdog-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:242e57253e84a736e6777ba756c48cf6a68d3d90cb9e01bd6bfd371a949ace3a"}, + {file = "watchdog-2.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3fa74b0ef4825f9112932675a002296cb2d3d3e400d7a44c32fafd1ecc83ada0"}, + {file = "watchdog-2.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:15bf5b165d7a6b48265411dad74fb0d33053f8270eb6575faad0e016035cf9f7"}, + {file = "watchdog-2.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:139262f678b4e6a7013261c772059bca358441de04fb0e0087489a34db9e3db0"}, + {file = "watchdog-2.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a214955769d2ef0f7aaa82f31863e3bdf6b083ce1b5f1c2e85cab0f66fba024"}, + {file = "watchdog-2.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e648df44a4c6ea6da4d9eb6722745c986b9d70268f25ae60f140082d7c8908e"}, + {file = "watchdog-2.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:473164a2de473f708ca194a992466eeefff73b58273bbb88e089c5a5a98fcda1"}, + {file = "watchdog-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebe756f788cb130fdc5c150ea8a4fda39cb4ee3a5873a345607c8b84fecf018b"}, + {file = "watchdog-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a623de186477e9e05f8461087f856412eae5cd005cc4bcb232ed5c6f9a8709f5"}, + {file = "watchdog-2.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:43d76d7888b26850b908208bb82383a193e8b0f25d0abaa84452f191b4acdea4"}, + {file = "watchdog-2.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5ddbbe87f9ed726940d174076da030cd01ec45433ef2b1b2e6094c84f2af17f1"}, + {file = "watchdog-2.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3fa1572f5a2f6d17d4d860edbc04488fef31b007c25c2f3b11203fb8179b7c67"}, + {file = "watchdog-2.3.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d9c656495172873bf1ddc7e39e80055fcdd21c4608cf68f23a28116dcba0b43"}, + {file = "watchdog-2.3.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:00f93782c67042d9525ec51628330b5faf5fb84bcb7ebaac05ea8528cfb20bba"}, + {file = "watchdog-2.3.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f1a655f4a49f9232311b9967f42cc2eaf43fd4903f3bed850dd4570fda5d5eff"}, + {file = "watchdog-2.3.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:aa4773160b9cb21ba369cb42d59a947087330b3a02480173033a6a6cc137a510"}, + {file = "watchdog-2.3.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:982f5416a2817003172994d865285dd6a2b3836f033cd3fa87d1a62096a162cc"}, + {file = "watchdog-2.3.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:45c13e7e6eea1013da419bf9aa9a8f5df7bbf3e5edce40bc6df84130febf39d5"}, + {file = "watchdog-2.3.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:7767a3da3307d9cf597832f692702441a97c259e5d0d560f2e57c43ad0d191d2"}, + {file = "watchdog-2.3.0-py3-none-win32.whl", hash = "sha256:8863913ea2c3f256d18c33d84546518636e391cd8f50d209b9a31221e0f7d3fd"}, + {file = "watchdog-2.3.0-py3-none-win_amd64.whl", hash = "sha256:6d79b5954db8f41d6a7f5763042b988f7a4afd40b7d141456061fa7c5b7f2159"}, + {file = "watchdog-2.3.0-py3-none-win_ia64.whl", hash = "sha256:a3559ee82a10976de1ec544b6ebe3b4aa398d491860a283d80ec0f550076d068"}, + {file = "watchdog-2.3.0.tar.gz", hash = "sha256:9d39effe6909be898ba3e7286a9e9b17a6a9f734fb1ef9dde3e9bb68715fca39"}, +] -[metadata] -lock-version = "1.1" -python-versions = "^3.10" -content-hash = "73bfa6e672d731d575d97e95b90f95f45a69d69bc54c2a625dc772bd61b49510" +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] -[metadata.files] -black = [] -cachecontrol = [ - {file = "CacheControl-0.12.11-py2.py3-none-any.whl", hash = "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b"}, - {file = "CacheControl-0.12.11.tar.gz", hash = "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144"}, -] -cachy = [ - {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"}, - {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, -] -certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, -] -cffi = [] -cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -charset-normalizer = [] -cleo = [ - {file = "cleo-0.8.1-py2.py3-none-any.whl", hash = "sha256:141cda6dc94a92343be626bb87a0b6c86ae291dfc732a57bf04310d4b4201753"}, - {file = "cleo-0.8.1.tar.gz", hash = "sha256:3d0e22d30117851b45970b6c14aca4ab0b18b1b53c8af57bed13208147e4069f"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -clikit = [ - {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"}, - {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -commonmark = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] -crashtest = [ - {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"}, - {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"}, -] -cryptography = [] -distlib = [ - {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, - {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, -] -filelock = [ - {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, - {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -html5lib = [ - {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, - {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, -] -identify = [ - {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, - {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, -] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -jeepney = [ - {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, - {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, -] -keyring = [ - {file = "keyring-23.6.0-py3-none-any.whl", hash = "sha256:372ff2fc43ab779e3f87911c26e6c7acc8bb440cbd82683e383ca37594cb0617"}, - {file = "keyring-23.6.0.tar.gz", hash = "sha256:3ac00c26e4c93739e19103091a9986a9f79665a78cf15a4df1dba7ea9ac8da2f"}, -] -lockfile = [ - {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, - {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -msgpack = [ - {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"}, - {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"}, - {file = "msgpack-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db"}, - {file = "msgpack-1.0.4-cp310-cp310-win32.whl", hash = "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef"}, - {file = "msgpack-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075"}, - {file = "msgpack-1.0.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae"}, - {file = "msgpack-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6"}, - {file = "msgpack-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661"}, - {file = "msgpack-1.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236"}, - {file = "msgpack-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44"}, - {file = "msgpack-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243"}, - {file = "msgpack-1.0.4-cp38-cp38-win32.whl", hash = "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2"}, - {file = "msgpack-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae"}, - {file = "msgpack-1.0.4-cp39-cp39-win32.whl", hash = "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c"}, - {file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"}, - {file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -nodeenv = [] -packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, -] -pastel = [ - {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, - {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -pexpect = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, -] -pkginfo = [ - {file = "pkginfo-1.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, - {file = "pkginfo-1.8.3.tar.gz", hash = "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -poetry = [] -poetry-core = [ - {file = "poetry-core-1.0.8.tar.gz", hash = "sha256:951fc7c1f8d710a94cb49019ee3742125039fc659675912ea614ac2aa405b118"}, - {file = "poetry_core-1.0.8-py2.py3-none-any.whl", hash = "sha256:54b0fab6f7b313886e547a52f8bf52b8cf43e65b2633c65117f8755289061924"}, -] -pre-commit = [ - {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, - {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, -] -psutil = [ - {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"}, - {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"}, - {file = "psutil-5.9.1-cp27-cp27m-win32.whl", hash = "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc"}, - {file = "psutil-5.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2"}, - {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0"}, - {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22"}, - {file = "psutil-5.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9"}, - {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8"}, - {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de"}, - {file = "psutil-5.9.1-cp310-cp310-win32.whl", hash = "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329"}, - {file = "psutil-5.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021"}, - {file = "psutil-5.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237"}, - {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453"}, - {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685"}, - {file = "psutil-5.9.1-cp36-cp36m-win32.whl", hash = "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36"}, - {file = "psutil-5.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d"}, - {file = "psutil-5.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc"}, - {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"}, - {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4"}, - {file = "psutil-5.9.1-cp37-cp37m-win32.whl", hash = "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b"}, - {file = "psutil-5.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680"}, - {file = "psutil-5.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1"}, - {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4"}, - {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b"}, - {file = "psutil-5.9.1-cp38-cp38-win32.whl", hash = "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689"}, - {file = "psutil-5.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0"}, - {file = "psutil-5.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81"}, - {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e"}, - {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537"}, - {file = "psutil-5.9.1-cp39-cp39-win32.whl", hash = "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574"}, - {file = "psutil-5.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5"}, - {file = "psutil-5.9.1.tar.gz", hash = "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954"}, -] -ptyprocess = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, -] -pylev = [ - {file = "pylev-1.4.0-py2.py3-none-any.whl", hash = "sha256:7b2e2aa7b00e05bb3f7650eb506fc89f474f70493271a35c242d9a92188ad3dd"}, - {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pyperclip = [ - {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, -] -pywin32-ctypes = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -requests = [] -requests-toolbelt = [ - {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, - {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, -] -rich = [ - {file = "rich-12.4.4-py3-none-any.whl", hash = "sha256:d2bbd99c320a2532ac71ff6a3164867884357da3e3301f0240090c5d2fdac7ec"}, - {file = "rich-12.4.4.tar.gz", hash = "sha256:4c586de507202505346f3e32d1363eb9ed6932f0c2f63184dea88983ff4971e2"}, -] -secretstorage = [ - {file = "SecretStorage-3.3.2-py3-none-any.whl", hash = "sha256:755dc845b6ad76dcbcbc07ea3da75ae54bb1ea529eb72d15f83d26499a5df319"}, - {file = "SecretStorage-3.3.2.tar.gz", hash = "sha256:0a8eb9645b320881c222e827c26f4cfcf55363e8b374a021981ef886657a912f"}, -] -shellingham = [ - {file = "shellingham-1.4.0-py2.py3-none-any.whl", hash = "sha256:536b67a0697f2e4af32ab176c00a50ac2899c5a05e0d8e2dadac8e58888283f9"}, - {file = "shellingham-1.4.0.tar.gz", hash = "sha256:4855c2458d6904829bd34c299f11fdeed7cfefbf8a2c522e4caea6cd76b3171e"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -textual = [ - {file = "textual-0.1.18-py3-none-any.whl", hash = "sha256:59110935418c597c1c50876edfd6799ad5b59d51a91ca6744fc45e36eb89638e"}, - {file = "textual-0.1.18.tar.gz", hash = "sha256:b2883f8ed291de58b9aa73de6d24bbaae0174687487458a4eb2a7c188a2acf23"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tomlkit = [] -urllib3 = [] -virtualenv = [] -webencodings = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "a70cd1bfe68647a4eee71f9810ee88196f05119787c5825e0f6a119662936b1a" diff --git a/pyproject.toml b/pyproject.toml index df23c498..73aa6e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dooit" -version = "0.2.1" +version = "1.0.0" description = "A TUI todo manager" authors = ["kraanzu "] maintainers = ["kraanzu "] @@ -10,12 +10,13 @@ homepage = "https://github.com/kraanzu/dooit" repository = "https://github.com/kraanzu/dooit" [tool.poetry.dependencies] -python = "^3.10" -textual = "^0.1.17" +python = "^3.8" +textual = "^0.12.1" pyperclip = "^1.8.2" PyYAML = "^6.0" -poetry = "^1.1.13" -psutil = "^5.9.1" +dateparser = "^1.1.2" +tzlocal = "2.1" +appdirs = "^1.4.4" [tool.poetry.dev-dependencies] black = "^22.3.0"