feat: implement MultiPickScreen for multi-selection in the TUI

This commit is contained in:
Даниил Грабарь
2026-05-23 11:07:01 +03:00
parent 9f59868cec
commit 3da290a4f3
2 changed files with 106 additions and 140 deletions
+55 -1
View File
@@ -8,7 +8,7 @@ from textual import on
from textual.app import ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Footer, Input, LoadingIndicator, ProgressBar, RadioButton, RadioSet, RichLog, Static
from textual.widgets import Checkbox, Footer, Input, LoadingIndicator, ProgressBar, RadioButton, RadioSet, RichLog, Static
# Shared CSS applied to every screen in the app.
# Общий CSS, применяемый ко всем экранам приложения.
@@ -78,6 +78,13 @@ RunScreen #hint {
color: $text-muted;
text-style: dim;
}
MultiPickScreen #choices {
height: auto;
margin-bottom: 1;
}
MultiPickScreen Checkbox {
margin: 0;
}
"""
@@ -294,6 +301,53 @@ class LogScreen(Screen[bool]):
self.dismiss(self._success)
# A screen that shows a list of checkboxes for multi-selection.
# The user toggles items with Space, confirms with Enter, cancels with Escape.
# Экран с чекбоксами для множественного выбора.
# Пользователь переключает пункты пробелом, подтверждает Enter, отменяет Escape.
class MultiPickScreen(Screen[Optional[List[str]]]):
"""Multi-choice checkbox screen. Returns the list of selected option strings, or None on Escape.
Экран множественного выбора с чекбоксами. Возвращает список выбранных строк или None при Escape.
"""
BINDINGS = [
Binding("enter", "submit", "Confirm", priority=True),
Binding("escape", "abort", "Cancel"),
]
def __init__(self, step: str, question: str, options: List[str],
defaults: Optional[List[str]] = None, note: str = ""):
super().__init__()
self._step = step
self._question = question
self._options = options
# All options are checked by default when defaults is None.
# Все пункты отмечены по умолчанию если defaults не передан.
self._defaults = set(defaults) if defaults is not None else set(options)
self._note = note
def compose(self) -> ComposeResult:
yield Static(self._step, id="step")
yield Static(self._question, id="question")
if self._note:
yield Static(self._note, id="note")
with Static(id="choices"):
for opt in self._options:
yield Checkbox(opt, value=(opt in self._defaults))
yield Footer()
def on_mount(self) -> None:
checkboxes = list(self.query(Checkbox))
if checkboxes:
checkboxes[0].focus()
def action_submit(self) -> None:
selected = [str(cb.label) for cb in self.query(Checkbox) if cb.value]
self.dismiss(selected)
def action_abort(self) -> None:
self.app.exit(None)
# A screen for a long-running process that the user can stop at any time.
# Shows a live log and offers S / Enter / Escape to stop or close.
# Экран для долго работающего процесса, который пользователь может остановить в любой момент.