From 5b768c004b31916a0097606b6a07b4311e84778a Mon Sep 17 00:00:00 2001 From: phg Date: Thu, 14 Aug 2025 20:27:48 +0200 Subject: [PATCH] Implement search functionality with input field and filtering in the hosts manager --- src/hosts/tui/app.py | 79 ++++++++++++++++++---------------- src/hosts/tui/help_modal.py | 63 +++++++++++++++++++++------ src/hosts/tui/keybindings.py | 2 +- src/hosts/tui/styles.py | 22 +++++++++- src/hosts/tui/table_handler.py | 26 +++++++++++ 5 files changed, 140 insertions(+), 52 deletions(-) diff --git a/src/hosts/tui/app.py b/src/hosts/tui/app.py index ef25774..ea1bff5 100644 --- a/src/hosts/tui/app.py +++ b/src/hosts/tui/app.py @@ -18,7 +18,6 @@ from .config_modal import ConfigModal from .password_modal import PasswordModal from .add_entry_modal import AddEntryModal from .delete_confirmation_modal import DeleteConfirmationModal -from .search_modal import SearchModal from .help_modal import HelpModal from .styles import HOSTS_MANAGER_CSS from .keybindings import HOSTS_MANAGER_BINDINGS @@ -46,6 +45,7 @@ class HostsManagerApp(App): entry_edit_mode: reactive[bool] = reactive(False) sort_column: reactive[str] = reactive("") # "ip" or "hostname" sort_ascending: reactive[bool] = reactive(True) + search_term: reactive[str] = reactive("") def __init__(self): super().__init__() @@ -71,6 +71,18 @@ class HostsManagerApp(App): yield Header() yield Footer() + # Spacer + yield Static("", classes="spacer") + + # Search bar above the panes + with Horizontal(classes="search-container") as search_container: + search_container.border_title = "Search" + yield Input( + placeholder="Filter by hostname, IP address, or comment...", + id="search-input", + classes="search-input", + ) + with Horizontal(classes="hosts-container"): # Left pane - entries table with Vertical(classes="left-pane") as left_pane: @@ -190,15 +202,38 @@ class HostsManagerApp(App): def on_key(self, event) -> None: """Handle key events to override default tab behavior in edit mode.""" + # Handle tab navigation for search bar and data table + if event.key == "tab" and not self.entry_edit_mode: + search_input = self.query_one("#search-input", Input) + entries_table = self.query_one("#entries-table", DataTable) + + # Check which widget currently has focus + if self.focused == search_input: + # Focus on entries table + entries_table.focus() + event.prevent_default() + return + elif self.focused == entries_table: + # Focus on search input + search_input.focus() + event.prevent_default() + return + # Delegate to edit handler for edit mode navigation if self.edit_handler.handle_entry_edit_key_event(event): return # Event was handled by edit handler def on_input_changed(self, event: Input.Changed) -> None: """Handle input field changes (no auto-save - changes saved on exit).""" - # Input changes are tracked but not automatically saved - # Changes will be validated and saved when exiting edit mode - pass + if event.input.id == "search-input": + # Update search term and filter entries + self.search_term = event.value.strip() + self.table_handler.populate_entries_table() + self.details_handler.update_entry_details() + else: + # Edit form input changes are tracked but not automatically saved + # Changes will be validated and saved when exiting edit mode + pass def on_checkbox_changed(self, event: Checkbox.Changed) -> None: """Handle checkbox changes (no auto-save - changes saved on exit).""" @@ -429,38 +464,10 @@ class HostsManagerApp(App): self.push_screen(DeleteConfirmationModal(entry), handle_delete_confirmation) def action_search(self) -> None: - """Show the search modal.""" - if not self.hosts_file.entries: - self.update_status("No entries to search") - return - - def handle_search_result(selected_index) -> None: - if selected_index is None: - self.update_status("Search cancelled") - return - - if 0 <= selected_index < len(self.hosts_file.entries): - # Update selected entry and refresh display - self.selected_entry_index = selected_index - self.table_handler.populate_entries_table() - - # Move cursor to the found entry - display_index = self.table_handler.actual_index_to_display_index( - selected_index - ) - table = self.query_one("#entries-table", DataTable) - if display_index < table.row_count: - table.move_cursor(row=display_index) - - self.details_handler.update_entry_details() - entry = self.hosts_file.entries[selected_index] - self.update_status( - f"Found: {entry.canonical_hostname} ({entry.ip_address})" - ) - else: - self.update_status("Selected entry not found") - - self.push_screen(SearchModal(self.hosts_file.entries), handle_search_result) + """Focus the search bar for filtering entries.""" + search_input = self.query_one("#search-input", Input) + search_input.focus() + self.update_status("Use the search bar to filter entries") def action_quit(self) -> None: """Quit the application.""" diff --git a/src/hosts/tui/help_modal.py b/src/hosts/tui/help_modal.py index a070f94..3c018d7 100644 --- a/src/hosts/tui/help_modal.py +++ b/src/hosts/tui/help_modal.py @@ -93,33 +93,70 @@ class HelpModal(ModalScreen): # Main Commands section with Vertical(classes="help-section"): yield Static("Main Commands", classes="help-section-title") - yield Static("[bold]r[/bold] Reload [bold]h[/bold] Help [bold]c[/bold] Config [bold]Ctrl+F[/bold] Search [bold]q[/bold] Quit", classes="help-item") - yield Static("[bold]i[/bold] Sort by IP [bold]n[/bold] Sort by hostname", classes="help-item") + yield Static( + "[bold]r[/bold] Reload [bold]h[/bold] Help [bold]c[/bold] Config [bold]Ctrl+F[/bold] Search [bold]q[/bold] Quit", + classes="help-item", + ) + yield Static( + "[bold]i[/bold] Sort by IP [bold]n[/bold] Sort by hostname", + classes="help-item", + ) # Edit Mode section with Vertical(classes="help-section"): yield Static("Edit Mode Commands", classes="help-section-title") - yield Static("[bold]Ctrl+E[/bold] - Toggle edit mode (requires sudo)", classes="help-item") - yield Static("[italic]Edit mode commands:[/italic] [bold]Space[/bold] Toggle [bold]a[/bold] Add [bold]d[/bold] Delete [bold]e[/bold] Edit", classes="help-item") - yield Static("[bold]Shift+↑/↓[/bold] Move entry [bold]Ctrl+S[/bold] Save file", classes="help-item") + yield Static( + "[bold]Ctrl+E[/bold] - Toggle edit mode (requires sudo)", + classes="help-item", + ) + yield Static( + "[italic]Edit mode commands:[/italic] [bold]Space[/bold] Toggle [bold]a[/bold] Add [bold]d[/bold] Delete [bold]e[/bold] Edit", + classes="help-item", + ) + yield Static( + "[bold]Shift+↑/↓[/bold] Move entry [bold]Ctrl+S[/bold] Save file", + classes="help-item", + ) # Form Navigation section with Vertical(classes="help-section"): - yield Static("Form & Modal Navigation", classes="help-section-title") - yield Static("[bold]Tab/Shift+Tab[/bold] Navigate fields [bold]Enter[/bold] Confirm/Save [bold]Escape[/bold] Cancel/Exit", classes="help-item") + yield Static( + "Form & Modal Navigation", classes="help-section-title" + ) + yield Static( + "[bold]Tab/Shift+Tab[/bold] Navigate fields [bold]Enter[/bold] Confirm/Save [bold]Escape[/bold] Cancel/Exit", + classes="help-item", + ) # Special Commands section with Vertical(classes="help-section"): - yield Static("Special Dialog Commands", classes="help-section-title") - yield Static("[bold]F3[/bold] Search in search dialog [bold]s[/bold] Save changes [bold]d[/bold] Discard changes", classes="help-item") + yield Static( + "Special Dialog Commands", classes="help-section-title" + ) + yield Static( + "[bold]F3[/bold] Search in search dialog [bold]s[/bold] Save changes [bold]d[/bold] Discard changes", + classes="help-item", + ) # Status and Tips section with Vertical(classes="help-section"): yield Static("Entry Status & Tips", classes="help-section-title") - yield Static("✓ Active (enabled) ✗ Inactive (commented out)", classes="help-item") - yield Static("• Edit mode commands require [bold]Ctrl+E[/bold] first", classes="help-item") - yield Static("• Search supports partial matches in IP, hostname, or comment", classes="help-item") - yield Static("• Edit mode creates automatic backups • System entries cannot be modified", classes="help-item") + yield Static( + "✓ Active (enabled) ✗ Inactive (commented out)", + classes="help-item", + ) + yield Static( + "• Edit mode commands require [bold]Ctrl+E[/bold] first", + classes="help-item", + ) + yield Static( + "• Search supports partial matches in IP, hostname, or comment", + classes="help-item", + ) + yield Static( + "• Edit mode creates automatic backups • System entries cannot be modified", + classes="help-item", + ) with Horizontal(classes="button-row"): yield Button( diff --git a/src/hosts/tui/keybindings.py b/src/hosts/tui/keybindings.py index e9d4150..e333d73 100644 --- a/src/hosts/tui/keybindings.py +++ b/src/hosts/tui/keybindings.py @@ -15,7 +15,7 @@ HOSTS_MANAGER_BINDINGS = [ Binding("i", "sort_by_ip", "Sort by IP"), Binding("n", "sort_by_hostname", "Sort by Hostname"), Binding("c", "config", "Config"), - Binding("ctrl+f", "search", "Search"), + Binding("ctrl+f", "search", "Focus Search"), Binding("ctrl+e", "toggle_edit_mode", "Edit Mode"), Binding("a", "add_entry", "Add Entry", show=False), Binding("d", "delete_entry", "Delete Entry", show=False), diff --git a/src/hosts/tui/styles.py b/src/hosts/tui/styles.py index cfee74a..f977afb 100644 --- a/src/hosts/tui/styles.py +++ b/src/hosts/tui/styles.py @@ -7,6 +7,19 @@ across the application. # CSS styles for the hosts manager application HOSTS_MANAGER_CSS = """ +.search-container { + border: round $primary; + height: 3; + padding: 0 1; + margin-bottom: 1; +} + +.search-input { + width: 1fr; + height: 1; + border: none; +} + .hosts-container { height: 1fr; } @@ -105,6 +118,11 @@ HOSTS_MANAGER_CSS = """ background: $surface; } -Header { height: 1; } -Header.-tall { height: 1; } /* Fix tall header also to height 1 */ +Header { + height: 1; +} + +Header.-tall { + height: 1; /* Fix tall header also to height 1 */ +} """ diff --git a/src/hosts/tui/table_handler.py b/src/hosts/tui/table_handler.py index 1d94362..58cfb44 100644 --- a/src/hosts/tui/table_handler.py +++ b/src/hosts/tui/table_handler.py @@ -28,6 +28,32 @@ class TableHandler: entry.ip_address, canonical_hostname ): continue + + # Apply search filter if search term is provided + if self.app.search_term: + search_term_lower = self.app.search_term.lower() + matches_search = False + + # Search in IP address + if search_term_lower in entry.ip_address.lower(): + matches_search = True + + # Search in hostnames + if not matches_search: + for hostname in entry.hostnames: + if search_term_lower in hostname.lower(): + matches_search = True + break + + # Search in comment + if not matches_search and entry.comment: + if search_term_lower in entry.comment.lower(): + matches_search = True + + # Skip entry if it doesn't match search term + if not matches_search: + continue + visible_entries.append(entry) return visible_entries