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:
parent
50628d78b7
commit
8d3d1e7c11
4 changed files with 221 additions and 3 deletions
|
@ -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:
|
||||
|
|
137
src/hosts/tui/custom_footer.py
Normal file
137
src/hosts/tui/custom_footer.py
Normal 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()
|
|
@ -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;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue