Add entry and delete entry functionality with modals in TUI application
- Implemented add_entry method in HostsManager for adding new entries. - Created AddEntryModal for user input when adding entries. - Implemented delete_entry method in HostsManager for removing entries. - Created DeleteConfirmationModal for user confirmation before deletion. - Integrated modals into HostsManagerApp for adding and deleting entries. - Added search functionality with SearchModal for finding entries by hostname or IP address. - Updated keybindings to include shortcuts for adding and deleting entries. - Added HelpModal to provide keyboard shortcuts and usage information.
This commit is contained in:
		
							parent
							
								
									8b8c02c6da
								
							
						
					
					
						commit
						07e7e4f70f
					
				
					 8 changed files with 966 additions and 10 deletions
				
			
		| 
						 | 
				
			
			@ -275,6 +275,67 @@ 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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										238
									
								
								src/hosts/tui/add_entry_modal.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								src/hosts/tui/add_entry_modal.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,238 @@
 | 
			
		|||
"""
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +16,10 @@ 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 .search_modal import SearchModal
 | 
			
		||||
from .help_modal import HelpModal
 | 
			
		||||
from .styles import HOSTS_MANAGER_CSS
 | 
			
		||||
from .keybindings import HOSTS_MANAGER_BINDINGS
 | 
			
		||||
from .table_handler import TableHandler
 | 
			
		||||
| 
						 | 
				
			
			@ -212,11 +216,8 @@ class HostsManagerApp(App):
 | 
			
		|||
        self.update_status("Hosts file reloaded")
 | 
			
		||||
 | 
			
		||||
    def action_help(self) -> None:
 | 
			
		||||
        """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"
 | 
			
		||||
        )
 | 
			
		||||
        """Show help modal."""
 | 
			
		||||
        self.push_screen(HelpModal())
 | 
			
		||||
 | 
			
		||||
    def action_config(self) -> None:
 | 
			
		||||
        """Show configuration modal."""
 | 
			
		||||
| 
						 | 
				
			
			@ -355,6 +356,112 @@ 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:
 | 
			
		||||
        """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)
 | 
			
		||||
 | 
			
		||||
    def action_quit(self) -> None:
 | 
			
		||||
        """Quit the application."""
 | 
			
		||||
        self.navigation_handler.quit_application()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										125
									
								
								src/hosts/tui/delete_confirmation_modal.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/hosts/tui/delete_confirmation_modal.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,125 @@
 | 
			
		|||
"""
 | 
			
		||||
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)
 | 
			
		||||
							
								
								
									
										141
									
								
								src/hosts/tui/help_modal.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/hosts/tui/help_modal.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,141 @@
 | 
			
		|||
"""
 | 
			
		||||
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()
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +15,10 @@ 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+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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										278
									
								
								src/hosts/tui/search_modal.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								src/hosts/tui/search_modal.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,278 @@
 | 
			
		|||
"""
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -324,14 +324,17 @@ class TestHostsManagerApp:
 | 
			
		|||
            patch("hosts.tui.app.Config", return_value=mock_config),
 | 
			
		||||
        ):
 | 
			
		||||
            app = HostsManagerApp()
 | 
			
		||||
            app.update_status = Mock()
 | 
			
		||||
            app.push_screen = Mock()
 | 
			
		||||
 | 
			
		||||
            app.action_help()
 | 
			
		||||
 | 
			
		||||
            # 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
 | 
			
		||||
            # 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)
 | 
			
		||||
 | 
			
		||||
    def test_action_config(self):
 | 
			
		||||
        """Test config action opens modal."""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue