euporie 2.6.1__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. euporie/console/tabs/console.py +51 -43
  2. euporie/core/__init__.py +5 -2
  3. euporie/core/app.py +74 -57
  4. euporie/core/comm/ipywidgets.py +7 -3
  5. euporie/core/config.py +51 -27
  6. euporie/core/convert/__init__.py +2 -0
  7. euporie/core/convert/datum.py +82 -45
  8. euporie/core/convert/formats/ansi.py +1 -2
  9. euporie/core/convert/formats/common.py +7 -11
  10. euporie/core/convert/formats/ft.py +10 -7
  11. euporie/core/convert/formats/png.py +7 -6
  12. euporie/core/convert/formats/sixel.py +1 -1
  13. euporie/core/convert/formats/svg.py +28 -0
  14. euporie/core/convert/mime.py +4 -7
  15. euporie/core/data_structures.py +24 -22
  16. euporie/core/filters.py +16 -2
  17. euporie/core/format.py +30 -4
  18. euporie/core/ft/ansi.py +2 -1
  19. euporie/core/ft/html.py +155 -42
  20. euporie/core/{widgets/graphics.py → graphics.py} +225 -227
  21. euporie/core/io.py +8 -0
  22. euporie/core/key_binding/bindings/__init__.py +8 -2
  23. euporie/core/key_binding/bindings/basic.py +9 -14
  24. euporie/core/key_binding/bindings/micro.py +0 -12
  25. euporie/core/key_binding/bindings/mouse.py +107 -80
  26. euporie/core/key_binding/bindings/page_navigation.py +129 -0
  27. euporie/core/key_binding/key_processor.py +9 -1
  28. euporie/core/layout/__init__.py +1 -0
  29. euporie/core/layout/containers.py +1011 -0
  30. euporie/core/layout/decor.py +381 -0
  31. euporie/core/layout/print.py +130 -0
  32. euporie/core/layout/screen.py +75 -0
  33. euporie/core/{widgets/page.py → layout/scroll.py} +166 -111
  34. euporie/core/log.py +1 -1
  35. euporie/core/margins.py +11 -5
  36. euporie/core/path.py +43 -176
  37. euporie/core/renderer.py +31 -8
  38. euporie/core/style.py +2 -0
  39. euporie/core/tabs/base.py +2 -1
  40. euporie/core/terminal.py +19 -21
  41. euporie/core/widgets/cell.py +2 -4
  42. euporie/core/widgets/cell_outputs.py +2 -2
  43. euporie/core/widgets/decor.py +3 -359
  44. euporie/core/widgets/dialog.py +5 -5
  45. euporie/core/widgets/display.py +32 -12
  46. euporie/core/widgets/file_browser.py +3 -4
  47. euporie/core/widgets/forms.py +36 -14
  48. euporie/core/widgets/inputs.py +171 -99
  49. euporie/core/widgets/layout.py +80 -5
  50. euporie/core/widgets/menu.py +1 -3
  51. euporie/core/widgets/pager.py +3 -3
  52. euporie/core/widgets/palette.py +3 -2
  53. euporie/core/widgets/status_bar.py +2 -6
  54. euporie/core/widgets/tree.py +3 -6
  55. euporie/notebook/app.py +8 -8
  56. euporie/notebook/tabs/notebook.py +2 -2
  57. euporie/notebook/widgets/side_bar.py +1 -1
  58. euporie/preview/tabs/notebook.py +2 -2
  59. euporie/web/tabs/web.py +6 -1
  60. euporie/web/widgets/webview.py +52 -32
  61. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/METADATA +9 -11
  62. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/RECORD +67 -60
  63. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/WHEEL +1 -1
  64. {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-console.desktop +0 -0
  65. {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-notebook.desktop +0 -0
  66. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/entry_points.txt +0 -0
  67. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1011 @@
1
+ """Overrides for PTK containers which only render visible lines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from functools import lru_cache, partial
7
+ from typing import TYPE_CHECKING
8
+
9
+ from prompt_toolkit.application.current import get_app
10
+ from prompt_toolkit.data_structures import Point
11
+ from prompt_toolkit.layout import containers
12
+ from prompt_toolkit.layout.containers import WindowAlign, WindowRenderInfo
13
+ from prompt_toolkit.layout.controls import (
14
+ FormattedTextControl,
15
+ UIContent,
16
+ fragment_list_width,
17
+ to_formatted_text,
18
+ )
19
+ from prompt_toolkit.layout.dimension import sum_layout_dimensions
20
+ from prompt_toolkit.layout.screen import _CHAR_CACHE
21
+ from prompt_toolkit.layout.utils import explode_text_fragments
22
+ from prompt_toolkit.mouse_events import MouseEvent
23
+ from prompt_toolkit.utils import get_cwidth, take_using_weights, to_str
24
+
25
+ from euporie.core.data_structures import DiInt
26
+ from euporie.core.layout.screen import BoundedWritePosition
27
+
28
+ if TYPE_CHECKING:
29
+ from typing import Callable
30
+
31
+ from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
32
+ from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
33
+ from prompt_toolkit.layout.containers import Float
34
+ from prompt_toolkit.layout.dimension import Dimension
35
+ from prompt_toolkit.layout.margins import Margin
36
+ from prompt_toolkit.layout.mouse_handlers import MouseHandlers
37
+ from prompt_toolkit.layout.screen import Screen, WritePosition
38
+
39
+
40
+ log = logging.getLogger(__name__)
41
+
42
+
43
+ class HSplit(containers.HSplit):
44
+ """Several layouts, one stacked above/under the other."""
45
+
46
+ def write_to_screen(
47
+ self,
48
+ screen: Screen,
49
+ mouse_handlers: MouseHandlers,
50
+ write_position: WritePosition,
51
+ parent_style: str,
52
+ erase_bg: bool,
53
+ z_index: int | None,
54
+ ) -> None:
55
+ """Render the prompt to a `Screen` instance.
56
+
57
+ :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
58
+ to which the output has to be written.
59
+ """
60
+ assert isinstance(write_position, BoundedWritePosition)
61
+ sizes = self._divide_heights(write_position)
62
+ style = parent_style + " " + to_str(self.style)
63
+ z_index = z_index if self.z_index is None else self.z_index
64
+
65
+ if sizes is None:
66
+ self.window_too_small.write_to_screen(
67
+ screen, mouse_handlers, write_position, style, erase_bg, z_index
68
+ )
69
+ else:
70
+ #
71
+ ypos = write_position.ypos
72
+ xpos = write_position.xpos
73
+ width = write_position.width
74
+ bbox = write_position.bbox
75
+
76
+ # Draw child panes.
77
+ for s, c in zip(sizes, self._all_children):
78
+ c.write_to_screen(
79
+ screen,
80
+ mouse_handlers,
81
+ BoundedWritePosition(
82
+ xpos,
83
+ ypos,
84
+ width,
85
+ s,
86
+ bbox=DiInt(
87
+ top=max(0, bbox.top - (ypos - write_position.ypos)),
88
+ right=bbox.right,
89
+ bottom=max(
90
+ 0,
91
+ bbox.bottom
92
+ - (
93
+ write_position.ypos
94
+ + write_position.height
95
+ - ypos
96
+ - s
97
+ ),
98
+ ),
99
+ left=bbox.left,
100
+ ),
101
+ ),
102
+ style,
103
+ erase_bg,
104
+ z_index,
105
+ )
106
+ ypos += s
107
+
108
+ # Fill in the remaining space. This happens when a child control
109
+ # refuses to take more space and we don't have any padding. Adding a
110
+ # dummy child control for this (in `self._all_children`) is not
111
+ # desired, because in some situations, it would take more space, even
112
+ # when it's not required. This is required to apply the styling.
113
+ remaining_height = write_position.ypos + write_position.height - ypos
114
+ if remaining_height > 0:
115
+ self._remaining_space_window.write_to_screen(
116
+ screen,
117
+ mouse_handlers,
118
+ BoundedWritePosition(
119
+ xpos,
120
+ ypos,
121
+ width,
122
+ remaining_height,
123
+ bbox=DiInt(
124
+ top=max(0, bbox.top - (ypos - write_position.ypos)),
125
+ right=bbox.right,
126
+ bottom=min(bbox.bottom, remaining_height),
127
+ left=bbox.left,
128
+ ),
129
+ ),
130
+ style,
131
+ erase_bg,
132
+ z_index,
133
+ )
134
+
135
+ def _divide_heights(self, write_position: WritePosition) -> list[int] | None:
136
+ """Return the heights for all rows, or None when there is not enough space."""
137
+ if not self.children:
138
+ return []
139
+
140
+ # Calculate heights.
141
+ width = write_position.width
142
+ height = write_position.height
143
+
144
+ return _get_divided_heights(
145
+ width,
146
+ height,
147
+ tuple(c.preferred_height(width, height) for c in self._all_children),
148
+ )
149
+
150
+
151
+ @lru_cache(maxsize=2048)
152
+ def _get_divided_heights(
153
+ width: int, height: int, dimensions: tuple[Dimension, ...]
154
+ ) -> list[int] | None:
155
+ # Sum dimensions
156
+ sum_dimensions = sum_layout_dimensions(list(dimensions))
157
+
158
+ # If there is not enough space for both.
159
+ # Don't do anything.
160
+ if sum_dimensions.min > height:
161
+ return None
162
+
163
+ # Find optimal sizes. (Start with minimal size, increase until we cover
164
+ # the whole height.)
165
+ sizes = [d.min for d in dimensions]
166
+
167
+ child_generator = take_using_weights(
168
+ items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
169
+ )
170
+
171
+ i = next(child_generator)
172
+
173
+ # Increase until we meet at least the 'preferred' size.
174
+ preferred_stop = min(height, sum_dimensions.preferred)
175
+ preferred_dimensions = [d.preferred for d in dimensions]
176
+
177
+ while sum(sizes) < preferred_stop:
178
+ if sizes[i] < preferred_dimensions[i]:
179
+ sizes[i] += 1
180
+ i = next(child_generator)
181
+
182
+ # Increase until we use all the available space. (or until "max")
183
+ if not get_app().is_done:
184
+ max_stop = min(height, sum_dimensions.max)
185
+ max_dimensions = [d.max for d in dimensions]
186
+
187
+ while sum(sizes) < max_stop:
188
+ if sizes[i] < max_dimensions[i]:
189
+ sizes[i] += 1
190
+ i = next(child_generator)
191
+
192
+ return sizes
193
+
194
+
195
+ class VSplit(containers.VSplit):
196
+ """Several layouts, one stacked left/right of the other."""
197
+
198
+ def write_to_screen(
199
+ self,
200
+ screen: Screen,
201
+ mouse_handlers: MouseHandlers,
202
+ write_position: WritePosition,
203
+ parent_style: str,
204
+ erase_bg: bool,
205
+ z_index: int | None,
206
+ ) -> None:
207
+ """Render the prompt to a `Screen` instance.
208
+
209
+ :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
210
+ to which the output has to be written.
211
+ """
212
+ assert isinstance(write_position, BoundedWritePosition)
213
+ if not self.children:
214
+ return
215
+
216
+ children = self._all_children
217
+ sizes = self._divide_widths(write_position.width)
218
+ style = parent_style + " " + to_str(self.style)
219
+ z_index = z_index if self.z_index is None else self.z_index
220
+
221
+ # If there is not enough space.
222
+ if sizes is None:
223
+ self.window_too_small.write_to_screen(
224
+ screen, mouse_handlers, write_position, style, erase_bg, z_index
225
+ )
226
+ return
227
+
228
+ # Calculate heights, take the largest possible, but not larger than
229
+ # write_position.height.
230
+ heights = [
231
+ child.preferred_height(width, write_position.height).preferred
232
+ for width, child in zip(sizes, children)
233
+ ]
234
+ height = max(write_position.height, min(write_position.height, max(heights)))
235
+
236
+ #
237
+ ypos = write_position.ypos
238
+ xpos = write_position.xpos
239
+ bbox = write_position.bbox
240
+
241
+ # Draw all child panes.
242
+ for s, c in zip(sizes, children):
243
+ c.write_to_screen(
244
+ screen,
245
+ mouse_handlers,
246
+ BoundedWritePosition(
247
+ xpos,
248
+ ypos,
249
+ s,
250
+ height,
251
+ DiInt(
252
+ top=bbox.top,
253
+ right=max(
254
+ bbox.right,
255
+ xpos + s - write_position.xpos - write_position.width,
256
+ ),
257
+ bottom=bbox.bottom,
258
+ left=max(0, bbox.left - write_position.xpos - xpos),
259
+ ),
260
+ ),
261
+ style,
262
+ erase_bg,
263
+ z_index,
264
+ )
265
+ xpos += s
266
+
267
+ # Fill in the remaining space. This happens when a child control
268
+ # refuses to take more space and we don't have any padding. Adding a
269
+ # dummy child control for this (in `self._all_children`) is not
270
+ # desired, because in some situations, it would take more space, even
271
+ # when it's not required. This is required to apply the styling.
272
+ remaining_width = write_position.xpos + write_position.width - xpos
273
+ if remaining_width > 0:
274
+ self._remaining_space_window.write_to_screen(
275
+ screen,
276
+ mouse_handlers,
277
+ BoundedWritePosition(
278
+ xpos,
279
+ ypos,
280
+ remaining_width,
281
+ height,
282
+ DiInt(
283
+ bbox.top,
284
+ max(0, bbox.left - write_position.xpos - xpos),
285
+ bbox.bottom,
286
+ max(
287
+ bbox.right,
288
+ write_position.xpos + write_position.width - xpos - s,
289
+ ),
290
+ ),
291
+ ),
292
+ style,
293
+ erase_bg,
294
+ z_index,
295
+ )
296
+
297
+
298
+ class Window(containers.Window):
299
+ """Container that holds a control."""
300
+
301
+ def write_to_screen(
302
+ self,
303
+ screen: Screen,
304
+ mouse_handlers: MouseHandlers,
305
+ write_position: WritePosition,
306
+ parent_style: str,
307
+ erase_bg: bool,
308
+ z_index: int | None,
309
+ ) -> None:
310
+ """Write window to screen."""
311
+ assert isinstance(write_position, BoundedWritePosition)
312
+ # If dont_extend_width/height was given, then reduce width/height in
313
+ # WritePosition, if the parent wanted us to paint in a bigger area.
314
+ # (This happens if this window is bundled with another window in a
315
+ # HSplit/VSplit, but with different size requirements.)
316
+ write_position = BoundedWritePosition(
317
+ xpos=write_position.xpos,
318
+ ypos=write_position.ypos,
319
+ width=write_position.width,
320
+ height=write_position.height,
321
+ bbox=write_position.bbox,
322
+ )
323
+
324
+ if self.dont_extend_width():
325
+ write_position.width = min(
326
+ write_position.width,
327
+ self.preferred_width(write_position.width).preferred,
328
+ )
329
+
330
+ if self.dont_extend_height():
331
+ write_position.height = min(
332
+ write_position.height,
333
+ self.preferred_height(
334
+ write_position.width, write_position.height
335
+ ).preferred,
336
+ )
337
+
338
+ # Draw
339
+ z_index = z_index if self.z_index is None else self.z_index
340
+
341
+ draw_func = partial(
342
+ self._write_to_screen_at_index,
343
+ screen,
344
+ mouse_handlers,
345
+ write_position,
346
+ parent_style,
347
+ erase_bg,
348
+ )
349
+
350
+ if z_index is None or z_index <= 0:
351
+ # When no z_index is given, draw right away.
352
+ draw_func()
353
+ else:
354
+ # Otherwise, postpone.
355
+ screen.draw_with_z_index(z_index=z_index, draw_func=draw_func)
356
+
357
+ def _write_to_screen_at_index(
358
+ self,
359
+ screen: Screen,
360
+ mouse_handlers: MouseHandlers,
361
+ write_position: WritePosition,
362
+ parent_style: str,
363
+ erase_bg: bool,
364
+ ) -> None:
365
+ assert isinstance(write_position, BoundedWritePosition)
366
+ # Don't bother writing invisible windows.
367
+ # (We save some time, but also avoid applying last-line styling.)
368
+ if write_position.height <= 0 or write_position.width <= 0:
369
+ return
370
+
371
+ # Calculate margin sizes.
372
+ left_margin_widths = [self._get_margin_width(m) for m in self.left_margins]
373
+ right_margin_widths = [self._get_margin_width(m) for m in self.right_margins]
374
+ total_margin_width = sum(left_margin_widths + right_margin_widths)
375
+
376
+ # Render UserControl.
377
+ ui_content = self.content.create_content(
378
+ write_position.width - total_margin_width, write_position.height
379
+ )
380
+ assert isinstance(ui_content, UIContent)
381
+
382
+ # Scroll content.
383
+ wrap_lines = self.wrap_lines()
384
+ self._scroll(
385
+ ui_content, write_position.width - total_margin_width, write_position.height
386
+ )
387
+
388
+ # Erase background and fill with `char`.
389
+ self._fill_bg(screen, write_position, erase_bg)
390
+
391
+ # Resolve `align` attribute.
392
+ align = self.align() if callable(self.align) else self.align
393
+
394
+ # Write body
395
+ bbox = write_position.bbox
396
+ visible_line_to_row_col, rowcol_to_yx = self._copy_body(
397
+ ui_content,
398
+ screen,
399
+ write_position,
400
+ sum(left_margin_widths),
401
+ write_position.width - total_margin_width - bbox.left - bbox.right,
402
+ self.vertical_scroll,
403
+ self.horizontal_scroll,
404
+ wrap_lines=wrap_lines,
405
+ highlight_lines=True,
406
+ vertical_scroll_2=self.vertical_scroll_2,
407
+ always_hide_cursor=self.always_hide_cursor(),
408
+ has_focus=get_app().layout.current_control == self.content,
409
+ align=align,
410
+ get_line_prefix=self.get_line_prefix,
411
+ )
412
+
413
+ # Remember render info. (Set before generating the margins. They need this.)
414
+ x_offset = write_position.xpos + sum(left_margin_widths)
415
+ y_offset = write_position.ypos
416
+
417
+ render_info = WindowRenderInfo(
418
+ window=self,
419
+ ui_content=ui_content,
420
+ horizontal_scroll=self.horizontal_scroll,
421
+ vertical_scroll=self.vertical_scroll,
422
+ window_width=write_position.width - total_margin_width,
423
+ window_height=write_position.height,
424
+ configured_scroll_offsets=self.scroll_offsets,
425
+ visible_line_to_row_col=visible_line_to_row_col,
426
+ rowcol_to_yx=rowcol_to_yx,
427
+ x_offset=x_offset,
428
+ y_offset=y_offset,
429
+ wrap_lines=wrap_lines,
430
+ )
431
+ self.render_info = render_info
432
+
433
+ # Set mouse handlers.
434
+ def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone:
435
+ """Turn screen coordinates into line coordinates."""
436
+ # Don't handle mouse events outside of the current modal part of
437
+ # the UI.
438
+ if self not in get_app().layout.walk_through_modal_area():
439
+ return NotImplemented
440
+
441
+ # Find row/col position first.
442
+ yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()}
443
+ y = mouse_event.position.y
444
+ x = mouse_event.position.x
445
+
446
+ # If clicked below the content area, look for a position in the
447
+ # last line instead.
448
+ max_y = write_position.ypos + len(visible_line_to_row_col) - 1
449
+ y = min(max_y, y)
450
+ result: NotImplementedOrNone
451
+
452
+ while x >= 0:
453
+ try:
454
+ row, col = yx_to_rowcol[y, x]
455
+ except KeyError:
456
+ # Try again. (When clicking on the right side of double
457
+ # width characters, or on the right side of the input.)
458
+ x -= 1
459
+ else:
460
+ # Found position, call handler of UIControl.
461
+ result = self.content.mouse_handler(
462
+ MouseEvent(
463
+ position=Point(x=col, y=row),
464
+ event_type=mouse_event.event_type,
465
+ button=mouse_event.button,
466
+ modifiers=mouse_event.modifiers,
467
+ )
468
+ )
469
+ break
470
+ else:
471
+ # nobreak.
472
+ # (No x/y coordinate found for the content. This happens in
473
+ # case of a DummyControl, that does not have any content.
474
+ # Report (0,0) instead.)
475
+ result = self.content.mouse_handler(
476
+ MouseEvent(
477
+ position=Point(x=0, y=0),
478
+ event_type=mouse_event.event_type,
479
+ button=mouse_event.button,
480
+ modifiers=mouse_event.modifiers,
481
+ )
482
+ )
483
+
484
+ # If it returns NotImplemented, handle it here.
485
+ if result == NotImplemented:
486
+ result = self._mouse_handler(mouse_event)
487
+
488
+ return result
489
+
490
+ mouse_handlers.set_mouse_handler_for_range(
491
+ x_min=write_position.xpos + max(sum(left_margin_widths), bbox.left),
492
+ x_max=write_position.xpos
493
+ + write_position.width
494
+ - max(total_margin_width, bbox.right),
495
+ y_min=write_position.ypos + bbox.top,
496
+ y_max=write_position.ypos + write_position.height - bbox.bottom,
497
+ handler=mouse_handler,
498
+ )
499
+
500
+ # Render and copy margins.
501
+ move_x = 0
502
+
503
+ def render_margin(m: Margin, width: int) -> UIContent:
504
+ """Render margin. Return `Screen`."""
505
+ # Retrieve margin fragments.
506
+ fragments = m.create_margin(render_info, width, write_position.height)
507
+
508
+ # Turn it into a UIContent object.
509
+ # already rendered those fragments using this size.)
510
+ return FormattedTextControl(fragments).create_content(
511
+ width + 1, write_position.height
512
+ )
513
+
514
+ for m, width in zip(self.left_margins, left_margin_widths):
515
+ if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.)
516
+ # Create screen for margin.
517
+ margin_content = render_margin(m, width)
518
+
519
+ # Copy and shift X.
520
+ self._copy_margin(margin_content, screen, write_position, move_x, width)
521
+ move_x += width
522
+
523
+ move_x = write_position.width - sum(right_margin_widths)
524
+
525
+ for m, width in zip(self.right_margins, right_margin_widths):
526
+ # Create screen for margin.
527
+ margin_content = render_margin(m, width)
528
+
529
+ # Copy and shift X.
530
+ self._copy_margin(margin_content, screen, write_position, move_x, width)
531
+ move_x += width
532
+
533
+ # Apply 'self.style'
534
+ self._apply_style(screen, write_position, parent_style)
535
+
536
+ # Additionally apply style to line with cursor if it is visible
537
+ if ui_content.show_cursor and not self.always_hide_cursor():
538
+ _col, _row = ui_content.cursor_position
539
+ if cp_yx := rowcol_to_yx.get((_row, _col)):
540
+ self._apply_style(
541
+ screen,
542
+ BoundedWritePosition(
543
+ xpos=write_position.xpos,
544
+ ypos=cp_yx[0],
545
+ width=write_position.width,
546
+ height=1,
547
+ ),
548
+ parent_style,
549
+ )
550
+
551
+ # Tell the screen that this user control has been painted at this
552
+ # position.
553
+ screen.visible_windows_to_write_positions[self] = write_position
554
+
555
+ def _copy_body(
556
+ self,
557
+ ui_content: UIContent,
558
+ new_screen: Screen,
559
+ write_position: WritePosition,
560
+ move_x: int,
561
+ width: int,
562
+ vertical_scroll: int = 0,
563
+ horizontal_scroll: int = 0,
564
+ wrap_lines: bool = False,
565
+ highlight_lines: bool = False,
566
+ vertical_scroll_2: int = 0,
567
+ always_hide_cursor: bool = False,
568
+ has_focus: bool = False,
569
+ align: WindowAlign = WindowAlign.LEFT,
570
+ get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None,
571
+ ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]:
572
+ """Copy the UIContent into the output screen."""
573
+ assert isinstance(write_position, BoundedWritePosition)
574
+ xpos = write_position.xpos + move_x
575
+ ypos = write_position.ypos
576
+ line_count = ui_content.line_count
577
+ new_buffer = new_screen.data_buffer
578
+ empty_char = _CHAR_CACHE["", ""]
579
+
580
+ bbox = write_position.bbox
581
+
582
+ # Map visible line number to (row, col) of input.
583
+ # 'col' will always be zero if line wrapping is off.
584
+ visible_line_to_row_col: dict[int, tuple[int, int]] = {}
585
+
586
+ # Maps (row, col) from the input to (y, x) screen coordinates.
587
+ rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
588
+
589
+ def copy_line(
590
+ line: StyleAndTextTuples,
591
+ lineno: int,
592
+ x: int,
593
+ y: int,
594
+ is_input: bool = False,
595
+ ) -> tuple[int, int]:
596
+ """Copy over a single line to the output screen."""
597
+ current_rowcol_to_yx = (
598
+ rowcol_to_yx if is_input else {}
599
+ ) # Throwaway dictionary.
600
+
601
+ # Draw line prefix.
602
+ if is_input and get_line_prefix:
603
+ prompt = to_formatted_text(get_line_prefix(lineno, 0))
604
+ x, y = copy_line(prompt, lineno, x, y, is_input=False)
605
+
606
+ # Scroll horizontally.
607
+ skipped = 0 # Characters skipped because of horizontal scrolling.
608
+ if horizontal_scroll and is_input:
609
+ h_scroll = horizontal_scroll
610
+ line = explode_text_fragments(line)
611
+ while h_scroll > 0 and line:
612
+ h_scroll -= get_cwidth(line[0][1])
613
+ skipped += 1
614
+ del line[:1] # Remove first character.
615
+
616
+ x -= h_scroll # When scrolling over double width character,
617
+ # this can end up being negative.
618
+
619
+ # Align this line. (Note that this doesn't work well when we use
620
+ # get_line_prefix and that function returns variable width prefixes.)
621
+ if align == WindowAlign.CENTER:
622
+ line_width = fragment_list_width(line)
623
+ if line_width < width:
624
+ x += (width - line_width) // 2
625
+ elif align == WindowAlign.RIGHT:
626
+ line_width = fragment_list_width(line)
627
+ if line_width < width:
628
+ x += width - line_width
629
+
630
+ col = 0
631
+ wrap_count = 0
632
+ for style, text, *_ in line:
633
+ new_buffer_row = new_buffer[y + ypos]
634
+
635
+ # Remember raw VT escape sequences. (E.g. FinalTerm's
636
+ # escape sequences.)
637
+ if "[ZeroWidthEscape]" in style:
638
+ new_screen.zero_width_escapes[y + ypos][x + xpos] += text
639
+ continue
640
+
641
+ for c in text:
642
+ char = _CHAR_CACHE[c, style]
643
+ char_width = char.width
644
+
645
+ # Wrap when the line width is exceeded.
646
+ if wrap_lines and x + char_width > width:
647
+ visible_line_to_row_col[y + 1] = (
648
+ lineno,
649
+ visible_line_to_row_col[y][1] + x,
650
+ )
651
+ y += 1
652
+ wrap_count += 1
653
+ x = 0
654
+
655
+ # Insert line prefix (continuation prompt).
656
+ if is_input and get_line_prefix:
657
+ prompt = to_formatted_text(
658
+ get_line_prefix(lineno, wrap_count)
659
+ )
660
+ x, y = copy_line(prompt, lineno, x, y, is_input=False)
661
+
662
+ new_buffer_row = new_buffer[y + ypos]
663
+
664
+ if y >= write_position.height:
665
+ return x, y # Break out of all for loops.
666
+
667
+ # Set character in screen and shift 'x'.
668
+ if x >= 0 and y >= 0 and x < width:
669
+ new_buffer_row[x + xpos] = char
670
+
671
+ # When we print a multi width character, make sure
672
+ # to erase the neighbors positions in the screen.
673
+ # (The empty string if different from everything,
674
+ # so next redraw this cell will repaint anyway.)
675
+ if char_width > 1:
676
+ for i in range(1, char_width):
677
+ new_buffer_row[x + xpos + i] = empty_char
678
+
679
+ # If this is a zero width characters, then it's
680
+ # probably part of a decomposed unicode character.
681
+ # See: https://en.wikipedia.org/wiki/Unicode_equivalence
682
+ # Merge it in the previous cell.
683
+ elif char_width == 0:
684
+ # Handle all character widths. If the previous
685
+ # character is a multiwidth character, then
686
+ # merge it two positions back.
687
+ for pw in [2, 1]: # Previous character width.
688
+ if (
689
+ x - pw >= 0
690
+ and new_buffer_row[x + xpos - pw].width == pw
691
+ ):
692
+ prev_char = new_buffer_row[x + xpos - pw]
693
+ char2 = _CHAR_CACHE[
694
+ prev_char.char + c, prev_char.style
695
+ ]
696
+ new_buffer_row[x + xpos - pw] = char2
697
+
698
+ # Keep track of write position for each character.
699
+ current_rowcol_to_yx[lineno, col + skipped] = (
700
+ y + ypos,
701
+ x + xpos,
702
+ )
703
+
704
+ col += 1
705
+ x += char_width
706
+ return x, y
707
+
708
+ # Copy content.
709
+ y = -vertical_scroll_2
710
+ lineno = vertical_scroll
711
+
712
+ # Render lines down to the end of the visible region (or to the cursor
713
+ # position, whichever is lower)
714
+ cursor_visible = ui_content.show_cursor and not self.always_hide_cursor()
715
+ while (
716
+ y < write_position.height - bbox.bottom
717
+ or (cursor_visible and lineno <= ui_content.cursor_position.y)
718
+ ) and lineno < line_count:
719
+ visible_line_to_row_col[y] = (lineno, horizontal_scroll)
720
+
721
+ # If lines are wrapped, we need to render all of them so we know how many
722
+ # rows each line occupies.
723
+ # Otherwise, we can skip rendering lines which are not visible.
724
+ # Also always render the line with the visible cursor so we know it's position
725
+ if (wrap_lines or bbox.top <= y) or (
726
+ cursor_visible and lineno == ui_content.cursor_position.y
727
+ ):
728
+ # Take the next line and copy it in the real screen.
729
+ line = ui_content.get_line(lineno)
730
+ # Copy margin and actual line.
731
+ x = 0
732
+ x, y = copy_line(line, lineno, x, y, is_input=True)
733
+
734
+ lineno += 1
735
+ y += 1
736
+
737
+ def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
738
+ """Translate row/col from UIContent to real Screen coordinates."""
739
+ try:
740
+ y, x = rowcol_to_yx[row, col]
741
+ except KeyError:
742
+ # Normally this should never happen. (It is a bug, if it happens.)
743
+ # But to be sure, return (0, 0)
744
+ return Point(x=0, y=0)
745
+
746
+ # raise ValueError(
747
+ # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
748
+ # 'horizontal_scroll=%r, height=%r' %
749
+ # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
750
+ else:
751
+ return Point(x=x, y=y)
752
+
753
+ # Set cursor and menu positions.
754
+ if ui_content.cursor_position:
755
+ screen_cursor_position = cursor_pos_to_screen_pos(
756
+ ui_content.cursor_position.y, ui_content.cursor_position.x
757
+ )
758
+
759
+ if has_focus:
760
+ new_screen.set_cursor_position(self, screen_cursor_position)
761
+
762
+ if always_hide_cursor:
763
+ new_screen.show_cursor = False
764
+ else:
765
+ new_screen.show_cursor = ui_content.show_cursor
766
+
767
+ self._highlight_digraph(new_screen)
768
+
769
+ if highlight_lines:
770
+ self._highlight_cursorlines(
771
+ new_screen,
772
+ screen_cursor_position,
773
+ xpos,
774
+ ypos,
775
+ width,
776
+ write_position.height,
777
+ )
778
+
779
+ # Draw input characters from the input processor queue.
780
+ if has_focus and ui_content.cursor_position:
781
+ self._show_key_processor_key_buffer(new_screen)
782
+
783
+ # Set menu position.
784
+ if ui_content.menu_position:
785
+ new_screen.set_menu_position(
786
+ self,
787
+ cursor_pos_to_screen_pos(
788
+ ui_content.menu_position.y, ui_content.menu_position.x
789
+ ),
790
+ )
791
+
792
+ # Update output screen height.
793
+ new_screen.height = max(new_screen.height, ypos + write_position.height)
794
+
795
+ return visible_line_to_row_col, rowcol_to_yx
796
+
797
+ def _copy_margin(
798
+ self,
799
+ margin_content: UIContent,
800
+ new_screen: Screen,
801
+ write_position: WritePosition,
802
+ move_x: int,
803
+ width: int,
804
+ ) -> None:
805
+ """Copy characters from the margin screen to the real screen."""
806
+ assert isinstance(write_position, BoundedWritePosition)
807
+ xpos = write_position.xpos + move_x
808
+ ypos = write_position.ypos
809
+ wp_bbox = write_position.bbox
810
+
811
+ margin_write_position = BoundedWritePosition(
812
+ xpos, ypos, width, write_position.height, wp_bbox
813
+ )
814
+ self._copy_body(margin_content, new_screen, margin_write_position, 0, width)
815
+
816
+ def _fill_bg(
817
+ self, screen: Screen, write_position: WritePosition, erase_bg: bool
818
+ ) -> None:
819
+ """Erase/fill the background."""
820
+ assert isinstance(write_position, BoundedWritePosition)
821
+ char: str | None
822
+ char = self.char() if callable(self.char) else self.char
823
+
824
+ if erase_bg or char:
825
+ wp = write_position
826
+ char_obj = _CHAR_CACHE[char or " ", ""]
827
+
828
+ bbox = wp.bbox
829
+ for y in range(wp.ypos + bbox.top, wp.ypos + wp.height - bbox.bottom):
830
+ row = screen.data_buffer[y]
831
+ for x in range(wp.xpos + bbox.left, wp.xpos + wp.width - bbox.right):
832
+ row[x] = char_obj
833
+
834
+ def _apply_style(
835
+ self,
836
+ new_screen: Screen,
837
+ write_position: WritePosition,
838
+ parent_style: str,
839
+ ) -> None:
840
+ # Apply `self.style`.
841
+ style = f"{parent_style} {to_str(self.style)}"
842
+
843
+ new_screen.fill_area(write_position, style=style, after=False)
844
+
845
+ # Apply the 'last-line' class to the last line of each Window. This can
846
+ # be used to apply an 'underline' to the user control.
847
+ if isinstance(write_position, BoundedWritePosition):
848
+ if write_position.bbox.bottom == 0:
849
+ wp = BoundedWritePosition(
850
+ write_position.xpos,
851
+ write_position.ypos + write_position.height - 1,
852
+ write_position.width,
853
+ 1,
854
+ )
855
+ new_screen.fill_area(wp, "class:last-line", after=True)
856
+ else:
857
+ new_screen.fill_area(write_position, "class:last-line", after=True)
858
+
859
+
860
+ class FloatContainer(containers.FloatContainer):
861
+ """A `FloatContainer` which uses :py`BoundedWritePosition`s."""
862
+
863
+ def _draw_float(
864
+ self,
865
+ fl: Float,
866
+ screen: Screen,
867
+ mouse_handlers: MouseHandlers,
868
+ write_position: WritePosition,
869
+ style: str,
870
+ erase_bg: bool,
871
+ z_index: int | None,
872
+ ) -> None:
873
+ """Draw a single Float."""
874
+ # When a menu_position was given, use this instead of the cursor
875
+ # position. (These cursor positions are absolute, translate again
876
+ # relative to the write_position.)
877
+ # Note: This should be inside the for-loop, because one float could
878
+ # set the cursor position to be used for the next one.
879
+ cpos = screen.get_menu_position(
880
+ fl.attach_to_window or get_app().layout.current_window
881
+ )
882
+ cursor_position = Point(
883
+ x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos
884
+ )
885
+
886
+ fl_width = fl.get_width()
887
+ fl_height = fl.get_height()
888
+ width: int
889
+ height: int
890
+ xpos: int
891
+ ypos: int
892
+
893
+ # Left & width given.
894
+ if fl.left is not None and fl_width is not None:
895
+ xpos = fl.left
896
+ width = fl_width
897
+ # Left & right given -> calculate width.
898
+ elif fl.left is not None and fl.right is not None:
899
+ xpos = fl.left
900
+ width = write_position.width - fl.left - fl.right
901
+ # Width & right given -> calculate left.
902
+ elif fl_width is not None and fl.right is not None:
903
+ xpos = write_position.width - fl.right - fl_width
904
+ width = fl_width
905
+ # Near x position of cursor.
906
+ elif fl.xcursor:
907
+ if fl_width is None:
908
+ width = fl.content.preferred_width(write_position.width).preferred
909
+ width = min(write_position.width, width)
910
+ else:
911
+ width = fl_width
912
+
913
+ xpos = cursor_position.x
914
+ if xpos + width > write_position.width:
915
+ xpos = max(0, write_position.width - width)
916
+ # Only width given -> center horizontally.
917
+ elif fl_width:
918
+ xpos = int((write_position.width - fl_width) / 2)
919
+ width = fl_width
920
+ # Otherwise, take preferred width from float content.
921
+ else:
922
+ width = fl.content.preferred_width(write_position.width).preferred
923
+
924
+ if fl.left is not None:
925
+ xpos = fl.left
926
+ elif fl.right is not None:
927
+ xpos = max(0, write_position.width - width - fl.right)
928
+ else: # Center horizontally.
929
+ xpos = max(0, int((write_position.width - width) / 2))
930
+
931
+ # Trim.
932
+ width = min(width, write_position.width - xpos)
933
+
934
+ # Top & height given.
935
+ if fl.top is not None and fl_height is not None:
936
+ ypos = fl.top
937
+ height = fl_height
938
+ # Top & bottom given -> calculate height.
939
+ elif fl.top is not None and fl.bottom is not None:
940
+ ypos = fl.top
941
+ height = write_position.height - fl.top - fl.bottom
942
+ # Height & bottom given -> calculate top.
943
+ elif fl_height is not None and fl.bottom is not None:
944
+ ypos = write_position.height - fl_height - fl.bottom
945
+ height = fl_height
946
+ # Near cursor.
947
+ elif fl.ycursor:
948
+ ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1)
949
+
950
+ if fl_height is None:
951
+ height = fl.content.preferred_height(
952
+ width, write_position.height
953
+ ).preferred
954
+ else:
955
+ height = fl_height
956
+
957
+ # Reduce height if not enough space. (We can use the height
958
+ # when the content requires it.)
959
+ if height > write_position.height - ypos:
960
+ if write_position.height - ypos + 1 >= ypos:
961
+ # When the space below the cursor is more than
962
+ # the space above, just reduce the height.
963
+ height = write_position.height - ypos
964
+ else:
965
+ # Otherwise, fit the float above the cursor.
966
+ height = min(height, cursor_position.y)
967
+ ypos = cursor_position.y - height
968
+
969
+ # Only height given -> center vertically.
970
+ elif fl_height:
971
+ ypos = int((write_position.height - fl_height) / 2)
972
+ height = fl_height
973
+ # Otherwise, take preferred height from content.
974
+ else:
975
+ height = fl.content.preferred_height(width, write_position.height).preferred
976
+
977
+ if fl.top is not None:
978
+ ypos = fl.top
979
+ elif fl.bottom is not None:
980
+ ypos = max(0, write_position.height - height - fl.bottom)
981
+ else: # Center vertically.
982
+ ypos = max(0, int((write_position.height - height) / 2))
983
+
984
+ # Trim.
985
+ height = min(height, write_position.height - ypos)
986
+
987
+ # Write float.
988
+ # (xpos and ypos can be negative: a float can be partially visible.)
989
+ if height > 0 and width > 0:
990
+ wp = BoundedWritePosition(
991
+ xpos=xpos + write_position.xpos,
992
+ ypos=ypos + write_position.ypos,
993
+ width=width,
994
+ height=height,
995
+ )
996
+
997
+ if not fl.hide_when_covering_content or self._area_is_empty(screen, wp):
998
+ fl.content.write_to_screen(
999
+ screen,
1000
+ mouse_handlers,
1001
+ wp,
1002
+ style,
1003
+ erase_bg=not fl.transparent(),
1004
+ z_index=z_index,
1005
+ )
1006
+
1007
+
1008
+ containers.HSplit = HSplit # type: ignore[misc]
1009
+ containers.VSplit = VSplit # type: ignore[misc]
1010
+ containers.Window = Window # type: ignore[misc]
1011
+ containers.FloatContainer = FloatContainer # type: ignore[misc]