Refactor hosts TUI application: replace Footer with CustomFooter, implement footer setup and status updates, and enhance styling for improved user experience

This commit is contained in:
Philip Henning 2025-08-16 20:58:41 +02:00
parent 50628d78b7
commit 8d3d1e7c11
4 changed files with 221 additions and 3 deletions

View file

@ -7,7 +7,7 @@ all the handlers and provides the primary user interface.
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical 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 textual.reactive import reactive
from ..core.parser import HostsParser from ..core.parser import HostsParser
@ -18,6 +18,7 @@ from .config_modal import ConfigModal
from .password_modal import PasswordModal from .password_modal import PasswordModal
from .add_entry_modal import AddEntryModal from .add_entry_modal import AddEntryModal
from .delete_confirmation_modal import DeleteConfirmationModal from .delete_confirmation_modal import DeleteConfirmationModal
from .custom_footer import CustomFooter
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
@ -71,7 +72,7 @@ class HostsManagerApp(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""Create child widgets for the app.""" """Create child widgets for the app."""
yield Header() yield Header()
yield Footer() yield CustomFooter(id="custom-footer")
# Search bar above the panes # Search bar above the panes
with Horizontal(classes="search-container") as search_container: with Horizontal(classes="search-container") as search_container:
@ -116,6 +117,7 @@ class HostsManagerApp(App):
def on_ready(self) -> None: def on_ready(self) -> None:
"""Called when the app is ready.""" """Called when the app is ready."""
self.load_hosts_file() self.load_hosts_file()
self._setup_footer()
def load_hosts_file(self) -> None: def load_hosts_file(self) -> None:
"""Load the hosts file and populate the table.""" """Load the hosts file and populate the table."""
@ -135,6 +137,38 @@ class HostsManagerApp(App):
except Exception as e: except Exception as e:
self.update_status(f"❌ Error loading hosts file: {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: def update_status(self, message: str = "") -> None:
"""Update the header subtitle and status bar with status information.""" """Update the header subtitle and status bar with status information."""
if message: if message:
@ -162,6 +196,9 @@ class HostsManagerApp(App):
# Format: "29 entries (6 active) | Read-only mode" # Format: "29 entries (6 active) | Read-only mode"
self.sub_title = f"{entry_count} entries ({active_count} active) | {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: def _clear_status_message(self) -> None:
"""Clear the temporary status message.""" """Clear the temporary status message."""
try: try:

View file

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

View file

@ -166,6 +166,50 @@ Header {
Header.-tall { Header.-tall {
height: 1; /* Fix tall header also to height 1 */ 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;
}
""" """
) )

View file

@ -537,7 +537,7 @@ class TestHostsManagerApp:
assert "q" in binding_keys assert "q" in binding_keys
assert "r" 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 "i" in binding_keys
assert "n" in binding_keys assert "n" in binding_keys
assert "c" in binding_keys assert "c" in binding_keys