Add search feature to calendar app with / keybinding using khal search

This commit is contained in:
Bendt
2025-12-19 16:34:21 -05:00
parent 599507068a
commit 95d3098bf3
3 changed files with 186 additions and 1 deletions

View File

@@ -13,7 +13,7 @@ from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, Horizontal, Vertical
from textual.logging import TextualHandler
from textual.widgets import Footer, Header, Static
from textual.widgets import Footer, Header, Static, Input
from textual.reactive import reactive
from src.calendar.backend import CalendarBackend, Event
@@ -116,6 +116,47 @@ class CalendarApp(App):
#event-detail.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;
}
#search-results {
dock: bottom;
height: 40%;
border-top: solid $primary;
background: $surface;
padding: 1;
}
#search-results.hidden {
display: none;
}
"""
BINDINGS = [
@@ -133,6 +174,8 @@ class CalendarApp(App):
Binding("r", "refresh", "Refresh", show=True),
Binding("enter", "view_event", "View", show=True),
Binding("a", "add_event", "Add", show=True),
Binding("slash", "search", "Search", show=True),
Binding("escape", "clear_search", "Clear Search", show=False),
Binding("?", "help", "Help", show=True),
]
@@ -143,10 +186,12 @@ class CalendarApp(App):
# Instance attributes
backend: Optional[CalendarBackend]
_invites: list[CalendarInvite]
_search_results: list[Event]
def __init__(self, backend: Optional[CalendarBackend] = None):
super().__init__()
self._invites = []
self._search_results = []
if backend:
self.backend = backend
@@ -159,11 +204,18 @@ class CalendarApp(App):
def compose(self) -> ComposeResult:
"""Create the app layout."""
yield Header()
yield Horizontal(
Static("\uf002 Search:", classes="search-label"), # nf-fa-search
Input(placeholder="Search events...", id="search-input"),
id="search-container",
classes="hidden",
)
with Horizontal(id="main-content"):
with Vertical(id="sidebar"):
yield MonthCalendar(id="sidebar-calendar")
yield InvitesPanel(id="sidebar-invites")
yield WeekGrid(id="week-grid")
yield Static(id="search-results", classes="hidden")
yield Static(id="event-detail", classes="hidden")
yield CalendarStatusBar(id="status-bar")
yield Footer()
@@ -532,6 +584,8 @@ Keybindings:
w - Toggle weekends (5/7 days)
s - Toggle sidebar
i - Focus invites panel
/ - Search events
Esc - Clear search
Enter - View event details
a - Add new event
r - Refresh
@@ -539,6 +593,82 @@ 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 UI."""
search_container = self.query_one("#search-container")
search_results = self.query_one("#search-results", Static)
search_input = self.query_one("#search-input", Input)
# Only act if search is visible
if not search_container.has_class("hidden") or not search_results.has_class(
"hidden"
):
search_input.value = ""
search_container.add_class("hidden")
search_results.add_class("hidden")
self._search_results = []
# Focus back to grid
grid = self.query_one("#week-grid", WeekGrid)
grid.focus()
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle Enter in search input - perform search."""
if event.input.id != "search-input":
return
query = event.value.strip()
if not query:
return
self._perform_search(query)
def _perform_search(self, query: str) -> None:
"""Perform event search and display results."""
if not self.backend:
return
# Check if backend has search_events method
if not hasattr(self.backend, "search_events"):
self.notify(
"Search not supported by this calendar backend", severity="warning"
)
return
results = self.backend.search_events(query)
self._search_results = results
# Update results display
search_results = self.query_one("#search-results", Static)
if results:
lines = [f"[b]Search results for '{query}': {len(results)} found[/b]", ""]
for event in results[:20]: # Limit display to 20 results
date_str = event.start.strftime("%Y-%m-%d %H:%M")
lines.append(f" {date_str} [b]{event.title}[/b]")
if event.location:
lines.append(f" [dim]{event.location}[/dim]")
if len(results) > 20:
lines.append(f" ... and {len(results) - 20} more")
search_results.update("\n".join(lines))
search_results.remove_class("hidden")
self.notify(f"Found {len(results)} event(s)")
else:
search_results.update(f"[b]No events found matching '{query}'[/b]")
search_results.remove_class("hidden")
self.notify("No events found")
# Focus back to grid
grid = self.query_one("#week-grid", WeekGrid)
grid.focus()
async def action_quit(self) -> None:
"""Quit the app and clean up IPC listener."""
if hasattr(self, "_ipc_listener"):