Refactor left pane to use DataTable for entries display and add sorting functionality by IP and hostname
This commit is contained in:
		
							parent
							
								
									407e37fffd
								
							
						
					
					
						commit
						15a3b6230f
					
				
					 1 changed files with 146 additions and 36 deletions
				
			
		| 
						 | 
				
			
			@ -6,7 +6,7 @@ This module contains the main application class and entry point function.
 | 
			
		|||
 | 
			
		||||
from textual.app import App, ComposeResult
 | 
			
		||||
from textual.containers import Horizontal, Vertical
 | 
			
		||||
from textual.widgets import Header, Footer, Static, ListView, ListItem, Label
 | 
			
		||||
from textual.widgets import Header, Footer, Static, DataTable
 | 
			
		||||
from textual.binding import Binding
 | 
			
		||||
from textual.reactive import reactive
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,12 +56,31 @@ class HostsManagerApp(App):
 | 
			
		|||
        height: 1;
 | 
			
		||||
        padding: 0 1;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /* DataTable styling to match background */
 | 
			
		||||
    #entries-table {
 | 
			
		||||
        background: $background;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #entries-table .datatable--header {
 | 
			
		||||
        background: $surface;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #entries-table .datatable--even-row {
 | 
			
		||||
        background: $background;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #entries-table .datatable--odd-row {
 | 
			
		||||
        background: $surface;
 | 
			
		||||
    }
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    BINDINGS = [
 | 
			
		||||
        Binding("q", "quit", "Quit"),
 | 
			
		||||
        Binding("r", "reload", "Reload"),
 | 
			
		||||
        Binding("h", "help", "Help"),
 | 
			
		||||
        Binding("i", "sort_by_ip", "Sort by IP"),
 | 
			
		||||
        Binding("n", "sort_by_hostname", "Sort by Hostname"),
 | 
			
		||||
        ("ctrl+c", "quit", "Quit"),
 | 
			
		||||
    ]
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			@ -69,6 +88,8 @@ class HostsManagerApp(App):
 | 
			
		|||
    hosts_file: reactive[HostsFile] = reactive(HostsFile())
 | 
			
		||||
    selected_entry_index: reactive[int] = reactive(0)
 | 
			
		||||
    edit_mode: reactive[bool] = reactive(False)
 | 
			
		||||
    sort_column: reactive[str] = reactive("")  # "ip" or "hostname"
 | 
			
		||||
    sort_ascending: reactive[bool] = reactive(True)
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +105,7 @@ class HostsManagerApp(App):
 | 
			
		|||
            left_pane = Vertical(classes="left-pane")
 | 
			
		||||
            left_pane.border_title = "Hosts Entries"
 | 
			
		||||
            with left_pane:
 | 
			
		||||
                yield ListView(id="entries-list")
 | 
			
		||||
                yield DataTable(id="entries-table")
 | 
			
		||||
            yield left_pane
 | 
			
		||||
            
 | 
			
		||||
            right_pane = Vertical(classes="right-pane")
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +131,7 @@ class HostsManagerApp(App):
 | 
			
		|||
        
 | 
			
		||||
        try:
 | 
			
		||||
            self.hosts_file = self.parser.parse()
 | 
			
		||||
            self.populate_entries_list()
 | 
			
		||||
            self.populate_entries_table()
 | 
			
		||||
            
 | 
			
		||||
            # Restore cursor position with a timer to ensure ListView is fully rendered
 | 
			
		||||
            self.set_timer(0.1, lambda: self.restore_cursor_position(current_entry))
 | 
			
		||||
| 
						 | 
				
			
			@ -127,25 +148,34 @@ class HostsManagerApp(App):
 | 
			
		|||
            self.log(f"Error loading hosts file: {e}")
 | 
			
		||||
            self.update_status(f"Error: {e}")
 | 
			
		||||
    
 | 
			
		||||
    def populate_entries_list(self) -> None:
 | 
			
		||||
        """Populate the left pane with hosts entries."""
 | 
			
		||||
        entries_list = self.query_one("#entries-list", ListView)
 | 
			
		||||
        entries_list.clear()
 | 
			
		||||
    def populate_entries_table(self) -> None:
 | 
			
		||||
        """Populate the left pane with hosts entries using DataTable."""
 | 
			
		||||
        table = self.query_one("#entries-table", DataTable)
 | 
			
		||||
        table.clear()
 | 
			
		||||
        
 | 
			
		||||
        for i, entry in enumerate(self.hosts_file.entries):
 | 
			
		||||
            # Format entry display
 | 
			
		||||
            hostnames_str = ", ".join(entry.hostnames)
 | 
			
		||||
            display_text = f"{entry.ip_address} → {hostnames_str}"
 | 
			
		||||
        # Configure DataTable properties
 | 
			
		||||
        table.zebra_stripes = True
 | 
			
		||||
        table.cursor_type = "row"
 | 
			
		||||
        table.show_header = True
 | 
			
		||||
        
 | 
			
		||||
        # Add columns only if they don't exist
 | 
			
		||||
        if not table.columns:
 | 
			
		||||
            table.add_columns("IP Address", "Canonical Hostname")
 | 
			
		||||
        
 | 
			
		||||
        # Update column headers with sort indicators
 | 
			
		||||
        self.update_column_headers()
 | 
			
		||||
        
 | 
			
		||||
        # Add rows
 | 
			
		||||
        for entry in self.hosts_file.entries:
 | 
			
		||||
            # Get the canonical hostname (first hostname)
 | 
			
		||||
            canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
 | 
			
		||||
            
 | 
			
		||||
            if entry.comment:
 | 
			
		||||
                display_text += f" # {entry.comment}"
 | 
			
		||||
            
 | 
			
		||||
            # Create list item with appropriate styling
 | 
			
		||||
            item = ListItem(
 | 
			
		||||
                Label(display_text),
 | 
			
		||||
                classes="entry-active" if entry.is_active else "entry-inactive"
 | 
			
		||||
            )
 | 
			
		||||
            entries_list.append(item)
 | 
			
		||||
            # Add row with styling based on active status
 | 
			
		||||
            if entry.is_active:
 | 
			
		||||
                table.add_row(entry.ip_address, canonical_hostname)
 | 
			
		||||
            else:
 | 
			
		||||
                # For inactive entries, we'll style them differently
 | 
			
		||||
                table.add_row(f"# {entry.ip_address}", canonical_hostname)
 | 
			
		||||
    
 | 
			
		||||
    def restore_cursor_position(self, previous_entry) -> None:
 | 
			
		||||
        """Restore cursor position after reload, maintaining selection if possible."""
 | 
			
		||||
| 
						 | 
				
			
			@ -168,14 +198,12 @@ class HostsManagerApp(App):
 | 
			
		|||
                # Entry not found, default to first entry
 | 
			
		||||
                self.selected_entry_index = 0
 | 
			
		||||
        
 | 
			
		||||
        # Update the ListView selection and ensure it's highlighted
 | 
			
		||||
        entries_list = self.query_one("#entries-list", ListView)
 | 
			
		||||
        if entries_list.children and self.selected_entry_index < len(entries_list.children):
 | 
			
		||||
            # Set the index and focus the ListView
 | 
			
		||||
            entries_list.index = self.selected_entry_index
 | 
			
		||||
            entries_list.focus()
 | 
			
		||||
            # Force refresh of the selection highlighting
 | 
			
		||||
            entries_list.refresh()
 | 
			
		||||
        # Update the DataTable cursor position
 | 
			
		||||
        table = self.query_one("#entries-table", DataTable)
 | 
			
		||||
        if table.row_count > 0 and self.selected_entry_index < table.row_count:
 | 
			
		||||
            # Move cursor to the selected row
 | 
			
		||||
            table.move_cursor(row=self.selected_entry_index)
 | 
			
		||||
            table.focus()
 | 
			
		||||
            # Update the details pane to match the selection
 | 
			
		||||
            self.update_entry_details()
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			@ -226,16 +254,16 @@ class HostsManagerApp(App):
 | 
			
		|||
            
 | 
			
		||||
            status_widget.update(status_text)
 | 
			
		||||
    
 | 
			
		||||
    def on_list_view_selected(self, event: ListView.Selected) -> None:
 | 
			
		||||
        """Handle entry selection in the left pane."""
 | 
			
		||||
        if event.list_view.id == "entries-list":
 | 
			
		||||
            self.selected_entry_index = event.list_view.index or 0
 | 
			
		||||
    def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
 | 
			
		||||
        """Handle row highlighting (cursor movement) in the DataTable."""
 | 
			
		||||
        if event.data_table.id == "entries-table":
 | 
			
		||||
            self.selected_entry_index = event.cursor_row
 | 
			
		||||
            self.update_entry_details()
 | 
			
		||||
    
 | 
			
		||||
    def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
 | 
			
		||||
        """Handle entry highlighting (cursor movement) in the left pane."""
 | 
			
		||||
        if event.list_view.id == "entries-list":
 | 
			
		||||
            self.selected_entry_index = event.list_view.index or 0
 | 
			
		||||
    def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
 | 
			
		||||
        """Handle row selection in the DataTable."""
 | 
			
		||||
        if event.data_table.id == "entries-table":
 | 
			
		||||
            self.selected_entry_index = event.cursor_row
 | 
			
		||||
            self.update_entry_details()
 | 
			
		||||
    
 | 
			
		||||
    def action_reload(self) -> None:
 | 
			
		||||
| 
						 | 
				
			
			@ -248,6 +276,88 @@ class HostsManagerApp(App):
 | 
			
		|||
        # For now, just update the status with help info
 | 
			
		||||
        self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help")
 | 
			
		||||
    
 | 
			
		||||
    def update_column_headers(self) -> None:
 | 
			
		||||
        """Update column headers with sort indicators."""
 | 
			
		||||
        table = self.query_one("#entries-table", DataTable)
 | 
			
		||||
        if not table.columns or len(table.columns) < 2:
 | 
			
		||||
            return
 | 
			
		||||
        
 | 
			
		||||
        # Get current column labels
 | 
			
		||||
        ip_label = "IP Address"
 | 
			
		||||
        hostname_label = "Canonical Hostname"
 | 
			
		||||
        
 | 
			
		||||
        # Add sort indicators
 | 
			
		||||
        if self.sort_column == "ip":
 | 
			
		||||
            arrow = "↑" if self.sort_ascending else "↓"
 | 
			
		||||
            ip_label = f"{arrow} IP Address"
 | 
			
		||||
        elif self.sort_column == "hostname":
 | 
			
		||||
            arrow = "↑" if self.sort_ascending else "↓"
 | 
			
		||||
            hostname_label = f"{arrow} Canonical Hostname"
 | 
			
		||||
        
 | 
			
		||||
        # Update column labels safely
 | 
			
		||||
        try:
 | 
			
		||||
            table.columns[0].label = ip_label
 | 
			
		||||
            table.columns[1].label = hostname_label
 | 
			
		||||
        except (IndexError, KeyError):
 | 
			
		||||
            # If we can't update the labels, just continue
 | 
			
		||||
            pass
 | 
			
		||||
    
 | 
			
		||||
    def action_sort_by_ip(self) -> None:
 | 
			
		||||
        """Sort entries by IP address, toggle ascending/descending."""
 | 
			
		||||
        # Toggle sort direction if already sorting by IP
 | 
			
		||||
        if self.sort_column == "ip":
 | 
			
		||||
            self.sort_ascending = not self.sort_ascending
 | 
			
		||||
        else:
 | 
			
		||||
            self.sort_column = "ip"
 | 
			
		||||
            self.sort_ascending = True
 | 
			
		||||
        
 | 
			
		||||
        # Sort the entries
 | 
			
		||||
        import ipaddress
 | 
			
		||||
        def ip_sort_key(entry):
 | 
			
		||||
            try:
 | 
			
		||||
                ip_str = entry.ip_address.lstrip('# ')
 | 
			
		||||
                ip_obj = ipaddress.ip_address(ip_str)
 | 
			
		||||
                # Create a tuple for sorting: (version, ip_int)
 | 
			
		||||
                # This ensures IPv4 comes before IPv6, and within each version they're sorted numerically
 | 
			
		||||
                return (ip_obj.version, int(ip_obj))
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                # If IP parsing fails, use string comparison with high sort priority
 | 
			
		||||
                return (999, entry.ip_address)
 | 
			
		||||
        
 | 
			
		||||
        self.hosts_file.entries.sort(key=ip_sort_key, reverse=not self.sort_ascending)
 | 
			
		||||
        self.populate_entries_table()
 | 
			
		||||
        
 | 
			
		||||
        direction = "ascending" if self.sort_ascending else "descending"
 | 
			
		||||
        self.update_status(f"Sorted by IP address ({direction})")
 | 
			
		||||
    
 | 
			
		||||
    def action_sort_by_hostname(self) -> None:
 | 
			
		||||
        """Sort entries by canonical hostname, toggle ascending/descending."""
 | 
			
		||||
        # Toggle sort direction if already sorting by hostname
 | 
			
		||||
        if self.sort_column == "hostname":
 | 
			
		||||
            self.sort_ascending = not self.sort_ascending
 | 
			
		||||
        else:
 | 
			
		||||
            self.sort_column = "hostname"
 | 
			
		||||
            self.sort_ascending = True
 | 
			
		||||
        
 | 
			
		||||
        # Sort the entries
 | 
			
		||||
        self.hosts_file.entries.sort(
 | 
			
		||||
            key=lambda entry: (entry.hostnames[0] if entry.hostnames else "").lower(),
 | 
			
		||||
            reverse=not self.sort_ascending
 | 
			
		||||
        )
 | 
			
		||||
        self.populate_entries_table()
 | 
			
		||||
        
 | 
			
		||||
        direction = "ascending" if self.sort_ascending else "descending"
 | 
			
		||||
        self.update_status(f"Sorted by hostname ({direction})")
 | 
			
		||||
    
 | 
			
		||||
    def on_data_table_header_selected(self, event: DataTable.HeaderSelected) -> None:
 | 
			
		||||
        """Handle column header clicks for sorting."""
 | 
			
		||||
        if event.data_table.id == "entries-table":
 | 
			
		||||
            # Check if the column key contains "IP Address" (handles sort indicators)
 | 
			
		||||
            if "IP Address" in str(event.column_key):
 | 
			
		||||
                self.action_sort_by_ip()
 | 
			
		||||
            elif "Canonical Hostname" in str(event.column_key):
 | 
			
		||||
                self.action_sort_by_hostname()
 | 
			
		||||
    
 | 
			
		||||
    def action_quit(self) -> None:
 | 
			
		||||
        """Quit the application."""
 | 
			
		||||
        self.exit()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue