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.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:
|
||||||
|
|
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 {
|
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;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue