Compare commits
2 commits
8d884aeb65
...
54a2e00e29
Author | SHA1 | Date | |
---|---|---|---|
54a2e00e29 | |||
c84c1aac2a |
10 changed files with 308 additions and 550 deletions
|
@ -11,6 +11,7 @@ from textual.screen import ModalScreen
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|
||||||
from ..core.models import HostEntry
|
from ..core.models import HostEntry
|
||||||
|
from .styles import ADD_ENTRY_MODAL_CSS
|
||||||
|
|
||||||
|
|
||||||
class AddEntryModal(ModalScreen):
|
class AddEntryModal(ModalScreen):
|
||||||
|
@ -20,51 +21,7 @@ class AddEntryModal(ModalScreen):
|
||||||
Provides a floating window with input fields for creating new entries.
|
Provides a floating window with input fields for creating new entries.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CSS = """
|
CSS = ADD_ENTRY_MODAL_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 = [
|
BINDINGS = [
|
||||||
Binding("escape", "cancel", "Cancel"),
|
Binding("escape", "cancel", "Cancel"),
|
||||||
|
|
|
@ -460,12 +460,6 @@ class HostsManagerApp(App):
|
||||||
|
|
||||||
self.push_screen(DeleteConfirmationModal(entry), handle_delete_confirmation)
|
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()
|
||||||
|
|
|
@ -11,6 +11,7 @@ from textual.screen import ModalScreen
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|
||||||
from ..core.config import Config
|
from ..core.config import Config
|
||||||
|
from .styles import CONFIG_MODAL_CSS
|
||||||
|
|
||||||
|
|
||||||
class ConfigModal(ModalScreen):
|
class ConfigModal(ModalScreen):
|
||||||
|
@ -20,44 +21,7 @@ class ConfigModal(ModalScreen):
|
||||||
Provides a floating window with configuration options.
|
Provides a floating window with configuration options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CSS = """
|
CSS = CONFIG_MODAL_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;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("escape", "cancel", "Cancel"),
|
Binding("escape", "cancel", "Cancel"),
|
||||||
|
|
|
@ -11,6 +11,7 @@ from textual.screen import ModalScreen
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|
||||||
from ..core.models import HostEntry
|
from ..core.models import HostEntry
|
||||||
|
from .styles import DELETE_CONFIRMATION_MODAL_CSS
|
||||||
|
|
||||||
|
|
||||||
class DeleteConfirmationModal(ModalScreen):
|
class DeleteConfirmationModal(ModalScreen):
|
||||||
|
@ -20,48 +21,7 @@ class DeleteConfirmationModal(ModalScreen):
|
||||||
Provides a confirmation dialog before deleting host entries.
|
Provides a confirmation dialog before deleting host entries.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CSS = """
|
CSS = DELETE_CONFIRMATION_MODAL_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 = [
|
BINDINGS = [
|
||||||
Binding("escape", "cancel", "Cancel"),
|
Binding("escape", "cancel", "Cancel"),
|
||||||
|
|
|
@ -10,6 +10,8 @@ from textual.widgets import Static, Button
|
||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|
||||||
|
from .styles import HELP_MODAL_CSS
|
||||||
|
|
||||||
|
|
||||||
class HelpModal(ModalScreen):
|
class HelpModal(ModalScreen):
|
||||||
"""
|
"""
|
||||||
|
@ -18,59 +20,7 @@ class HelpModal(ModalScreen):
|
||||||
Provides comprehensive help information for using the application.
|
Provides comprehensive help information for using the application.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CSS = """
|
CSS = HELP_MODAL_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 = [
|
BINDINGS = [
|
||||||
Binding("escape", "close", "Close"),
|
Binding("escape", "close", "Close"),
|
||||||
|
@ -94,7 +44,7 @@ class HelpModal(ModalScreen):
|
||||||
with Vertical(classes="help-section"):
|
with Vertical(classes="help-section"):
|
||||||
yield Static("Main Commands", classes="help-section-title")
|
yield Static("Main Commands", classes="help-section-title")
|
||||||
yield Static(
|
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",
|
classes="help-item",
|
||||||
)
|
)
|
||||||
yield Static(
|
yield Static(
|
||||||
|
@ -134,7 +84,7 @@ class HelpModal(ModalScreen):
|
||||||
"Special Dialog Commands", classes="help-section-title"
|
"Special Dialog Commands", classes="help-section-title"
|
||||||
)
|
)
|
||||||
yield Static(
|
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",
|
classes="help-item",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ 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("a", "add_entry", "Add Entry", show=False),
|
||||||
Binding("d", "delete_entry", "Delete Entry", show=False),
|
Binding("d", "delete_entry", "Delete Entry", show=False),
|
||||||
|
|
|
@ -10,6 +10,8 @@ from textual.widgets import Static, Button, Input
|
||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|
||||||
|
from .styles import PASSWORD_MODAL_CSS
|
||||||
|
|
||||||
|
|
||||||
class PasswordModal(ModalScreen):
|
class PasswordModal(ModalScreen):
|
||||||
"""
|
"""
|
||||||
|
@ -18,52 +20,7 @@ class PasswordModal(ModalScreen):
|
||||||
Provides a floating window for entering sudo password with proper masking.
|
Provides a floating window for entering sudo password with proper masking.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CSS = """
|
CSS = PASSWORD_MODAL_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;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("escape", "cancel", "Cancel"),
|
Binding("escape", "cancel", "Cancel"),
|
||||||
|
|
|
@ -10,6 +10,8 @@ from textual.widgets import Static, Button, Label
|
||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|
||||||
|
from .styles import SAVE_CONFIRMATION_MODAL_CSS
|
||||||
|
|
||||||
|
|
||||||
class SaveConfirmationModal(ModalScreen):
|
class SaveConfirmationModal(ModalScreen):
|
||||||
"""
|
"""
|
||||||
|
@ -18,45 +20,7 @@ class SaveConfirmationModal(ModalScreen):
|
||||||
Provides a confirmation dialog asking whether to save or discard changes.
|
Provides a confirmation dialog asking whether to save or discard changes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CSS = """
|
CSS = SAVE_CONFIRMATION_MODAL_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;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("escape", "cancel", "Cancel"),
|
Binding("escape", "cancel", "Cancel"),
|
||||||
|
|
|
@ -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
|
|
|
@ -128,3 +128,294 @@ Header.-tall {
|
||||||
height: 1; /* Fix tall header also to height 1 */
|
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;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue