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