"""Command output modal dialog for OpenRAG TUI.""" import asyncio import inspect from typing import Callable, List, Optional, AsyncIterator, Any from textual.app import ComposeResult from textual.worker import Worker from textual.containers import Container, ScrollableContainer from textual.screen import ModalScreen from textual.widgets import Button, Static, Label, RichLog from rich.console import Console class CommandOutputModal(ModalScreen): """Modal dialog for displaying command output in real-time.""" DEFAULT_CSS = """ CommandOutputModal { align: center middle; } #dialog { width: 90%; height: 90%; border: thick $primary; background: $surface; padding: 0; } #title { background: $primary; color: $text; padding: 1 2; text-align: center; width: 100%; text-style: bold; } #output-container { height: 1fr; padding: 0; margin: 0 1; } #command-output { height: 100%; border: solid $accent; padding: 1 2; margin: 1 0; background: $surface-darken-1; } #button-row { width: 100%; height: auto; align: center middle; padding: 1; margin-top: 1; } #button-row Button { margin: 0 1; min-width: 16; } """ def __init__( self, title: str, command_generator: AsyncIterator[tuple[bool, str]], on_complete: Optional[Callable] = None, ): """Initialize the modal dialog. Args: title: Title of the modal dialog command_generator: Async generator that yields (is_complete, message) tuples on_complete: Optional callback to run when command completes """ super().__init__() self.title_text = title self.command_generator = command_generator self.on_complete = on_complete def compose(self) -> ComposeResult: """Create the modal dialog layout.""" with Container(id="dialog"): yield Label(self.title_text, id="title") with ScrollableContainer(id="output-container"): yield RichLog(id="command-output", highlight=True, markup=True) with Container(id="button-row"): yield Button("Close", variant="primary", id="close-btn") def on_mount(self) -> None: """Start the command when the modal is mounted.""" # Start the command but don't store the worker self.run_worker(self._run_command(), exclusive=False) def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" if event.button.id == "close-btn": self.dismiss() async def _run_command(self) -> None: """Run the command and update the output in real-time.""" output = self.query_one("#command-output", RichLog) try: async for is_complete, message in self.command_generator: # Simple approach: just append each line as it comes output.write(message + "\n") # Scroll to bottom container = self.query_one("#output-container", ScrollableContainer) container.scroll_end(animate=False) # If command is complete, update UI if is_complete: output.write( "[bold green]Command completed successfully[/bold green]\n" ) # Call the completion callback if provided if self.on_complete: await asyncio.sleep(0.5) # Small delay for better UX def _invoke_callback() -> None: callback_result = self.on_complete() if inspect.isawaitable(callback_result): asyncio.create_task(callback_result) self.call_after_refresh(_invoke_callback) except Exception as e: output.write(f"[bold red]Error: {e}[/bold red]\n") # Enable the close button and focus it close_btn = self.query_one("#close-btn", Button) close_btn.disabled = False close_btn.focus() # Made with Bob