toml colors

This commit is contained in:
jpt 2025-04-10 23:43:44 -05:00
parent 77ac5a5cd2
commit 8323323dea
12 changed files with 94 additions and 62 deletions

View File

@ -9,6 +9,7 @@ dependencies = [
"lxml>=5.3.0", "lxml>=5.3.0",
"peewee>=3.17.8", "peewee>=3.17.8",
"textual>=1.0.0", "textual>=1.0.0",
"tomlkit>=0.13.2",
"typer>=0.15.1", "typer>=0.15.1",
] ]

14
src/tt/config.py Normal file
View File

@ -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")

View File

@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from peewee import fn, JOIN 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]: def get_category_summary(num: int = 5) -> list[dict]:
@ -20,7 +20,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
overdue_count = ( overdue_count = (
Task.select(Task.category, fn.COUNT(Task.id).alias("overdue")) Task.select(Task.category, fn.COUNT(Task.id).alias("overdue"))
.where( .where(
(~Task.deleted) & (Task.due < now) & (Task.status != TaskStatus.DONE.value) (~Task.deleted) & (Task.due < now) & (Task.status != "done")
) )
.group_by(Task.category) .group_by(Task.category)
) )
@ -32,7 +32,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
(~Task.deleted) (~Task.deleted)
& (Task.due >= now) & (Task.due >= now)
& (Task.due <= week_from_now) & (Task.due <= week_from_now)
& (Task.status != TaskStatus.DONE.value) & (Task.status != "done")
) )
.group_by(Task.category) .group_by(Task.category)
) )
@ -41,16 +41,16 @@ def get_category_summary(num: int = 5) -> list[dict]:
query = ( query = (
Category.select( Category.select(
Category.name, Category.name,
fn.COALESCE(fn.SUM(Task.status == TaskStatus.ZERO.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "zero"), 0).alias(
"zero_count" "zero_count"
), ),
fn.COALESCE(fn.SUM(Task.status == TaskStatus.WIP.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias(
"wip_count" "wip_count"
), ),
fn.COALESCE(fn.SUM(Task.status == TaskStatus.BLOCKED.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias(
"blocked_count" "blocked_count"
), ),
fn.COALESCE(fn.SUM(Task.status == TaskStatus.DONE.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias(
"done_count" "done_count"
), ),
fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"), fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"),
@ -148,7 +148,7 @@ def get_due_soon(
(~Task.deleted) (~Task.deleted)
& (Task.due.is_null(False)) & (Task.due.is_null(False))
& (Task.due != "") & (Task.due != "")
& (Task.status != TaskStatus.DONE.value) & (Task.status != "done")
) )
) )

View File

@ -2,7 +2,8 @@ import json
from datetime import datetime from datetime import datetime
from peewee import fn from peewee import fn
from peewee import Case, Value 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): def category_lookup(category):
@ -18,7 +19,7 @@ def get_task(item_id: int) -> Task:
def add_task( def add_task(
text: str, text: str,
category: str, category: str,
status: str = TaskStatus.ZERO.value, status: str,
due: datetime | None = None, due: datetime | None = None,
type: str = "", type: str = "",
) -> Task: ) -> Task:
@ -62,7 +63,7 @@ def _parse_sort_string(sort_string, status_order):
if field == "status": if field == "status":
if not status_order: 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 # CASE statement that maps each status to its position in the order
order_case = Case( order_case = Case(
Task.status, Task.status,

View File

@ -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): class GeneratorType(Enum):
DAYS_BETWEEN = "days-btwn" DAYS_BETWEEN = "days-btwn"
MONTHLY = "monthly" MONTHLY = "monthly"
@ -48,10 +39,7 @@ class Category(BaseModel):
class Task(BaseModel): class Task(BaseModel):
text = TextField() text = TextField()
status = CharField( status = CharField()
choices=[(status.value, status.name) for status in TaskStatus],
default=TaskStatus.ZERO.value,
)
due = DateTimeField(null=True) due = DateTimeField(null=True)
category = ForeignKeyField(Category, backref="tasks", null=True) category = ForeignKeyField(Category, backref="tasks", null=True)
type = CharField() type = CharField()

View File

@ -1,6 +1,7 @@
import csv import csv
from datetime import datetime 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): def import_tasks_from_csv(filename: str):
@ -20,9 +21,7 @@ def import_tasks_from_csv(filename: str):
# Validate status # Validate status
status = row["status"].lower() if row["status"] else "zero" status = row["status"].lower() if row["status"] else "zero"
try: if status not in STATUSES:
TaskStatus(status)
except ValueError:
print(f"Warning: Invalid status '{status}', defaulting to 'zero'") print(f"Warning: Invalid status '{status}', defaulting to 'zero'")
status = "zero" status = "zero"

View File

@ -10,7 +10,6 @@ from textual.containers import Container
from ..utils import ( from ..utils import (
remove_rich_tag, remove_rich_tag,
filter_to_string, filter_to_string,
advance_enum_val,
get_text_from_editor, get_text_from_editor,
) )
from .keymodal import KeyModal from .keymodal import KeyModal
@ -62,20 +61,19 @@ class EnumColumnConfig(TableColumnConfig):
**kwargs **kwargs
): ):
super().__init__(field, display_name, **kwargs) super().__init__(field, display_name, **kwargs)
self.enumCls = enum self.enum = enum
def preprocess(self, val): def preprocess(self, val):
try: if val in self.enum:
self.enumCls(val)
return val return val
except ValueError: else:
raise NotifyValidationError( 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): def start_change(self, app, current_value):
# a weird hack? pass app here and correct modal gets pushed # 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)

View File

@ -1,6 +1,8 @@
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.binding import Binding from textual.binding import Binding
from textual.widgets import RadioSet, RadioButton, Label from textual.widgets import RadioSet, RadioButton, Label
from .. import config
from ..utils import get_color_enum
class ChoiceModal(ModalScreen): class ChoiceModal(ModalScreen):
@ -28,10 +30,11 @@ class ChoiceModal(ModalScreen):
super().__init__() super().__init__()
def compose(self): def compose(self):
yield Label(f"{self._enum.__name__}")
yield RadioSet( 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 for e in self._enum
] ]
) )
@ -78,13 +81,15 @@ class DateModal(ModalScreen):
] ]
def __init__(self, date): def __init__(self, date):
self.pieces = [int(p) for p in date.split('-')] self.pieces = [int(p) for p in date.split("-")]
self.selected = 1 # start on month self.selected = 1 # start on month
super().__init__() super().__init__()
def compose(self): def compose(self):
for idx, piece in enumerate(self.pieces): 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): def action_cursor_left(self):
# cycle Y/M/D # cycle Y/M/D
@ -102,7 +107,6 @@ class DateModal(ModalScreen):
else: else:
lbl.remove_class("selected-date") lbl.remove_class("selected-date")
def max_for(self, piece): def max_for(self, piece):
days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if piece == 0: if piece == 0:

View File

@ -2,20 +2,18 @@ import json
from textual.widgets import Input from textual.widgets import Input
from datetime import datetime from datetime import datetime
from .. import config
from ..controller.tasks import ( from ..controller.tasks import (
get_task, get_task,
get_tasks, get_tasks,
add_task, add_task,
update_task, update_task,
TaskStatus,
save_view, save_view,
get_saved_view, get_saved_view,
) )
from ..utils import ( from ..utils import (
get_colored_category, get_color_enum,
get_colored_status,
get_colored_date, get_colored_date,
remove_rich_tag,
) )
from .editor import ( from .editor import (
TableEditor, TableEditor,
@ -44,7 +42,7 @@ class TT(TableEditor):
EnumColumnConfig( EnumColumnConfig(
"status", "status",
"Status", "Status",
enum=TaskStatus, enum=config.STATUSES,
default="zero", default="zero",
), ),
TableColumnConfig("type", "Type", default=""), TableColumnConfig("type", "Type", default=""),
@ -102,10 +100,12 @@ class TT(TableEditor):
sort=self.sort_string, sort=self.sort_string,
) )
for item in items: for item in items:
category = get_colored_category( category = get_color_enum(
item.category.name if item.category else " - " 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) due = get_colored_date(item.due)
if "\n" in item.text: if "\n" in item.text:

View File

@ -32,15 +32,9 @@ def advance_enum_val(enum_type, cur_val):
return members[next_idx] return members[next_idx]
def get_colored_status(status: str) -> str: def get_color_enum(value: str, enum: dict[str, dict], default: str) -> str:
colors = { color = enum.get(value, {"color": default})["color"]
"zero": "#666666", return f"[{color}]{value}[/]"
"wip": "#33aa99",
"blocked": "#cc9900",
"done": "#009900",
}
color = colors.get(status, "#666666")
return f"[{color}]{status}[/]"
def get_colored_category(category: str) -> str: def get_colored_category(category: str) -> str:

21
tt.toml Normal file
View File

@ -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"},
]

14
uv.lock generated
View File

@ -1,4 +1,5 @@
version = 1 version = 1
revision = 1
requires-python = ">=3.10" requires-python = ">=3.10"
[[package]] [[package]]
@ -160,7 +161,7 @@ name = "click"
version = "8.1.8" version = "8.1.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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 } sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [ 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 }, { 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]] [[package]]
name = "tt" name = "tt"
version = "0.1.0" version = "0.1.0"
@ -910,6 +920,7 @@ dependencies = [
{ name = "lxml" }, { name = "lxml" },
{ name = "peewee" }, { name = "peewee" },
{ name = "textual" }, { name = "textual" },
{ name = "tomlkit" },
{ name = "typer" }, { name = "typer" },
] ]
@ -926,6 +937,7 @@ requires-dist = [
{ name = "lxml", specifier = ">=5.3.0" }, { name = "lxml", specifier = ">=5.3.0" },
{ name = "peewee", specifier = ">=3.17.8" }, { name = "peewee", specifier = ">=3.17.8" },
{ name = "textual", specifier = ">=1.0.0" }, { name = "textual", specifier = ">=1.0.0" },
{ name = "tomlkit", specifier = ">=0.13.2" },
{ name = "typer", specifier = ">=0.15.1" }, { name = "typer", specifier = ">=0.15.1" },
] ]