This commit is contained in:
Bendt
2025-12-18 14:54:51 -05:00
parent 36d48c18d1
commit a63aadffcb
2 changed files with 240 additions and 30 deletions

View File

@@ -106,6 +106,7 @@ class TasksApp(App):
Binding("x", "delete_task", "Delete", show=False), Binding("x", "delete_task", "Delete", show=False),
Binding("p", "filter_project", "Project", show=True), Binding("p", "filter_project", "Project", show=True),
Binding("t", "filter_tag", "Tag", show=True), Binding("t", "filter_tag", "Tag", show=True),
Binding("o", "sort_tasks", "Sort", show=True),
Binding("c", "clear_filters", "Clear", show=True), Binding("c", "clear_filters", "Clear", show=True),
Binding("r", "refresh", "Refresh", show=True), Binding("r", "refresh", "Refresh", show=True),
Binding("y", "sync", "Sync", show=True), Binding("y", "sync", "Sync", show=True),
@@ -119,6 +120,8 @@ class TasksApp(App):
tags: list[str] tags: list[str]
current_project_filter: Optional[str] current_project_filter: Optional[str]
current_tag_filters: list[str] current_tag_filters: list[str]
current_sort_column: str
current_sort_ascending: bool
backend: Optional[TaskBackend] backend: Optional[TaskBackend]
config: Optional[TasksAppConfig] config: Optional[TasksAppConfig]
@@ -130,6 +133,8 @@ class TasksApp(App):
self.tags = [] self.tags = []
self.current_project_filter = None self.current_project_filter = None
self.current_tag_filters = [] self.current_tag_filters = []
self.current_sort_column = "priority"
self.current_sort_ascending = True
self.config = get_config() self.config = get_config()
if backend: if backend:
@@ -241,6 +246,9 @@ class TasksApp(App):
tags=self.current_tag_filters if self.current_tag_filters else None, tags=self.current_tag_filters if self.current_tag_filters else None,
) )
# Sort tasks
self._sort_tasks()
# Also load projects and tags for filtering # Also load projects and tags for filtering
self.projects = self.backend.get_projects() self.projects = self.backend.get_projects()
self.tags = self.backend.get_tags() self.tags = self.backend.get_tags()
@@ -248,6 +256,31 @@ class TasksApp(App):
# Update table # Update table
self._update_table() self._update_table()
def _sort_tasks(self) -> None:
"""Sort tasks based on current sort settings."""
def get_sort_key(task: Task):
col = self.current_sort_column
if col == "priority":
# P0 should come first (lowest value), so use enum value index
priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
return priority_order.get(task.priority.value, 99)
elif col == "project":
return task.project or ""
elif col == "summary":
return task.summary.lower()
elif col == "due":
# Tasks without due date go to the end
if task.due is None:
return "9999-99-99"
return task.due.isoformat()
elif col == "status":
return task.status.value
else:
return ""
self.tasks.sort(key=get_sort_key, reverse=not self.current_sort_ascending)
def _update_table(self) -> None: def _update_table(self) -> None:
"""Update the task table with current tasks.""" """Update the task table with current tasks."""
table = self.query_one("#task-table", DataTable) table = self.query_one("#task-table", DataTable)
@@ -420,6 +453,24 @@ class TasksApp(App):
handle_tag_selection, handle_tag_selection,
) )
def action_sort_tasks(self) -> None:
"""Open sort dialog."""
from .screens.FilterScreens import SortScreen, SortConfig
def handle_sort_selection(config: SortConfig | None) -> None:
if config is not None:
self.current_sort_column = config.column
self.current_sort_ascending = config.ascending
self._sort_tasks()
self._update_table()
direction = "asc" if config.ascending else "desc"
self.notify(f"Sorted by {config.column} ({direction})")
self.push_screen(
SortScreen(self.current_sort_column, self.current_sort_ascending),
handle_sort_selection,
)
def action_clear_filters(self) -> None: def action_clear_filters(self) -> None:
"""Clear all filters.""" """Clear all filters."""
self.current_project_filter = None self.current_project_filter = None
@@ -457,6 +508,7 @@ Keybindings:
x - Delete task x - Delete task
p - Filter by project p - Filter by project
t - Filter by tag t - Filter by tag
o - Sort tasks
c - Clear filters c - Clear filters
r - Refresh r - Refresh
y - Sync with remote y - Sync with remote

View File

@@ -5,9 +5,9 @@ from typing import Optional
from textual import on from textual import on
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.binding import Binding from textual.binding import Binding
from textual.containers import Container, Vertical from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.widgets import Label, SelectionList, Button from textual.widgets import Label, SelectionList, Button, RadioButton, RadioSet
from textual.widgets.selection_list import Selection from textual.widgets.selection_list import Selection
@@ -21,37 +21,39 @@ class ProjectFilterScreen(ModalScreen[Optional[str]]):
DEFAULT_CSS = """ DEFAULT_CSS = """
ProjectFilterScreen { ProjectFilterScreen {
align: center middle; align: center bottom;
} }
ProjectFilterScreen #filter-container { ProjectFilterScreen #filter-container {
width: 50; width: 100%;
height: auto; height: auto;
max-height: 80%; max-height: 12;
background: $surface; background: $surface;
border: thick $primary; border-top: thick $primary;
padding: 1 2; padding: 1 2;
layout: horizontal;
} }
ProjectFilterScreen #filter-title { ProjectFilterScreen #filter-title {
text-style: bold; text-style: bold;
width: 1fr; width: auto;
height: 1; height: 1;
text-align: center; margin-right: 2;
margin-bottom: 1; padding-top: 1;
} }
ProjectFilterScreen SelectionList { ProjectFilterScreen SelectionList {
width: 1fr;
height: auto; height: auto;
max-height: 15; max-height: 8;
margin-bottom: 1;
} }
ProjectFilterScreen #filter-buttons { ProjectFilterScreen #filter-buttons {
width: 1fr; width: auto;
height: auto; height: auto;
layout: horizontal;
align: center middle; align: center middle;
margin-top: 1; margin-left: 2;
} }
ProjectFilterScreen Button { ProjectFilterScreen Button {
@@ -76,8 +78,8 @@ class ProjectFilterScreen(ModalScreen[Optional[str]]):
self._current_filter = current_filter self._current_filter = current_filter
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with Container(id="filter-container"): with Horizontal(id="filter-container"):
yield Label("Select Project", id="filter-title") yield Label("Project:", id="filter-title")
selections = [ selections = [
Selection( Selection(
@@ -90,7 +92,7 @@ class ProjectFilterScreen(ModalScreen[Optional[str]]):
yield SelectionList[str](*selections, id="project-list") yield SelectionList[str](*selections, id="project-list")
with Container(id="filter-buttons"): with Horizontal(id="filter-buttons"):
yield Button("Cancel", id="cancel", variant="default") yield Button("Cancel", id="cancel", variant="default")
yield Button("Clear", id="clear", variant="warning") yield Button("Clear", id="clear", variant="warning")
yield Button("Apply", id="apply", variant="primary") yield Button("Apply", id="apply", variant="primary")
@@ -133,37 +135,39 @@ class TagFilterScreen(ModalScreen[list[str]]):
DEFAULT_CSS = """ DEFAULT_CSS = """
TagFilterScreen { TagFilterScreen {
align: center middle; align: center bottom;
} }
TagFilterScreen #filter-container { TagFilterScreen #filter-container {
width: 50; width: 100%;
height: auto; height: auto;
max-height: 80%; max-height: 12;
background: $surface; background: $surface;
border: thick $primary; border-top: thick $primary;
padding: 1 2; padding: 1 2;
layout: horizontal;
} }
TagFilterScreen #filter-title { TagFilterScreen #filter-title {
text-style: bold; text-style: bold;
width: 1fr; width: auto;
height: 1; height: 1;
text-align: center; margin-right: 2;
margin-bottom: 1; padding-top: 1;
} }
TagFilterScreen SelectionList { TagFilterScreen SelectionList {
width: 1fr;
height: auto; height: auto;
max-height: 15; max-height: 8;
margin-bottom: 1;
} }
TagFilterScreen #filter-buttons { TagFilterScreen #filter-buttons {
width: 1fr; width: auto;
height: auto; height: auto;
layout: horizontal;
align: center middle; align: center middle;
margin-top: 1; margin-left: 2;
} }
TagFilterScreen Button { TagFilterScreen Button {
@@ -188,8 +192,8 @@ class TagFilterScreen(ModalScreen[list[str]]):
self._current_filters = current_filters self._current_filters = current_filters
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with Container(id="filter-container"): with Horizontal(id="filter-container"):
yield Label("Select Tags (multi-select)", id="filter-title") yield Label("Tags:", id="filter-title")
selections = [ selections = [
Selection( Selection(
@@ -202,7 +206,7 @@ class TagFilterScreen(ModalScreen[list[str]]):
yield SelectionList[str](*selections, id="tag-list") yield SelectionList[str](*selections, id="tag-list")
with Container(id="filter-buttons"): with Horizontal(id="filter-buttons"):
yield Button("Cancel", id="cancel", variant="default") yield Button("Cancel", id="cancel", variant="default")
yield Button("Clear", id="clear", variant="warning") yield Button("Clear", id="clear", variant="warning")
yield Button("Apply", id="apply", variant="primary") yield Button("Apply", id="apply", variant="primary")
@@ -230,3 +234,157 @@ class TagFilterScreen(ModalScreen[list[str]]):
def action_select(self) -> None: def action_select(self) -> None:
self.handle_apply() self.handle_apply()
class SortConfig:
"""Configuration for sort settings."""
def __init__(self, column: str = "priority", ascending: bool = True):
self.column = column
self.ascending = ascending
class SortScreen(ModalScreen[Optional[SortConfig]]):
"""Modal screen for selecting sort column and direction."""
BINDINGS = [
Binding("escape", "cancel", "Cancel"),
Binding("enter", "select", "Select"),
]
DEFAULT_CSS = """
SortScreen {
align: center middle;
}
SortScreen #sort-container {
width: 40;
height: auto;
background: $surface;
border: thick $primary;
padding: 1 2;
}
SortScreen #sort-title {
text-style: bold;
width: 100%;
height: 1;
text-align: center;
margin-bottom: 1;
}
SortScreen .section-label {
margin-top: 1;
margin-bottom: 0;
color: $text-muted;
}
SortScreen RadioSet {
width: 100%;
height: auto;
background: transparent;
border: none;
padding: 0;
}
SortScreen RadioButton {
width: 100%;
background: transparent;
padding: 0;
height: 1;
}
SortScreen #sort-buttons {
width: 100%;
height: 3;
align: center middle;
margin-top: 1;
}
SortScreen Button {
margin: 0 1;
}
"""
# Available columns for sorting
SORT_COLUMNS = [
("priority", "Priority"),
("project", "Project"),
("summary", "Summary"),
("due", "Due"),
("status", "Status"),
]
def __init__(
self,
current_column: str = "priority",
current_ascending: bool = True,
**kwargs,
):
"""Initialize the sort screen.
Args:
current_column: Currently selected sort column
current_ascending: Current sort direction (True=ascending)
"""
super().__init__(**kwargs)
self._current_column = current_column
self._current_ascending = current_ascending
def compose(self) -> ComposeResult:
with Vertical(id="sort-container"):
yield Label("Sort Tasks", id="sort-title")
# Column selection
yield Label("Sort by:", classes="section-label")
with RadioSet(id="column-set"):
for key, display in self.SORT_COLUMNS:
yield RadioButton(
display, value=key == self._current_column, id=f"col-{key}"
)
# Direction selection
yield Label("Direction:", classes="section-label")
with RadioSet(id="direction-set"):
yield RadioButton(
"Ascending", value=self._current_ascending, id="dir-asc"
)
yield RadioButton(
"Descending", value=not self._current_ascending, id="dir-desc"
)
with Horizontal(id="sort-buttons"):
yield Button("Cancel", id="cancel", variant="default")
yield Button("Apply", id="apply", variant="primary")
def on_mount(self) -> None:
"""Focus the column radio set."""
self.query_one("#column-set", RadioSet).focus()
@on(Button.Pressed, "#apply")
def handle_apply(self) -> None:
column_set = self.query_one("#column-set", RadioSet)
direction_set = self.query_one("#direction-set", RadioSet)
# Get selected column from pressed button id
column = self._current_column
if column_set.pressed_button and column_set.pressed_button.id:
# Extract column key from id like "col-priority"
column = column_set.pressed_button.id.replace("col-", "")
# Get direction
ascending = self._current_ascending
if direction_set.pressed_button and direction_set.pressed_button.id:
ascending = direction_set.pressed_button.id == "dir-asc"
self.dismiss(SortConfig(column=column, ascending=ascending))
@on(Button.Pressed, "#cancel")
def handle_cancel(self) -> None:
self.dismiss(None)
def action_cancel(self) -> None:
self.dismiss(None)
def action_select(self) -> None:
self.handle_apply()