from typing import Any, Optional, Tuple, Union from rich import box from rich.color import Color from rich.console import Group, RenderableType from rich.style import Style from rich.text import Text from rich_toolkit._rich_components import Panel from rich_toolkit.container import Container from rich_toolkit.element import CursorOffset, Element from rich_toolkit.form import Form from rich_toolkit.input import Input from rich_toolkit.menu import Menu from rich_toolkit.progress import Progress from .base import BaseStyle class BorderedStyle(BaseStyle): box = box.SQUARE def empty_line(self) -> RenderableType: return "" def _box( self, content: RenderableType, title: Union[str, Text, None], is_active: bool, border_color: Color, after: Tuple[str, ...] = (), ) -> RenderableType: return Group( Panel( content, title=title, title_align="left", highlight=is_active, width=50, box=self.box, border_style=Style(color=border_color), ), *after, ) def render_container( self, element: Container, is_active: bool = False, done: bool = False, parent: Optional[Element] = None, ) -> RenderableType: content = super().render_container(element, is_active, done, parent) if isinstance(element, Form): return self._box(content, element.title, is_active, Color.parse("white")) return content def render_input( self, element: Input, is_active: bool = False, done: bool = False, parent: Optional[Element] = None, **metadata: Any, ) -> RenderableType: validation_message: Tuple[str, ...] = () if isinstance(parent, Form): return super().render_input(element, is_active, done, parent, **metadata) if message := self.render_validation_message(element): validation_message = (message,) title = self.render_input_label( element, is_active=is_active, parent=parent, ) # Determine border color based on validation state if element.valid is False: try: border_color = self.console.get_style("error").color or Color.parse( "red" ) except Exception: # Fallback if error style is not defined border_color = Color.parse("red") else: border_color = Color.parse("white") return self._box( self.render_input_value(element, is_active=is_active, parent=parent), title, is_active, border_color, after=validation_message, ) def render_menu( self, element: Menu, is_active: bool = False, done: bool = False, parent: Optional[Element] = None, **metadata: Any, ) -> RenderableType: validation_message: Tuple[str, ...] = () menu = Text(justify="left") selected_prefix = Text(element.current_selection_char + " ") not_selected_prefix = Text(element.selection_char + " ") separator = Text("\t" if element.inline else "\n") content: list[RenderableType] = [] if done: content.append( Text( element.options[element.selected]["name"], style=self.console.get_style("result"), ) ) else: # Get visible range for scrolling all_options = element.options start, end = element.visible_options_range visible_options = all_options[start:end] # Check if scrolling is needed (to reserve consistent space for indicators) needs_scrolling = element._needs_scrolling() # Always reserve space for "more above" indicator when scrolling is enabled if needs_scrolling: if element.has_more_above: menu.append(Text(element.MORE_ABOVE_INDICATOR + "\n", style="dim")) else: menu.append(Text(" " * len(element.MORE_ABOVE_INDICATOR) + "\n")) for idx, option in enumerate(visible_options): actual_idx = start + idx if actual_idx == element.selected: prefix = selected_prefix style = self.console.get_style("selected") else: prefix = not_selected_prefix style = self.console.get_style("text") is_last = idx == len(visible_options) - 1 menu.append( Text.assemble( prefix, option["name"], separator if not is_last else "", style=style, ) ) # Always reserve space for "more below" indicator when scrolling is enabled if needs_scrolling: if element.has_more_below: menu.append(Text("\n" + element.MORE_BELOW_INDICATOR, style="dim")) else: menu.append(Text("\n" + " " * len(element.MORE_BELOW_INDICATOR))) if not element.options: menu = Text("No results found", style=self.console.get_style("text")) filter = ( [ Text.assemble( (element.filter_prompt, self.console.get_style("text")), (element.text, self.console.get_style("text")), "\n", ) ] if element.allow_filtering else [] ) content.extend(filter) content.append(menu) if message := self.render_validation_message(element): validation_message = (message,) result = Group(*content) return self._box( result, self.render_input_label(element), is_active, Color.parse("white"), after=validation_message, ) def render_progress( self, element: Progress, is_active: bool = False, done: bool = False, parent: Optional[Element] = None, ) -> RenderableType: content: str | Group | Text = element.current_message title: Union[str, Text, None] = None title = element.title if element.logs and element._inline_logs: lines_to_show = ( element.logs[-element.lines_to_show :] if element.lines_to_show > 0 else element.logs ) content = Group( *[ self.render_element( line, index=index, max_lines=element.lines_to_show, total_lines=len(element.logs), ) for index, line in enumerate(lines_to_show) ] ) border_color = Color.parse("white") if not done: colors = self._get_animation_colors( steps=10, animation_status="started", breathe=True ) border_color = colors[self.animation_counter % 10] return self._box(content, title, is_active, border_color=border_color) def get_cursor_offset_for_element( self, element: Element, parent: Optional[Element] = None ) -> CursorOffset: top_offset = element.cursor_offset.top left_offset = element.cursor_offset.left + 2 if isinstance(element, Input) and element.inline: # we don't support inline inputs yet in border style top_offset += 1 inline_left_offset = (len(element.label) - 1) if element.label else 0 left_offset = element.cursor_offset.left - inline_left_offset if isinstance(parent, Form): top_offset += 1 return CursorOffset(top=top_offset, left=left_offset)