toml colors
This commit is contained in:
parent
77ac5a5cd2
commit
8323323dea
@ -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
14
src/tt/config.py
Normal 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")
|
@ -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")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
14
src/tt/db.py
14
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):
|
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()
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
21
tt.toml
Normal 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
14
uv.lock
generated
@ -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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user