diff --git a/pyproject.toml b/pyproject.toml index faefe35..e9a565d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "lxml>=5.3.0", "peewee>=3.17.8", "textual>=1.0.0", + "tomlkit>=0.13.2", "typer>=0.15.1", ] diff --git a/src/tt/config.py b/src/tt/config.py new file mode 100644 index 0000000..8cede64 --- /dev/null +++ b/src/tt/config.py @@ -0,0 +1,14 @@ +import tomlkit + +def get_enum(name): + with open("tt.toml", "r") as f: + config = tomlkit.load(f) + + for enum in config.get("enums", []): + if enum["name"] == name: + return {v["value"]: v for v in enum["values"]} + + raise ValueError(f"no such enum! {name}") + +STATUSES = get_enum("status") +PROJECTS = get_enum("projects") diff --git a/src/tt/controller/summaries.py b/src/tt/controller/summaries.py index 1925d89..b8bcbb6 100644 --- a/src/tt/controller/summaries.py +++ b/src/tt/controller/summaries.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta from peewee import fn, JOIN -from ..db import Task, Category, TaskStatus +from ..db import Task, Category def get_category_summary(num: int = 5) -> list[dict]: @@ -20,7 +20,7 @@ def get_category_summary(num: int = 5) -> list[dict]: overdue_count = ( Task.select(Task.category, fn.COUNT(Task.id).alias("overdue")) .where( - (~Task.deleted) & (Task.due < now) & (Task.status != TaskStatus.DONE.value) + (~Task.deleted) & (Task.due < now) & (Task.status != "done") ) .group_by(Task.category) ) @@ -32,7 +32,7 @@ def get_category_summary(num: int = 5) -> list[dict]: (~Task.deleted) & (Task.due >= now) & (Task.due <= week_from_now) - & (Task.status != TaskStatus.DONE.value) + & (Task.status != "done") ) .group_by(Task.category) ) @@ -41,16 +41,16 @@ def get_category_summary(num: int = 5) -> list[dict]: query = ( Category.select( Category.name, - fn.COALESCE(fn.SUM(Task.status == TaskStatus.ZERO.value), 0).alias( + fn.COALESCE(fn.SUM(Task.status == "zero"), 0).alias( "zero_count" ), - fn.COALESCE(fn.SUM(Task.status == TaskStatus.WIP.value), 0).alias( + fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias( "wip_count" ), - fn.COALESCE(fn.SUM(Task.status == TaskStatus.BLOCKED.value), 0).alias( + fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias( "blocked_count" ), - fn.COALESCE(fn.SUM(Task.status == TaskStatus.DONE.value), 0).alias( + fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias( "done_count" ), fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"), @@ -148,7 +148,7 @@ def get_due_soon( (~Task.deleted) & (Task.due.is_null(False)) & (Task.due != "") - & (Task.status != TaskStatus.DONE.value) + & (Task.status != "done") ) ) diff --git a/src/tt/controller/tasks.py b/src/tt/controller/tasks.py index d2b4fb6..fc547e4 100644 --- a/src/tt/controller/tasks.py +++ b/src/tt/controller/tasks.py @@ -2,7 +2,8 @@ import json from datetime import datetime from peewee import fn from peewee import Case, Value -from ..db import db, Task, Category, TaskStatus, SavedSearch +from ..db import db, Task, Category, SavedSearch +from .. import config def category_lookup(category): @@ -18,7 +19,7 @@ def get_task(item_id: int) -> Task: def add_task( text: str, category: str, - status: str = TaskStatus.ZERO.value, + status: str, due: datetime | None = None, type: str = "", ) -> Task: @@ -62,7 +63,7 @@ def _parse_sort_string(sort_string, status_order): if field == "status": if not status_order: - status_order = [s.value for s in TaskStatus] + status_order = list(config.STATUSES.keys()) # CASE statement that maps each status to its position in the order order_case = Case( Task.status, diff --git a/src/tt/db.py b/src/tt/db.py index 514c454..a67c51e 100644 --- a/src/tt/db.py +++ b/src/tt/db.py @@ -20,15 +20,6 @@ db = SqliteDatabase( }, ) - -class TaskStatus(Enum): - # order is used for progression in toggle - ZERO = "zero" - WIP = "wip" - BLOCKED = "blocked" - DONE = "done" - - class GeneratorType(Enum): DAYS_BETWEEN = "days-btwn" MONTHLY = "monthly" @@ -48,10 +39,7 @@ class Category(BaseModel): class Task(BaseModel): text = TextField() - status = CharField( - choices=[(status.value, status.name) for status in TaskStatus], - default=TaskStatus.ZERO.value, - ) + status = CharField() due = DateTimeField(null=True) category = ForeignKeyField(Category, backref="tasks", null=True) type = CharField() diff --git a/src/tt/import_csv.py b/src/tt/import_csv.py index 0acd8ca..911f7b2 100644 --- a/src/tt/import_csv.py +++ b/src/tt/import_csv.py @@ -1,6 +1,7 @@ import csv from datetime import datetime -from tt.db import initialize_db, Task, Category, TaskStatus +from tt.db import initialize_db, Task, Category +from tt.config import STATUSES def import_tasks_from_csv(filename: str): @@ -20,9 +21,7 @@ def import_tasks_from_csv(filename: str): # Validate status status = row["status"].lower() if row["status"] else "zero" - try: - TaskStatus(status) - except ValueError: + if status not in STATUSES: print(f"Warning: Invalid status '{status}', defaulting to 'zero'") status = "zero" diff --git a/src/tt/tui/editor.py b/src/tt/tui/editor.py index 57b23fc..4be867a 100644 --- a/src/tt/tui/editor.py +++ b/src/tt/tui/editor.py @@ -10,7 +10,6 @@ from textual.containers import Container from ..utils import ( remove_rich_tag, filter_to_string, - advance_enum_val, get_text_from_editor, ) from .keymodal import KeyModal @@ -62,20 +61,19 @@ class EnumColumnConfig(TableColumnConfig): **kwargs ): super().__init__(field, display_name, **kwargs) - self.enumCls = enum + self.enum = enum def preprocess(self, val): - try: - self.enumCls(val) + if val in self.enum: return val - except ValueError: + else: raise NotifyValidationError( - f"Invalid value {val}. Use: {[s.value for s in self.enumCls]}" + f"Invalid value {val}. Use: {list(self.enum)}" ) def start_change(self, app, current_value): # a weird hack? pass app here and correct modal gets pushed - app.push_screen(ChoiceModal(self.enumCls, current_value), app.apply_change) + app.push_screen(ChoiceModal(self.enum, current_value), app.apply_change) diff --git a/src/tt/tui/modals.py b/src/tt/tui/modals.py index 05993ce..5338fd0 100644 --- a/src/tt/tui/modals.py +++ b/src/tt/tui/modals.py @@ -1,6 +1,8 @@ from textual.screen import ModalScreen from textual.binding import Binding from textual.widgets import RadioSet, RadioButton, Label +from .. import config +from ..utils import get_color_enum class ChoiceModal(ModalScreen): @@ -28,10 +30,11 @@ class ChoiceModal(ModalScreen): super().__init__() def compose(self): - yield Label(f"{self._enum.__name__}") yield RadioSet( *[ - RadioButton(str(e.value), value=self.selected == str(e.value)) + RadioButton( + get_color_enum(e.value, config.STATUSES, "red"), value=self.selected == str(e.value) + ) for e in self._enum ] ) @@ -72,19 +75,21 @@ class DateModal(ModalScreen): ("k", "cursor_up", "Up"), ("h", "cursor_left", "Left"), ("l", "cursor_right", "Right"), -# ("0,1,2,3,4,5,6,7,8,9", "num_entry", "#"), + # ("0,1,2,3,4,5,6,7,8,9", "num_entry", "#"), Binding("enter", "select", "Select", priority=True), ("escape", "cancel", "cancel"), ] def __init__(self, date): - self.pieces = [int(p) for p in date.split('-')] - self.selected = 1 # start on month + self.pieces = [int(p) for p in date.split("-")] + self.selected = 1 # start on month super().__init__() def compose(self): for idx, piece in enumerate(self.pieces): - yield Label(str(piece), classes="selected-date" if idx == self.selected else "") + yield Label( + str(piece), classes="selected-date" if idx == self.selected else "" + ) def action_cursor_left(self): # cycle Y/M/D @@ -101,7 +106,6 @@ class DateModal(ModalScreen): lbl.add_class("selected-date") else: lbl.remove_class("selected-date") - def max_for(self, piece): days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] @@ -111,7 +115,7 @@ class DateModal(ModalScreen): return 12 else: # -1 for offset array - return days_in_month[self.pieces[1]-1] + return days_in_month[self.pieces[1] - 1] def _move_piece(self, by): cur_value = self.pieces[self.selected] @@ -123,7 +127,7 @@ class DateModal(ModalScreen): self.pieces[self.selected] = cur_value cur_label = self.query("Label")[self.selected] cur_label.update(str(cur_value)) - + def action_cursor_down(self): self._move_piece(-1) diff --git a/src/tt/tui/tasks.py b/src/tt/tui/tasks.py index 539add6..47aa41a 100644 --- a/src/tt/tui/tasks.py +++ b/src/tt/tui/tasks.py @@ -2,20 +2,18 @@ import json from textual.widgets import Input from datetime import datetime +from .. import config from ..controller.tasks import ( get_task, get_tasks, add_task, update_task, - TaskStatus, save_view, get_saved_view, ) from ..utils import ( - get_colored_category, - get_colored_status, + get_color_enum, get_colored_date, - remove_rich_tag, ) from .editor import ( TableEditor, @@ -44,7 +42,7 @@ class TT(TableEditor): EnumColumnConfig( "status", "Status", - enum=TaskStatus, + enum=config.STATUSES, default="zero", ), TableColumnConfig("type", "Type", default=""), @@ -102,10 +100,12 @@ class TT(TableEditor): sort=self.sort_string, ) for item in items: - category = get_colored_category( - item.category.name if item.category else " - " + category = get_color_enum( + item.category.name if item.category else " - ", + config.PROJECTS, + "grey" ) - status = get_colored_status(item.status) + status = get_color_enum(item.status, config.STATUSES, "red") due = get_colored_date(item.due) if "\n" in item.text: diff --git a/src/tt/utils.py b/src/tt/utils.py index 860beb3..578c391 100644 --- a/src/tt/utils.py +++ b/src/tt/utils.py @@ -32,15 +32,9 @@ def advance_enum_val(enum_type, cur_val): return members[next_idx] -def get_colored_status(status: str) -> str: - colors = { - "zero": "#666666", - "wip": "#33aa99", - "blocked": "#cc9900", - "done": "#009900", - } - color = colors.get(status, "#666666") - return f"[{color}]{status}[/]" +def get_color_enum(value: str, enum: dict[str, dict], default: str) -> str: + color = enum.get(value, {"color": default})["color"] + return f"[{color}]{value}[/]" def get_colored_category(category: str) -> str: diff --git a/tt.toml b/tt.toml new file mode 100644 index 0000000..3d9b289 --- /dev/null +++ b/tt.toml @@ -0,0 +1,21 @@ +[[enums]] +name = "status" +values = [ + { value = "zero", color = "#666666" }, + { value = "blocked", color = "#33a99" }, + { value = "wip", color = "#cc9900" }, + { value = "done", color = "#009900" }, +] + +[[enums]] +name = "projects" +values = [ + { value = "SECT", color = "purple" }, + { value = "life", color = "#00cc00" }, + { value = "CAPP", color = "#cc0000" }, + { value = "ilikethis", color = "#cccc00" }, + { value = "krang", color = "#ff00ff"}, + { value = "artworld", color = "#0000cc"}, + { value = "TT", color = "#00ff00"}, +] + diff --git a/uv.lock b/uv.lock index 4686f81..6f60ee5 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" [[package]] @@ -160,7 +161,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -901,6 +902,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + [[package]] name = "tt" version = "0.1.0" @@ -910,6 +920,7 @@ dependencies = [ { name = "lxml" }, { name = "peewee" }, { name = "textual" }, + { name = "tomlkit" }, { name = "typer" }, ] @@ -926,6 +937,7 @@ requires-dist = [ { name = "lxml", specifier = ">=5.3.0" }, { name = "peewee", specifier = ">=3.17.8" }, { name = "textual", specifier = ">=1.0.0" }, + { name = "tomlkit", specifier = ">=0.13.2" }, { name = "typer", specifier = ">=0.15.1" }, ]