diff --git a/src/hosts/core/manager.py b/src/hosts/core/manager.py index 5bbd280..1b44c38 100644 --- a/src/hosts/core/manager.py +++ b/src/hosts/core/manager.py @@ -275,67 +275,6 @@ class HostsManager: except Exception as e: return False, f"Error moving entry: {e}" - def add_entry( - self, - hosts_file: HostsFile, - entry: HostEntry, - ) -> Tuple[bool, str]: - """ - Add a new entry to the hosts file. - - Args: - hosts_file: The hosts file to modify - entry: The new entry to add - - Returns: - Tuple of (success, message) - """ - if not self.edit_mode: - return False, "Not in edit mode" - - try: - # Add the new entry at the end - hosts_file.entries.append(entry) - return True, "Entry added successfully" - - except Exception as e: - return False, f"Error adding entry: {e}" - - def delete_entry( - self, - hosts_file: HostsFile, - index: int, - ) -> Tuple[bool, str]: - """ - Delete an entry from the hosts file. - - Args: - hosts_file: The hosts file to modify - index: Index of the entry to delete - - Returns: - Tuple of (success, message) - """ - if not self.edit_mode: - return False, "Not in edit mode" - - if not (0 <= index < len(hosts_file.entries)): - return False, "Invalid entry index" - - try: - entry = hosts_file.entries[index] - - # Prevent deletion of default system entries - if entry.is_default_entry(): - return False, "Cannot delete default system entries" - - # Remove the entry - deleted_entry = hosts_file.entries.pop(index) - return True, f"Entry deleted: {deleted_entry.canonical_hostname}" - - except Exception as e: - return False, f"Error deleting entry: {e}" - def update_entry( self, hosts_file: HostsFile, diff --git a/src/hosts/tui/add_entry_modal.py b/src/hosts/tui/add_entry_modal.py deleted file mode 100644 index d5b1de7..0000000 --- a/src/hosts/tui/add_entry_modal.py +++ /dev/null @@ -1,238 +0,0 @@ -""" -Add Entry modal window for the hosts TUI application. - -This module provides a floating modal window for creating new host entries. -""" - -from textual.app import ComposeResult -from textual.containers import Vertical, Horizontal -from textual.widgets import Static, Button, Input, Checkbox, Label -from textual.screen import ModalScreen -from textual.binding import Binding - -from ..core.models import HostEntry - - -class AddEntryModal(ModalScreen): - """ - Modal screen for adding new host entries. - - Provides a floating window with input fields for creating new entries. - """ - - CSS = """ - AddEntryModal { - align: center middle; - } - - .add-entry-container { - width: 80; - height: 25; - background: $surface; - border: thick $primary; - padding: 1; - } - - .add-entry-title { - text-align: center; - text-style: bold; - color: $primary; - margin-bottom: 1; - } - - .add-entry-section { - margin: 1 0; - } - - .add-entry-input { - margin: 0 2; - width: 1fr; - } - - .button-row { - margin-top: 2; - align: center middle; - } - - .add-entry-button { - margin: 0 1; - min-width: 10; - } - - .validation-error { - color: $error; - margin: 0 2; - text-style: italic; - } - """ - - BINDINGS = [ - Binding("escape", "cancel", "Cancel"), - Binding("ctrl+s", "save", "Save"), - ] - - def __init__(self): - super().__init__() - - def compose(self) -> ComposeResult: - """Create the add entry modal layout.""" - with Vertical(classes="add-entry-container"): - yield Static("Add New Host Entry", classes="add-entry-title") - - with Vertical(classes="add-entry-section"): - yield Label("IP Address:") - yield Input( - placeholder="e.g., 192.168.1.1 or 2001:db8::1", - id="ip-address-input", - classes="add-entry-input", - ) - yield Static("", id="ip-error", classes="validation-error") - - with Vertical(classes="add-entry-section"): - yield Label("Hostnames (comma-separated):") - yield Input( - placeholder="e.g., example.com, www.example.com", - id="hostnames-input", - classes="add-entry-input", - ) - yield Static("", id="hostnames-error", classes="validation-error") - - with Vertical(classes="add-entry-section"): - yield Label("Comment (optional):") - yield Input( - placeholder="e.g., Development server", - id="comment-input", - classes="add-entry-input", - ) - - with Vertical(classes="add-entry-section"): - yield Checkbox("Active (enabled)", value=True, id="active-checkbox") - - with Horizontal(classes="button-row"): - yield Button( - "Add Entry", - variant="primary", - id="add-button", - classes="add-entry-button", - ) - yield Button( - "Cancel", - variant="default", - id="cancel-button", - classes="add-entry-button", - ) - - def on_mount(self) -> None: - """Focus IP address input when modal opens.""" - ip_input = self.query_one("#ip-address-input", Input) - ip_input.focus() - - def on_button_pressed(self, event: Button.Pressed) -> None: - """Handle button presses.""" - if event.button.id == "add-button": - self.action_save() - elif event.button.id == "cancel-button": - self.action_cancel() - - def action_save(self) -> None: - """Validate and save new entry.""" - # Clear previous errors - self._clear_errors() - - # Get form values - ip_address = self.query_one("#ip-address-input", Input).value.strip() - hostnames_str = self.query_one("#hostnames-input", Input).value.strip() - comment = self.query_one("#comment-input", Input).value.strip() - is_active = self.query_one("#active-checkbox", Checkbox).value - - # Validate input - if not self._validate_input(ip_address, hostnames_str): - return - - try: - # Parse hostnames - hostnames = [h.strip() for h in hostnames_str.split(",") if h.strip()] - - # Create new entry - new_entry = HostEntry( - ip_address=ip_address, - hostnames=hostnames, - comment=comment if comment else None, - is_active=is_active, - ) - - # Close modal and return the new entry - self.dismiss(new_entry) - - except ValueError as e: - # Display validation error - if "IP address" in str(e).lower(): - self._show_error("ip-error", str(e)) - else: - self._show_error("hostnames-error", str(e)) - - def action_cancel(self) -> None: - """Cancel entry creation and close modal.""" - self.dismiss(None) - - def _validate_input(self, ip_address: str, hostnames_str: str) -> bool: - """ - Validate user input. - - Args: - ip_address: IP address to validate - hostnames_str: Comma-separated hostnames to validate - - Returns: - True if input is valid, False otherwise - """ - valid = True - - # Validate IP address - if not ip_address: - self._show_error("ip-error", "IP address is required") - valid = False - - # Validate hostnames - if not hostnames_str: - self._show_error("hostnames-error", "At least one hostname is required") - valid = False - else: - hostnames = [h.strip() for h in hostnames_str.split(",") if h.strip()] - if not hostnames: - self._show_error("hostnames-error", "At least one hostname is required") - valid = False - else: - # Basic hostname validation - for hostname in hostnames: - if ( - " " in hostname - or not hostname.replace(".", "") - .replace("-", "") - .replace("_", "") - .isalnum() - ): - self._show_error( - "hostnames-error", f"Invalid hostname format: {hostname}" - ) - valid = False - break - - return valid - - def _show_error(self, error_id: str, message: str) -> None: - """Show validation error message.""" - try: - error_widget = self.query_one(f"#{error_id}", Static) - error_widget.update(message) - except Exception: - pass - - def _clear_errors(self) -> None: - """Clear all validation error messages.""" - for error_id in ["ip-error", "hostnames-error"]: - try: - error_widget = self.query_one(f"#{error_id}", Static) - error_widget.update("") - except Exception: - pass diff --git a/src/hosts/tui/app.py b/src/hosts/tui/app.py index f5de683..db3d161 100644 --- a/src/hosts/tui/app.py +++ b/src/hosts/tui/app.py @@ -16,9 +16,6 @@ from ..core.config import Config from ..core.manager import HostsManager from .config_modal import ConfigModal from .password_modal import PasswordModal -from .add_entry_modal import AddEntryModal -from .delete_confirmation_modal import DeleteConfirmationModal -from .help_modal import HelpModal from .styles import HOSTS_MANAGER_CSS from .keybindings import HOSTS_MANAGER_BINDINGS from .table_handler import TableHandler @@ -45,7 +42,6 @@ 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,15 +67,6 @@ class HostsManagerApp(App): yield Header() yield Footer() - # 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: @@ -199,38 +186,15 @@ 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).""" - 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 + # 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).""" @@ -248,8 +212,11 @@ class HostsManagerApp(App): self.update_status("Hosts file reloaded") def action_help(self) -> None: - """Show help modal.""" - self.push_screen(HelpModal()) + """Show help information.""" + # For now, just update the status with help info + self.update_status( + "Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config, e Edit" + ) def action_config(self) -> None: """Show configuration modal.""" @@ -388,84 +355,6 @@ class HostsManagerApp(App): """Save the hosts file to disk.""" self.navigation_handler.save_hosts_file() - def action_add_entry(self) -> None: - """Show the add entry modal.""" - if not self.edit_mode: - self.update_status( - "❌ Cannot add entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode." - ) - return - - def handle_add_entry_result(new_entry) -> None: - if new_entry is None: - # User cancelled - self.update_status("Entry creation cancelled") - return - - # Add the entry using the manager - success, message = self.manager.add_entry(self.hosts_file, new_entry) - if success: - # Refresh the table - self.table_handler.populate_entries_table() - # Move cursor to the newly added entry (last entry) - self.selected_entry_index = len(self.hosts_file.entries) - 1 - self.table_handler.restore_cursor_position(new_entry) - self.update_status(f"✅ {message}") - else: - self.update_status(f"❌ {message}") - - self.push_screen(AddEntryModal(), handle_add_entry_result) - - def action_delete_entry(self) -> None: - """Show the delete confirmation modal for the selected entry.""" - if not self.edit_mode: - self.update_status( - "❌ Cannot delete entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode." - ) - return - - if not self.hosts_file.entries: - self.update_status("No entries to delete") - return - - if self.selected_entry_index >= len(self.hosts_file.entries): - self.update_status("Invalid entry selected") - return - - entry = self.hosts_file.entries[self.selected_entry_index] - if entry.is_default_entry(): - self.update_status("❌ Cannot delete system default entry") - return - - def handle_delete_confirmation(confirmed: bool) -> None: - if not confirmed: - self.update_status("Entry deletion cancelled") - return - - # Delete the entry using the manager - success, message = self.manager.delete_entry( - self.hosts_file, self.selected_entry_index - ) - if success: - # Adjust selected index if needed - if self.selected_entry_index >= len(self.hosts_file.entries): - self.selected_entry_index = max(0, len(self.hosts_file.entries) - 1) - - # Refresh the table - self.table_handler.populate_entries_table() - self.details_handler.update_entry_details() - self.update_status(f"✅ {message}") - else: - self.update_status(f"❌ {message}") - - self.push_screen(DeleteConfirmationModal(entry), handle_delete_confirmation) - - def action_search(self) -> None: - """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.""" self.navigation_handler.quit_application() diff --git a/src/hosts/tui/delete_confirmation_modal.py b/src/hosts/tui/delete_confirmation_modal.py deleted file mode 100644 index 8e1f4d5..0000000 --- a/src/hosts/tui/delete_confirmation_modal.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Delete confirmation modal window for the hosts TUI application. - -This module provides a confirmation dialog for deleting host entries. -""" - -from textual.app import ComposeResult -from textual.containers import Vertical, Horizontal -from textual.widgets import Static, Button -from textual.screen import ModalScreen -from textual.binding import Binding - -from ..core.models import HostEntry - - -class DeleteConfirmationModal(ModalScreen): - """ - Modal screen for confirming entry deletion. - - Provides a confirmation dialog before deleting host entries. - """ - - CSS = """ - DeleteConfirmationModal { - align: center middle; - } - - .delete-container { - width: 60; - height: 15; - background: $surface; - border: thick $error; - padding: 1; - } - - .delete-title { - text-align: center; - text-style: bold; - color: $error; - margin-bottom: 1; - } - - .delete-message { - text-align: center; - margin: 1 0; - } - - .entry-info { - text-align: center; - text-style: bold; - color: $primary; - margin: 1 0; - } - - .button-row { - margin-top: 2; - align: center middle; - } - - .delete-button { - margin: 0 1; - min-width: 10; - } - """ - - BINDINGS = [ - Binding("escape", "cancel", "Cancel"), - Binding("enter", "confirm", "Delete"), - ] - - def __init__(self, entry: HostEntry): - super().__init__() - self.entry = entry - - def compose(self) -> ComposeResult: - """Create the delete confirmation modal layout.""" - with Vertical(classes="delete-container"): - yield Static("Delete Entry", classes="delete-title") - - yield Static( - "Are you sure you want to delete this entry?", classes="delete-message" - ) - - # Show entry details - hostnames_str = ", ".join(self.entry.hostnames) - yield Static( - f"{self.entry.ip_address} → {hostnames_str}", classes="entry-info" - ) - - if self.entry.comment: - yield Static(f"Comment: {self.entry.comment}", classes="delete-message") - - with Horizontal(classes="button-row"): - yield Button( - "Delete", - variant="error", - id="delete-button", - classes="delete-button", - ) - yield Button( - "Cancel", - variant="default", - id="cancel-button", - classes="delete-button", - ) - - def on_mount(self) -> None: - """Focus cancel button by default for safety.""" - cancel_button = self.query_one("#cancel-button", Button) - cancel_button.focus() - - def on_button_pressed(self, event: Button.Pressed) -> None: - """Handle button presses.""" - if event.button.id == "delete-button": - self.action_confirm() - elif event.button.id == "cancel-button": - self.action_cancel() - - def action_confirm(self) -> None: - """Confirm deletion and close modal.""" - self.dismiss(True) - - def action_cancel(self) -> None: - """Cancel deletion and close modal.""" - self.dismiss(False) diff --git a/src/hosts/tui/help_modal.py b/src/hosts/tui/help_modal.py deleted file mode 100644 index 3c018d7..0000000 --- a/src/hosts/tui/help_modal.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Help modal window for the hosts TUI application. - -This module provides a help dialog showing keyboard shortcuts and usage information. -""" - -from textual.app import ComposeResult -from textual.containers import Vertical, Horizontal, ScrollableContainer -from textual.widgets import Static, Button -from textual.screen import ModalScreen -from textual.binding import Binding - - -class HelpModal(ModalScreen): - """ - Modal screen showing help and keyboard shortcuts. - - Provides comprehensive help information for using the application. - """ - - CSS = """ - HelpModal { - align: center middle; - } - - .help-container { - width: 90; - height: 40; - background: $surface; - border: thick $primary; - padding: 1; - } - - .help-title { - text-align: center; - text-style: bold; - color: $primary; - margin-bottom: 1; - } - - .help-content { - height: 35; - margin: 1 0; - } - - .help-section { - margin-bottom: 1; - } - - .help-section-title { - text-style: bold; - color: $primary; - margin-bottom: 0; - } - - .help-item { - margin: 0 2; - } - - .keyboard-shortcut { - text-style: bold; - color: $accent; - } - - .button-row { - margin-top: 1; - align: center middle; - } - - .help-button { - min-width: 10; - } - """ - - BINDINGS = [ - Binding("escape", "close", "Close"), - Binding("enter", "close", "Close"), - ] - - def compose(self) -> ComposeResult: - """Create the help modal layout.""" - with Vertical(classes="help-container"): - yield Static("/etc/hosts Manager - Help", classes="help-title") - - with ScrollableContainer(classes="help-content"): - # Navigation section - with Vertical(classes="help-section"): - yield Static("Navigation", classes="help-section-title") - yield Static("↑ ↓ - Navigate entries", classes="help-item") - yield Static("Enter - Select entry", classes="help-item") - yield Static("Tab - Navigate between panes", classes="help-item") - - # 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", - ) - - # 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", - ) - - # 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", - ) - - # 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", - ) - - # 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", - ) - - with Horizontal(classes="button-row"): - yield Button( - "Close", variant="primary", id="close-button", classes="help-button" - ) - - def on_mount(self) -> None: - """Focus close button when modal opens.""" - close_button = self.query_one("#close-button", Button) - close_button.focus() - - def on_button_pressed(self, event: Button.Pressed) -> None: - """Handle button presses.""" - if event.button.id == "close-button": - self.action_close() - - def action_close(self) -> None: - """Close the help modal.""" - self.dismiss() diff --git a/src/hosts/tui/keybindings.py b/src/hosts/tui/keybindings.py index e333d73..ad4b72b 100644 --- a/src/hosts/tui/keybindings.py +++ b/src/hosts/tui/keybindings.py @@ -15,10 +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", "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), Binding("e", "edit_entry", "Edit Entry", show=False), Binding("space", "toggle_entry", "Toggle Entry", show=False), Binding("ctrl+s", "save_file", "Save", show=False), diff --git a/src/hosts/tui/search_modal.py b/src/hosts/tui/search_modal.py deleted file mode 100644 index 88ba7fe..0000000 --- a/src/hosts/tui/search_modal.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -Search modal window for the hosts TUI application. - -This module provides a floating search window for finding entries by hostname or IP address. -""" - -from textual.app import ComposeResult -from textual.containers import Vertical, Horizontal -from textual.widgets import Static, Button, Input, DataTable, Label -from textual.screen import ModalScreen -from textual.binding import Binding - -from ..core.models import HostEntry - - -class SearchModal(ModalScreen): - """ - Modal screen for searching host entries. - - Provides a search interface and displays matching results. - """ - - CSS = """ - SearchModal { - align: center middle; - } - - .search-container { - width: 90; - height: 30; - background: $surface; - border: thick $primary; - padding: 1; - } - - .search-title { - text-align: center; - text-style: bold; - color: $primary; - margin-bottom: 1; - } - - .search-section { - margin: 1 0; - } - - .search-input { - margin: 0 2; - width: 1fr; - } - - .results-section { - margin: 1 0; - height: 15; - } - - .search-results { - margin: 0 2; - height: 13; - } - - .button-row { - margin-top: 1; - align: center middle; - } - - .search-button { - margin: 0 1; - min-width: 10; - } - - .results-info { - margin: 0 2; - color: $text-muted; - text-style: italic; - } - """ - - BINDINGS = [ - Binding("escape", "cancel", "Cancel"), - Binding("enter", "select", "Select"), - Binding("f3", "search", "Search"), - ] - - def __init__(self, entries): - super().__init__() - self.entries = entries - self.search_results = [] - - def compose(self) -> ComposeResult: - """Create the search modal layout.""" - with Vertical(classes="search-container"): - yield Static("Search Host Entries", classes="search-title") - - with Vertical(classes="search-section"): - yield Label("Search term (hostname or IP address):") - yield Input( - placeholder="e.g., example.com or 192.168.1.1", - id="search-input", - classes="search-input", - ) - - with Vertical(classes="results-section"): - yield Static("Search Results:", classes="results-info") - yield DataTable( - id="search-results-table", - classes="search-results", - show_header=True, - zebra_stripes=True, - ) - - with Horizontal(classes="button-row"): - yield Button( - "Search", - variant="primary", - id="search-button", - classes="search-button", - ) - yield Button( - "Select", - variant="success", - id="select-button", - classes="search-button", - ) - yield Button( - "Close", - variant="default", - id="close-button", - classes="search-button", - ) - - def on_mount(self) -> None: - """Initialize the search results table and focus search input.""" - # Set up the results table - results_table = self.query_one("#search-results-table", DataTable) - results_table.add_column("IP Address", key="ip") - results_table.add_column("Canonical Hostname", key="hostname") - results_table.add_column("Status", key="status") - results_table.add_column("Comment", key="comment") - - # Focus search input - search_input = self.query_one("#search-input", Input) - search_input.focus() - - # Disable select button initially - select_button = self.query_one("#select-button", Button) - select_button.disabled = True - - def on_button_pressed(self, event: Button.Pressed) -> None: - """Handle button presses.""" - if event.button.id == "search-button": - self.action_search() - elif event.button.id == "select-button": - self.action_select() - elif event.button.id == "close-button": - self.action_cancel() - - def on_input_submitted(self, event: Input.Submitted) -> None: - """Handle enter key in search input.""" - if event.input.id == "search-input": - self.action_search() - - def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: - """Handle row selection in results table.""" - if event.data_table.id == "search-results-table": - # Enable select button when a row is selected - select_button = self.query_one("#select-button", Button) - select_button.disabled = False - - def action_search(self) -> None: - """Perform search based on input.""" - search_input = self.query_one("#search-input", Input) - search_term = search_input.value.strip().lower() - - if not search_term: - self._update_results_info("Please enter a search term") - return - - # Perform search - self.search_results = self._search_entries(search_term) - - # Update results table - results_table = self.query_one("#search-results-table", DataTable) - results_table.clear() - - if not self.search_results: - self._update_results_info(f"No entries found matching '{search_term}'") - select_button = self.query_one("#select-button", Button) - select_button.disabled = True - return - - # Add results to table - for entry in self.search_results: - status = "✓ Active" if entry.is_active else "✗ Inactive" - comment = entry.comment or "" - results_table.add_row( - entry.ip_address, - entry.canonical_hostname, - status, - comment, - key=str(id(entry)), - ) - - self._update_results_info(f"Found {len(self.search_results)} matching entries") - - def action_select(self) -> None: - """Select the currently highlighted entry and close modal.""" - results_table = self.query_one("#search-results-table", DataTable) - - if results_table.cursor_row is None or not self.search_results: - return - - # Get the selected entry - cursor_row = results_table.cursor_row - if 0 <= cursor_row < len(self.search_results): - selected_entry = self.search_results[cursor_row] - # Find the original index of this entry - original_index = self._find_entry_index(selected_entry) - self.dismiss(original_index) - else: - self.dismiss(None) - - def action_cancel(self) -> None: - """Cancel search and close modal.""" - self.dismiss(None) - - def _search_entries(self, search_term: str) -> list[HostEntry]: - """ - Search entries by hostname or IP address. - - Args: - search_term: The search term to match against - - Returns: - List of matching entries - """ - results = [] - - for entry in self.entries: - # Search in IP address - if search_term in entry.ip_address.lower(): - results.append(entry) - continue - - # Search in hostnames - for hostname in entry.hostnames: - if search_term in hostname.lower(): - results.append(entry) - break - else: - # Search in comment - if entry.comment and search_term in entry.comment.lower(): - results.append(entry) - - return results - - def _find_entry_index(self, target_entry: HostEntry) -> int: - """ - Find the index of an entry in the original entries list. - - Args: - target_entry: Entry to find - - Returns: - Index of the entry, or -1 if not found - """ - for i, entry in enumerate(self.entries): - if entry is target_entry: - return i - return -1 - - def _update_results_info(self, message: str) -> None: - """Update the results info label.""" - try: - results_info = self.query_one(".results-info", Static) - results_info.update(message) - except Exception: - pass diff --git a/src/hosts/tui/styles.py b/src/hosts/tui/styles.py index ec61e37..cfee74a 100644 --- a/src/hosts/tui/styles.py +++ b/src/hosts/tui/styles.py @@ -7,36 +7,21 @@ across the application. # CSS styles for the hosts manager application HOSTS_MANAGER_CSS = """ -.search-container { - border: round $primary; - height: 3; - padding: 0 1; - margin-top: 1; - margin-bottom: 0; -} - -.search-input { - width: 1fr; - height: 1; - border: none; -} - .hosts-container { - # height: 1fr; - margin-top: 0; + height: 1fr; } .left-pane { width: 60%; border: round $primary; - margin: 0; + margin: 1; padding: 1; } .right-pane { width: 40%; border: round $primary; - margin: 0; + margin: 1; padding: 1; } @@ -120,11 +105,6 @@ 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 */ """ diff --git a/src/hosts/tui/table_handler.py b/src/hosts/tui/table_handler.py index 58cfb44..1d94362 100644 --- a/src/hosts/tui/table_handler.py +++ b/src/hosts/tui/table_handler.py @@ -28,32 +28,6 @@ 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 diff --git a/tests/test_main.py b/tests/test_main.py index aca63f8..dd35d64 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -324,17 +324,14 @@ class TestHostsManagerApp: patch("hosts.tui.app.Config", return_value=mock_config), ): app = HostsManagerApp() - app.push_screen = Mock() + app.update_status = Mock() app.action_help() - # Should push the help modal screen - app.push_screen.assert_called_once() - # Verify the modal is a HelpModal instance - from hosts.tui.help_modal import HelpModal - - args = app.push_screen.call_args[0] - assert isinstance(args[0], HelpModal) + # Should update status with help message + app.update_status.assert_called_once() + call_args = app.update_status.call_args[0][0] + assert "Help:" in call_args def test_action_config(self): """Test config action opens modal."""