From c84c1aac2a5102fa972dae3e5d44489b69631817 Mon Sep 17 00:00:00 2001 From: phg Date: Thu, 14 Aug 2025 20:41:12 +0200 Subject: [PATCH 1/2] Remove search functionality and related modal from the TUI application to streamline the user interface. --- src/hosts/tui/app.py | 5 - src/hosts/tui/help_modal.py | 4 +- src/hosts/tui/keybindings.py | 1 - src/hosts/tui/search_modal.py | 278 ---------------------------------- 4 files changed, 2 insertions(+), 286 deletions(-) delete mode 100644 src/hosts/tui/search_modal.py diff --git a/src/hosts/tui/app.py b/src/hosts/tui/app.py index f5de683..ab72db3 100644 --- a/src/hosts/tui/app.py +++ b/src/hosts/tui/app.py @@ -460,11 +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.""" diff --git a/src/hosts/tui/help_modal.py b/src/hosts/tui/help_modal.py index 3c018d7..56249a5 100644 --- a/src/hosts/tui/help_modal.py +++ b/src/hosts/tui/help_modal.py @@ -94,7 +94,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 +134,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", ) diff --git a/src/hosts/tui/keybindings.py b/src/hosts/tui/keybindings.py index e333d73..50994e5 100644 --- a/src/hosts/tui/keybindings.py +++ b/src/hosts/tui/keybindings.py @@ -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), diff --git a/src/hosts/tui/search_modal.py b/src/hosts/tui/search_modal.py deleted file mode 100644 index 88ba7fe..0000000 --- a/src/hosts/tui/search_modal.py +++ /dev/null @@ -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 From 54a2e00e2958df365dfb58537710442b19ebf431 Mon Sep 17 00:00:00 2001 From: phg Date: Thu, 14 Aug 2025 20:59:15 +0200 Subject: [PATCH 2/2] Refactor modal CSS definitions to use centralized style constants for improved maintainability and consistency across the TUI application. --- src/hosts/tui/add_entry_modal.py | 47 +--- src/hosts/tui/app.py | 1 - src/hosts/tui/config_modal.py | 40 +-- src/hosts/tui/delete_confirmation_modal.py | 44 +--- src/hosts/tui/help_modal.py | 56 +--- src/hosts/tui/password_modal.py | 49 +--- src/hosts/tui/save_confirmation_modal.py | 42 +-- src/hosts/tui/styles.py | 291 +++++++++++++++++++++ 8 files changed, 306 insertions(+), 264 deletions(-) diff --git a/src/hosts/tui/add_entry_modal.py b/src/hosts/tui/add_entry_modal.py index d5b1de7..35612cd 100644 --- a/src/hosts/tui/add_entry_modal.py +++ b/src/hosts/tui/add_entry_modal.py @@ -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"), diff --git a/src/hosts/tui/app.py b/src/hosts/tui/app.py index ab72db3..5c8679e 100644 --- a/src/hosts/tui/app.py +++ b/src/hosts/tui/app.py @@ -460,7 +460,6 @@ class HostsManagerApp(App): self.push_screen(DeleteConfirmationModal(entry), handle_delete_confirmation) - def action_quit(self) -> None: """Quit the application.""" self.navigation_handler.quit_application() diff --git a/src/hosts/tui/config_modal.py b/src/hosts/tui/config_modal.py index 618853c..3f12dbd 100644 --- a/src/hosts/tui/config_modal.py +++ b/src/hosts/tui/config_modal.py @@ -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"), diff --git a/src/hosts/tui/delete_confirmation_modal.py b/src/hosts/tui/delete_confirmation_modal.py index 8e1f4d5..bc99152 100644 --- a/src/hosts/tui/delete_confirmation_modal.py +++ b/src/hosts/tui/delete_confirmation_modal.py @@ -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"), diff --git a/src/hosts/tui/help_modal.py b/src/hosts/tui/help_modal.py index 56249a5..4e1bbc5 100644 --- a/src/hosts/tui/help_modal.py +++ b/src/hosts/tui/help_modal.py @@ -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"), diff --git a/src/hosts/tui/password_modal.py b/src/hosts/tui/password_modal.py index fc6b189..f4098c1 100644 --- a/src/hosts/tui/password_modal.py +++ b/src/hosts/tui/password_modal.py @@ -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"), diff --git a/src/hosts/tui/save_confirmation_modal.py b/src/hosts/tui/save_confirmation_modal.py index a49f8f1..8b9f1cf 100644 --- a/src/hosts/tui/save_confirmation_modal.py +++ b/src/hosts/tui/save_confirmation_modal.py @@ -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"), diff --git a/src/hosts/tui/styles.py b/src/hosts/tui/styles.py index ec61e37..7b913a4 100644 --- a/src/hosts/tui/styles.py +++ b/src/hosts/tui/styles.py @@ -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; +} +""" +)