euporie 2.8.1__py3-none-any.whl → 2.8.5__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 (129) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +267 -147
  5. euporie/core/__init__.py +1 -9
  6. euporie/core/__main__.py +31 -5
  7. euporie/core/_settings.py +104 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +70 -0
  10. euporie/core/app/_settings.py +427 -0
  11. euporie/core/{app.py → app/app.py} +214 -572
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +182 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +154 -57
  21. euporie/core/{widgets → bars}/status.py +9 -26
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +21 -12
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +11 -5
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +131 -60
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +46 -30
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +11 -8
  39. euporie/core/convert/utils.py +50 -23
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +72 -82
  42. euporie/core/format.py +13 -2
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +36 -36
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +216 -124
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +100 -139
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +2 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +5 -7
  58. euporie/core/key_binding/bindings/mouse.py +26 -24
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/bindings/vi.py +46 -0
  61. euporie/core/key_binding/key_processor.py +43 -2
  62. euporie/core/key_binding/registry.py +2 -0
  63. euporie/core/key_binding/utils.py +22 -2
  64. euporie/core/keys.py +7156 -93
  65. euporie/core/layout/cache.py +35 -25
  66. euporie/core/layout/containers.py +280 -74
  67. euporie/core/layout/decor.py +5 -5
  68. euporie/core/layout/mouse.py +1 -1
  69. euporie/core/layout/print.py +16 -3
  70. euporie/core/layout/scroll.py +26 -28
  71. euporie/core/log.py +75 -60
  72. euporie/core/lsp.py +118 -24
  73. euporie/core/margins.py +60 -31
  74. euporie/core/path.py +2 -1
  75. euporie/core/renderer.py +58 -17
  76. euporie/core/style.py +60 -40
  77. euporie/core/suggest.py +103 -85
  78. euporie/core/tabs/__init__.py +34 -0
  79. euporie/core/tabs/_settings.py +113 -0
  80. euporie/core/tabs/base.py +11 -435
  81. euporie/core/tabs/kernel.py +420 -0
  82. euporie/core/tabs/notebook.py +20 -54
  83. euporie/core/utils.py +98 -6
  84. euporie/core/validation.py +1 -1
  85. euporie/core/widgets/_settings.py +188 -0
  86. euporie/core/widgets/cell.py +90 -158
  87. euporie/core/widgets/cell_outputs.py +25 -36
  88. euporie/core/widgets/decor.py +11 -41
  89. euporie/core/widgets/dialog.py +55 -44
  90. euporie/core/widgets/display.py +27 -24
  91. euporie/core/widgets/file_browser.py +5 -26
  92. euporie/core/widgets/forms.py +16 -12
  93. euporie/core/widgets/inputs.py +37 -81
  94. euporie/core/widgets/layout.py +7 -6
  95. euporie/core/widgets/logo.py +49 -0
  96. euporie/core/widgets/menu.py +13 -11
  97. euporie/core/widgets/pager.py +8 -11
  98. euporie/core/widgets/palette.py +6 -6
  99. euporie/hub/app.py +52 -31
  100. euporie/notebook/_commands.py +24 -0
  101. euporie/notebook/_settings.py +107 -0
  102. euporie/notebook/app.py +109 -210
  103. euporie/notebook/filters.py +1 -1
  104. euporie/notebook/tabs/__init__.py +46 -7
  105. euporie/notebook/tabs/_commands.py +714 -0
  106. euporie/notebook/tabs/_settings.py +32 -0
  107. euporie/notebook/tabs/display.py +2 -2
  108. euporie/notebook/tabs/edit.py +12 -7
  109. euporie/notebook/tabs/json.py +3 -3
  110. euporie/notebook/tabs/log.py +1 -18
  111. euporie/notebook/tabs/notebook.py +21 -674
  112. euporie/notebook/widgets/_commands.py +11 -0
  113. euporie/notebook/widgets/_settings.py +19 -0
  114. euporie/notebook/widgets/side_bar.py +14 -34
  115. euporie/preview/_settings.py +104 -0
  116. euporie/preview/app.py +8 -30
  117. euporie/preview/tabs/notebook.py +15 -86
  118. euporie/web/tabs/web.py +4 -6
  119. euporie/web/widgets/webview.py +5 -12
  120. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
  121. euporie-2.8.5.dist-info/RECORD +172 -0
  122. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
  125. euporie/core/launch.py +0 -59
  126. euporie/core/terminal.py +0 -527
  127. euporie-2.8.1.dist-info/RECORD +0 -146
  128. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -3,16 +3,18 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from functools import partial
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  from prompt_toolkit.filters import (
9
10
  buffer_has_focus,
10
11
  )
11
- from prompt_toolkit.key_binding import ConditionalKeyBindings
12
12
 
13
13
  from euporie.core.commands import add_cmd
14
14
  from euporie.core.filters import (
15
- micro_mode,
15
+ char_after_cursor,
16
+ has_matching_bracket,
17
+ insert_mode,
16
18
  replace_mode,
17
19
  )
18
20
  from euporie.core.key_binding.registry import (
@@ -37,6 +39,12 @@ class TextEntry:
37
39
  {
38
40
  "euporie.core.key_binding.bindings.basic.TextEntry": {
39
41
  "type-key": "<any>",
42
+ "complete-bracket-()": "(",
43
+ "complete-bracket-[]": "[",
44
+ "complete-bracket-{}": "{",
45
+ "close-bracket-()": ")",
46
+ "close-bracket-[]": "]",
47
+ "close-bracket-{}": "}",
40
48
  },
41
49
  }
42
50
  )
@@ -44,16 +52,18 @@ class TextEntry:
44
52
 
45
53
  def load_basic_bindings(config: Config | None = None) -> KeyBindingsBase:
46
54
  """Load basic key-bindings for text entry."""
47
- return ConditionalKeyBindings(
48
- load_registered_bindings(
49
- "euporie.core.key_binding.bindings.basic.TextEntry", config=config
50
- ),
51
- micro_mode,
55
+ # Load additional key definitions
56
+ from euporie.core import keys # noqa: F401
57
+
58
+ return load_registered_bindings(
59
+ "euporie.core.key_binding.bindings.basic.TextEntry", config=config
52
60
  )
53
61
 
54
62
 
55
63
  # Commands
56
64
 
65
+ ## Typing keys
66
+
57
67
 
58
68
  @add_cmd(filter=buffer_has_focus, save_before=if_no_repeat, hidden=True)
59
69
  def type_key(event: KeyPressEvent) -> None:
@@ -63,3 +73,33 @@ def type_key(event: KeyPressEvent) -> None:
63
73
  event.current_buffer.insert_text(
64
74
  event.data * event.arg, overwrite=replace_mode()
65
75
  )
76
+
77
+
78
+ ## Add automatic bracket completion
79
+
80
+
81
+ def _complete_bracket(right: str, event: KeyPressEvent) -> None:
82
+ event.current_buffer.insert_text(right, move_cursor=False)
83
+ event.key_processor.feed(event.key_sequence[0], first=True)
84
+
85
+
86
+ def _close_bracket(right: str, event: KeyPressEvent) -> None:
87
+ event.current_buffer.cursor_position += 1
88
+
89
+
90
+ for left, right in [("(", ")"), ("[", "]"), ("{", "}")]:
91
+ add_cmd(
92
+ name=f"complete-bracket-{left}{right}",
93
+ filter=buffer_has_focus & insert_mode & ~char_after_cursor(right),
94
+ save_before=if_no_repeat,
95
+ hidden=True,
96
+ )(partial(_complete_bracket, right))
97
+ add_cmd(
98
+ name=f"close-bracket-{left}{right}",
99
+ filter=buffer_has_focus
100
+ & insert_mode
101
+ & char_after_cursor(right)
102
+ & has_matching_bracket,
103
+ save_before=if_no_repeat,
104
+ hidden=True,
105
+ )(partial(_close_bracket, right))
@@ -18,18 +18,14 @@ from prompt_toolkit.key_binding.bindings.named_commands import (
18
18
  )
19
19
 
20
20
  from euporie.core.commands import add_cmd
21
- from euporie.core.filters import buffer_is_code, cursor_in_leading_ws, insert_mode
21
+ from euporie.core.filters import cursor_in_leading_ws, insert_mode
22
22
  from euporie.core.key_binding.registry import register_bindings
23
23
 
24
24
  log = logging.getLogger(__name__)
25
25
 
26
26
 
27
27
  add_cmd(
28
- filter=buffer_has_focus
29
- & insert_mode
30
- & ~has_selection
31
- & buffer_is_code
32
- & ~cursor_in_leading_ws,
28
+ filter=buffer_has_focus & insert_mode & ~has_selection & ~cursor_in_leading_ws,
33
29
  hidden=True,
34
30
  name="next-completion",
35
31
  description="Show the completion menu and select the next completion.",
@@ -65,12 +61,11 @@ def accept_completion() -> None:
65
61
  complete_state = buffer.complete_state
66
62
  if complete_state and isinstance(complete_state.current_completion, Completion):
67
63
  buffer.apply_completion(complete_state.current_completion)
68
- get_app().layout.focus(buffer)
69
64
 
70
65
 
71
66
  register_bindings(
72
67
  {
73
- "euporie.core.app.BaseApp": {
68
+ "euporie.core.app.app:BaseApp": {
74
69
  "next-completion": "c-i",
75
70
  "previous-completion": "s-tab",
76
71
  "cancel-completion": "escape",
@@ -7,10 +7,8 @@ import re
7
7
  from functools import partial
8
8
  from typing import TYPE_CHECKING
9
9
 
10
- from aenum import extend_enum
11
10
  from prompt_toolkit.buffer import indent, unindent
12
11
  from prompt_toolkit.document import Document
13
- from prompt_toolkit.enums import EditingMode
14
12
  from prompt_toolkit.filters import (
15
13
  buffer_has_focus,
16
14
  has_selection,
@@ -40,8 +38,8 @@ from prompt_toolkit.key_binding.bindings.scroll import (
40
38
  from prompt_toolkit.keys import Keys
41
39
  from prompt_toolkit.selection import SelectionState, SelectionType
42
40
 
41
+ from euporie.core.app.current import get_app
43
42
  from euporie.core.commands import add_cmd, get_cmd
44
- from euporie.core.current import get_app
45
43
  from euporie.core.filters import (
46
44
  buffer_is_code,
47
45
  buffer_is_markdown,
@@ -74,9 +72,6 @@ class EditMode:
74
72
  """Micro style editor key-bindings."""
75
73
 
76
74
 
77
- # Register micro edit mode
78
- extend_enum(EditingMode, "MICRO", "MICRO")
79
-
80
75
  # Register default bindings for micro edit mode
81
76
  register_bindings(
82
77
  {
@@ -680,6 +675,9 @@ def dent_buffer(event: KeyPressEvent, indenting: bool = True) -> None:
680
675
  )
681
676
  * sign
682
677
  )
678
+ selection_state.original_cursor_position = max(
679
+ min(selection_state.original_cursor_position, len(buffer.text)), 0
680
+ )
683
681
  # Maintain the selection state before indentation
684
682
  buffer.selection_state = selection_state
685
683
 
@@ -722,7 +720,7 @@ def indent_lines(event: KeyPressEvent) -> None:
722
720
  @add_cmd(
723
721
  filter=buffer_has_focus
724
722
  & (cursor_in_leading_ws | has_selection)
725
- & ~cursor_at_start_of_line,
723
+ & (~cursor_at_start_of_line | cursor_at_start_of_line),
726
724
  )
727
725
  def unindent_lines(event: KeyPressEvent) -> None:
728
726
  """Unindent the current or selected lines."""
@@ -22,8 +22,9 @@ from prompt_toolkit.key_binding.bindings.mouse import (
22
22
  from prompt_toolkit.keys import Keys
23
23
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType, MouseModifier
24
24
  from prompt_toolkit.mouse_events import MouseEvent as PtkMouseEvent
25
+ from prompt_toolkit.renderer import HeightIsUnknownError
25
26
 
26
- from euporie.core.app import BaseApp
27
+ from euporie.core.app.app import BaseApp
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
@@ -102,29 +103,23 @@ def _parse_mouse_data(
102
103
  # Parse event type.
103
104
  if sgr:
104
105
  if sgr_pixels:
105
- # Calculate cell position
106
+ # Scale down pixel-wise mouse position to cell based, and calculate
107
+ # relative position of mouse within the cell
106
108
  cell_x, cell_y = cell_size_xy
107
109
  px, py = x, y
108
110
  fx, fy = px / cell_x + 1, py / cell_y + 1
109
111
  x, y = int(fx), int(fy)
110
112
  rx, ry = fx - x, fy - y
111
-
112
113
  try:
113
- (
114
- mouse_button,
115
- mouse_event_type,
116
- mouse_modifiers,
117
- ) = xterm_sgr_mouse_events[mouse_event, m]
114
+ (mouse_button, mouse_event_type, mouse_modifiers) = (
115
+ xterm_sgr_mouse_events[mouse_event, m]
116
+ )
118
117
  except KeyError:
119
118
  return None
120
119
 
121
120
  else:
122
121
  # Some other terminals, like urxvt, Hyper terminal, ...
123
- (
124
- mouse_button,
125
- mouse_event_type,
126
- mouse_modifiers,
127
- ) = urxvt_mouse_events.get(
122
+ (mouse_button, mouse_event_type, mouse_modifiers) = urxvt_mouse_events.get(
128
123
  mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER)
129
124
  )
130
125
 
@@ -153,13 +148,14 @@ def load_mouse_bindings() -> KeyBindings:
153
148
  def _(event: KeyPressEvent) -> NotImplementedOrNone:
154
149
  """Handle incoming mouse event, include SGR-pixel mode."""
155
150
  # Ensure mypy knows this would only run in a euporie appo
156
- assert isinstance(app := event.app, BaseApp)
151
+ app = event.app
152
+ assert isinstance(app, BaseApp)
157
153
 
158
- if not event.app.renderer.height_is_known:
154
+ if not app.renderer.height_is_known:
159
155
  return NotImplemented
160
156
 
161
157
  mouse_event = _MOUSE_EVENT_CACHE[
162
- event.data, app.term_info.sgr_pixel_status.value, app.term_info.cell_size_px
158
+ event.data, app.term_sgr_pixel, app.cell_size_px
163
159
  ]
164
160
 
165
161
  if mouse_event is None:
@@ -169,10 +165,9 @@ def load_mouse_bindings() -> KeyBindings:
169
165
  if mouse_event.event_type is not None:
170
166
  # Take region above the layout into account. The reported
171
167
  # coordinates are absolute to the visible part of the terminal.
172
- from prompt_toolkit.renderer import HeightIsUnknownError
173
-
174
168
  x, y = mouse_event.position
175
169
 
170
+ # Adjust position to take into account space above non-full screen apps
176
171
  try:
177
172
  rows_above = app.renderer.rows_above_layout
178
173
  except HeightIsUnknownError:
@@ -180,28 +175,35 @@ def load_mouse_bindings() -> KeyBindings:
180
175
  else:
181
176
  y -= rows_above
182
177
 
183
- # Save global mouse position
184
- app.mouse_position = mouse_event.position
178
+ # Save mouse position within the app
179
+ app.mouse_position = Point(x=x, y=y)
185
180
 
186
181
  # Apply limits to mouse position if enabled
187
182
  if (mouse_limits := app.mouse_limits) is not None:
188
183
  x = max(
189
184
  mouse_limits.xpos,
190
- min(x, mouse_limits.xpos + (mouse_limits.width - 1)),
185
+ min(x, mouse_limits.xpos + (mouse_limits.width) - 1),
191
186
  )
192
187
  y = max(
193
188
  mouse_limits.ypos,
194
- min(y, mouse_limits.ypos + (mouse_limits.height - 1)),
189
+ min(y, mouse_limits.ypos + (mouse_limits.height) - 1),
195
190
  )
196
191
 
197
- mouse_event.position = Point(x=x, y=y)
192
+ # Do not modify the mouse event in the cache, instead create a new instance
193
+ mouse_event = MouseEvent(
194
+ position=Point(x=x, y=y),
195
+ event_type=mouse_event.event_type,
196
+ button=mouse_event.button,
197
+ modifiers=mouse_event.modifiers,
198
+ cell_position=mouse_event.cell_position,
199
+ )
198
200
 
199
201
  # Call the mouse handler from the renderer.
200
202
  # Note: This can return `NotImplemented` if no mouse handler was
201
203
  # found for this position, or if no repainting needs to
202
204
  # happen. this way, we avoid excessive repaints during mouse
203
205
  # movements.
204
- handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
206
+ handler = app.renderer.mouse_handlers.mouse_handlers[y][x]
205
207
 
206
208
  return handler(mouse_event)
207
209
 
@@ -0,0 +1,193 @@
1
+ """Contains key handlers for terminal queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from euporie.core.commands import add_cmd
9
+ from euporie.core.key_binding.registry import (
10
+ load_registered_bindings,
11
+ register_bindings,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from prompt_toolkit.key_binding import KeyBindingsBase, KeyPressEvent
16
+
17
+ from euporie.core.config import Config
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+ _COLOR_NAMES: dict[str, str] = {
22
+ "10": "fg",
23
+ "11": "bg",
24
+ "4;0": "ansiblack",
25
+ "4;1": "ansired",
26
+ "4;2": "ansigreen",
27
+ "4;3": "ansiyellow",
28
+ "4;4": "ansiblue",
29
+ "4;5": "ansipurple",
30
+ "4;6": "ansicyan",
31
+ "4;7": "ansiwhite",
32
+ "4;8": "ansirbightblack",
33
+ "4;9": "ansirbightred",
34
+ "4;10": "ansirbightgreen",
35
+ "4;11": "ansirbightyellow",
36
+ "4;12": "ansirbightblue",
37
+ "4;13": "ansirbightpurple",
38
+ "4;14": "ansirbightcyan",
39
+ "4;15": "ansirbightwhite",
40
+ }
41
+
42
+
43
+ def get_match(event: KeyPressEvent) -> dict[str, str] | None:
44
+ """Get pattern matches from a key press event."""
45
+ if (
46
+ (parser := getattr(event.app.input, "vt100_parser", None))
47
+ and (patterns := getattr(parser, "patterns", None))
48
+ and (pattern := patterns.get(event.key_sequence[-1].key))
49
+ and (match := pattern.match(event.data))
50
+ and (values := match.groupdict())
51
+ ):
52
+ return values
53
+ return None
54
+
55
+
56
+ @add_cmd(hidden=True, is_global=True)
57
+ def _set_terminal_color(event: KeyPressEvent) -> object:
58
+ """Run when the terminal receives a terminal color query response.
59
+
60
+ Args:
61
+ event: The key press event received when the termina sends a response
62
+
63
+ Returns:
64
+ :py:obj:`NotImplemented`, so the application is not invalidated when a
65
+ response from the terminal is received
66
+
67
+ """
68
+ from euporie.core.app.app import BaseApp
69
+
70
+ if isinstance(app := event.app, BaseApp) and (colors := get_match(event)):
71
+ c = colors["c"]
72
+ r, g, b = colors.get("r", "00"), colors.get("g", "00"), colors.get("b", "00")
73
+ app.term_colors[_COLOR_NAMES.get(c, c)] = f"#{r[:2]}{g[:2]}{b[:2]}"
74
+ app.update_style()
75
+ return NotImplemented
76
+
77
+
78
+ @add_cmd(hidden=True, is_global=True)
79
+ def _set_terminal_pixel_size(event: KeyPressEvent) -> object:
80
+ """Run when the terminal receives a pixel dimension query response."""
81
+ from euporie.core.app.app import BaseApp
82
+
83
+ if (
84
+ isinstance(app := event.app, BaseApp)
85
+ and (values := get_match(event))
86
+ and (x := values.get("x"))
87
+ and (y := values.get("y"))
88
+ ):
89
+ app.term_size_px = int(x), int(y)
90
+ return NotImplemented
91
+
92
+
93
+ @add_cmd(hidden=True, is_global=True)
94
+ def _set_terminal_graphics_sixel(event: KeyPressEvent) -> object:
95
+ """Run when the terminal receives a sixel graphics support query response."""
96
+ from euporie.core.app.app import BaseApp
97
+
98
+ if (
99
+ isinstance(app := event.app, BaseApp)
100
+ and (values := get_match(event))
101
+ and values.get("sixel")
102
+ ):
103
+ app.term_graphics_sixel = True
104
+ return NotImplemented
105
+
106
+
107
+ @add_cmd(hidden=True, is_global=True)
108
+ def _set_terminal_graphics_iterm(event: KeyPressEvent) -> object:
109
+ """Run when the terminal receives a iterm graphics support query response."""
110
+ from euporie.core.app.app import BaseApp
111
+
112
+ if (
113
+ isinstance(app := event.app, BaseApp)
114
+ and (values := get_match(event))
115
+ and (term := values.get("term"))
116
+ and term.startswith(("WezTerm", "Konsole", "mlterm"))
117
+ ):
118
+ app.term_graphics_iterm = True
119
+ return NotImplemented
120
+
121
+
122
+ @add_cmd(hidden=True, is_global=True)
123
+ def _set_terminal_graphics_kitty(event: KeyPressEvent) -> object:
124
+ """Run when the terminal receives a kitty graphics support query response."""
125
+ from euporie.core.app.app import BaseApp
126
+
127
+ if (
128
+ isinstance(app := event.app, BaseApp)
129
+ and (values := get_match(event))
130
+ and values.get("status") == "OK"
131
+ ):
132
+ app.term_graphics_kitty = True
133
+ return NotImplemented
134
+
135
+
136
+ @add_cmd(hidden=True, is_global=True)
137
+ def _set_terminal_sgr_pixel(event: KeyPressEvent) -> object:
138
+ """Run when the terminal receives a SGR-pixel mode support query response."""
139
+ from euporie.core.app.app import BaseApp
140
+
141
+ if (
142
+ isinstance(app := event.app, BaseApp)
143
+ and (values := get_match(event))
144
+ and (values.get("Pm") in {"1", "3"})
145
+ ):
146
+ app.term_sgr_pixel = True
147
+ return NotImplemented
148
+
149
+
150
+ @add_cmd(hidden=True, is_global=True)
151
+ def _set_terminal_clipboard_data(event: KeyPressEvent) -> object:
152
+ """Run when the terminal receives a clipboard data query response."""
153
+ from base64 import b64decode
154
+
155
+ from euporie.core.app.app import BaseApp
156
+ from euporie.core.clipboard import Osc52Clipboard
157
+
158
+ if (
159
+ isinstance(app := event.app, BaseApp)
160
+ and isinstance(clipboard := app.clipboard, Osc52Clipboard)
161
+ and (values := get_match(event))
162
+ ):
163
+ value = values.get("data", "")
164
+ text = b64decode(value).decode()
165
+ log.warning(repr(text))
166
+ clipboard.sync(text)
167
+ return NotImplemented
168
+
169
+
170
+ class TerminalQueries:
171
+ """Key bindings for terminal query responses."""
172
+
173
+
174
+ register_bindings(
175
+ {
176
+ "euporie.core.io.TerminalInfo": {
177
+ "set-terminal-color": "<colors-response>",
178
+ "set-terminal-pixel-size": "<pixel-size-response>",
179
+ "set-terminal-graphics-kitty": "<kitty-graphics-status-response>",
180
+ "set-terminal-graphics-sixel": "<sixel-graphics-status-response>",
181
+ "set-terminal-graphics-iterm": "<iterm-graphics-status-response>",
182
+ "set-terminal-sgr-pixel": "<sgr-pixel-status-response>",
183
+ "set-terminal-clipboard-data": "<clipboard-data-response>",
184
+ }
185
+ }
186
+ )
187
+
188
+
189
+ def load_terminal_bindings(config: Config | None = None) -> KeyBindingsBase:
190
+ """Load editor key-bindings in the style of the ``micro`` text editor."""
191
+ return load_registered_bindings(
192
+ "euporie.core.key_binding.bindings.terminal.TerminalQueries", config=config
193
+ )
@@ -0,0 +1,46 @@
1
+ """Add additional keys to the prompt_toolkit vi key-bindings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ from prompt_toolkit.buffer import indent, unindent
8
+ from prompt_toolkit.filters.app import vi_insert_mode
9
+ from prompt_toolkit.key_binding.bindings.vi import (
10
+ load_vi_bindings as load_ptk_vi_bindings,
11
+ )
12
+
13
+ from euporie.core.filters import cursor_in_leading_ws
14
+
15
+ if TYPE_CHECKING:
16
+ from prompt_toolkit.key_binding.key_bindings import (
17
+ ConditionalKeyBindings,
18
+ KeyBindings,
19
+ KeyBindingsBase,
20
+ )
21
+ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
22
+
23
+
24
+ def load_vi_bindings() -> KeyBindingsBase:
25
+ """Load vi keybindings from PTK, adding additional bindings."""
26
+ # We know the type of the vi bindings
27
+ vi_bindings = cast(
28
+ "KeyBindings",
29
+ cast("ConditionalKeyBindings", load_ptk_vi_bindings()).key_bindings,
30
+ )
31
+ handle = vi_bindings.add
32
+
33
+ @handle("c-i", filter=vi_insert_mode & cursor_in_leading_ws)
34
+ def _indent(event: KeyPressEvent) -> None:
35
+ """Indent lines."""
36
+ buffer = event.current_buffer
37
+ current_row = buffer.document.cursor_position_row
38
+ indent(buffer, current_row, current_row + event.arg)
39
+
40
+ @handle("s-tab", filter=vi_insert_mode & cursor_in_leading_ws)
41
+ def _unindent(event: KeyPressEvent) -> None:
42
+ """Unindent lines."""
43
+ current_row = event.current_buffer.document.cursor_position_row
44
+ unindent(event.current_buffer, current_row, current_row + event.arg)
45
+
46
+ return vi_bindings
@@ -4,21 +4,39 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import logging
7
+ import time
7
8
  from typing import TYPE_CHECKING
8
9
 
9
10
  from prompt_toolkit.application.current import get_app
11
+ from prompt_toolkit.key_binding.key_processor import KeyPress, _Flush
10
12
  from prompt_toolkit.key_binding.key_processor import KeyProcessor as PtKeyProcessor
11
- from prompt_toolkit.key_binding.key_processor import _Flush
12
13
  from prompt_toolkit.keys import Keys
13
14
 
15
+ from euporie.core.keys import MoreKeys
16
+
14
17
  if TYPE_CHECKING:
15
18
  from typing import Any
16
19
 
17
- from prompt_toolkit.key_binding.key_processor import KeyPress
18
20
 
19
21
  log = logging.getLogger(__name__)
20
22
 
21
23
 
24
+ def _kp_init(
25
+ self: KeyPress, key: Keys | MoreKeys | str, data: str | None = None
26
+ ) -> None:
27
+ """Include more keys when creating a KeyPress."""
28
+ assert isinstance(key, (Keys | MoreKeys)) or len(key) == 1, (
29
+ f"key {key!r} ({type(key)}) not recoognised {MoreKeys(key)}"
30
+ )
31
+ if data is None:
32
+ data = key.value if isinstance(key, (Keys, MoreKeys)) else key
33
+ self.key = key
34
+ self.data = data
35
+
36
+
37
+ setattr(KeyPress, "__init__", _kp_init) # noqa: B010
38
+
39
+
22
40
  class KeyProcessor(PtKeyProcessor):
23
41
  """A subclass of prompt_toolkit's keyprocessor.
24
42
 
@@ -125,3 +143,26 @@ class KeyProcessor(PtKeyProcessor):
125
143
  # Skip timeout if the last key was flush.
126
144
  if not is_flush:
127
145
  self._start_timeout()
146
+
147
+ def await_key(self, key: Keys | MoreKeys, timeout: float = 1.0) -> None:
148
+ """Wait for a particular key, processing it before all other keys.
149
+
150
+ Args:
151
+ key: The key to wait for
152
+ timeout: How long to wait for the key, in seconds
153
+ """
154
+ # Wait up to 1 second for response from terminal
155
+ start = time.monotonic()
156
+ input = get_app().input
157
+ tkp = self.__class__(key_bindings=self._bindings)
158
+ while (time.monotonic() - start) < timeout:
159
+ time.sleep(0.05)
160
+ for press in input.read_keys():
161
+ if press.key == key:
162
+ # If we find the key we're after, process it immediately
163
+ tkp.feed_multiple([press, _Flush])
164
+ tkp.process_keys()
165
+ return
166
+ else:
167
+ # If we get other keys, add them to the input queue
168
+ self.feed(press)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from functools import lru_cache
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from prompt_toolkit.key_binding import KeyBindings
@@ -31,6 +32,7 @@ def register_bindings(bindings: dict[str, KeyBindingDefs]) -> None:
31
32
  BINDINGS[group][command] = keys
32
33
 
33
34
 
35
+ @lru_cache
34
36
  def load_registered_bindings(
35
37
  *names: str, config: Config | None = None
36
38
  ) -> KeyBindingsBase:
@@ -4,9 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from prompt_toolkit.key_binding.key_bindings import _parse_key
7
+ from prompt_toolkit.key_binding import key_bindings
8
+ from prompt_toolkit.key_binding.key_bindings import _parse_key as _ptk_parse_key
9
+ from prompt_toolkit.keys import Keys
8
10
 
9
- from euporie.core.keys import Keys
11
+ from euporie.core.keys import MoreKeys
10
12
 
11
13
  if TYPE_CHECKING:
12
14
  from prompt_toolkit.key_binding import KeyPressEvent
@@ -28,6 +30,24 @@ def if_no_repeat(event: KeyPressEvent) -> bool:
28
30
  return not event.is_repeat
29
31
 
30
32
 
33
+ def _parse_key(key: AnyKeys | MoreKeys | str) -> Keys | MoreKeys | str:
34
+ """Parse a key or string, including additional keys."""
35
+ if isinstance(key, (Keys, str)):
36
+ try:
37
+ return _ptk_parse_key(key)
38
+ except ValueError:
39
+ pass
40
+ if isinstance(key, MoreKeys):
41
+ return key
42
+ try:
43
+ return MoreKeys(key)
44
+ except ValueError as err:
45
+ raise ValueError("Key binding not recognised") from err
46
+
47
+
48
+ key_bindings._parse_key = _parse_key
49
+
50
+
31
51
  def parse_keys(keys: AnyKeys) -> list[tuple[str | Keys, ...]]:
32
52
  """Pare a list of keys."""
33
53
  output: list[tuple[str | Keys, ...]] = []