summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/layout/margins.py
blob: cc9dd964b4eecefd4febac3413c26ee05ce44b1f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
"""
Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`.
"""
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Callable

from prompt_toolkit.filters import FilterOrBool, to_filter
from prompt_toolkit.formatted_text import (
    StyleAndTextTuples,
    fragment_list_to_text,
    to_formatted_text,
)
from prompt_toolkit.utils import get_cwidth

from .controls import UIContent

if TYPE_CHECKING:
    from .containers import WindowRenderInfo

__all__ = [
    "Margin",
    "NumberedMargin",
    "ScrollbarMargin",
    "ConditionalMargin",
    "PromptMargin",
]


class Margin(metaclass=ABCMeta):
    """
    Base interface for a margin.
    """

    @abstractmethod
    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
        """
        Return the width that this margin is going to consume.

        :param get_ui_content: Callable that asks the user control to create
            a :class:`.UIContent` instance. This can be used for instance to
            obtain the number of lines.
        """
        return 0

    @abstractmethod
    def create_margin(
        self, window_render_info: WindowRenderInfo, width: int, height: int
    ) -> StyleAndTextTuples:
        """
        Creates a margin.
        This should return a list of (style_str, text) tuples.

        :param window_render_info:
            :class:`~prompt_toolkit.layout.containers.WindowRenderInfo`
            instance, generated after rendering and copying the visible part of
            the :class:`~prompt_toolkit.layout.controls.UIControl` into the
            :class:`~prompt_toolkit.layout.containers.Window`.
        :param width: The width that's available for this margin. (As reported
            by :meth:`.get_width`.)
        :param height: The height that's available for this margin. (The height
            of the :class:`~prompt_toolkit.layout.containers.Window`.)
        """
        return []


class NumberedMargin(Margin):
    """
    Margin that displays the line numbers.

    :param relative: Number relative to the cursor position. Similar to the Vi
                     'relativenumber' option.
    :param display_tildes: Display tildes after the end of the document, just
        like Vi does.
    """

    def __init__(
        self, relative: FilterOrBool = False, display_tildes: FilterOrBool = False
    ) -> None:
        self.relative = to_filter(relative)
        self.display_tildes = to_filter(display_tildes)

    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
        line_count = get_ui_content().line_count
        return max(3, len("%s" % line_count) + 1)

    def create_margin(
        self, window_render_info: WindowRenderInfo, width: int, height: int
    ) -> StyleAndTextTuples:
        relative = self.relative()

        style = "class:line-number"
        style_current = "class:line-number.current"

        # Get current line number.
        current_lineno = window_render_info.ui_content.cursor_position.y

        # Construct margin.
        result: StyleAndTextTuples = []
        last_lineno = None

        for y, lineno in enumerate(window_render_info.displayed_lines):
            # Only display line number if this line is not a continuation of the previous line.
            if lineno != last_lineno:
                if lineno is None:
                    pass
                elif lineno == current_lineno:
                    # Current line.
                    if relative:
                        # Left align current number in relative mode.
                        result.append((style_current, "%i" % (lineno + 1)))
                    else:
                        result.append(
                            (style_current, ("%i " % (lineno + 1)).rjust(width))
                        )
                else:
                    # Other lines.
                    if relative:
                        lineno = abs(lineno - current_lineno) - 1

                    result.append((style, ("%i " % (lineno + 1)).rjust(width)))

            last_lineno = lineno
            result.append(("", "\n"))

        # Fill with tildes.
        if self.display_tildes():
            while y < window_render_info.window_height:
                result.append(("class:tilde", "~\n"))
                y += 1

        return result


class ConditionalMargin(Margin):
    """
    Wrapper around other :class:`.Margin` classes to show/hide them.
    """

    def __init__(self, margin: Margin, filter: FilterOrBool) -> None:
        self.margin = margin
        self.filter = to_filter(filter)

    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
        if self.filter():
            return self.margin.get_width(get_ui_content)
        else:
            return 0

    def create_margin(
        self, window_render_info: WindowRenderInfo, width: int, height: int
    ) -> StyleAndTextTuples:
        if width and self.filter():
            return self.margin.create_margin(window_render_info, width, height)
        else:
            return []


class ScrollbarMargin(Margin):
    """
    Margin displaying a scrollbar.

    :param display_arrows: Display scroll up/down arrows.
    """

    def __init__(
        self,
        display_arrows: FilterOrBool = False,
        up_arrow_symbol: str = "^",
        down_arrow_symbol: str = "v",
    ) -> None:
        self.display_arrows = to_filter(display_arrows)
        self.up_arrow_symbol = up_arrow_symbol
        self.down_arrow_symbol = down_arrow_symbol

    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
        return 1

    def create_margin(
        self, window_render_info: WindowRenderInfo, width: int, height: int
    ) -> StyleAndTextTuples:
        content_height = window_render_info.content_height
        window_height = window_render_info.window_height
        display_arrows = self.display_arrows()

        if display_arrows:
            window_height -= 2

        try:
            fraction_visible = len(window_render_info.displayed_lines) / float(
                content_height
            )
            fraction_above = window_render_info.vertical_scroll / float(content_height)

            scrollbar_height = int(
                min(window_height, max(1, window_height * fraction_visible))
            )
            scrollbar_top = int(window_height * fraction_above)
        except ZeroDivisionError:
            return []
        else:

            def is_scroll_button(row: int) -> bool:
                "True if we should display a button on this row."
                return scrollbar_top <= row <= scrollbar_top + scrollbar_height

            # Up arrow.
            result: StyleAndTextTuples = []
            if display_arrows:
                result.extend(
                    [
                        ("class:scrollbar.arrow", self.up_arrow_symbol),
                        ("class:scrollbar", "\n"),
                    ]
                )

            # Scrollbar body.
            scrollbar_background = "class:scrollbar.background"
            scrollbar_background_start = "class:scrollbar.background,scrollbar.start"
            scrollbar_button = "class:scrollbar.button"
            scrollbar_button_end = "class:scrollbar.button,scrollbar.end"

            for i in range(window_height):
                if is_scroll_button(i):
                    if not is_scroll_button(i + 1):
                        # Give the last cell a different style, because we
                        # want to underline this.
                        result.append((scrollbar_button_end, " "))
                    else:
                        result.append((scrollbar_button, " "))
                else:
                    if is_scroll_button(i + 1):
                        result.append((scrollbar_background_start, " "))
                    else:
                        result.append((scrollbar_background, " "))
                result.append(("", "\n"))

            # Down arrow
            if display_arrows:
                result.append(("class:scrollbar.arrow", self.down_arrow_symbol))

            return result


class PromptMargin(Margin):
    """
    [Deprecated]

    Create margin that displays a prompt.
    This can display one prompt at the first line, and a continuation prompt
    (e.g, just dots) on all the following lines.

    This `PromptMargin` implementation has been largely superseded in favor of
    the `get_line_prefix` attribute of `Window`. The reason is that a margin is
    always a fixed width, while `get_line_prefix` can return a variable width
    prefix in front of every line, making it more powerful, especially for line
    continuations.

    :param get_prompt: Callable returns formatted text or a list of
        `(style_str, type)` tuples to be shown as the prompt at the first line.
    :param get_continuation: Callable that takes three inputs. The width (int),
        line_number (int), and is_soft_wrap (bool). It should return formatted
        text or a list of `(style_str, type)` tuples for the next lines of the
        input.
    """

    def __init__(
        self,
        get_prompt: Callable[[], StyleAndTextTuples],
        get_continuation: None
        | (Callable[[int, int, bool], StyleAndTextTuples]) = None,
    ) -> None:
        self.get_prompt = get_prompt
        self.get_continuation = get_continuation

    def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
        "Width to report to the `Window`."
        # Take the width from the first line.
        text = fragment_list_to_text(self.get_prompt())
        return get_cwidth(text)

    def create_margin(
        self, window_render_info: WindowRenderInfo, width: int, height: int
    ) -> StyleAndTextTuples:
        get_continuation = self.get_continuation
        result: StyleAndTextTuples = []

        # First line.
        result.extend(to_formatted_text(self.get_prompt()))

        # Next lines.
        if get_continuation:
            last_y = None

            for y in window_render_info.displayed_lines[1:]:
                result.append(("", "\n"))
                result.extend(
                    to_formatted_text(get_continuation(width, y, y == last_y))
                )
                last_y = y

        return result