euporie 2.8.4__py3-none-any.whl → 2.8.6__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 (131) 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 +58 -62
  5. euporie/core/__init__.py +1 -1
  6. euporie/core/__main__.py +28 -11
  7. euporie/core/_settings.py +109 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +95 -0
  10. euporie/core/app/_settings.py +457 -0
  11. euporie/core/{app.py → app/app.py} +212 -576
  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 +205 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +20 -16
  21. euporie/core/{widgets → bars}/status.py +6 -23
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +16 -7
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +10 -20
  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 +116 -53
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +9 -23
  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 +4 -6
  39. euporie/core/convert/utils.py +41 -4
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +98 -40
  42. euporie/core/format.py +2 -3
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +12 -21
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +386 -133
  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} +45 -108
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +1 -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 +1 -6
  58. euporie/core/key_binding/bindings/mouse.py +2 -2
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/key_processor.py +43 -2
  61. euporie/core/key_binding/registry.py +2 -0
  62. euporie/core/key_binding/utils.py +22 -2
  63. euporie/core/keys.py +7156 -93
  64. euporie/core/layout/cache.py +3 -3
  65. euporie/core/layout/containers.py +48 -4
  66. euporie/core/layout/decor.py +2 -2
  67. euporie/core/layout/mouse.py +1 -1
  68. euporie/core/layout/print.py +2 -1
  69. euporie/core/layout/scroll.py +39 -34
  70. euporie/core/log.py +76 -64
  71. euporie/core/lsp.py +118 -24
  72. euporie/core/margins.py +1 -1
  73. euporie/core/path.py +62 -13
  74. euporie/core/renderer.py +58 -17
  75. euporie/core/style.py +57 -39
  76. euporie/core/suggest.py +103 -85
  77. euporie/core/tabs/__init__.py +32 -0
  78. euporie/core/tabs/_settings.py +113 -0
  79. euporie/core/tabs/base.py +80 -470
  80. euporie/core/tabs/kernel.py +419 -0
  81. euporie/core/tabs/notebook.py +24 -101
  82. euporie/core/utils.py +92 -15
  83. euporie/core/validation.py +1 -1
  84. euporie/core/widgets/_settings.py +188 -0
  85. euporie/core/widgets/cell.py +19 -50
  86. euporie/core/widgets/cell_outputs.py +25 -36
  87. euporie/core/widgets/decor.py +11 -41
  88. euporie/core/widgets/dialog.py +62 -27
  89. euporie/core/widgets/display.py +12 -15
  90. euporie/core/widgets/file_browser.py +2 -23
  91. euporie/core/widgets/forms.py +8 -5
  92. euporie/core/widgets/inputs.py +13 -70
  93. euporie/core/widgets/layout.py +2 -1
  94. euporie/core/widgets/logo.py +49 -0
  95. euporie/core/widgets/menu.py +10 -8
  96. euporie/core/widgets/pager.py +6 -10
  97. euporie/core/widgets/palette.py +6 -6
  98. euporie/hub/app.py +52 -35
  99. euporie/notebook/_commands.py +24 -0
  100. euporie/notebook/_settings.py +107 -0
  101. euporie/notebook/app.py +49 -171
  102. euporie/notebook/filters.py +1 -1
  103. euporie/notebook/tabs/__init__.py +46 -7
  104. euporie/notebook/tabs/_commands.py +714 -0
  105. euporie/notebook/tabs/_settings.py +32 -0
  106. euporie/notebook/tabs/display.py +4 -4
  107. euporie/notebook/tabs/edit.py +11 -44
  108. euporie/notebook/tabs/json.py +5 -5
  109. euporie/notebook/tabs/log.py +1 -18
  110. euporie/notebook/tabs/notebook.py +11 -660
  111. euporie/notebook/widgets/_commands.py +11 -0
  112. euporie/notebook/widgets/_settings.py +19 -0
  113. euporie/notebook/widgets/side_bar.py +14 -34
  114. euporie/preview/_settings.py +104 -0
  115. euporie/preview/app.py +6 -31
  116. euporie/preview/tabs/notebook.py +6 -72
  117. euporie/web/__init__.py +1 -0
  118. euporie/web/tabs/__init__.py +14 -0
  119. euporie/web/tabs/web.py +11 -6
  120. euporie/web/widgets/__init__.py +1 -0
  121. euporie/web/widgets/webview.py +5 -15
  122. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
  123. euporie-2.8.6.dist-info/RECORD +175 -0
  124. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
  125. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
  126. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
  127. euporie/core/launch.py +0 -64
  128. euporie/core/terminal.py +0 -522
  129. euporie-2.8.4.dist-info/RECORD +0 -147
  130. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
  131. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -0,0 +1,114 @@
1
+ """Contain classes relating to kernel management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ import sys
9
+ import threading
10
+ from typing import TYPE_CHECKING
11
+
12
+ from jupyter_client import AsyncKernelManager
13
+ from jupyter_client.provisioning.local_provisioner import LocalProvisioner
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any, Callable, TextIO
17
+
18
+ from jupyter_client.connect import KernelConnectionInfo
19
+
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ class LoggingLocalProvisioner(LocalProvisioner): # type:ignore[misc]
25
+ """A Jupyter kernel provisionser which logs kernel output."""
26
+
27
+ async def launch_kernel(
28
+ self, cmd: list[str], **kwargs: Any
29
+ ) -> KernelConnectionInfo:
30
+ """Launch a kernel with a command."""
31
+ await super().launch_kernel(cmd, **kwargs)
32
+
33
+ def log_kernel_output(pipe: TextIO, log_func: Callable) -> None:
34
+ try:
35
+ with pipe:
36
+ for line in iter(pipe.readline, ""):
37
+ log_func(line.rstrip())
38
+ except StopIteration:
39
+ pass
40
+
41
+ if self.process is not None:
42
+ # Start thread to listen for kernel output
43
+ threading.Thread(
44
+ target=log_kernel_output,
45
+ args=(self.process.stdout, log.warning),
46
+ daemon=True,
47
+ ).start()
48
+
49
+ return self.connection_info
50
+
51
+
52
+ def set_default_provisioner() -> None:
53
+ """Set the default kernel provisioner to euporie's logging provisioner."""
54
+ from jupyter_client.provisioning import KernelProvisionerFactory as KPF
55
+
56
+ KPF.instance().default_provisioner_name = "logging-local-provisioner"
57
+
58
+
59
+ class EuporieKernelManager(AsyncKernelManager):
60
+ """Kernel Manager subclass.
61
+
62
+ ``jupyter_client`` replaces a plain ``python`` command with the current executable,
63
+ but this is not desirable if the client is running in its own prefix (e.g. with
64
+ ``pipx``). We work around this here.
65
+
66
+ See https://github.com/jupyter/jupyter_client/issues/949
67
+ """
68
+
69
+ def format_kernel_cmd(self, extra_arguments: list[str] | None = None) -> list[str]:
70
+ """Replace templated args (e.g. {connection_file})."""
71
+ extra_arguments = extra_arguments or []
72
+ assert self.kernel_spec is not None
73
+ cmd = self.kernel_spec.argv + extra_arguments
74
+
75
+ v_major, v_minor = sys.version_info[:2]
76
+ if cmd and cmd[0] in {
77
+ "python",
78
+ f"python{v_major}",
79
+ f"python{v_major}.{v_minor}",
80
+ }:
81
+ # If the command is `python` without an absolute path and euporie is
82
+ # running in the same prefix as the kernel_spec file is located, use
83
+ # sys.executable: otherwise fall back to the executable in the base prefix
84
+ if (
85
+ os.path.commonpath((sys.prefix, self.kernel_spec.resource_dir))
86
+ == sys.prefix
87
+ ):
88
+ cmd[0] = sys.executable
89
+ else:
90
+ cmd[0] = sys._base_executable # type: ignore [attr-defined]
91
+
92
+ # Make sure to use the realpath for the connection_file
93
+ # On windows, when running with the store python, the connection_file path
94
+ # is not usable by non python kernels because the path is being rerouted when
95
+ # inside of a store app.
96
+ # See this bug here: https://bugs.python.org/issue41196
97
+ ns = {
98
+ "connection_file": os.path.realpath(self.connection_file),
99
+ "prefix": sys.prefix,
100
+ }
101
+
102
+ if self.kernel_spec:
103
+ ns["resource_dir"] = self.kernel_spec.resource_dir
104
+
105
+ if self._launch_args:
106
+ ns.update({str(k): str(v) for k, v in self._launch_args.items()})
107
+
108
+ pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
109
+
110
+ def _from_ns(match: re.Match) -> str:
111
+ """Get the key out of ns if it's there, otherwise no change."""
112
+ return ns.get(match.group(1), match.group())
113
+
114
+ return [pat.sub(_from_ns, arg) for arg in cmd]
@@ -1,12 +1,5 @@
1
1
  """Define collections of generic key-bindings which do not belong to widgets."""
2
2
 
3
- from euporie.core.key_binding.bindings import (
4
- basic,
5
- completion,
6
- micro,
7
- mouse,
8
- page_navigation,
9
- vi,
10
- )
3
+ from . import basic, completion, micro, mouse, page_navigation, vi
11
4
 
12
5
  __all__ = ["basic", "completion", "micro", "mouse", "page_navigation", "vi"]
@@ -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
  {
@@ -24,7 +24,7 @@ from prompt_toolkit.mouse_events import MouseButton, MouseEventType, MouseModifi
24
24
  from prompt_toolkit.mouse_events import MouseEvent as PtkMouseEvent
25
25
  from prompt_toolkit.renderer import HeightIsUnknownError
26
26
 
27
- from euporie.core.app import BaseApp
27
+ from euporie.core.app.app import BaseApp
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
@@ -155,7 +155,7 @@ def load_mouse_bindings() -> KeyBindings:
155
155
  return NotImplemented
156
156
 
157
157
  mouse_event = _MOUSE_EVENT_CACHE[
158
- 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
159
159
  ]
160
160
 
161
161
  if mouse_event is None:
@@ -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
+ )
@@ -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, ...]] = []