171 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			171 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
	
from typing import Any, Dict, List, Optional
 | 
						|
 | 
						|
from rich._loop import loop_first_last
 | 
						|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
 | 
						|
from rich.segment import Segment
 | 
						|
from rich.style import Style
 | 
						|
from rich.text import Text
 | 
						|
from typing_extensions import Literal
 | 
						|
 | 
						|
from rich_toolkit.container import Container
 | 
						|
from rich_toolkit.element import CursorOffset, Element
 | 
						|
from rich_toolkit.form import Form
 | 
						|
from rich_toolkit.progress import Progress
 | 
						|
from rich_toolkit.styles.base import BaseStyle
 | 
						|
 | 
						|
 | 
						|
class FancyPanel:
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        renderable: RenderableType,
 | 
						|
        style: BaseStyle,
 | 
						|
        title: Optional[str] = None,
 | 
						|
        metadata: Optional[Dict[str, Any]] = None,
 | 
						|
        is_animated: Optional[bool] = None,
 | 
						|
        animation_counter: Optional[int] = None,
 | 
						|
        done: bool = False,
 | 
						|
    ) -> None:
 | 
						|
        self.renderable = renderable
 | 
						|
        self._title = title
 | 
						|
        self.metadata = metadata or {}
 | 
						|
        self.width = None
 | 
						|
        self.expand = True
 | 
						|
        self.is_animated = is_animated
 | 
						|
        self.counter = animation_counter or 0
 | 
						|
        self.style = style
 | 
						|
        self.done = done
 | 
						|
 | 
						|
    def _get_decoration(self, suffix: str = "") -> Segment:
 | 
						|
        char = "┌" if self.metadata.get("title") else "◆"
 | 
						|
 | 
						|
        animated = not self.done and self.is_animated
 | 
						|
 | 
						|
        animation_status: Literal["started", "stopped", "error"] = (
 | 
						|
            "started" if animated else "stopped"
 | 
						|
        )
 | 
						|
 | 
						|
        color = self.style._get_animation_colors(
 | 
						|
            steps=14, breathe=True, animation_status=animation_status
 | 
						|
        )[self.counter % 14]
 | 
						|
 | 
						|
        return Segment(char + suffix, style=Style.from_color(color))
 | 
						|
 | 
						|
    def _strip_trailing_newlines(
 | 
						|
        self, lines: List[List[Segment]]
 | 
						|
    ) -> List[List[Segment]]:
 | 
						|
        # remove all empty lines from the end of the list
 | 
						|
 | 
						|
        while lines and all(segment.text.strip() == "" for segment in lines[-1]):
 | 
						|
            lines.pop()
 | 
						|
 | 
						|
        return lines
 | 
						|
 | 
						|
    def __rich_console__(
 | 
						|
        self, console: "Console", options: "ConsoleOptions"
 | 
						|
    ) -> "RenderResult":
 | 
						|
        renderable = self.renderable
 | 
						|
 | 
						|
        lines = console.render_lines(renderable)
 | 
						|
        lines = self._strip_trailing_newlines(lines)
 | 
						|
 | 
						|
        line_start = self._get_decoration()
 | 
						|
 | 
						|
        new_line = Segment.line()
 | 
						|
 | 
						|
        if self._title is not None:
 | 
						|
            yield line_start
 | 
						|
            yield Segment(" ")
 | 
						|
            yield self._title
 | 
						|
 | 
						|
        for first, last, line in loop_first_last(lines):
 | 
						|
            if first and not self._title:
 | 
						|
                decoration = (
 | 
						|
                    Segment("┌ ")
 | 
						|
                    if self.metadata.get("title", False)
 | 
						|
                    else self._get_decoration(suffix=" ")
 | 
						|
                )
 | 
						|
            elif last and self.metadata.get("started", True):
 | 
						|
                decoration = Segment("└ ")
 | 
						|
            else:
 | 
						|
                decoration = Segment("│ ")
 | 
						|
 | 
						|
            yield decoration
 | 
						|
            yield from line
 | 
						|
 | 
						|
            if not last:
 | 
						|
                yield new_line
 | 
						|
 | 
						|
 | 
						|
class FancyStyle(BaseStyle):
 | 
						|
    _should_show_progress_title = False
 | 
						|
 | 
						|
    def __init__(self, *args, **kwargs) -> None:
 | 
						|
        super().__init__(*args, **kwargs)
 | 
						|
 | 
						|
        self.cursor_offset = 2
 | 
						|
        self.decoration_size = 2
 | 
						|
 | 
						|
    def _should_decorate(self, element: Any, parent: Optional[Element] = None) -> bool:
 | 
						|
        return not isinstance(parent, (Progress, Container))
 | 
						|
 | 
						|
    def render_element(
 | 
						|
        self,
 | 
						|
        element: Any,
 | 
						|
        is_active: bool = False,
 | 
						|
        done: bool = False,
 | 
						|
        parent: Optional[Element] = None,
 | 
						|
        **metadata: Any,
 | 
						|
    ) -> RenderableType:
 | 
						|
        title: Optional[str] = None
 | 
						|
 | 
						|
        is_animated = False
 | 
						|
 | 
						|
        if isinstance(element, Progress):
 | 
						|
            title = element.title
 | 
						|
            is_animated = True
 | 
						|
 | 
						|
        rendered = super().render_element(
 | 
						|
            element=element, is_active=is_active, done=done, parent=parent, **metadata
 | 
						|
        )
 | 
						|
 | 
						|
        if self._should_decorate(element, parent):
 | 
						|
            rendered = FancyPanel(
 | 
						|
                rendered,
 | 
						|
                title=title,
 | 
						|
                metadata=metadata,
 | 
						|
                is_animated=is_animated,
 | 
						|
                done=done,
 | 
						|
                animation_counter=self.animation_counter,
 | 
						|
                style=self,
 | 
						|
            )
 | 
						|
 | 
						|
        return rendered
 | 
						|
 | 
						|
    def empty_line(self) -> Text:
 | 
						|
        """Return an empty line with decoration.
 | 
						|
 | 
						|
        Returns:
 | 
						|
            A text object representing an empty line
 | 
						|
        """
 | 
						|
        return Text("│", style="fancy.normal")
 | 
						|
 | 
						|
    def get_cursor_offset_for_element(
 | 
						|
        self, element: Element, parent: Optional[Element] = None
 | 
						|
    ) -> CursorOffset:
 | 
						|
        """Get the cursor offset for an element.
 | 
						|
 | 
						|
        Args:
 | 
						|
            element: The element to get the cursor offset for
 | 
						|
 | 
						|
        Returns:
 | 
						|
            The cursor offset
 | 
						|
        """
 | 
						|
 | 
						|
        if isinstance(element, Form):
 | 
						|
            return element.cursor_offset
 | 
						|
        else:
 | 
						|
            return CursorOffset(
 | 
						|
                top=element.cursor_offset.top,
 | 
						|
                left=self.decoration_size + element.cursor_offset.left,
 | 
						|
            )
 |