new: create mux-0kmtui tool
This commit is contained in:
179
mux-0kmtui/src/mux_0kmtui/widgets/server_selector.py
Normal file
179
mux-0kmtui/src/mux_0kmtui/widgets/server_selector.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""Server selection widget with multi-select support."""
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Vertical
|
||||
from textual.message import Message
|
||||
from textual.widgets import Static, SelectionList, Input
|
||||
from textual.widgets.selection_list import Selection
|
||||
|
||||
from ..config import VPSHost
|
||||
|
||||
|
||||
class ServerSelector(Vertical):
|
||||
"""Widget for selecting multiple VPS servers with search/filter support."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
ServerSelector {
|
||||
border: solid $primary;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ServerSelector .selector-header {
|
||||
height: 1;
|
||||
background: $primary;
|
||||
color: $text;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
ServerSelector #search-input {
|
||||
height: 1;
|
||||
border: none;
|
||||
padding: 0 1;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
ServerSelector #search-input:focus {
|
||||
border: none;
|
||||
}
|
||||
|
||||
ServerSelector .selector-info {
|
||||
height: 1;
|
||||
background: $surface;
|
||||
color: $text-muted;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
ServerSelector SelectionList {
|
||||
height: 1fr;
|
||||
}
|
||||
"""
|
||||
|
||||
class SelectionChanged(Message):
|
||||
"""Message sent when selection changes."""
|
||||
|
||||
def __init__(self, selected: list[VPSHost]) -> None:
|
||||
super().__init__()
|
||||
self.selected = selected
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hosts: list[VPSHost],
|
||||
*,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
self._hosts = {h.name: h for h in hosts}
|
||||
self._selected_names: set[str] = set()
|
||||
self._current_filter: str = ""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("Servers (Space=toggle)", classes="selector-header")
|
||||
yield Input(placeholder="Filter by name or tag...", id="search-input")
|
||||
yield Static("0 selected", id="selection-info", classes="selector-info")
|
||||
yield SelectionList[str](
|
||||
*self._build_selections(),
|
||||
id="server-list",
|
||||
)
|
||||
|
||||
def _build_selections(self) -> list[Selection[str]]:
|
||||
"""Build selection items, filtered by current search."""
|
||||
selections = []
|
||||
filter_lower = self._current_filter.lower()
|
||||
|
||||
for host in self._hosts.values():
|
||||
# Check if host matches filter (name or any tag)
|
||||
if filter_lower:
|
||||
name_match = filter_lower in host.name.lower()
|
||||
tag_match = any(filter_lower in tag.lower() for tag in host.tags)
|
||||
if not (name_match or tag_match):
|
||||
continue
|
||||
|
||||
# Build display label with tags
|
||||
if host.tags:
|
||||
tags_str = " [" + ", ".join(host.tags) + "]"
|
||||
label = f"{host.name}{tags_str}"
|
||||
else:
|
||||
label = host.name
|
||||
|
||||
is_selected = host.name in self._selected_names
|
||||
selections.append(Selection(label, host.name, is_selected))
|
||||
|
||||
return selections
|
||||
|
||||
def _rebuild_list(self) -> None:
|
||||
"""Rebuild the selection list with current filter."""
|
||||
selection_list = self.query_one("#server-list", SelectionList)
|
||||
selection_list.clear_options()
|
||||
for selection in self._build_selections():
|
||||
selection_list.add_option(selection)
|
||||
self._update_info()
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Handle search input changes."""
|
||||
if event.input.id == "search-input":
|
||||
self._current_filter = event.value
|
||||
self._rebuild_list()
|
||||
|
||||
def on_selection_list_selected_changed(
|
||||
self, event: SelectionList.SelectedChanged
|
||||
) -> None:
|
||||
"""Handle selection changes."""
|
||||
# Update internal tracking of selected names
|
||||
visible_selected = set(event.selection_list.selected)
|
||||
|
||||
# For visible items, sync with the selection list
|
||||
for option in event.selection_list._options:
|
||||
name = option.value
|
||||
if name in visible_selected:
|
||||
self._selected_names.add(name)
|
||||
elif name in self._selected_names:
|
||||
# Only remove if it's visible and was deselected
|
||||
self._selected_names.discard(name)
|
||||
|
||||
self._update_info()
|
||||
self.post_message(self.SelectionChanged(self.get_selected()))
|
||||
|
||||
def _update_info(self) -> None:
|
||||
"""Update the selection info label."""
|
||||
info = self.query_one("#selection-info", Static)
|
||||
count = len(self._selected_names)
|
||||
visible_count = len(self.query_one("#server-list", SelectionList)._options)
|
||||
total_count = len(self._hosts)
|
||||
|
||||
if self._current_filter:
|
||||
info.update(f"{count} selected ({visible_count}/{total_count} shown)")
|
||||
else:
|
||||
info.update(f"{count} selected" if count != 1 else "1 selected")
|
||||
|
||||
def get_selected(self) -> list[VPSHost]:
|
||||
"""Get currently selected hosts (including filtered-out ones)."""
|
||||
return [
|
||||
self._hosts[name] for name in self._selected_names if name in self._hosts
|
||||
]
|
||||
|
||||
def select_all(self) -> None:
|
||||
"""Select all visible servers."""
|
||||
selection_list = self.query_one("#server-list", SelectionList)
|
||||
selection_list.select_all()
|
||||
# Also add to internal tracking
|
||||
for option in selection_list._options:
|
||||
self._selected_names.add(option.value)
|
||||
self._update_info()
|
||||
|
||||
def deselect_all(self) -> None:
|
||||
"""Deselect all visible servers."""
|
||||
selection_list = self.query_one("#server-list", SelectionList)
|
||||
# Remove visible items from internal tracking
|
||||
for option in selection_list._options:
|
||||
self._selected_names.discard(option.value)
|
||||
selection_list.deselect_all()
|
||||
self._update_info()
|
||||
|
||||
def get_all_tags(self) -> set[str]:
|
||||
"""Get all unique tags from all hosts."""
|
||||
tags = set()
|
||||
for host in self._hosts.values():
|
||||
tags.update(host.tags)
|
||||
return tags
|
||||
Reference in New Issue
Block a user