Compare commits

..

2 commits

10 changed files with 308 additions and 550 deletions

View file

@ -11,6 +11,7 @@ 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):
@ -20,51 +21,7 @@ class AddEntryModal(ModalScreen):
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;
}
"""
CSS = ADD_ENTRY_MODAL_CSS
BINDINGS = [
Binding("escape", "cancel", "Cancel"),

View file

@ -460,12 +460,6 @@ class HostsManagerApp(App):
self.push_screen(DeleteConfirmationModal(entry), handle_delete_confirmation)
def action_search(self) -> None:
"""Focus the search bar for filtering entries."""
search_input = self.query_one("#search-input", Input)
search_input.focus()
self.update_status("Use the search bar to filter entries")
def action_quit(self) -> None:
"""Quit the application."""
self.navigation_handler.quit_application()

View file

@ -11,6 +11,7 @@ from textual.screen import ModalScreen
from textual.binding import Binding
from ..core.config import Config
from .styles import CONFIG_MODAL_CSS
class ConfigModal(ModalScreen):
@ -20,44 +21,7 @@ class ConfigModal(ModalScreen):
Provides a floating window with configuration options.
"""
CSS = """
ConfigModal {
align: center middle;
}
.config-container {
width: 80;
height: 20;
background: $surface;
border: thick $primary;
padding: 1;
}
.config-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.config-section {
margin: 1 0;
}
.config-option {
margin: 0 2;
}
.button-row {
margin-top: 2;
align: center middle;
}
.config-button {
margin: 0 1;
min-width: 10;
}
"""
CSS = CONFIG_MODAL_CSS
BINDINGS = [
Binding("escape", "cancel", "Cancel"),

View file

@ -11,6 +11,7 @@ from textual.screen import ModalScreen
from textual.binding import Binding
from ..core.models import HostEntry
from .styles import DELETE_CONFIRMATION_MODAL_CSS
class DeleteConfirmationModal(ModalScreen):
@ -20,48 +21,7 @@ class DeleteConfirmationModal(ModalScreen):
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;
}
"""
CSS = DELETE_CONFIRMATION_MODAL_CSS
BINDINGS = [
Binding("escape", "cancel", "Cancel"),

View file

@ -10,6 +10,8 @@ from textual.widgets import Static, Button
from textual.screen import ModalScreen
from textual.binding import Binding
from .styles import HELP_MODAL_CSS
class HelpModal(ModalScreen):
"""
@ -18,59 +20,7 @@ class HelpModal(ModalScreen):
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;
}
"""
CSS = HELP_MODAL_CSS
BINDINGS = [
Binding("escape", "close", "Close"),
@ -94,7 +44,7 @@ class HelpModal(ModalScreen):
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",
"[bold]r[/bold] Reload [bold]h[/bold] Help [bold]c[/bold] Config [bold]q[/bold] Quit",
classes="help-item",
)
yield Static(
@ -134,7 +84,7 @@ class HelpModal(ModalScreen):
"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",
"[bold]s[/bold] Save changes [bold]d[/bold] Discard changes",
classes="help-item",
)

View file

@ -15,7 +15,6 @@ HOSTS_MANAGER_BINDINGS = [
Binding("i", "sort_by_ip", "Sort by IP"),
Binding("n", "sort_by_hostname", "Sort by Hostname"),
Binding("c", "config", "Config"),
Binding("ctrl+f", "search", "Focus Search"),
Binding("ctrl+e", "toggle_edit_mode", "Edit Mode"),
Binding("a", "add_entry", "Add Entry", show=False),
Binding("d", "delete_entry", "Delete Entry", show=False),

View file

@ -10,6 +10,8 @@ from textual.widgets import Static, Button, Input
from textual.screen import ModalScreen
from textual.binding import Binding
from .styles import PASSWORD_MODAL_CSS
class PasswordModal(ModalScreen):
"""
@ -18,52 +20,7 @@ class PasswordModal(ModalScreen):
Provides a floating window for entering sudo password with proper masking.
"""
CSS = """
PasswordModal {
align: center middle;
}
.password-container {
width: 60;
height: 12;
background: $surface;
border: thick $primary;
padding: 1;
}
.password-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.password-message {
text-align: center;
color: $text;
margin-bottom: 1;
}
.password-input {
margin: 1 0;
}
.button-row {
margin-top: 1;
align: center middle;
}
.password-button {
margin: 0 1;
min-width: 10;
}
.error-message {
color: $error;
text-align: center;
margin: 1 0;
}
"""
CSS = PASSWORD_MODAL_CSS
BINDINGS = [
Binding("escape", "cancel", "Cancel"),

View file

@ -10,6 +10,8 @@ from textual.widgets import Static, Button, Label
from textual.screen import ModalScreen
from textual.binding import Binding
from .styles import SAVE_CONFIRMATION_MODAL_CSS
class SaveConfirmationModal(ModalScreen):
"""
@ -18,45 +20,7 @@ class SaveConfirmationModal(ModalScreen):
Provides a confirmation dialog asking whether to save or discard changes.
"""
CSS = """
SaveConfirmationModal {
align: center middle;
}
.save-confirmation-container {
width: 60;
height: 15;
background: $surface;
border: thick $primary;
padding: 1;
}
.save-confirmation-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.save-confirmation-message {
text-align: center;
margin-bottom: 2;
color: $text;
}
.button-row {
align: center middle;
}
.save-confirmation-button {
margin: 0 1;
min-width: 12;
}
.save-confirmation-button:focus {
border: thick $accent;
}
"""
CSS = SAVE_CONFIRMATION_MODAL_CSS
BINDINGS = [
Binding("escape", "cancel", "Cancel"),

View file

@ -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

View file

@ -128,3 +128,294 @@ Header.-tall {
height: 1; /* Fix tall header also to height 1 */
}
"""
# Common CSS classes shared across components
COMMON_CSS = """
.button-row {
margin-top: 1;
align: center middle;
}
.hidden {
display: none;
}
"""
# Help Modal CSS
HELP_MODAL_CSS = (
COMMON_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;
}
.help-button {
min-width: 10;
}
"""
)
# Add Entry Modal CSS
ADD_ENTRY_MODAL_CSS = (
COMMON_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;
}
"""
)
# Delete Confirmation Modal CSS
DELETE_CONFIRMATION_MODAL_CSS = (
COMMON_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;
}
"""
)
# Password Modal CSS
PASSWORD_MODAL_CSS = (
COMMON_CSS
+ """
PasswordModal {
align: center middle;
}
.password-container {
width: 60;
height: 12;
background: $surface;
border: thick $primary;
padding: 1;
}
.password-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.password-message {
text-align: center;
color: $text;
margin-bottom: 1;
}
.password-input {
margin: 1 0;
}
.password-button {
margin: 0 1;
min-width: 10;
}
.error-message {
color: $error;
text-align: center;
margin: 1 0;
}
"""
)
# Config Modal CSS
CONFIG_MODAL_CSS = (
COMMON_CSS
+ """
ConfigModal {
align: center middle;
}
.config-container {
width: 80;
height: 20;
background: $surface;
border: thick $primary;
padding: 1;
}
.config-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.config-section {
margin: 1 0;
}
.config-option {
margin: 0 2;
}
.button-row {
margin-top: 2;
align: center middle;
}
.config-button {
margin: 0 1;
min-width: 10;
}
"""
)
# Save Confirmation Modal CSS
SAVE_CONFIRMATION_MODAL_CSS = (
COMMON_CSS
+ """
SaveConfirmationModal {
align: center middle;
}
.save-confirmation-container {
width: 60;
height: 15;
background: $surface;
border: thick $primary;
padding: 1;
}
.save-confirmation-title {
text-align: center;
text-style: bold;
color: $primary;
margin-bottom: 1;
}
.save-confirmation-message {
text-align: center;
margin-bottom: 2;
color: $text;
}
.save-confirmation-button {
margin: 0 1;
min-width: 12;
}
.save-confirmation-button:focus {
border: thick $accent;
}
"""
)