diff --git a/src/hosts/tui/app.py b/src/hosts/tui/app.py index 003727c..cd1a6f1 100644 --- a/src/hosts/tui/app.py +++ b/src/hosts/tui/app.py @@ -7,7 +7,7 @@ all the handlers and provides the primary user interface. from textual.app import App, ComposeResult from textual.containers import Horizontal, Vertical -from textual.widgets import Header, Footer, Static, DataTable, Input, Checkbox, Label +from textual.widgets import Header, Static, DataTable, Input, Checkbox, Label from textual.reactive import reactive from ..core.parser import HostsParser @@ -18,6 +18,7 @@ from .config_modal import ConfigModal from .password_modal import PasswordModal from .add_entry_modal import AddEntryModal from .delete_confirmation_modal import DeleteConfirmationModal +from .custom_footer import CustomFooter from .styles import HOSTS_MANAGER_CSS from .keybindings import HOSTS_MANAGER_BINDINGS from .table_handler import TableHandler @@ -71,7 +72,7 @@ class HostsManagerApp(App): def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header() - yield Footer() + yield CustomFooter(id="custom-footer") # Search bar above the panes with Horizontal(classes="search-container") as search_container: @@ -116,6 +117,7 @@ class HostsManagerApp(App): def on_ready(self) -> None: """Called when the app is ready.""" self.load_hosts_file() + self._setup_footer() def load_hosts_file(self) -> None: """Load the hosts file and populate the table.""" @@ -135,6 +137,38 @@ class HostsManagerApp(App): except Exception as e: self.update_status(f"❌ Error loading hosts file: {e}") + def _setup_footer(self) -> None: + """Setup the footer with initial content.""" + try: + footer = self.query_one("#custom-footer", CustomFooter) + + # Left section - common actions + footer.add_left_item("q: Quit") + footer.add_left_item("r: Reload") + footer.add_left_item("?: Help") + + # Right section - sort and edit actions + footer.add_right_item("i: Sort IP") + footer.add_right_item("n: Sort Host") + footer.add_right_item("Ctrl+E: Edit Mode") + + # Status section will be updated by update_status + self._update_footer_status() + except Exception: + pass # Footer not ready yet + + def _update_footer_status(self) -> None: + """Update the footer status section.""" + try: + footer = self.query_one("#custom-footer", CustomFooter) + mode = "Edit" if self.edit_mode else "Read-only" + entry_count = len(self.hosts_file.entries) + active_count = len(self.hosts_file.get_active_entries()) + status = f"{entry_count} entries ({active_count} active) | {mode}" + footer.set_status(status) + except Exception: + pass # Footer not ready yet + def update_status(self, message: str = "") -> None: """Update the header subtitle and status bar with status information.""" if message: @@ -162,6 +196,9 @@ class HostsManagerApp(App): # Format: "29 entries (6 active) | Read-only mode" self.sub_title = f"{entry_count} entries ({active_count} active) | {mode}" + # Also update the footer status + self._update_footer_status() + def _clear_status_message(self) -> None: """Clear the temporary status message.""" try: diff --git a/src/hosts/tui/custom_footer.py b/src/hosts/tui/custom_footer.py new file mode 100644 index 0000000..255e885 --- /dev/null +++ b/src/hosts/tui/custom_footer.py @@ -0,0 +1,137 @@ +""" +Custom footer widget with three sections: left, right, and status. + +This module provides a custom footer that divides the footer into three sections: +- Left: Items added from the left side of the screen +- Right: Items added from the right side of the screen +- Status: Right edge section separated by a vertical line +""" + +from textual.app import ComposeResult +from textual.containers import Horizontal +from textual.widgets import Static +from textual.widget import Widget + + +class CustomFooter(Widget): + """ + A custom footer widget with three sections. + + Layout: [Left items] [spacer] [Right items] | [Status] + """ + + DEFAULT_CSS = """ + CustomFooter { + background: $surface; + color: $text; + dock: bottom; + height: 1; + padding: 0 1; + width: 100%; + } + + CustomFooter > Horizontal { + height: 1; + width: 100%; + } + + .footer-left { + width: auto; + text-align: left; + text-style: dim; + } + + .footer-spacer { + width: 1fr; + } + + .footer-right { + width: auto; + text-align: right; + text-style: dim; + } + + .footer-separator { + width: auto; + color: $primary; + text-style: dim; + } + + .footer-status { + width: auto; + text-align: right; + color: $accent; + text-style: bold; + } + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._left_items = [] + self._right_items = [] + self._status_text = "" + + def compose(self) -> ComposeResult: + """Create the footer layout.""" + with Horizontal(): + yield Static("", id="footer-left", classes="footer-left") + yield Static("", id="footer-spacer", classes="footer-spacer") + yield Static("", id="footer-right", classes="footer-right") + yield Static(" │ ", id="footer-separator", classes="footer-separator") + yield Static("", id="footer-status", classes="footer-status") + + def add_left_item(self, item: str) -> None: + """Add an item to the left section.""" + self._left_items.append(item) + self._update_left_section() + + def add_right_item(self, item: str) -> None: + """Add an item to the right section.""" + self._right_items.append(item) + self._update_right_section() + + def clear_left_items(self) -> None: + """Clear all items from the left section.""" + self._left_items.clear() + self._update_left_section() + + def clear_right_items(self) -> None: + """Clear all items from the right section.""" + self._right_items.clear() + self._update_right_section() + + def set_status(self, status: str) -> None: + """Set the status text.""" + self._status_text = status + self._update_status_section() + + def _update_left_section(self) -> None: + """Update the left section display.""" + try: + left_static = self.query_one("#footer-left", Static) + left_static.update(" ".join(self._left_items)) + except Exception: + pass # Widget not ready yet + + def _update_right_section(self) -> None: + """Update the right section display.""" + try: + right_static = self.query_one("#footer-right", Static) + right_static.update(" ".join(self._right_items)) + except Exception: + pass # Widget not ready yet + + def _update_status_section(self) -> None: + """Update the status section display.""" + try: + status_static = self.query_one("#footer-status", Static) + status_static.update(self._status_text) + except Exception: + pass # Widget not ready yet + + def on_mount(self) -> None: + """Called when the widget is mounted.""" + # Initialize all sections + self._update_left_section() + self._update_right_section() + self._update_status_section() diff --git a/src/hosts/tui/styles.py b/src/hosts/tui/styles.py index 2ddc2bb..551d4a4 100644 --- a/src/hosts/tui/styles.py +++ b/src/hosts/tui/styles.py @@ -166,6 +166,50 @@ Header { Header.-tall { height: 1; /* Fix tall header also to height 1 */ } + +/* Custom Footer Styling */ +CustomFooter { + background: $surface; + color: $text; + dock: bottom; + height: 1; + padding: 0 1; + width: 100%; +} + +CustomFooter > Horizontal { + height: 1; + width: 100%; +} + +.footer-left { + width: auto; + text-align: left; + text-style: dim; +} + +.footer-spacer { + width: 1fr; +} + +.footer-right { + width: auto; + text-align: right; + text-style: dim; +} + +.footer-separator { + width: auto; + color: $primary; + text-style: dim; +} + +.footer-status { + width: auto; + text-align: right; + color: $accent; + text-style: bold; +} """ ) diff --git a/tests/test_main.py b/tests/test_main.py index b75c39a..6dcfc68 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -537,7 +537,7 @@ class TestHostsManagerApp: assert "q" in binding_keys assert "r" in binding_keys - assert "h" in binding_keys + assert "question_mark" in binding_keys # Help binding (? key) assert "i" in binding_keys assert "n" in binding_keys assert "c" in binding_keys