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 .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,13 +202,36 @@ 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
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
@ -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."""

View file

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

View file

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

View file

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

View file

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