add coloring and sorting
This commit is contained in:
parent
55623a0781
commit
bff65bfff0
@ -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)
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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}[/]"
|
||||||
|
Loading…
Reference in New Issue
Block a user