euporie 2.6.2__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 +52 -38
  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.2.dist-info → euporie-2.7.0.dist-info}/METADATA +9 -11
  62. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/RECORD +67 -60
  63. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/WHEEL +1 -1
  64. {euporie-2.6.2.data → euporie-2.7.0.data}/data/share/applications/euporie-console.desktop +0 -0
  65. {euporie-2.6.2.data → euporie-2.7.0.data}/data/share/applications/euporie-notebook.desktop +0 -0
  66. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/entry_points.txt +0 -0
  67. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,14 +21,14 @@ from prompt_toolkit.layout.controls import UIContent
21
21
  from prompt_toolkit.layout.dimension import Dimension, to_dimension
22
22
  from prompt_toolkit.layout.layout import walk
23
23
  from prompt_toolkit.layout.mouse_handlers import MouseHandlers
24
- from prompt_toolkit.layout.screen import Char, Screen, WritePosition
25
24
  from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseModifier
26
25
 
27
26
  from euporie.core.data_structures import DiInt
27
+ from euporie.core.layout.screen import BoundedWritePosition, Screen
28
28
  from euporie.core.utils import run_in_thread_with_context
29
29
 
30
30
  if TYPE_CHECKING:
31
- from typing import Callable, Iterable, Literal, Sequence
31
+ from typing import Callable, Literal, Sequence
32
32
 
33
33
  from prompt_toolkit.key_binding.key_bindings import (
34
34
  KeyBindingsBase,
@@ -36,6 +36,8 @@ if TYPE_CHECKING:
36
36
  )
37
37
  from prompt_toolkit.layout.containers import AnyContainer
38
38
  from prompt_toolkit.layout.dimension import AnyDimension
39
+ from prompt_toolkit.layout.screen import Screen as PtkScreen
40
+ from prompt_toolkit.layout.screen import WritePosition
39
41
  from prompt_toolkit.utils import Event
40
42
 
41
43
  MouseHandler = Callable[[MouseEvent], object]
@@ -43,31 +45,6 @@ if TYPE_CHECKING:
43
45
  log = logging.getLogger(__name__)
44
46
 
45
47
 
46
- class BoundedWritePosition(WritePosition):
47
- """A write position which also hold bounding box information."""
48
-
49
- def __init__(
50
- self,
51
- xpos: int,
52
- ypos: int,
53
- width: int,
54
- height: int,
55
- bbox: DiInt,
56
- ) -> None:
57
- """Create a new instance of the write position."""
58
- super().__init__(xpos, ypos, width, height)
59
- self.bbox = bbox
60
-
61
- def __repr__(self) -> str:
62
- """Return a string representation of the write position."""
63
- return (
64
- f"{self.__class__.__name__}("
65
- f"x={self.xpos}, y={self.ypos}, "
66
- f"w={self.width}, h={self.height}, "
67
- f"bbox={self.bbox})"
68
- )
69
-
70
-
71
48
  class ChildRenderInfo:
72
49
  """A class which holds information about a :py:class:`ScrollingContainer` child."""
73
50
 
@@ -83,15 +60,16 @@ class ChildRenderInfo:
83
60
  self.child = weakref.proxy(child)
84
61
  self.container = to_container(child)
85
62
 
86
- self.screen = Screen(default_char=Char(char=" "))
63
+ self.screen = Screen()
87
64
  self.mouse_handlers = MouseHandlers()
88
65
 
89
- self.render_counter = -1
90
66
  self._invalid = True
91
- self._invalidate_events: list[Event[object]] = []
67
+ self._invalidate_events: set[Event[object]] = set()
92
68
  self._layout_hash = 0
93
69
  self.height = 0
94
70
  self.width = 0
71
+ self._rendered_lines: set[int] = set()
72
+ self._rowcols_to_yx: dict[Window, dict[tuple[int, int], tuple[int, int]]] = {}
95
73
 
96
74
  def invalidate(self) -> None:
97
75
  """Flag the child's rendering as out-of-date."""
@@ -110,6 +88,8 @@ class ChildRenderInfo:
110
88
  available_width: int,
111
89
  available_height: int,
112
90
  style: str = "",
91
+ start: int | None = None,
92
+ end: int | None = None,
113
93
  ) -> None:
114
94
  """Render the child container at a given size.
115
95
 
@@ -117,74 +97,103 @@ class ChildRenderInfo:
117
97
  available_width: The height available for rendering
118
98
  available_height: The width available for rendering
119
99
  style: The parent style to apply when rendering
100
+ bbox: The bounding box for the content to render
101
+ start: Rows between top of output and top of scrollable pane
102
+ end: Rows between top of output and bottom of scrollable pane
120
103
 
121
104
  """
122
- # Check if refresh is needed
123
- refresh = False
124
- new_layout_hash = self.layout_hash
125
- if self.render_counter != (new_render_counter := get_app().render_counter) and (
126
- self._invalid
105
+ if (
106
+ self._layout_hash != (new_layout_hash := self.layout_hash)
107
+ or self._invalid
127
108
  or self.width != available_width
128
- or self._layout_hash != (new_layout_hash := self.layout_hash)
129
109
  ):
130
- self.render_counter = new_render_counter
131
- refresh = True
132
-
133
- # Refresh if needed
134
- if refresh:
135
- self._invalid = False
136
110
  self._layout_hash = new_layout_hash
137
- # log.debug("Re-rendering cell %s", self.child.index)
138
- self.width = available_width
139
- self.height = self.container.preferred_height(
111
+ self._rowcols_to_yx.clear()
112
+ self._rendered_lines.clear()
113
+ self.mouse_handlers.mouse_handlers.clear()
114
+ self.screen = Screen()
115
+
116
+ # Recalculate child height if this child has been invalidated
117
+ height = self.height = self.container.preferred_height(
140
118
  available_width, available_height
141
119
  ).preferred
120
+
121
+ else:
122
+ height = self.height
123
+
124
+ # Calculate which lines are visible and which need rendering
125
+ if start is not None:
126
+ skip_top = max(0, -start)
127
+ elif end is not None:
128
+ skip_top = max(0, -end + height)
129
+ else:
130
+ skip_top = 0
131
+ skip_bottom = max(0, height - available_height - skip_top)
132
+
133
+ visible_lines = set(range(skip_top, height - skip_bottom))
134
+ required_lines = visible_lines - self._rendered_lines
135
+
136
+ # Refresh if needed
137
+ if required_lines:
138
+ screen = self.screen
142
139
  # TODO - allow horizontal scrolling too
143
140
  # self.width = self.container.preferred_width(available_width).width
144
141
  self.width = available_width
145
142
 
146
- # Reset the local screen
147
- screen = self.screen
148
- screen.data_buffer.clear()
149
- screen.zero_width_escapes.clear()
150
- screen.cursor_positions.clear()
151
- screen.menu_positions.clear()
152
- screen.visible_windows_to_write_positions.clear()
153
- screen._draw_float_functions.clear()
154
- screen.width = screen.height = 0
143
+ self._invalid = False
155
144
 
156
145
  self.container.write_to_screen(
157
146
  screen,
158
147
  self.mouse_handlers,
159
- WritePosition(
160
- 0,
161
- 0,
162
- self.width,
163
- self.height,
148
+ BoundedWritePosition(
149
+ xpos=0,
150
+ ypos=0,
151
+ width=self.width,
152
+ height=self.height,
153
+ # Only render lines not already on the current screen
154
+ bbox=DiInt(
155
+ top=min(required_lines),
156
+ right=0,
157
+ bottom=height - max(required_lines) - 1,
158
+ left=0,
159
+ ),
164
160
  ),
165
161
  style,
166
162
  erase_bg=True,
167
163
  z_index=0,
168
164
  )
169
- self.screen.draw_all_floats()
170
-
171
- # Collect invalidation events
172
- def gather_events() -> Iterable[Event[object]]:
173
- for container in walk(self.container):
174
- if isinstance(container, Window):
175
- for event in container.content.get_invalidate_events():
176
- event += self._invalidate_handler
177
- yield event
178
-
179
- # Remove all the original event handlers
180
- for event in self._invalidate_events:
165
+ screen.draw_all_floats()
166
+
167
+ events = set()
168
+ rowcols_to_yx = self._rowcols_to_yx
169
+
170
+ for container in walk(self.container):
171
+ if isinstance(container, Window):
172
+ # Collect invalidation events
173
+ for event in container.content.get_invalidate_events():
174
+ event += self._invalidate_handler
175
+ events.add(event)
176
+
177
+ if (render_info := container.render_info) is not None:
178
+ # Update row/col to x/y mapping based on the lines we just rendereed
179
+ if container not in rowcols_to_yx:
180
+ rowcols_to_yx[container] = {}
181
+
182
+ rowcols_to_yx[container].update(render_info._rowcol_to_yx)
183
+ render_info._rowcol_to_yx = rowcols_to_yx[container]
184
+
185
+ # Update the record of lines that've been rendered to the temporary screen
186
+ self._rendered_lines |= required_lines
187
+
188
+ # Remove handler from old invalidation events
189
+ for event in events - set(self._invalidate_events):
181
190
  event -= self._invalidate_handler
182
191
  # Update the list of handlers
183
- self._invalidate_events = list(gather_events())
192
+ self._invalidate_events = events
184
193
 
185
194
  def blit(
186
195
  self,
187
- screen: Screen,
196
+ screen: PtkScreen,
188
197
  mouse_handlers: MouseHandlers,
189
198
  left: int,
190
199
  top: int,
@@ -241,11 +250,10 @@ class ChildRenderInfo:
241
250
  window_width=info.window_width,
242
251
  window_height=info.window_height,
243
252
  configured_scroll_offsets=info.configured_scroll_offsets,
244
- # visible_line_to_row_col=info.visible_line_to_row_col,
245
253
  visible_line_to_row_col=visible_line_to_row_col,
246
254
  rowcol_to_yx={
247
- (row, col): (y + top, x + left)
248
- for (row, col), (y, x) in info._rowcol_to_yx.items()
255
+ # (row, col): (y + top, x + left)
256
+ # for (row, col), (y, x) in info._rowcol_to_yx.items()
249
257
  },
250
258
  x_offset=info._x_offset + left,
251
259
  y_offset=info._y_offset + top,
@@ -272,30 +280,38 @@ class ChildRenderInfo:
272
280
  mouse_event: MouseEvent,
273
281
  ) -> NotImplementedOrNone:
274
282
  response: NotImplementedOrNone = NotImplemented
275
- new_event = MouseEvent(
276
- position=Point(
277
- x=mouse_event.position.x - left,
278
- y=mouse_event.position.y - top,
279
- ),
280
- event_type=mouse_event.event_type,
281
- button=mouse_event.button,
282
- modifiers=mouse_event.modifiers,
283
- )
284
283
 
285
- response = handler(new_event)
284
+ if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
285
+ response = self.parent.scroll(-1)
286
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
287
+ response = self.parent.scroll(1)
288
+
289
+ else:
290
+ new_event = MouseEvent(
291
+ position=Point(
292
+ x=mouse_event.position.x - left,
293
+ y=mouse_event.position.y - top,
294
+ ),
295
+ event_type=mouse_event.event_type,
296
+ button=mouse_event.button,
297
+ modifiers=mouse_event.modifiers,
298
+ )
299
+ response = handler(new_event)
300
+ # Request a re-render of the child for non-scroll events
301
+ if response is None:
302
+ self.invalidate()
286
303
 
287
304
  # Refresh the child if there was a response
288
305
  if response is None:
289
- self.invalidate()
290
306
  return response
291
307
 
292
308
  # This relies on windows returning NotImplemented when scrolled
293
309
  # to the start or end
294
- if response is NotImplemented:
295
- if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
296
- response = self.parent.scroll(-1)
297
- elif mouse_event.event_type == MouseEventType.SCROLL_UP:
298
- response = self.parent.scroll(1)
310
+ # if response is NotImplemented:
311
+ # if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
312
+ # response = self.parent.scroll(-1)
313
+ # elif mouse_event.event_type == MouseEventType.SCROLL_UP:
314
+ # response = self.parent.scroll(1)
299
315
 
300
316
  # Select the clicked child if clicked
301
317
  if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
@@ -357,6 +373,7 @@ class ChildRenderInfo:
357
373
  x=left + point.x, y=top + point.y
358
374
  )
359
375
  screen.show_cursor = True
376
+
360
377
  # Copy menu positions
361
378
  for window, point in self.screen.menu_positions.items():
362
379
  screen.menu_positions[window] = Point(x=left + point.x, y=top + point.y)
@@ -373,6 +390,7 @@ class ScrollingContainer(Container):
373
390
  height: AnyDimension = None,
374
391
  width: AnyDimension = None,
375
392
  style: str | Callable[[], str] = "",
393
+ scroll_offsets: ScrollOffsets | None = None,
376
394
  ) -> None:
377
395
  """Initiate the `ScrollingContainer`."""
378
396
  if callable(children):
@@ -401,11 +419,14 @@ class ScrollingContainer(Container):
401
419
  self.visible_indicies: set[int] = {0}
402
420
  self.index_positions: dict[int, int | None] = {}
403
421
 
404
- self.last_write_position = WritePosition(0, 0, 0, 0)
422
+ self.last_write_position: WritePosition = BoundedWritePosition(0, 0, 0, 0)
405
423
 
406
424
  self.width = to_dimension(width).preferred
407
425
  self.height = to_dimension(height).preferred
408
426
  self.style = style
427
+ self.scroll_offsets = scroll_offsets or ScrollOffsets(
428
+ top=1, bottom=1, left=1, right=1
429
+ )
409
430
 
410
431
  self.scroll_to_cursor = False
411
432
  self.scrolling = 0
@@ -413,7 +434,7 @@ class ScrollingContainer(Container):
413
434
  def pre_render_children(self, width: int, height: int) -> None:
414
435
  """Render all unrendered children in a background thread."""
415
436
  # Prevent multiple calls
416
- self.pre_rendered = 0.00001
437
+ self.pre_rendered = 0.000000001
417
438
 
418
439
  def render_in_thread() -> None:
419
440
  """Render children in thread."""
@@ -427,6 +448,7 @@ class ScrollingContainer(Container):
427
448
  get_app().invalidate()
428
449
 
429
450
  run_in_thread_with_context(render_in_thread)
451
+ # render_in_thread()
430
452
 
431
453
  def reset(self) -> None:
432
454
  """Reet the state of this container and all the children."""
@@ -627,8 +649,11 @@ class ScrollingContainer(Container):
627
649
  return NotImplemented
628
650
 
629
651
  # Very basic scrolling acceleration
630
- self.scrolling += n
631
- return None
652
+ if n:
653
+ self.scrolling += n
654
+ return None
655
+ else:
656
+ return NotImplemented
632
657
 
633
658
  def mouse_scroll_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
634
659
  """Mouse handler to scroll the pane."""
@@ -642,7 +667,7 @@ class ScrollingContainer(Container):
642
667
 
643
668
  def write_to_screen(
644
669
  self,
645
- screen: Screen,
670
+ screen: PtkScreen,
646
671
  mouse_handlers: MouseHandlers,
647
672
  write_position: WritePosition,
648
673
  parent_style: str,
@@ -706,17 +731,22 @@ class ScrollingContainer(Container):
706
731
  available_width=available_width,
707
732
  available_height=available_height,
708
733
  style=f"{parent_style} {self.style}",
734
+ start=self.selected_child_position,
709
735
  )
710
- current_window = layout.current_window
711
- cursor_position = selected_child_render_info.screen.cursor_positions.get(
712
- current_window
713
- )
714
- if cursor_position:
736
+ if (
737
+ cursor_position
738
+ := selected_child_render_info.screen.cursor_positions.get(
739
+ layout.current_window
740
+ )
741
+ ):
715
742
  cursor_row = self.selected_child_position + cursor_position.y
716
- if cursor_row < 0:
717
- self.selected_child_position -= cursor_row
718
- elif available_height <= cursor_row:
719
- self.selected_child_position -= cursor_row - available_height + 1
743
+ scroll_offsets = self.scroll_offsets
744
+ if cursor_row < scroll_offsets.top + scroll_offsets.top:
745
+ self.selected_child_position -= cursor_row - scroll_offsets.top
746
+ elif cursor_row >= available_height - (scroll_offsets.bottom + 1):
747
+ self.selected_child_position -= (
748
+ cursor_row - available_height + (scroll_offsets.bottom + 1)
749
+ )
720
750
  # Set this to false again, allowing us to scroll the cursor out of view
721
751
  self.scroll_to_cursor = False
722
752
 
@@ -756,6 +786,7 @@ class ScrollingContainer(Container):
756
786
  available_width=available_width,
757
787
  available_height=available_height,
758
788
  style=f"{parent_style} {self.style}",
789
+ start=line,
759
790
  )
760
791
  if line + child_render_info.height > 0 and line < available_height:
761
792
  self.index_positions[i] = line
@@ -779,7 +810,7 @@ class ScrollingContainer(Container):
779
810
  Window(char=" ").write_to_screen(
780
811
  screen,
781
812
  mouse_handlers,
782
- WritePosition(
813
+ BoundedWritePosition(
783
814
  xpos, ypos + line, available_width, available_height - line
784
815
  ),
785
816
  parent_style,
@@ -797,7 +828,9 @@ class ScrollingContainer(Container):
797
828
  available_width=available_width,
798
829
  available_height=available_height,
799
830
  style=f"{parent_style} {self.style}",
831
+ end=line,
800
832
  )
833
+ # TODOD - prevent lagged child height
801
834
  line -= child_render_info.height
802
835
  if line + child_render_info.height > 0 and line < available_height:
803
836
  self.index_positions[i] = line
@@ -820,7 +853,7 @@ class ScrollingContainer(Container):
820
853
  Window(char=" ").write_to_screen(
821
854
  screen,
822
855
  mouse_handlers,
823
- WritePosition(xpos, ypos, available_width, line),
856
+ BoundedWritePosition(xpos, ypos, available_width, line),
824
857
  parent_style,
825
858
  erase_bg,
826
859
  z_index,
@@ -918,6 +951,7 @@ class ScrollingContainer(Container):
918
951
  """
919
952
  if index is None:
920
953
  index = self.selected_slice.start
954
+ index = max(0, min(len(self.children) - 1, index))
921
955
  return self.children[index]
922
956
 
923
957
  def scroll_to(
@@ -937,7 +971,6 @@ class ScrollingContainer(Container):
937
971
  new_top = self.index_positions[index]
938
972
  else:
939
973
  if index < self._selected_slice.start:
940
- # new_top = 0
941
974
  new_top = max(
942
975
  0, child_render_info.height - self.last_write_position.height
943
976
  )
@@ -956,6 +989,13 @@ class ScrollingContainer(Container):
956
989
  0,
957
990
  )
958
991
 
992
+ # If the child height is not know, we'll need to render it to determine its height
993
+ if not child_render_info.height:
994
+ child_render_info.render(
995
+ self.last_write_position.width,
996
+ self.last_write_position.height,
997
+ )
998
+
959
999
  if new_top is None or new_top < 0:
960
1000
  if anchor == "top":
961
1001
  self.selected_child_position = 0
@@ -974,6 +1014,21 @@ class ScrollingContainer(Container):
974
1014
  else:
975
1015
  self.selected_child_position = new_top
976
1016
 
1017
+ heights = [
1018
+ self.get_child_render_info(index).height or 1
1019
+ for index in range(len(self._children))
1020
+ ]
1021
+ # Do not allow bottom child to scroll above screen bottom
1022
+ self.selected_child_position += max(
1023
+ 0,
1024
+ self.last_write_position.height
1025
+ - (self.selected_child_position + sum(heights[index:])),
1026
+ )
1027
+ # Do not allow top child to scroll below screen top
1028
+ self.selected_child_position -= max(
1029
+ 0, self.selected_child_position - sum(heights[:index])
1030
+ )
1031
+
977
1032
  @property
978
1033
  def known_sizes(self) -> dict[int, int]:
979
1034
  """A dictionary mapping child indices to height values."""
@@ -1026,7 +1081,7 @@ class PrintingContainer(Container):
1026
1081
 
1027
1082
  def write_to_screen(
1028
1083
  self,
1029
- screen: Screen,
1084
+ screen: PtkScreen,
1030
1085
  mouse_handlers: MouseHandlers,
1031
1086
  write_position: WritePosition,
1032
1087
  parent_style: str,
@@ -1060,7 +1115,7 @@ class PrintingContainer(Container):
1060
1115
  child.write_to_screen(
1061
1116
  screen,
1062
1117
  mouse_handlers,
1063
- WritePosition(xpos, ypos, write_position.width, height),
1118
+ BoundedWritePosition(xpos, ypos, write_position.width, height),
1064
1119
  parent_style,
1065
1120
  erase_bg,
1066
1121
  z_index,
euporie/core/log.py CHANGED
@@ -371,7 +371,7 @@ def setup_logs(config: Config | None = None) -> None:
371
371
  "disable_existing_loggers": False,
372
372
  "formatters": {
373
373
  "file_format": {
374
- "format": "{asctime} {levelname:<7} [{name}.{funcName}:{lineno}] {message}",
374
+ "format": "{asctime}.{msecs:03.0f} {levelname:<7} [{name}.{funcName}:{lineno}] {message}",
375
375
  "style": "{",
376
376
  "datefmt": "%Y-%m-%d %H:%M:%S",
377
377
  },
euporie/core/margins.py CHANGED
@@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, cast
9
9
 
10
10
  from prompt_toolkit.data_structures import Point
11
11
  from prompt_toolkit.filters import FilterOrBool, to_filter
12
- from prompt_toolkit.layout.containers import Container, Window
13
12
  from prompt_toolkit.layout.controls import FormattedTextControl
14
13
  from prompt_toolkit.layout.dimension import Dimension
15
14
  from prompt_toolkit.layout.margins import Margin
@@ -18,6 +17,7 @@ from prompt_toolkit.mouse_events import MouseEvent as PtkMouseEvent
18
17
 
19
18
  from euporie.core.current import get_app
20
19
  from euporie.core.key_binding.bindings.mouse import MouseEvent, RelativePosition
20
+ from euporie.core.layout.containers import Window
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from typing import Callable, Protocol
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
27
27
  KeyBindingsBase,
28
28
  NotImplementedOrNone,
29
29
  )
30
- from prompt_toolkit.layout.containers import WindowRenderInfo
30
+ from prompt_toolkit.layout.containers import Container, WindowRenderInfo
31
31
  from prompt_toolkit.layout.controls import UIContent
32
32
  from prompt_toolkit.layout.mouse_handlers import MouseHandlers
33
33
  from prompt_toolkit.layout.screen import Screen, WritePosition
@@ -61,6 +61,7 @@ class MarginContainer(Window):
61
61
  self.target = target
62
62
  self.render_info: WindowRenderInfo | None = None
63
63
  self.content = FormattedTextControl(self.create_fragments)
64
+ self.always_hide_cursor = to_filter(True)
64
65
 
65
66
  def create_fragments(self) -> StyleAndTextTuples:
66
67
  """Generate text fragments to display."""
@@ -430,8 +431,9 @@ class ScrollbarMargin(ClickableMargin):
430
431
  func = window._scroll_down if delta < 0 else window._scroll_up
431
432
  for _ in range(abs(delta)):
432
433
  func()
434
+ # Hack to speed up scrolling on the :py:`ScrollingContainer`
433
435
  elif hasattr(window, "scrolling"):
434
- render_info.window.scrolling = delta
436
+ setattr(render_info.window, "scrolling", delta) # noqa: B010
435
437
 
436
438
  # Mouse down events
437
439
  elif mouse_event.event_type == MouseEventType.MOUSE_DOWN:
@@ -522,8 +524,11 @@ class NumberedDiffMargin(Margin):
522
524
  pass
523
525
  linestr = str(lineno + 1).rjust(width - 2)
524
526
  style = self.style
525
- if lineno == current_lineno and get_app().layout.has_focus(
526
- window_render_info.window
527
+ if (
528
+ lineno == current_lineno
529
+ and get_app().layout.has_focus(window_render_info.window)
530
+ # Only highlight line number if there are multiple lines
531
+ and len(window_render_info.displayed_lines) > 1
527
532
  ):
528
533
  style = f"{style} class:line-number.current"
529
534
  result.extend(
@@ -535,6 +540,7 @@ class NumberedDiffMargin(Margin):
535
540
  )
536
541
  last_lineno = lineno
537
542
  result.append(("", "\n"))
543
+
538
544
  return result
539
545
 
540
546