Implement search functionality with input field and filtering in the hosts manager
This commit is contained in:
		
							parent
							
								
									07e7e4f70f
								
							
						
					
					
						commit
						5b768c004b
					
				
					 5 changed files with 140 additions and 52 deletions
				
			
		| 
						 | 
					@ -18,7 +18,6 @@ from .config_modal import ConfigModal
 | 
				
			||||||
from .password_modal import PasswordModal
 | 
					from .password_modal import PasswordModal
 | 
				
			||||||
from .add_entry_modal import AddEntryModal
 | 
					from .add_entry_modal import AddEntryModal
 | 
				
			||||||
from .delete_confirmation_modal import DeleteConfirmationModal
 | 
					from .delete_confirmation_modal import DeleteConfirmationModal
 | 
				
			||||||
from .search_modal import SearchModal
 | 
					 | 
				
			||||||
from .help_modal import HelpModal
 | 
					from .help_modal import HelpModal
 | 
				
			||||||
from .styles import HOSTS_MANAGER_CSS
 | 
					from .styles import HOSTS_MANAGER_CSS
 | 
				
			||||||
from .keybindings import HOSTS_MANAGER_BINDINGS
 | 
					from .keybindings import HOSTS_MANAGER_BINDINGS
 | 
				
			||||||
| 
						 | 
					@ -46,6 +45,7 @@ class HostsManagerApp(App):
 | 
				
			||||||
    entry_edit_mode: reactive[bool] = reactive(False)
 | 
					    entry_edit_mode: reactive[bool] = reactive(False)
 | 
				
			||||||
    sort_column: reactive[str] = reactive("")  # "ip" or "hostname"
 | 
					    sort_column: reactive[str] = reactive("")  # "ip" or "hostname"
 | 
				
			||||||
    sort_ascending: reactive[bool] = reactive(True)
 | 
					    sort_ascending: reactive[bool] = reactive(True)
 | 
				
			||||||
 | 
					    search_term: reactive[str] = reactive("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
        super().__init__()
 | 
					        super().__init__()
 | 
				
			||||||
| 
						 | 
					@ -71,6 +71,18 @@ class HostsManagerApp(App):
 | 
				
			||||||
        yield Header()
 | 
					        yield Header()
 | 
				
			||||||
        yield Footer()
 | 
					        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"):
 | 
					        with Horizontal(classes="hosts-container"):
 | 
				
			||||||
            # Left pane - entries table
 | 
					            # Left pane - entries table
 | 
				
			||||||
            with Vertical(classes="left-pane") as left_pane:
 | 
					            with Vertical(classes="left-pane") as left_pane:
 | 
				
			||||||
| 
						 | 
					@ -190,15 +202,38 @@ class HostsManagerApp(App):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def on_key(self, event) -> None:
 | 
					    def on_key(self, event) -> None:
 | 
				
			||||||
        """Handle key events to override default tab behavior in edit mode."""
 | 
					        """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
 | 
					        # Delegate to edit handler for edit mode navigation
 | 
				
			||||||
        if self.edit_handler.handle_entry_edit_key_event(event):
 | 
					        if self.edit_handler.handle_entry_edit_key_event(event):
 | 
				
			||||||
            return  # Event was handled by edit handler
 | 
					            return  # Event was handled by edit handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def on_input_changed(self, event: Input.Changed) -> None:
 | 
					    def on_input_changed(self, event: Input.Changed) -> None:
 | 
				
			||||||
        """Handle input field changes (no auto-save - changes saved on exit)."""
 | 
					        """Handle input field changes (no auto-save - changes saved on exit)."""
 | 
				
			||||||
        # Input changes are tracked but not automatically saved
 | 
					        if event.input.id == "search-input":
 | 
				
			||||||
        # Changes will be validated and saved when exiting edit mode
 | 
					            # Update search term and filter entries
 | 
				
			||||||
        pass
 | 
					            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:
 | 
					    def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
 | 
				
			||||||
        """Handle checkbox changes (no auto-save - changes saved on exit)."""
 | 
					        """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)
 | 
					        self.push_screen(DeleteConfirmationModal(entry), handle_delete_confirmation)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def action_search(self) -> None:
 | 
					    def action_search(self) -> None:
 | 
				
			||||||
        """Show the search modal."""
 | 
					        """Focus the search bar for filtering entries."""
 | 
				
			||||||
        if not self.hosts_file.entries:
 | 
					        search_input = self.query_one("#search-input", Input)
 | 
				
			||||||
            self.update_status("No entries to search")
 | 
					        search_input.focus()
 | 
				
			||||||
            return
 | 
					        self.update_status("Use the search bar to filter entries")
 | 
				
			||||||
 | 
					 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def action_quit(self) -> None:
 | 
					    def action_quit(self) -> None:
 | 
				
			||||||
        """Quit the application."""
 | 
					        """Quit the application."""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,33 +93,70 @@ class HelpModal(ModalScreen):
 | 
				
			||||||
                # Main Commands section
 | 
					                # Main Commands section
 | 
				
			||||||
                with Vertical(classes="help-section"):
 | 
					                with Vertical(classes="help-section"):
 | 
				
			||||||
                    yield Static("Main Commands", classes="help-section-title")
 | 
					                    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(
 | 
				
			||||||
                    yield Static("[bold]i[/bold] Sort by IP  [bold]n[/bold] Sort by hostname", classes="help-item")
 | 
					                        "[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
 | 
					                # Edit Mode section
 | 
				
			||||||
                with Vertical(classes="help-section"):
 | 
					                with Vertical(classes="help-section"):
 | 
				
			||||||
                    yield Static("Edit Mode Commands", classes="help-section-title")
 | 
					                    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(
 | 
				
			||||||
                    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")
 | 
					                        "[bold]Ctrl+E[/bold] - Toggle edit mode (requires sudo)",
 | 
				
			||||||
                    yield Static("[bold]Shift+↑/↓[/bold] Move entry  [bold]Ctrl+S[/bold] Save file", classes="help-item")
 | 
					                        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
 | 
					                # Form Navigation section
 | 
				
			||||||
                with Vertical(classes="help-section"):
 | 
					                with Vertical(classes="help-section"):
 | 
				
			||||||
                    yield Static("Form & Modal Navigation", classes="help-section-title")
 | 
					                    yield Static(
 | 
				
			||||||
                    yield Static("[bold]Tab/Shift+Tab[/bold] Navigate fields  [bold]Enter[/bold] Confirm/Save  [bold]Escape[/bold] Cancel/Exit", classes="help-item")
 | 
					                        "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
 | 
					                # Special Commands section
 | 
				
			||||||
                with Vertical(classes="help-section"):
 | 
					                with Vertical(classes="help-section"):
 | 
				
			||||||
                    yield Static("Special Dialog Commands", classes="help-section-title")
 | 
					                    yield Static(
 | 
				
			||||||
                    yield Static("[bold]F3[/bold] Search in search dialog  [bold]s[/bold] Save changes  [bold]d[/bold] Discard changes", classes="help-item")
 | 
					                        "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
 | 
					                # Status and Tips section
 | 
				
			||||||
                with Vertical(classes="help-section"):
 | 
					                with Vertical(classes="help-section"):
 | 
				
			||||||
                    yield Static("Entry Status & Tips", classes="help-section-title")
 | 
					                    yield Static("Entry Status & Tips", classes="help-section-title")
 | 
				
			||||||
                    yield Static("✓ Active (enabled)  ✗ Inactive (commented out)", classes="help-item")
 | 
					                    yield Static(
 | 
				
			||||||
                    yield Static("• Edit mode commands require [bold]Ctrl+E[/bold] first", classes="help-item")
 | 
					                        "✓ Active (enabled)  ✗ Inactive (commented out)",
 | 
				
			||||||
                    yield Static("• Search supports partial matches in IP, hostname, or comment", classes="help-item")
 | 
					                        classes="help-item",
 | 
				
			||||||
                    yield Static("• Edit mode creates automatic backups • System entries cannot be modified", 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"):
 | 
					            with Horizontal(classes="button-row"):
 | 
				
			||||||
                yield Button(
 | 
					                yield Button(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ HOSTS_MANAGER_BINDINGS = [
 | 
				
			||||||
    Binding("i", "sort_by_ip", "Sort by IP"),
 | 
					    Binding("i", "sort_by_ip", "Sort by IP"),
 | 
				
			||||||
    Binding("n", "sort_by_hostname", "Sort by Hostname"),
 | 
					    Binding("n", "sort_by_hostname", "Sort by Hostname"),
 | 
				
			||||||
    Binding("c", "config", "Config"),
 | 
					    Binding("c", "config", "Config"),
 | 
				
			||||||
    Binding("ctrl+f", "search", "Search"),
 | 
					    Binding("ctrl+f", "search", "Focus Search"),
 | 
				
			||||||
    Binding("ctrl+e", "toggle_edit_mode", "Edit Mode"),
 | 
					    Binding("ctrl+e", "toggle_edit_mode", "Edit Mode"),
 | 
				
			||||||
    Binding("a", "add_entry", "Add Entry", show=False),
 | 
					    Binding("a", "add_entry", "Add Entry", show=False),
 | 
				
			||||||
    Binding("d", "delete_entry", "Delete Entry", show=False),
 | 
					    Binding("d", "delete_entry", "Delete Entry", show=False),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,19 @@ across the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# CSS styles for the hosts manager application
 | 
					# CSS styles for the hosts manager application
 | 
				
			||||||
HOSTS_MANAGER_CSS = """
 | 
					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 {
 | 
					.hosts-container {
 | 
				
			||||||
    height: 1fr;
 | 
					    height: 1fr;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -105,6 +118,11 @@ HOSTS_MANAGER_CSS = """
 | 
				
			||||||
    background: $surface;
 | 
					    background: $surface;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Header { height: 1; }
 | 
					Header {
 | 
				
			||||||
Header.-tall { height: 1; }  /* Fix tall header also to height 1 */
 | 
					    height: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Header.-tall {
 | 
				
			||||||
 | 
					    height: 1; /* Fix tall header also to height 1 */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,32 @@ class TableHandler:
 | 
				
			||||||
                entry.ip_address, canonical_hostname
 | 
					                entry.ip_address, canonical_hostname
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                continue
 | 
					                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)
 | 
					            visible_entries.append(entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return visible_entries
 | 
					        return visible_entries
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue