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,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."""
|
||||||
|
|
|
@ -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