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