Files
lightweight-cobot/cobot/tui.py
T
Даниил Грабарь 5f16825cf7 Refactor Docker and Local Setup Commands
- Enhanced the docker_setup.py to streamline image building and pulling processes with improved logging and error handling.
- Introduced a new LogScreen for better user feedback during long-running tasks.
- Updated local_setup.py to utilize logging for installation steps and improved error handling.
- Refactored robot_setup.py to simplify configuration saving and user interaction.
- Added a new LogScreen class in tui.py for consistent logging across different setup processes.
- Modified install.sh to adjust the installation directory for better organization.
2026-05-20 17:41:11 +03:00

173 lines
4.8 KiB
Python

from __future__ import annotations
from typing import Callable, List, Optional
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, RadioButton, RadioSet, RichLog, Static
SCREEN_CSS = """
Screen {
padding: 2 4;
}
#step {
color: $text-muted;
text-style: dim;
}
#question {
text-style: bold;
color: $accent;
margin-top: 1;
margin-bottom: 1;
}
#note {
color: $text-muted;
text-style: dim;
margin-bottom: 1;
}
RadioSet {
height: auto;
border: none;
padding: 0;
margin-bottom: 1;
}
Input {
margin-bottom: 1;
}
LogScreen #log {
height: 1fr;
border: none;
padding: 0 1;
margin-top: 1;
}
LogScreen #hint {
margin-top: 1;
color: $text;
}
"""
class PickScreen(Screen[Optional[str]]):
BINDINGS = [
Binding("enter", "submit", "Confirm", priority=True),
Binding("escape", "abort", "Cancel"),
]
def __init__(self, step: str, question: str, options: List[str], default: str, note: str = ""):
super().__init__()
self._step = step
self._question = question
self._options = options
self._default = default
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 RadioSet(id="choices"):
for opt in self._options:
yield RadioButton(opt, value=(opt == self._default))
yield Footer()
def on_mount(self) -> None:
self.query_one(RadioSet).focus()
def action_submit(self) -> None:
radio_set = self.query_one("#choices", RadioSet)
buttons = list(radio_set.query(RadioButton))
idx = getattr(radio_set, "_selected", None)
if idx is not None and 0 <= idx < len(buttons):
self.dismiss(str(buttons[idx].label))
else:
btn = radio_set.pressed_button
self.dismiss(str(btn.label) if btn else self._default)
def action_abort(self) -> None:
self.app.exit(None)
class InputScreen(Screen[Optional[str]]):
BINDINGS = [
Binding("enter", "submit", "Confirm", priority=True),
Binding("escape", "abort", "Cancel"),
]
def __init__(self, step: str, question: str, default: str, note: str = ""):
super().__init__()
self._step = step
self._question = question
self._default = default
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")
yield Input(id="value", value=self._default)
yield Footer()
def on_mount(self) -> None:
self.query_one(Input).focus()
@on(Input.Submitted)
def _submitted(self, event: Input.Submitted) -> None:
self.dismiss(event.value.strip() or self._default)
def action_submit(self) -> None:
val = self.query_one(Input).value.strip()
self.dismiss(val or self._default)
def action_abort(self) -> None:
self.app.exit(None)
class LogScreen(Screen[bool]):
"""Streams task output into a scrollable log; press Enter to close when done."""
BINDINGS = [Binding("enter,escape", "close", "Close", show=False)]
def __init__(self, title: str, task: Callable[[LogScreen], None]):
super().__init__()
self._title = title
self._run_fn = task
self._finished = False
def compose(self) -> ComposeResult:
yield Static(self._title, id="step")
yield RichLog(id="log", highlight=True, markup=True, wrap=True)
yield Static("", id="hint")
yield Footer()
def on_mount(self) -> None:
self.query_one(RichLog).focus()
self.app.run_worker(lambda: self._run_fn(self), thread=True)
def write(self, line: str) -> None:
"""Thread-safe: append a line to the log."""
self.app.call_from_thread(self._append, line)
def _append(self, line: str) -> None:
self.query_one(RichLog).write(line)
def finish(self, success: bool) -> None:
"""Thread-safe: mark task done and prompt the user to close."""
self.app.call_from_thread(self._do_finish, success)
def _do_finish(self, success: bool) -> None:
self._finished = True
msg = (
"[green]Done![/green] Press [bold]Enter[/bold] to close."
if success
else "[red]Failed.[/red] Press [bold]Enter[/bold] to close."
)
self.query_one("#hint", Static).update(msg)
def action_close(self) -> None:
if self._finished:
self.dismiss(True)