add coloring and sorting

This commit is contained in:
James Turk 2025-01-04 01:50:14 -06:00
parent 55623a0781
commit bff65bfff0
3 changed files with 82 additions and 19 deletions

View File

@ -38,11 +38,35 @@ def update_task(
return task return task
def _parse_sort_string(sort_string, model_class):
"""
Convert sort string like 'field1,-field2' to peewee order_by expressions.
"""
sort_expressions = []
if not sort_string:
return sort_expressions
for field in sort_string.split(","):
is_desc = field.startswith("-")
field_name = field[1:] if is_desc else field
# special handling for due_date with COALESCE
if field_name == "due_date":
expr = fn.COALESCE(getattr(model_class, field_name), datetime(3000, 12, 31))
sort_expressions.append(expr.desc() if is_desc else expr)
else:
field_expr = getattr(model_class, field_name)
sort_expressions.append(field_expr.desc() if is_desc else field_expr)
return sort_expressions
def get_tasks( def get_tasks(
search_text: str | None = None, search_text: str | None = None,
category: int | None = None, category: int | None = None,
status: str | None = None, statuses: tuple[str] | None = None,
include_done: bool = False, sort: str = "",
) -> list[Task]: ) -> list[Task]:
query = Task.select().where(~Task.deleted) query = Task.select().where(~Task.deleted)
@ -50,17 +74,11 @@ def get_tasks(
query = query.where(fn.Lower(Task.text).contains(search_text.lower())) query = query.where(fn.Lower(Task.text).contains(search_text.lower()))
if category: if category:
query = query.where(Task.category == Category.get(name=category)) query = query.where(Task.category == Category.get(name=category))
if status: if statuses:
query = query.where(Task.status == status) query = query.where(Task.status.in_(statuses))
if not include_done: sort_expressions = _parse_sort_string(sort, Task)
# by default, exclude done tasks query = query.order_by(*sort_expressions)
query = query.where(Task.status != TaskStatus.DONE.value)
# order by due date (null last) and created date
query = query.order_by(
fn.COALESCE(Task.due, datetime(3000, 12, 31)), Task.created_at
)
return list(query) return list(query)

View File

@ -17,11 +17,12 @@ from .utils import (
advance_enum_val, advance_enum_val,
get_colored_category, get_colored_category,
get_colored_status, get_colored_status,
get_colored_date,
) )
# Parity
# TODO: add filtering on status # TODO: add filtering on status
# TODO: sorting
# TODO: saved searches # TODO: saved searches
# TODO: date coloring rules (days away, days of week, week of year)
# Nice to Have # Nice to Have
# TODO: CLI # TODO: CLI
# TODO: config- default category, types, colors # TODO: config- default category, types, colors
@ -97,6 +98,7 @@ class TT(App):
# filtering & editing # filtering & editing
("/", "start_search", "search tasks by name"), ("/", "start_search", "search tasks by name"),
("f", "start_filter", "filter tasks by category"), ("f", "start_filter", "filter tasks by category"),
("s", "start_sort", "sort tasks"),
("escape", "cancel_edit", "Cancel Edit"), ("escape", "cancel_edit", "Cancel Edit"),
# edits # edits
("c", "start_edit", "edit current cell"), ("c", "start_edit", "edit current cell"),
@ -112,6 +114,7 @@ class TT(App):
super().__init__() super().__init__()
self.search_query = "" self.search_query = ""
self.search_category = "" self.search_category = ""
self.sort_string = "due,status"
self.saved_cursor_pos = (0, 0) self.saved_cursor_pos = (0, 0)
self.save_on_move = None self.save_on_move = None
@ -123,7 +126,7 @@ class TT(App):
self.input_bar = Container(id="input_bar") self.input_bar = Container(id="input_bar")
self.status_bar = Container(id="status_bar") self.status_bar = Container(id="status_bar")
self.left_status = Static("LEFT", id="left_status") self.left_status = Static("LEFT", id="left_status")
self.right_status = Static("RIGHT", id="right_status") self.right_status = Static(self.sort_string, id="right_status")
yield self.header yield self.header
yield Container(self.table) yield Container(self.table)
with Container(id="footer"): with Container(id="footer"):
@ -170,20 +173,22 @@ class TT(App):
# show table # show table
self.table.clear() self.table.clear()
tasks = get_tasks(self.search_query, category=self.search_category) tasks = get_tasks(
self.search_query, category=self.search_category, sort=self.sort_string
)
for task in tasks: for task in tasks:
due_str = task.due.strftime("%Y-%m-%d") if task.due else " - "
category = get_colored_category( category = get_colored_category(
task.category.name if task.category else " - " task.category.name if task.category else " - "
) )
status = get_colored_status(task.status) status = get_colored_status(task.status)
due = get_colored_date(task.due)
self.table.add_row( self.table.add_row(
str(task.id), str(task.id),
task.text, task.text,
status, status,
task.type, task.type,
due_str, due,
category, category,
key=str(task.id), key=str(task.id),
) )
@ -269,6 +274,8 @@ class TT(App):
self.input_label.update("search: ") self.input_label.update("search: ")
elif mode == "edit": elif mode == "edit":
self.input_label.update("edit: ") self.input_label.update("edit: ")
elif mode == "sort":
self.input_label.update("sort: ")
elif mode == "filter": elif mode == "filter":
self.input_label.update("filter: ") self.input_label.update("filter: ")
else: else:
@ -288,6 +295,9 @@ class TT(App):
def action_start_filter(self): def action_start_filter(self):
self._show_input("filter", self.search_category) self._show_input("filter", self.search_category)
def action_start_sort(self):
self._show_input("sort", self.sort_string)
def _save_cursor(self): def _save_cursor(self):
self.saved_cursor_pos = (self.table.cursor_row, self.table.cursor_column) self.saved_cursor_pos = (self.table.cursor_row, self.table.cursor_column)
@ -309,6 +319,10 @@ class TT(App):
elif self.mode == "filter": elif self.mode == "filter":
self.search_category = event.value self.search_category = event.value
self.refresh_tasks(restore_cursor=False) self.refresh_tasks(restore_cursor=False)
elif self.mode == "sort":
self.sort_string = event.value
self.refresh_tasks(restore_cursor=False)
self.right_status.update(self.sort_string)
elif self.mode == "edit": elif self.mode == "edit":
self.apply_change(event.value) self.apply_change(event.value)
else: else:
@ -383,7 +397,7 @@ class KeyBindingsScreen(ModalScreen):
if binding[0] not in ["h", "j", "k", "l", "g", "G", "escape"]: if binding[0] not in ["h", "j", "k", "l", "g", "G", "escape"]:
table.add_row(binding[0], binding[2]) table.add_row(binding[0], binding[2])
yield Static("Keybindings", classes="title") yield Static("tt keybindings", classes="title")
yield Static(table) yield Static(table)
yield Static("Press any key to dismiss", classes="footer") yield Static("Press any key to dismiss", classes="footer")

View File

@ -1,4 +1,5 @@
import re import re
import datetime
def remove_rich_tag(text): def remove_rich_tag(text):
@ -30,3 +31,33 @@ def get_colored_category(category: str) -> str:
hue = hash_val % 360 hue = hash_val % 360
color = f"rgb({hue},200,200) on default" color = f"rgb({hue},200,200) on default"
return f"[{color}]{category}[/]" return f"[{color}]{category}[/]"
def get_colored_date(date: datetime.date) -> str:
if not date:
return ""
as_str = date.strftime("%Y-%m-%d")
today = datetime.date.today()
if date.date() < today:
color = "#FF0000"
else:
# Calculate weeks into future
delta = date.date() - today
weeks = delta.days // 7
colors = [
"#FF4000",
"#FF8000",
"#FFA533",
"#FFD24D",
"#FFE680",
"#FFF4B3",
"#FFFFFF",
]
if weeks >= len(colors):
color_index = -1
else:
color_index = weeks
color = colors[color_index]
return f"[{color}]{as_str}[/]"