hosts/src/hosts/tui/add_entry_modal.py

201 lines
6.8 KiB
Python

"""
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
from textual.screen import ModalScreen
from textual.binding import Binding
from ..core.models import HostEntry
from .styles import ADD_ENTRY_MODAL_CSS
class AddEntryModal(ModalScreen):
"""
Modal screen for adding new host entries.
Provides a floating window with input fields for creating new entries.
"""
CSS = ADD_ENTRY_MODAL_CSS
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="default-section") as ip_address:
ip_address.border_title = "IP Address"
yield Input(
placeholder="e.g., 192.168.1.1 or 2001:db8::1",
id="ip-address-input",
classes="default-input",
)
yield Static("", id="ip-error", classes="validation-error")
with Vertical(classes="default-section") as hostnames:
hostnames.border_title = "Hostnames"
yield Input(
placeholder="e.g., example.com, www.example.com",
id="hostnames-input",
classes="default-input",
)
yield Static("", id="hostnames-error", classes="validation-error")
with Vertical(classes="default-section") as comment:
comment.border_title = "Comment (optional)"
yield Input(
placeholder="e.g., Development server",
id="comment-input",
classes="default-input",
)
with Vertical(classes="default-section") as active:
active.border_title = "Activate Entry"
yield Checkbox(
"Active",
value=True,
id="active-checkbox",
classes="default-checkbox",
)
with Horizontal(classes="button-row"):
yield Button(
"Add Entry (CTRL+S)",
variant="primary",
id="add-button",
classes="default-button",
)
yield Button(
"Cancel (ESC)",
variant="default",
id="cancel-button",
classes="default-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