wip
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user