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.app import App, ComposeResult
|
||||||
from textual.containers import Horizontal, Vertical
|
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.binding import Binding
|
||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
|
|
||||||
|
@ -56,12 +56,31 @@ class HostsManagerApp(App):
|
||||||
height: 1;
|
height: 1;
|
||||||
padding: 0 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 = [
|
BINDINGS = [
|
||||||
Binding("q", "quit", "Quit"),
|
Binding("q", "quit", "Quit"),
|
||||||
Binding("r", "reload", "Reload"),
|
Binding("r", "reload", "Reload"),
|
||||||
Binding("h", "help", "Help"),
|
Binding("h", "help", "Help"),
|
||||||
|
Binding("i", "sort_by_ip", "Sort by IP"),
|
||||||
|
Binding("n", "sort_by_hostname", "Sort by Hostname"),
|
||||||
("ctrl+c", "quit", "Quit"),
|
("ctrl+c", "quit", "Quit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -69,6 +88,8 @@ class HostsManagerApp(App):
|
||||||
hosts_file: reactive[HostsFile] = reactive(HostsFile())
|
hosts_file: reactive[HostsFile] = reactive(HostsFile())
|
||||||
selected_entry_index: reactive[int] = reactive(0)
|
selected_entry_index: reactive[int] = reactive(0)
|
||||||
edit_mode: reactive[bool] = reactive(False)
|
edit_mode: reactive[bool] = reactive(False)
|
||||||
|
sort_column: reactive[str] = reactive("") # "ip" or "hostname"
|
||||||
|
sort_ascending: reactive[bool] = reactive(True)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -84,7 +105,7 @@ class HostsManagerApp(App):
|
||||||
left_pane = Vertical(classes="left-pane")
|
left_pane = Vertical(classes="left-pane")
|
||||||
left_pane.border_title = "Hosts Entries"
|
left_pane.border_title = "Hosts Entries"
|
||||||
with left_pane:
|
with left_pane:
|
||||||
yield ListView(id="entries-list")
|
yield DataTable(id="entries-table")
|
||||||
yield left_pane
|
yield left_pane
|
||||||
|
|
||||||
right_pane = Vertical(classes="right-pane")
|
right_pane = Vertical(classes="right-pane")
|
||||||
|
@ -110,7 +131,7 @@ class HostsManagerApp(App):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.hosts_file = self.parser.parse()
|
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
|
# Restore cursor position with a timer to ensure ListView is fully rendered
|
||||||
self.set_timer(0.1, lambda: self.restore_cursor_position(current_entry))
|
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.log(f"Error loading hosts file: {e}")
|
||||||
self.update_status(f"Error: {e}")
|
self.update_status(f"Error: {e}")
|
||||||
|
|
||||||
def populate_entries_list(self) -> None:
|
def populate_entries_table(self) -> None:
|
||||||
"""Populate the left pane with hosts entries."""
|
"""Populate the left pane with hosts entries using DataTable."""
|
||||||
entries_list = self.query_one("#entries-list", ListView)
|
table = self.query_one("#entries-table", DataTable)
|
||||||
entries_list.clear()
|
table.clear()
|
||||||
|
|
||||||
for i, entry in enumerate(self.hosts_file.entries):
|
# Configure DataTable properties
|
||||||
# Format entry display
|
table.zebra_stripes = True
|
||||||
hostnames_str = ", ".join(entry.hostnames)
|
table.cursor_type = "row"
|
||||||
display_text = f"{entry.ip_address} → {hostnames_str}"
|
table.show_header = True
|
||||||
|
|
||||||
if entry.comment:
|
# Add columns only if they don't exist
|
||||||
display_text += f" # {entry.comment}"
|
if not table.columns:
|
||||||
|
table.add_columns("IP Address", "Canonical Hostname")
|
||||||
|
|
||||||
# Create list item with appropriate styling
|
# Update column headers with sort indicators
|
||||||
item = ListItem(
|
self.update_column_headers()
|
||||||
Label(display_text),
|
|
||||||
classes="entry-active" if entry.is_active else "entry-inactive"
|
# Add rows
|
||||||
)
|
for entry in self.hosts_file.entries:
|
||||||
entries_list.append(item)
|
# Get the canonical hostname (first hostname)
|
||||||
|
canonical_hostname = entry.hostnames[0] if entry.hostnames else ""
|
||||||
|
|
||||||
|
# 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:
|
def restore_cursor_position(self, previous_entry) -> None:
|
||||||
"""Restore cursor position after reload, maintaining selection if possible."""
|
"""Restore cursor position after reload, maintaining selection if possible."""
|
||||||
|
@ -168,14 +198,12 @@ class HostsManagerApp(App):
|
||||||
# Entry not found, default to first entry
|
# Entry not found, default to first entry
|
||||||
self.selected_entry_index = 0
|
self.selected_entry_index = 0
|
||||||
|
|
||||||
# Update the ListView selection and ensure it's highlighted
|
# Update the DataTable cursor position
|
||||||
entries_list = self.query_one("#entries-list", ListView)
|
table = self.query_one("#entries-table", DataTable)
|
||||||
if entries_list.children and self.selected_entry_index < len(entries_list.children):
|
if table.row_count > 0 and self.selected_entry_index < table.row_count:
|
||||||
# Set the index and focus the ListView
|
# Move cursor to the selected row
|
||||||
entries_list.index = self.selected_entry_index
|
table.move_cursor(row=self.selected_entry_index)
|
||||||
entries_list.focus()
|
table.focus()
|
||||||
# Force refresh of the selection highlighting
|
|
||||||
entries_list.refresh()
|
|
||||||
# Update the details pane to match the selection
|
# Update the details pane to match the selection
|
||||||
self.update_entry_details()
|
self.update_entry_details()
|
||||||
|
|
||||||
|
@ -226,16 +254,16 @@ class HostsManagerApp(App):
|
||||||
|
|
||||||
status_widget.update(status_text)
|
status_widget.update(status_text)
|
||||||
|
|
||||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
||||||
"""Handle entry selection in the left pane."""
|
"""Handle row highlighting (cursor movement) in the DataTable."""
|
||||||
if event.list_view.id == "entries-list":
|
if event.data_table.id == "entries-table":
|
||||||
self.selected_entry_index = event.list_view.index or 0
|
self.selected_entry_index = event.cursor_row
|
||||||
self.update_entry_details()
|
self.update_entry_details()
|
||||||
|
|
||||||
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||||
"""Handle entry highlighting (cursor movement) in the left pane."""
|
"""Handle row selection in the DataTable."""
|
||||||
if event.list_view.id == "entries-list":
|
if event.data_table.id == "entries-table":
|
||||||
self.selected_entry_index = event.list_view.index or 0
|
self.selected_entry_index = event.cursor_row
|
||||||
self.update_entry_details()
|
self.update_entry_details()
|
||||||
|
|
||||||
def action_reload(self) -> None:
|
def action_reload(self) -> None:
|
||||||
|
@ -248,6 +276,88 @@ class HostsManagerApp(App):
|
||||||
# For now, just update the status with help info
|
# For now, just update the status with help info
|
||||||
self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help")
|
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:
|
def action_quit(self) -> None:
|
||||||
"""Quit the application."""
|
"""Quit the application."""
|
||||||
self.exit()
|
self.exit()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue