Add search feature to tasks app with / keybinding and live filtering

This commit is contained in:
Bendt
2025-12-19 16:30:45 -05:00
parent 1337d84369
commit 505fdbcd3d

View File

@@ -12,7 +12,7 @@ from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import ScrollableContainer, Vertical, Horizontal
from textual.logging import TextualHandler
from textual.widgets import DataTable, Footer, Header, Static, Markdown
from textual.widgets import DataTable, Footer, Header, Static, Markdown, Input
from .config import get_config, TasksAppConfig
from .backend import Task, TaskBackend, TaskPriority, TaskStatus, Project
@@ -154,6 +154,35 @@ class TasksApp(App):
#notes-pane.hidden {
display: none;
}
#search-container {
dock: top;
height: 3;
width: 100%;
background: $surface;
border-bottom: solid $primary;
padding: 0 1;
}
#search-container.hidden {
display: none;
}
#search-container > Horizontal {
height: 100%;
width: 100%;
align: left middle;
}
#search-container .search-label {
width: auto;
padding: 0 1;
color: $primary;
}
#search-input {
width: 1fr;
}
"""
BINDINGS = [
@@ -175,6 +204,8 @@ class TasksApp(App):
Binding("r", "refresh", "Refresh", show=True),
Binding("y", "sync", "Sync", show=True),
Binding("?", "help", "Help", show=True),
Binding("slash", "search", "Search", show=True),
Binding("escape", "clear_search", "Clear Search", show=False),
Binding("enter", "view_task", "View", show=False),
]
@@ -209,6 +240,7 @@ class TasksApp(App):
self.notes_visible = False
self.detail_visible = False
self.sidebar_visible = True # Start with sidebar visible
self.current_search_query = "" # Current search filter
self.config = get_config()
if backend:
@@ -222,6 +254,12 @@ class TasksApp(App):
def compose(self) -> ComposeResult:
"""Create the app layout."""
yield Header()
yield Horizontal(
Static("\uf002 Search:", classes="search-label"), # nf-fa-search
Input(placeholder="Filter tasks...", id="search-input"),
id="search-container",
classes="hidden",
)
yield FilterSidebar(id="sidebar")
yield Vertical(
DataTable(id="task-table", cursor_type="row"),
@@ -414,10 +452,11 @@ class TasksApp(App):
self._update_sidebar()
def _filter_tasks(self, tasks: list[Task]) -> list[Task]:
"""Filter tasks by current project and tag filters using OR logic.
"""Filter tasks by current project, tag filters, and search query.
- If project filter is set, only show tasks from that project
- If tag filters are set, show tasks that have ANY of the selected tags (OR)
- If search query is set, filter by summary, notes, project, and tags
"""
filtered = tasks
@@ -433,6 +472,18 @@ class TasksApp(App):
if any(tag in t.tags for tag in self.current_tag_filters)
]
# Filter by search query (case-insensitive match on summary, notes, project, tags)
if self.current_search_query:
query = self.current_search_query.lower()
filtered = [
t
for t in filtered
if query in t.summary.lower()
or (t.notes and query in t.notes.lower())
or (t.project and query in t.project.lower())
or any(query in tag.lower() for tag in t.tags)
]
return filtered
def _update_sidebar(self) -> None:
@@ -491,6 +542,8 @@ class TasksApp(App):
status_bar.total_tasks = len(self.tasks)
filters = []
if self.current_search_query:
filters.append(f"\uf002 {self.current_search_query}") # nf-fa-search
if self.current_project_filter:
filters.append(f"project:{self.current_project_filter}")
for tag in self.current_tag_filters:
@@ -760,9 +813,10 @@ class TasksApp(App):
self.notify(f"Sorted by {event.column} ({direction})")
def action_clear_filters(self) -> None:
"""Clear all filters."""
"""Clear all filters including search."""
self.current_project_filter = None
self.current_tag_filters = []
self.current_search_query = ""
# Also clear sidebar selections
try:
@@ -771,6 +825,15 @@ class TasksApp(App):
except Exception:
pass
# Clear and hide search input
try:
search_container = self.query_one("#search-container")
search_input = self.query_one("#search-input", Input)
search_input.value = ""
search_container.add_class("hidden")
except Exception:
pass
self.load_tasks()
self.notify("Filters cleared", severity="information")
@@ -806,6 +869,8 @@ Keybindings:
x - Delete task
w - Toggle filter sidebar
c - Clear all filters
/ - Search tasks
Esc - Clear search
r - Refresh
y - Sync with remote
Enter - View task details
@@ -813,6 +878,54 @@ Keybindings:
"""
self.notify(help_text.strip(), timeout=10)
# Search actions
def action_search(self) -> None:
"""Show search input and focus it."""
search_container = self.query_one("#search-container")
search_container.remove_class("hidden")
search_input = self.query_one("#search-input", Input)
search_input.focus()
def action_clear_search(self) -> None:
"""Clear search and hide search input."""
search_container = self.query_one("#search-container")
search_input = self.query_one("#search-input", Input)
# Only act if search is visible or there's a query
if not search_container.has_class("hidden") or self.current_search_query:
search_input.value = ""
self.current_search_query = ""
search_container.add_class("hidden")
self.load_tasks()
# Focus back to table
table = self.query_one("#task-table", DataTable)
table.focus()
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle Enter in search input - apply search and focus table."""
if event.input.id != "search-input":
return
query = event.value.strip()
self.current_search_query = query
self.load_tasks()
# Focus back to table
table = self.query_one("#task-table", DataTable)
table.focus()
if query:
self.notify(f"Searching: {query}")
def on_input_changed(self, event: Input.Changed) -> None:
"""Handle live search as user types."""
if event.input.id != "search-input":
return
# Live search - filter as user types
self.current_search_query = event.value.strip()
self.load_tasks()
# Notes actions
def action_toggle_notes(self) -> None:
"""Toggle the notes-only pane visibility."""