Compare commits
No commits in common. "92cc3c5b40900c97851dd0b7d804687bbe9eb5d2" and "2c5310fe0d36f3eec9e9151ed8c1ed047d585357" have entirely different histories.
92cc3c5b40
...
2c5310fe0d
@ -9,7 +9,6 @@ dependencies = [
|
||||
"lxml>=5.3.0",
|
||||
"peewee>=3.17.8",
|
||||
"textual>=1.0.0",
|
||||
"tomlkit>=0.13.2",
|
||||
"typer>=0.15.1",
|
||||
]
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
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 peewee import fn, JOIN
|
||||
from ..db import Task, Category
|
||||
from ..db import Task, Category, TaskStatus
|
||||
|
||||
|
||||
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 != "done")
|
||||
(~Task.deleted) & (Task.due < now) & (Task.status != TaskStatus.DONE.value)
|
||||
)
|
||||
.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 != "done")
|
||||
& (Task.status != TaskStatus.DONE.value)
|
||||
)
|
||||
.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 == "zero"), 0).alias(
|
||||
fn.COALESCE(fn.SUM(Task.status == TaskStatus.ZERO.value), 0).alias(
|
||||
"zero_count"
|
||||
),
|
||||
fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias(
|
||||
fn.COALESCE(fn.SUM(Task.status == TaskStatus.WIP.value), 0).alias(
|
||||
"wip_count"
|
||||
),
|
||||
fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias(
|
||||
fn.COALESCE(fn.SUM(Task.status == TaskStatus.BLOCKED.value), 0).alias(
|
||||
"blocked_count"
|
||||
),
|
||||
fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias(
|
||||
fn.COALESCE(fn.SUM(Task.status == TaskStatus.DONE.value), 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 != "done")
|
||||
& (Task.status != TaskStatus.DONE.value)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -2,8 +2,7 @@ import json
|
||||
from datetime import datetime
|
||||
from peewee import fn
|
||||
from peewee import Case, Value
|
||||
from ..db import db, Task, Category, SavedSearch
|
||||
from .. import config
|
||||
from ..db import db, Task, Category, TaskStatus, SavedSearch
|
||||
|
||||
|
||||
def category_lookup(category):
|
||||
@ -19,7 +18,7 @@ def get_task(item_id: int) -> Task:
|
||||
def add_task(
|
||||
text: str,
|
||||
category: str,
|
||||
status: str,
|
||||
status: str = TaskStatus.ZERO.value,
|
||||
due: datetime | None = None,
|
||||
type: str = "",
|
||||
) -> Task:
|
||||
@ -63,7 +62,7 @@ def _parse_sort_string(sort_string, status_order):
|
||||
|
||||
if field == "status":
|
||||
if not status_order:
|
||||
status_order = list(config.STATUSES.keys())
|
||||
status_order = [s.value for s in TaskStatus]
|
||||
# CASE statement that maps each status to its position in the order
|
||||
order_case = Case(
|
||||
Task.status,
|
||||
|
14
src/tt/db.py
14
src/tt/db.py
@ -20,6 +20,15 @@ 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"
|
||||
@ -39,7 +48,10 @@ class Category(BaseModel):
|
||||
|
||||
class Task(BaseModel):
|
||||
text = TextField()
|
||||
status = CharField()
|
||||
status = CharField(
|
||||
choices=[(status.value, status.name) for status in TaskStatus],
|
||||
default=TaskStatus.ZERO.value,
|
||||
)
|
||||
due = DateTimeField(null=True)
|
||||
category = ForeignKeyField(Category, backref="tasks", null=True)
|
||||
type = CharField()
|
||||
|
@ -1,7 +1,6 @@
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from tt.db import initialize_db, Task, Category
|
||||
from tt.config import STATUSES
|
||||
from tt.db import initialize_db, Task, Category, TaskStatus
|
||||
|
||||
|
||||
def import_tasks_from_csv(filename: str):
|
||||
@ -21,7 +20,9 @@ def import_tasks_from_csv(filename: str):
|
||||
|
||||
# Validate status
|
||||
status = row["status"].lower() if row["status"] else "zero"
|
||||
if status not in STATUSES:
|
||||
try:
|
||||
TaskStatus(status)
|
||||
except ValueError:
|
||||
print(f"Warning: Invalid status '{status}', defaulting to 'zero'")
|
||||
status = "zero"
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import datetime
|
||||
from textual.app import App
|
||||
from textual.widgets import (
|
||||
DataTable,
|
||||
@ -11,15 +10,30 @@ 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
|
||||
from .modals import ChoiceModal, DateModal, ConfirmModal
|
||||
|
||||
ELLIPSIS = "…"
|
||||
|
||||
|
||||
class NotifyValidationError(Exception):
|
||||
"""will notify and continue if raised"""
|
||||
|
||||
|
||||
def _enum_preprocessor(enumCls):
|
||||
"""generate a default preprocessor to enforce enums"""
|
||||
|
||||
def preprocessor(val):
|
||||
try:
|
||||
enumCls(val)
|
||||
return val
|
||||
except ValueError:
|
||||
raise NotifyValidationError(
|
||||
f"Invalid value. Use: {[s.value for s in enumCls]}"
|
||||
)
|
||||
|
||||
|
||||
class TableColumnConfig:
|
||||
def __init__(
|
||||
@ -28,6 +42,8 @@ class TableColumnConfig:
|
||||
display_name: str,
|
||||
*,
|
||||
default=None,
|
||||
enum=None,
|
||||
preprocessor=None,
|
||||
enable_editor=False,
|
||||
filterable=True,
|
||||
read_only=False,
|
||||
@ -35,57 +51,16 @@ class TableColumnConfig:
|
||||
self.field = field
|
||||
self.display_name = display_name
|
||||
self.default = default
|
||||
self.enum = enum
|
||||
self.enable_editor = enable_editor
|
||||
self.filterable = filterable
|
||||
self.read_only = read_only
|
||||
|
||||
def preprocess(self, val):
|
||||
return val # no-op
|
||||
|
||||
def start_change(self, app, current_value):
|
||||
if current_value.endswith(ELLIPSIS):
|
||||
app.action_start_edit()
|
||||
if preprocessor:
|
||||
self.preprocessor = preprocessor
|
||||
elif self.enum:
|
||||
self.preprocessor = _enum_preprocessor(self.enum)
|
||||
else:
|
||||
# default edit mode
|
||||
app._show_input("edit", current_value)
|
||||
|
||||
|
||||
class EnumColumnConfig(TableColumnConfig):
|
||||
def __init__(
|
||||
self,
|
||||
field: str,
|
||||
display_name: str,
|
||||
enum,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(field, display_name, **kwargs)
|
||||
self.enum = enum
|
||||
|
||||
def preprocess(self, val):
|
||||
if val in self.enum:
|
||||
return val
|
||||
else:
|
||||
raise NotifyValidationError(
|
||||
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.enum, current_value), app.apply_change)
|
||||
|
||||
|
||||
|
||||
class DateColumnConfig(TableColumnConfig):
|
||||
def preprocess(self, val):
|
||||
try:
|
||||
return datetime.strptime(val, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
|
||||
|
||||
def start_change(self, app, current_value):
|
||||
app.push_screen(DateModal(current_value), app.apply_change)
|
||||
ELLIPSIS = "…"
|
||||
|
||||
self.preprocessor = lambda x: x
|
||||
|
||||
|
||||
class TableEditor(App):
|
||||
@ -159,7 +134,7 @@ class TableEditor(App):
|
||||
("?", "show_keys", "show keybindings"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, default_view="default"):
|
||||
super().__init__()
|
||||
self.filters = {}
|
||||
self.sort_string = "" # TODO: default sort
|
||||
@ -237,10 +212,7 @@ class TableEditor(App):
|
||||
self.table.move_cursor(row=0, column=1)
|
||||
|
||||
def action_delete_item(self):
|
||||
self.push_screen(ConfirmModal(f"delete ?"), self._delete_item_callback)
|
||||
|
||||
def _delete_item_callback(self, confirm):
|
||||
if confirm and self.table.cursor_column == 0:
|
||||
if self.table.cursor_column == 0:
|
||||
cur_row = self.table.cursor_row
|
||||
item_id = int(self.table.get_cell_at((cur_row, 0)))
|
||||
# deletable items need a delete
|
||||
@ -257,8 +229,10 @@ class TableEditor(App):
|
||||
continue
|
||||
val = self.filters.get(fc.field, fc.default)
|
||||
if val is not None:
|
||||
if "," in val:
|
||||
val = val.split(",")[0] # TODO: fix hack for enums
|
||||
# enums use comma separated filters
|
||||
if fc.enum:
|
||||
prepopulated[fc.field] = val.split(",")[0]
|
||||
else:
|
||||
prepopulated[fc.field] = val
|
||||
|
||||
new_item = self.add_item_callback(**prepopulated)
|
||||
@ -273,21 +247,21 @@ class TableEditor(App):
|
||||
def _active_item_id(self):
|
||||
return int(self.table.get_cell_at((self.table.cursor_row, 0)))
|
||||
|
||||
# def action_toggle_cell(self):
|
||||
# cur_row = self.table.cursor_row
|
||||
# cur_col = self.table.cursor_column
|
||||
# cconf = self._active_column_config()
|
||||
def action_toggle_cell(self):
|
||||
cur_row = self.table.cursor_row
|
||||
cur_col = self.table.cursor_column
|
||||
cconf = self._active_column_config()
|
||||
|
||||
# if cconf.enum:
|
||||
# item_id = self._active_item_id()
|
||||
# current_val = self.table.get_cell_at((cur_row, cur_col))
|
||||
# next_val = advance_enum_val(cconf.enum, current_val)
|
||||
# self.table.update_cell_at((cur_row, cur_col), next_val)
|
||||
# # trigger item_id to be saved on the next cursor move
|
||||
# # this avoids filtered columns disappearing right away
|
||||
# # and tons of DB writes
|
||||
# update = {cconf.field: next_val}
|
||||
# self._register_save_on_move(item_id, **update)
|
||||
if cconf.enum:
|
||||
item_id = self._active_item_id()
|
||||
current_val = self.table.get_cell_at((cur_row, cur_col))
|
||||
next_val = advance_enum_val(cconf.enum, current_val)
|
||||
self.table.update_cell_at((cur_row, cur_col), next_val)
|
||||
# trigger item_id to be saved on the next cursor move
|
||||
# this avoids filtered columns disappearing right away
|
||||
# and tons of DB writes
|
||||
update = {cconf.field: next_val}
|
||||
self._register_save_on_move(item_id, **update)
|
||||
|
||||
def _register_save_on_move(self, item_id, **kwargs):
|
||||
if self.save_on_move and self.save_on_move["item_id"] != item_id:
|
||||
@ -359,15 +333,15 @@ class TableEditor(App):
|
||||
if cconf.read_only:
|
||||
return
|
||||
|
||||
# save cursor before callback, so correct position updates
|
||||
self._save_cursor()
|
||||
current_value = self.table.get_cell_at(
|
||||
(self.table.cursor_row, self.table.cursor_column)
|
||||
)
|
||||
if current_value.endswith(ELLIPSIS):
|
||||
self.notify("multi-line text, use (e)dit")
|
||||
return
|
||||
current_value = remove_rich_tag(current_value)
|
||||
|
||||
# delegate to start_change, which will call back to self
|
||||
cconf.start_change(self, current_value)
|
||||
self._show_input("edit", current_value)
|
||||
|
||||
def action_start_edit(self):
|
||||
cconf = self._active_column_config()
|
||||
@ -418,7 +392,7 @@ class TableEditor(App):
|
||||
|
||||
# preprocess/validate the field being saved
|
||||
try:
|
||||
update_data[field] = cconf.preprocess(new_value)
|
||||
update_data[field] = cconf.preprocessor(new_value)
|
||||
self.update_item_callback(item_id, **update_data)
|
||||
self.refresh_data()
|
||||
except NotifyValidationError as e:
|
||||
|
@ -1,186 +0,0 @@
|
||||
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 ConfirmModal(ModalScreen):
|
||||
CSS = """
|
||||
ConfirmModal {
|
||||
align: center middle;
|
||||
background: $primary 30%;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("y", "confirm", "Down"),
|
||||
("n,escape", "cancel", "cancel"),
|
||||
]
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
super().__init__()
|
||||
|
||||
def compose(self):
|
||||
yield Label(self.message)
|
||||
yield Label("(y)es")
|
||||
yield Label("(n)o")
|
||||
|
||||
def action_confirm(self):
|
||||
self.dismiss(True)
|
||||
|
||||
def action_cancel(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
class ChoiceModal(ModalScreen):
|
||||
CSS = """
|
||||
ChoiceModal {
|
||||
align: center middle;
|
||||
background: $primary 30%;
|
||||
}
|
||||
ChoiceModal Label {
|
||||
height: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("j", "cursor_down", "Down"),
|
||||
("k", "cursor_up", "Up"),
|
||||
Binding("enter", "select", "Select", priority=True),
|
||||
("c", "select", "Select"),
|
||||
("escape", "cancel", "cancel"),
|
||||
]
|
||||
|
||||
def __init__(self, enum, selected):
|
||||
self._enum = enum
|
||||
self.selected = selected
|
||||
super().__init__()
|
||||
|
||||
def compose(self):
|
||||
yield RadioSet(
|
||||
*[
|
||||
RadioButton(
|
||||
get_color_enum(e.value, config.STATUSES, "red"), value=self.selected == str(e.value)
|
||||
)
|
||||
for e in self._enum
|
||||
]
|
||||
)
|
||||
|
||||
def action_cursor_down(self):
|
||||
self.query_one(RadioSet).action_next_button()
|
||||
|
||||
def action_cursor_up(self):
|
||||
self.query_one(RadioSet).action_previous_button()
|
||||
|
||||
def action_select(self):
|
||||
rs = self.query_one(RadioSet)
|
||||
rs.action_toggle_button()
|
||||
pressed = rs.pressed_button
|
||||
self.dismiss(str(pressed.label))
|
||||
|
||||
def action_cancel(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
class DateModal(ModalScreen):
|
||||
CSS = """
|
||||
DateModal {
|
||||
layout: horizontal;
|
||||
align: center middle;
|
||||
background: $primary 30%;
|
||||
}
|
||||
DateModal Label {
|
||||
border: solid grey;
|
||||
}
|
||||
DateModal Label.selected-date {
|
||||
border: solid green;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("j", "cursor_down", "Down"),
|
||||
("k", "cursor_up", "Up"),
|
||||
("h", "cursor_left", "Left"),
|
||||
("l", "cursor_right", "Right"),
|
||||
# ("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
|
||||
super().__init__()
|
||||
|
||||
def compose(self):
|
||||
for idx, piece in enumerate(self.pieces):
|
||||
yield Label(
|
||||
str(piece), classes="selected-date" if idx == self.selected else ""
|
||||
)
|
||||
|
||||
def action_cursor_left(self):
|
||||
# cycle Y/M/D
|
||||
self.selected = (self.selected - 1) % 3
|
||||
self._update_highlight()
|
||||
|
||||
def action_cursor_right(self):
|
||||
self.selected = (self.selected + 1) % 3
|
||||
self._update_highlight()
|
||||
|
||||
def _update_highlight(self):
|
||||
for idx, lbl in enumerate(self.query("Label")):
|
||||
if idx == self.selected:
|
||||
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]
|
||||
if piece == 0:
|
||||
return 3000
|
||||
elif piece == 1:
|
||||
return 12
|
||||
else:
|
||||
# -1 for offset array
|
||||
return days_in_month[self.pieces[1] - 1]
|
||||
|
||||
def _move_piece(self, by):
|
||||
cur_value = self.pieces[self.selected]
|
||||
cur_value += by
|
||||
if cur_value == 0:
|
||||
cur_value = self.max_for(self.selected)
|
||||
if cur_value > self.max_for(self.selected):
|
||||
cur_value = 1
|
||||
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)
|
||||
|
||||
def action_cursor_up(self):
|
||||
self._move_piece(1)
|
||||
|
||||
def on_key(self, event) -> None:
|
||||
key = event.key
|
||||
if key in "0123456789":
|
||||
cur_value = self.pieces[self.selected]
|
||||
# If we can append the values like 1+2 = 12
|
||||
# then do that, otherwise reset the value
|
||||
# so (for months) 1+3 = 3
|
||||
appended = int(str(cur_value) + key)
|
||||
if appended <= self.max_for(self.selected):
|
||||
cur_value = appended
|
||||
else:
|
||||
cur_value = int(key)
|
||||
self.pieces[self.selected] = cur_value
|
||||
self._move_piece(0)
|
||||
event.prevent_default()
|
||||
|
||||
def action_select(self):
|
||||
date = "-".join(str(p) for p in self.pieces)
|
||||
self.dismiss(date)
|
||||
|
||||
def action_cancel(self):
|
||||
self.app.pop_screen()
|
@ -11,7 +11,6 @@ from ..db import GeneratorType
|
||||
from .editor import (
|
||||
TableEditor,
|
||||
TableColumnConfig,
|
||||
EnumColumnConfig,
|
||||
)
|
||||
|
||||
|
||||
@ -19,14 +18,14 @@ class TaskGenEditor(TableEditor):
|
||||
TABLE_CONFIG = (
|
||||
TableColumnConfig("id", "ID"),
|
||||
TableColumnConfig("template", "Template", default="recur {val}"),
|
||||
EnumColumnConfig(
|
||||
TableColumnConfig(
|
||||
"type",
|
||||
"Type",
|
||||
default=GeneratorType.DAYS_BETWEEN.value,
|
||||
enum=GeneratorType,
|
||||
),
|
||||
TableColumnConfig("val", "Value", default="1"),
|
||||
TableColumnConfig("next_at", "Next @", read_only=True),
|
||||
TableColumnConfig("next_at", "Next @", default="", read_only=True),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
|
@ -2,40 +2,47 @@ 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_color_enum,
|
||||
get_colored_category,
|
||||
get_colored_status,
|
||||
get_colored_date,
|
||||
)
|
||||
from .editor import (
|
||||
TableEditor,
|
||||
TableColumnConfig,
|
||||
EnumColumnConfig,
|
||||
DateColumnConfig,
|
||||
NotifyValidationError,
|
||||
ELLIPSIS,
|
||||
)
|
||||
|
||||
|
||||
def due_preprocessor(val):
|
||||
try:
|
||||
return datetime.strptime(val, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
|
||||
|
||||
|
||||
class TT(TableEditor):
|
||||
TABLE_CONFIG = (
|
||||
TableColumnConfig("id", "ID"),
|
||||
TableColumnConfig("text", "Task", default="new task", enable_editor=True),
|
||||
EnumColumnConfig(
|
||||
TableColumnConfig(
|
||||
"status",
|
||||
"Status",
|
||||
enum=config.STATUSES,
|
||||
default="zero",
|
||||
enum=TaskStatus,
|
||||
),
|
||||
TableColumnConfig("type", "Type", default=""),
|
||||
DateColumnConfig("due", "Due", default=""),
|
||||
TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor),
|
||||
TableColumnConfig("category", "Category", default="main"),
|
||||
)
|
||||
|
||||
@ -89,12 +96,10 @@ class TT(TableEditor):
|
||||
sort=self.sort_string,
|
||||
)
|
||||
for item in items:
|
||||
category = get_color_enum(
|
||||
item.category.name if item.category else " - ",
|
||||
config.PROJECTS,
|
||||
"grey"
|
||||
category = get_colored_category(
|
||||
item.category.name if item.category else " - "
|
||||
)
|
||||
status = get_color_enum(item.status, config.STATUSES, "red")
|
||||
status = get_colored_status(item.status)
|
||||
due = get_colored_date(item.due)
|
||||
|
||||
if "\n" in item.text:
|
||||
|
@ -32,9 +32,15 @@ def advance_enum_val(enum_type, cur_val):
|
||||
return members[next_idx]
|
||||
|
||||
|
||||
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_status(status: str) -> str:
|
||||
colors = {
|
||||
"zero": "#666666",
|
||||
"wip": "#33aa99",
|
||||
"blocked": "#cc9900",
|
||||
"done": "#009900",
|
||||
}
|
||||
color = colors.get(status, "#666666")
|
||||
return f"[{color}]{status}[/]"
|
||||
|
||||
|
||||
def get_colored_category(category: str) -> str:
|
||||
|
21
tt.toml
21
tt.toml
@ -1,21 +0,0 @@
|
||||
[[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,5 +1,4 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
@ -161,7 +160,7 @@ name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
@ -902,15 +901,6 @@ 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"
|
||||
@ -920,7 +910,6 @@ dependencies = [
|
||||
{ name = "lxml" },
|
||||
{ name = "peewee" },
|
||||
{ name = "textual" },
|
||||
{ name = "tomlkit" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
@ -937,7 +926,6 @@ 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" },
|
||||
]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user