Implement search functionality with input field and filtering in the hosts manager

This commit is contained in:
Philip Henning 2025-08-14 20:27:48 +02:00
parent 07e7e4f70f
commit 5b768c004b
5 changed files with 140 additions and 52 deletions

View file

@ -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,13 +202,36 @@ 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":
# 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 # Changes will be validated and saved when exiting edit mode
pass pass
@ -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."""

View file

@ -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(

View file

@ -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),

View file

@ -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 */
}
""" """

View file

@ -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