1""" 
    2Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`. 
    3""" 
    4 
    5from __future__ import annotations 
    6 
    7from abc import ABCMeta, abstractmethod 
    8from typing import TYPE_CHECKING, Callable 
    9 
    10from prompt_toolkit.filters import FilterOrBool, to_filter 
    11from prompt_toolkit.formatted_text import ( 
    12    StyleAndTextTuples, 
    13    fragment_list_to_text, 
    14    to_formatted_text, 
    15) 
    16from prompt_toolkit.utils import get_cwidth 
    17 
    18from .controls import UIContent 
    19 
    20if TYPE_CHECKING: 
    21    from .containers import WindowRenderInfo 
    22 
    23__all__ = [ 
    24    "Margin", 
    25    "NumberedMargin", 
    26    "ScrollbarMargin", 
    27    "ConditionalMargin", 
    28    "PromptMargin", 
    29] 
    30 
    31 
    32class Margin(metaclass=ABCMeta): 
    33    """ 
    34    Base interface for a margin. 
    35    """ 
    36 
    37    @abstractmethod 
    38    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: 
    39        """ 
    40        Return the width that this margin is going to consume. 
    41 
    42        :param get_ui_content: Callable that asks the user control to create 
    43            a :class:`.UIContent` instance. This can be used for instance to 
    44            obtain the number of lines. 
    45        """ 
    46        return 0 
    47 
    48    @abstractmethod 
    49    def create_margin( 
    50        self, window_render_info: WindowRenderInfo, width: int, height: int 
    51    ) -> StyleAndTextTuples: 
    52        """ 
    53        Creates a margin. 
    54        This should return a list of (style_str, text) tuples. 
    55 
    56        :param window_render_info: 
    57            :class:`~prompt_toolkit.layout.containers.WindowRenderInfo` 
    58            instance, generated after rendering and copying the visible part of 
    59            the :class:`~prompt_toolkit.layout.controls.UIControl` into the 
    60            :class:`~prompt_toolkit.layout.containers.Window`. 
    61        :param width: The width that's available for this margin. (As reported 
    62            by :meth:`.get_width`.) 
    63        :param height: The height that's available for this margin. (The height 
    64            of the :class:`~prompt_toolkit.layout.containers.Window`.) 
    65        """ 
    66        return [] 
    67 
    68 
    69class NumberedMargin(Margin): 
    70    """ 
    71    Margin that displays the line numbers. 
    72 
    73    :param relative: Number relative to the cursor position. Similar to the Vi 
    74                     'relativenumber' option. 
    75    :param display_tildes: Display tildes after the end of the document, just 
    76        like Vi does. 
    77    """ 
    78 
    79    def __init__( 
    80        self, relative: FilterOrBool = False, display_tildes: FilterOrBool = False 
    81    ) -> None: 
    82        self.relative = to_filter(relative) 
    83        self.display_tildes = to_filter(display_tildes) 
    84 
    85    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: 
    86        line_count = get_ui_content().line_count 
    87        return max(3, len(f"{line_count}") + 1) 
    88 
    89    def create_margin( 
    90        self, window_render_info: WindowRenderInfo, width: int, height: int 
    91    ) -> StyleAndTextTuples: 
    92        relative = self.relative() 
    93 
    94        style = "class:line-number" 
    95        style_current = "class:line-number.current" 
    96 
    97        # Get current line number. 
    98        current_lineno = window_render_info.ui_content.cursor_position.y 
    99 
    100        # Construct margin. 
    101        result: StyleAndTextTuples = [] 
    102        last_lineno = None 
    103 
    104        for y, lineno in enumerate(window_render_info.displayed_lines): 
    105            # Only display line number if this line is not a continuation of the previous line. 
    106            if lineno != last_lineno: 
    107                if lineno is None: 
    108                    pass 
    109                elif lineno == current_lineno: 
    110                    # Current line. 
    111                    if relative: 
    112                        # Left align current number in relative mode. 
    113                        result.append((style_current, "%i" % (lineno + 1))) 
    114                    else: 
    115                        result.append( 
    116                            (style_current, ("%i " % (lineno + 1)).rjust(width)) 
    117                        ) 
    118                else: 
    119                    # Other lines. 
    120                    if relative: 
    121                        lineno = abs(lineno - current_lineno) - 1 
    122 
    123                    result.append((style, ("%i " % (lineno + 1)).rjust(width))) 
    124 
    125            last_lineno = lineno 
    126            result.append(("", "\n")) 
    127 
    128        # Fill with tildes. 
    129        if self.display_tildes(): 
    130            while y < window_render_info.window_height: 
    131                result.append(("class:tilde", "~\n")) 
    132                y += 1 
    133 
    134        return result 
    135 
    136 
    137class ConditionalMargin(Margin): 
    138    """ 
    139    Wrapper around other :class:`.Margin` classes to show/hide them. 
    140    """ 
    141 
    142    def __init__(self, margin: Margin, filter: FilterOrBool) -> None: 
    143        self.margin = margin 
    144        self.filter = to_filter(filter) 
    145 
    146    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: 
    147        if self.filter(): 
    148            return self.margin.get_width(get_ui_content) 
    149        else: 
    150            return 0 
    151 
    152    def create_margin( 
    153        self, window_render_info: WindowRenderInfo, width: int, height: int 
    154    ) -> StyleAndTextTuples: 
    155        if width and self.filter(): 
    156            return self.margin.create_margin(window_render_info, width, height) 
    157        else: 
    158            return [] 
    159 
    160 
    161class ScrollbarMargin(Margin): 
    162    """ 
    163    Margin displaying a scrollbar. 
    164 
    165    :param display_arrows: Display scroll up/down arrows. 
    166    """ 
    167 
    168    def __init__( 
    169        self, 
    170        display_arrows: FilterOrBool = False, 
    171        up_arrow_symbol: str = "^", 
    172        down_arrow_symbol: str = "v", 
    173    ) -> None: 
    174        self.display_arrows = to_filter(display_arrows) 
    175        self.up_arrow_symbol = up_arrow_symbol 
    176        self.down_arrow_symbol = down_arrow_symbol 
    177 
    178    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: 
    179        return 1 
    180 
    181    def create_margin( 
    182        self, window_render_info: WindowRenderInfo, width: int, height: int 
    183    ) -> StyleAndTextTuples: 
    184        content_height = window_render_info.content_height 
    185        window_height = window_render_info.window_height 
    186        display_arrows = self.display_arrows() 
    187 
    188        if display_arrows: 
    189            window_height -= 2 
    190 
    191        try: 
    192            fraction_visible = len(window_render_info.displayed_lines) / float( 
    193                content_height 
    194            ) 
    195            fraction_above = window_render_info.vertical_scroll / float(content_height) 
    196 
    197            scrollbar_height = int( 
    198                min(window_height, max(1, window_height * fraction_visible)) 
    199            ) 
    200            scrollbar_top = int(window_height * fraction_above) 
    201        except ZeroDivisionError: 
    202            return [] 
    203        else: 
    204 
    205            def is_scroll_button(row: int) -> bool: 
    206                "True if we should display a button on this row." 
    207                return scrollbar_top <= row <= scrollbar_top + scrollbar_height 
    208 
    209            # Up arrow. 
    210            result: StyleAndTextTuples = [] 
    211            if display_arrows: 
    212                result.extend( 
    213                    [ 
    214                        ("class:scrollbar.arrow", self.up_arrow_symbol), 
    215                        ("class:scrollbar", "\n"), 
    216                    ] 
    217                ) 
    218 
    219            # Scrollbar body. 
    220            scrollbar_background = "class:scrollbar.background" 
    221            scrollbar_background_start = "class:scrollbar.background,scrollbar.start" 
    222            scrollbar_button = "class:scrollbar.button" 
    223            scrollbar_button_end = "class:scrollbar.button,scrollbar.end" 
    224 
    225            for i in range(window_height): 
    226                if is_scroll_button(i): 
    227                    if not is_scroll_button(i + 1): 
    228                        # Give the last cell a different style, because we 
    229                        # want to underline this. 
    230                        result.append((scrollbar_button_end, " ")) 
    231                    else: 
    232                        result.append((scrollbar_button, " ")) 
    233                else: 
    234                    if is_scroll_button(i + 1): 
    235                        result.append((scrollbar_background_start, " ")) 
    236                    else: 
    237                        result.append((scrollbar_background, " ")) 
    238                result.append(("", "\n")) 
    239 
    240            # Down arrow 
    241            if display_arrows: 
    242                result.append(("class:scrollbar.arrow", self.down_arrow_symbol)) 
    243 
    244            return result 
    245 
    246 
    247class PromptMargin(Margin): 
    248    """ 
    249    [Deprecated] 
    250 
    251    Create margin that displays a prompt. 
    252    This can display one prompt at the first line, and a continuation prompt 
    253    (e.g, just dots) on all the following lines. 
    254 
    255    This `PromptMargin` implementation has been largely superseded in favor of 
    256    the `get_line_prefix` attribute of `Window`. The reason is that a margin is 
    257    always a fixed width, while `get_line_prefix` can return a variable width 
    258    prefix in front of every line, making it more powerful, especially for line 
    259    continuations. 
    260 
    261    :param get_prompt: Callable returns formatted text or a list of 
    262        `(style_str, type)` tuples to be shown as the prompt at the first line. 
    263    :param get_continuation: Callable that takes three inputs. The width (int), 
    264        line_number (int), and is_soft_wrap (bool). It should return formatted 
    265        text or a list of `(style_str, type)` tuples for the next lines of the 
    266        input. 
    267    """ 
    268 
    269    def __init__( 
    270        self, 
    271        get_prompt: Callable[[], StyleAndTextTuples], 
    272        get_continuation: None 
    273        | (Callable[[int, int, bool], StyleAndTextTuples]) = None, 
    274    ) -> None: 
    275        self.get_prompt = get_prompt 
    276        self.get_continuation = get_continuation 
    277 
    278    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: 
    279        "Width to report to the `Window`." 
    280        # Take the width from the first line. 
    281        text = fragment_list_to_text(self.get_prompt()) 
    282        return get_cwidth(text) 
    283 
    284    def create_margin( 
    285        self, window_render_info: WindowRenderInfo, width: int, height: int 
    286    ) -> StyleAndTextTuples: 
    287        get_continuation = self.get_continuation 
    288        result: StyleAndTextTuples = [] 
    289 
    290        # First line. 
    291        result.extend(to_formatted_text(self.get_prompt())) 
    292 
    293        # Next lines. 
    294        if get_continuation: 
    295            last_y = None 
    296 
    297            for y in window_render_info.displayed_lines[1:]: 
    298                result.append(("", "\n")) 
    299                result.extend( 
    300                    to_formatted_text(get_continuation(width, y, y == last_y)) 
    301                ) 
    302                last_y = y 
    303 
    304        return result