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
@@ -1,5 +1,11 @@
1
1
  """Define collections of generic key-bindings which do not belong to widgets."""
2
2
 
3
- from euporie.core.key_binding.bindings import basic, completion, micro, mouse
3
+ from euporie.core.key_binding.bindings import (
4
+ basic,
5
+ completion,
6
+ micro,
7
+ mouse,
8
+ page_navigation,
9
+ )
4
10
 
5
- __all__ = ["basic", "completion", "micro", "mouse"]
11
+ __all__ = ["basic", "completion", "micro", "mouse", "page_navigation"]
@@ -32,15 +32,14 @@ log = logging.getLogger(__name__)
32
32
  class TextEntry:
33
33
  """Basic key-bindings for text entry."""
34
34
 
35
-
36
- # Register default bindings for micro edit mode
37
- register_bindings(
38
- {
39
- "euporie.core.key_binding.bindings.basic.TextEntry": {
40
- "type-key": "<any>",
41
- },
42
- }
43
- )
35
+ # Register default bindings for micro edit mode
36
+ register_bindings(
37
+ {
38
+ "euporie.core.key_binding.bindings.basic.TextEntry": {
39
+ "type-key": "<any>",
40
+ },
41
+ }
42
+ )
44
43
 
45
44
 
46
45
  def load_basic_bindings(config: Config | None = None) -> KeyBindingsBase:
@@ -56,11 +55,7 @@ def load_basic_bindings(config: Config | None = None) -> KeyBindingsBase:
56
55
  # Commands
57
56
 
58
57
 
59
- @add_cmd(
60
- filter=buffer_has_focus,
61
- save_before=if_no_repeat,
62
- hidden=True,
63
- )
58
+ @add_cmd(filter=buffer_has_focus, save_before=if_no_repeat, hidden=True)
64
59
  def type_key(event: KeyPressEvent) -> None:
65
60
  """Enter a key."""
66
61
  # Do not insert escape sequences
@@ -36,8 +36,6 @@ from prompt_toolkit.key_binding.bindings.scroll import (
36
36
  scroll_half_page_up,
37
37
  scroll_one_line_down,
38
38
  scroll_one_line_up,
39
- scroll_page_down,
40
- scroll_page_up,
41
39
  )
42
40
  from prompt_toolkit.keys import Keys
43
41
  from prompt_toolkit.selection import SelectionState, SelectionType
@@ -160,8 +158,6 @@ register_bindings(
160
158
  "duplicate-selection": "c-d",
161
159
  "paste-clipboard": "c-v",
162
160
  "select-all": "c-a",
163
- "scroll-page-up": "pageup",
164
- "scroll-page-down": "pagedown",
165
161
  "delete": "delete",
166
162
  "toggle-case": "f4",
167
163
  "toggle-overwrite-mode": "insert",
@@ -310,14 +306,6 @@ add_cmd(
310
306
  title="Scroll up one line",
311
307
  filter=buffer_has_focus,
312
308
  )(scroll_one_line_up)
313
- add_cmd(
314
- title="Scroll one one page",
315
- filter=buffer_has_focus,
316
- )(scroll_page_down)
317
- add_cmd(
318
- title="Scroll up one page",
319
- filter=buffer_has_focus,
320
- )(scroll_page_up)
321
309
 
322
310
 
323
311
  @add_cmd(
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from typing import TYPE_CHECKING, NamedTuple
7
7
 
8
+ from prompt_toolkit.cache import FastDictCache
8
9
  from prompt_toolkit.data_structures import Point
9
10
  from prompt_toolkit.key_binding.bindings.mouse import (
10
11
  MOUSE_MOVE,
@@ -59,99 +60,131 @@ class MouseEvent(PtkMouseEvent):
59
60
  self.cell_position = cell_position or RelativePosition(0.5, 0.5)
60
61
 
61
62
 
63
+ def _parse_mouse_data(
64
+ data: str, sgr_pixels: bool, cell_size_xy: tuple[int, int]
65
+ ) -> MouseEvent | None:
66
+ """Convert key-press data to a mouse event."""
67
+ rx = ry = 0.5
68
+
69
+ # Parse incoming packet.
70
+ if data[2] == "M":
71
+ # Typical.
72
+ mouse_event, x, y = map(ord, data[3:])
73
+
74
+ # TODO: Is it possible to add modifiers here?
75
+ mouse_button, mouse_event_type, mouse_modifiers = typical_mouse_events[
76
+ mouse_event
77
+ ]
78
+
79
+ # Handle situations where `PosixStdinReader` used surrogateescapes.
80
+ if x >= 0xDC00:
81
+ x -= 0xDC00
82
+ if y >= 0xDC00:
83
+ y -= 0xDC00
84
+
85
+ x -= 32
86
+ y -= 32
87
+ else:
88
+ # Urxvt and Xterm SGR.
89
+ # When the '<' is not present, we are not using the Xterm SGR mode,
90
+ # but Urxvt instead.
91
+ data = data[2:]
92
+ if data[:1] == "<":
93
+ sgr = True
94
+ data = data[1:]
95
+ else:
96
+ sgr = False
97
+
98
+ # Extract coordinates.
99
+ mouse_event, x, y = map(int, data[:-1].split(";"))
100
+ m = data[-1]
101
+
102
+ # Parse event type.
103
+ if sgr:
104
+ if sgr_pixels:
105
+ # Calculate cell position
106
+ cell_x, cell_y = cell_size_xy
107
+ px, py = x, y
108
+ fx, fy = px / cell_x + 1, py / cell_y + 1
109
+ x, y = int(fx), int(fy)
110
+ rx, ry = fx - x, fy - y
111
+
112
+ try:
113
+ (
114
+ mouse_button,
115
+ mouse_event_type,
116
+ mouse_modifiers,
117
+ ) = xterm_sgr_mouse_events[mouse_event, m]
118
+ except KeyError:
119
+ return None
120
+
121
+ else:
122
+ # Some other terminals, like urxvt, Hyper terminal, ...
123
+ (
124
+ mouse_button,
125
+ mouse_event_type,
126
+ mouse_modifiers,
127
+ ) = urxvt_mouse_events.get(
128
+ mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER)
129
+ )
130
+
131
+ x -= 1
132
+ y -= 1
133
+
134
+ return MouseEvent(
135
+ position=Point(x=x, y=y),
136
+ event_type=mouse_event_type,
137
+ button=mouse_button,
138
+ modifiers=mouse_modifiers,
139
+ cell_position=RelativePosition(rx, ry),
140
+ )
141
+
142
+
143
+ _MOUSE_EVENT_CACHE: FastDictCache[
144
+ tuple[str, bool, tuple[int, int]], MouseEvent | None
145
+ ] = FastDictCache(get_value=_parse_mouse_data)
146
+
147
+
62
148
  def load_mouse_bindings() -> KeyBindings:
63
149
  """Additional key-bindings to deal with SGR-pixel mouse positioning."""
64
150
  key_bindings = load_ptk_mouse_bindings()
65
151
 
66
- @key_bindings.add(Keys.Vt100MouseEvent)
152
+ @key_bindings.add(Keys.Vt100MouseEvent, eager=True)
67
153
  def _(event: KeyPressEvent) -> NotImplementedOrNone:
68
154
  """Handle incoming mouse event, include SGR-pixel mode."""
69
- # Ensure mypy knows this would only run in a euporie app
70
- assert isinstance(event.app, BaseApp)
71
-
72
- rx = ry = 0.5
155
+ # Ensure mypy knows this would only run in a euporie appo
156
+ assert isinstance(app := event.app, BaseApp)
73
157
 
74
- # Parse incoming packet.
75
- if event.data[2] == "M":
76
- # Typical.
77
- mouse_event, x, y = map(ord, event.data[3:])
158
+ if not event.app.renderer.height_is_known:
159
+ return NotImplemented
78
160
 
79
- # TODO: Is it possible to add modifiers here?
80
- mouse_button, mouse_event_type, mouse_modifiers = typical_mouse_events[
81
- mouse_event
82
- ]
161
+ mouse_event = _MOUSE_EVENT_CACHE[
162
+ event.data, app.term_info.sgr_pixel_status.value, app.term_info.cell_size_px
163
+ ]
83
164
 
84
- # Handle situations where `PosixStdinReader` used surrogateescapes.
85
- if x >= 0xDC00:
86
- x -= 0xDC00
87
- if y >= 0xDC00:
88
- y -= 0xDC00
89
-
90
- x -= 32
91
- y -= 32
92
- else:
93
- # Urxvt and Xterm SGR.
94
- # When the '<' is not present, we are not using the Xterm SGR mode,
95
- # but Urxvt instead.
96
- data = event.data[2:]
97
- if data[:1] == "<":
98
- sgr = True
99
- data = data[1:]
100
- else:
101
- sgr = False
102
-
103
- # Extract coordinates.
104
- mouse_event, x, y = map(int, data[:-1].split(";"))
105
- m = data[-1]
106
-
107
- # Parse event type.
108
- if sgr:
109
- if event.app.term_info.sgr_pixel_status.value:
110
- # Calculate cell position
111
- cell_px, cell_py = event.app.term_info.cell_size_px
112
- px, py = x, y
113
- fx, fy = px / cell_px + 1, py / cell_py + 1
114
- x, y = int(fx), int(fy)
115
- rx, ry = fx - x, fy - y
116
-
117
- try:
118
- (
119
- mouse_button,
120
- mouse_event_type,
121
- mouse_modifiers,
122
- ) = xterm_sgr_mouse_events[mouse_event, m]
123
- except KeyError:
124
- return NotImplemented
125
-
126
- else:
127
- # Some other terminals, like urxvt, Hyper terminal, ...
128
- (
129
- mouse_button,
130
- mouse_event_type,
131
- mouse_modifiers,
132
- ) = urxvt_mouse_events.get(
133
- mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER)
134
- )
135
-
136
- x -= 1
137
- y -= 1
165
+ if mouse_event is None:
166
+ return NotImplemented
138
167
 
139
168
  # Only handle mouse events when we know the window height.
140
- if event.app.renderer.height_is_known and mouse_event_type is not None:
169
+ if mouse_event.event_type is not None:
141
170
  # Take region above the layout into account. The reported
142
171
  # coordinates are absolute to the visible part of the terminal.
143
172
  from prompt_toolkit.renderer import HeightIsUnknownError
144
173
 
174
+ x, y = mouse_event.position
175
+
145
176
  try:
146
- y -= event.app.renderer.rows_above_layout
177
+ rows_above = app.renderer.rows_above_layout
147
178
  except HeightIsUnknownError:
148
179
  return NotImplemented
180
+ else:
181
+ y -= rows_above
149
182
 
150
183
  # Save global mouse position
151
- event.app.mouse_position = Point(x=x, y=y)
184
+ app.mouse_position = mouse_event.position
152
185
 
153
186
  # Apply limits to mouse position if enabled
154
- if (mouse_limits := event.app.mouse_limits) is not None:
187
+ if (mouse_limits := app.mouse_limits) is not None:
155
188
  x = max(
156
189
  mouse_limits.xpos,
157
190
  min(x, mouse_limits.xpos + (mouse_limits.width - 1)),
@@ -161,6 +194,8 @@ def load_mouse_bindings() -> KeyBindings:
161
194
  min(y, mouse_limits.ypos + (mouse_limits.height - 1)),
162
195
  )
163
196
 
197
+ mouse_event.position = Point(x=x, y=y)
198
+
164
199
  # Call the mouse handler from the renderer.
165
200
  # Note: This can return `NotImplemented` if no mouse handler was
166
201
  # found for this position, or if no repainting needs to
@@ -168,15 +203,7 @@ def load_mouse_bindings() -> KeyBindings:
168
203
  # movements.
169
204
  handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
170
205
 
171
- return handler(
172
- MouseEvent(
173
- position=Point(x=x, y=y),
174
- event_type=mouse_event_type,
175
- button=mouse_button,
176
- modifiers=mouse_modifiers,
177
- cell_position=RelativePosition(rx, ry),
178
- )
179
- )
206
+ return handler(mouse_event)
180
207
 
181
208
  return NotImplemented
182
209
 
@@ -0,0 +1,129 @@
1
+ """Define page navigation key-bindings for buffers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from prompt_toolkit.application.current import get_app
9
+ from prompt_toolkit.filters import buffer_has_focus
10
+ from prompt_toolkit.key_binding.bindings.page_navigation import (
11
+ load_emacs_page_navigation_bindings,
12
+ load_vi_page_navigation_bindings,
13
+ )
14
+ from prompt_toolkit.key_binding.key_bindings import (
15
+ ConditionalKeyBindings,
16
+ merge_key_bindings,
17
+ )
18
+
19
+ from euporie.core.commands import add_cmd
20
+ from euporie.core.filters import micro_mode
21
+ from euporie.core.key_binding.registry import (
22
+ load_registered_bindings,
23
+ register_bindings,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from prompt_toolkit.key_binding import KeyBindingsBase, KeyPressEvent
28
+
29
+ from euporie.core.config import Config
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+
34
+ class PageNavigation:
35
+ """Key-bindings for page navigation."""
36
+
37
+ # Register default bindings for micro edit mode
38
+ register_bindings(
39
+ {
40
+ "euporie.core.key_binding.bindings.page_navigation.PageNavigation": {
41
+ "scroll-page-up": "pageup",
42
+ "scroll-page-down": "pagedown",
43
+ },
44
+ }
45
+ )
46
+
47
+
48
+ def load_page_navigation_bindings(config: Config | None = None) -> KeyBindingsBase:
49
+ """Load page navigation key-bindings for text entry."""
50
+ return ConditionalKeyBindings(
51
+ merge_key_bindings(
52
+ [
53
+ load_emacs_page_navigation_bindings(),
54
+ load_vi_page_navigation_bindings(),
55
+ ConditionalKeyBindings(
56
+ load_registered_bindings(
57
+ "euporie.core.key_binding.bindings.page_navigation.PageNavigation",
58
+ config=config,
59
+ ),
60
+ micro_mode,
61
+ ),
62
+ ]
63
+ ),
64
+ buffer_has_focus,
65
+ )
66
+
67
+
68
+ # Commands
69
+
70
+
71
+ @add_cmd(filter=buffer_has_focus, hidden=True)
72
+ def scroll_page_down(event: KeyPressEvent) -> None:
73
+ """Scroll page down (prefer the cursor at the top of the page, after scrolling)."""
74
+ w = event.app.layout.current_window
75
+ b = event.app.current_buffer
76
+
77
+ if w and w.render_info:
78
+ # Scroll down one page.
79
+ line_index = b.document.cursor_position_row
80
+ page = w.render_info.window_height
81
+
82
+ if (
83
+ (screen := get_app().renderer._last_screen)
84
+ and (wp := screen.visible_windows_to_write_positions.get(w))
85
+ and (bbox := getattr(wp, "bbox", None))
86
+ ):
87
+ page -= bbox.top + bbox.bottom
88
+
89
+ line_index = max(line_index + page, w.vertical_scroll + 1)
90
+ w.vertical_scroll = line_index
91
+
92
+ b.cursor_position = b.document.translate_row_col_to_index(line_index, 0)
93
+ b.cursor_position += b.document.get_start_of_line_position(
94
+ after_whitespace=True
95
+ )
96
+
97
+
98
+ @add_cmd(filter=buffer_has_focus, hidden=True)
99
+ def scroll_page_up(event: KeyPressEvent) -> None:
100
+ """Scroll page up (prefer the cursor at the bottom of the page, after scrolling)."""
101
+ w = event.app.layout.current_window
102
+ b = event.app.current_buffer
103
+
104
+ if w and w.render_info:
105
+ line_index = b.document.cursor_position_row
106
+ page = w.render_info.window_height
107
+
108
+ if (
109
+ (screen := get_app().renderer._last_screen)
110
+ and (wp := screen.visible_windows_to_write_positions.get(w))
111
+ and (bbox := getattr(wp, "bbox", None))
112
+ ):
113
+ page -= bbox.top + bbox.bottom
114
+
115
+ # Put cursor at the first visible line. (But make sure that the cursor
116
+ # moves at least one line up.)
117
+ line_index = max(
118
+ 0,
119
+ min(line_index - page, b.document.cursor_position_row - 1),
120
+ )
121
+
122
+ b.cursor_position = b.document.translate_row_col_to_index(line_index, 0)
123
+ b.cursor_position += b.document.get_start_of_line_position(
124
+ after_whitespace=True
125
+ )
126
+
127
+ # Set the scroll offset. We can safely set it to zero; the Window will
128
+ # make sure that it scrolls at least until the cursor becomes visible.
129
+ w.vertical_scroll = 0
@@ -67,6 +67,7 @@ class KeyProcessor(PtKeyProcessor):
67
67
  def process_keys(self) -> None:
68
68
  """Process all the keys in the input queue."""
69
69
  app = get_app()
70
+ input_queue = self.input_queue
70
71
 
71
72
  def not_empty() -> bool:
72
73
  # When the application result is set, stop processing keys. (E.g.
@@ -86,7 +87,14 @@ class KeyProcessor(PtKeyProcessor):
86
87
  self.input_queue.remove(cpr)
87
88
  return cpr
88
89
  else:
89
- return self.input_queue.popleft()
90
+ return input_queue.popleft()
91
+
92
+ # Throttle repeated mouse events - limit to 10 per flush
93
+ if len(input_queue) >= 10 and not any(
94
+ input_queue[i].key != Keys.Vt100MouseEvent for i in range(10)
95
+ ):
96
+ for _ in range(len(input_queue) - 10):
97
+ input_queue.popleft()
90
98
 
91
99
  is_flush = False
92
100
 
@@ -0,0 +1 @@
1
+ """Contains containers and controls relating to layout."""