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 .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."""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 */
 | 
			
		||||
}
 | 
			
		||||
"""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue