Compare commits
No commits in common. "8d884aeb65e4af35f8032ada9b3851e1094f8f65" and "8b8c02c6daa205861ffcc8113b3d252f6dfecb52" have entirely different histories.
8d884aeb65
...
8b8c02c6da
10 changed files with 18 additions and 1061 deletions
|
@ -275,67 +275,6 @@ class HostsManager:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"Error moving entry: {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(
|
def update_entry(
|
||||||
self,
|
self,
|
||||||
hosts_file: HostsFile,
|
hosts_file: HostsFile,
|
||||||
|
|
|
@ -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
|
|
|
@ -16,9 +16,6 @@ from ..core.config import Config
|
||||||
from ..core.manager import HostsManager
|
from ..core.manager import HostsManager
|
||||||
from .config_modal import ConfigModal
|
from .config_modal import ConfigModal
|
||||||
from .password_modal import PasswordModal
|
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 .styles import HOSTS_MANAGER_CSS
|
||||||
from .keybindings import HOSTS_MANAGER_BINDINGS
|
from .keybindings import HOSTS_MANAGER_BINDINGS
|
||||||
from .table_handler import TableHandler
|
from .table_handler import TableHandler
|
||||||
|
@ -45,7 +42,6 @@ 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,15 +67,6 @@ class HostsManagerApp(App):
|
||||||
yield Header()
|
yield Header()
|
||||||
yield Footer()
|
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"):
|
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:
|
||||||
|
@ -199,38 +186,15 @@ 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)."""
|
||||||
if event.input.id == "search-input":
|
# Input changes are tracked but not automatically saved
|
||||||
# Update search term and filter entries
|
# Changes will be validated and saved when exiting edit mode
|
||||||
self.search_term = event.value.strip()
|
pass
|
||||||
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
|
|
||||||
|
|
||||||
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
||||||
"""Handle checkbox changes (no auto-save - changes saved on exit)."""
|
"""Handle checkbox changes (no auto-save - changes saved on exit)."""
|
||||||
|
@ -248,8 +212,11 @@ class HostsManagerApp(App):
|
||||||
self.update_status("Hosts file reloaded")
|
self.update_status("Hosts file reloaded")
|
||||||
|
|
||||||
def action_help(self) -> None:
|
def action_help(self) -> None:
|
||||||
"""Show help modal."""
|
"""Show help information."""
|
||||||
self.push_screen(HelpModal())
|
# 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:
|
def action_config(self) -> None:
|
||||||
"""Show configuration modal."""
|
"""Show configuration modal."""
|
||||||
|
@ -388,84 +355,6 @@ class HostsManagerApp(App):
|
||||||
"""Save the hosts file to disk."""
|
"""Save the hosts file to disk."""
|
||||||
self.navigation_handler.save_hosts_file()
|
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:
|
def action_quit(self) -> None:
|
||||||
"""Quit the application."""
|
"""Quit the application."""
|
||||||
self.navigation_handler.quit_application()
|
self.navigation_handler.quit_application()
|
||||||
|
|
|
@ -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)
|
|
|
@ -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()
|
|
|
@ -15,10 +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", "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("d", "delete_entry", "Delete Entry", show=False),
|
|
||||||
Binding("e", "edit_entry", "Edit Entry", show=False),
|
Binding("e", "edit_entry", "Edit Entry", show=False),
|
||||||
Binding("space", "toggle_entry", "Toggle Entry", show=False),
|
Binding("space", "toggle_entry", "Toggle Entry", show=False),
|
||||||
Binding("ctrl+s", "save_file", "Save", show=False),
|
Binding("ctrl+s", "save_file", "Save", show=False),
|
||||||
|
|
|
@ -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
|
|
|
@ -7,36 +7,21 @@ 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-top: 1;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 1fr;
|
|
||||||
height: 1;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hosts-container {
|
.hosts-container {
|
||||||
# height: 1fr;
|
height: 1fr;
|
||||||
margin-top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-pane {
|
.left-pane {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
border: round $primary;
|
border: round $primary;
|
||||||
margin: 0;
|
margin: 1;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-pane {
|
.right-pane {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
border: round $primary;
|
border: round $primary;
|
||||||
margin: 0;
|
margin: 1;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,11 +105,6 @@ HOSTS_MANAGER_CSS = """
|
||||||
background: $surface;
|
background: $surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
Header {
|
Header { height: 1; }
|
||||||
height: 1;
|
Header.-tall { height: 1; } /* Fix tall header also to height 1 */
|
||||||
}
|
|
||||||
|
|
||||||
Header.-tall {
|
|
||||||
height: 1; /* Fix tall header also to height 1 */
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -28,32 +28,6 @@ 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
|
||||||
|
|
|
@ -324,17 +324,14 @@ class TestHostsManagerApp:
|
||||||
patch("hosts.tui.app.Config", return_value=mock_config),
|
patch("hosts.tui.app.Config", return_value=mock_config),
|
||||||
):
|
):
|
||||||
app = HostsManagerApp()
|
app = HostsManagerApp()
|
||||||
app.push_screen = Mock()
|
app.update_status = Mock()
|
||||||
|
|
||||||
app.action_help()
|
app.action_help()
|
||||||
|
|
||||||
# Should push the help modal screen
|
# Should update status with help message
|
||||||
app.push_screen.assert_called_once()
|
app.update_status.assert_called_once()
|
||||||
# Verify the modal is a HelpModal instance
|
call_args = app.update_status.call_args[0][0]
|
||||||
from hosts.tui.help_modal import HelpModal
|
assert "Help:" in call_args
|
||||||
|
|
||||||
args = app.push_screen.call_args[0]
|
|
||||||
assert isinstance(args[0], HelpModal)
|
|
||||||
|
|
||||||
def test_action_config(self):
|
def test_action_config(self):
|
||||||
"""Test config action opens modal."""
|
"""Test config action opens modal."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue