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
euporie/core/io.py CHANGED
@@ -5,40 +5,115 @@ from __future__ import annotations
5
5
  import logging
6
6
  import re
7
7
  from base64 import b64encode
8
- from typing import TYPE_CHECKING
8
+ from functools import lru_cache
9
+ from typing import TYPE_CHECKING, cast
9
10
 
10
11
  from prompt_toolkit.input import vt100_parser
12
+ from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
11
13
  from prompt_toolkit.input.base import DummyInput, _dummy_context_manager
14
+ from prompt_toolkit.output.color_depth import ColorDepth
12
15
  from prompt_toolkit.output.vt100 import Vt100_Output as PtkVt100_Output
13
16
 
17
+ from euporie.core.app.current import get_app
18
+ from euporie.core.filters import in_screen, in_tmux
19
+
14
20
  if TYPE_CHECKING:
15
- from typing import IO, Any, Callable, ContextManager, TextIO
21
+ from contextlib import AbstractContextManager
22
+ from typing import IO, Any, Callable, TextIO
16
23
 
17
24
  from prompt_toolkit.keys import Keys
18
25
 
26
+ from euporie.core.config import Config
27
+
19
28
  log = logging.getLogger(__name__)
20
29
 
21
- _response_prefix_re = re.compile(
22
- r"""^\x1b(
23
- \][^\\\x07]* # Operating System Commands
24
- |
25
- _[^\\]* # Application Program Command
26
- |
27
- \[\?[\d;]* # Primary device attribute responses
28
- |
29
- P[ -~]*(\x1b|x1b\\)?
30
- )\Z""",
31
- re.VERBOSE,
32
- )
30
+ COLOR_DEPTHS = {
31
+ 1: ColorDepth.DEPTH_1_BIT,
32
+ 4: ColorDepth.DEPTH_4_BIT,
33
+ 8: ColorDepth.DEPTH_8_BIT,
34
+ 24: ColorDepth.DEPTH_24_BIT,
35
+ }
36
+
37
+
38
+ @lru_cache
39
+ def _have_termios_tty_fcntl() -> bool:
40
+ try:
41
+ import fcntl # noqa F401
42
+ import termios # noqa F401
43
+ import tty # noqa F401
44
+ except ModuleNotFoundError:
45
+ return False
46
+ else:
47
+ return True
48
+
49
+
50
+ def _tiocgwinsz() -> tuple[int, int, int, int]:
51
+ """Get the size and pixel dimensions of the terminal with `termios`."""
52
+ import array
53
+
54
+ output = array.array("H", [0, 0, 0, 0])
55
+ if _have_termios_tty_fcntl():
56
+ import fcntl
57
+ import termios
58
+
59
+ try:
60
+ fcntl.ioctl(1, termios.TIOCGWINSZ, output)
61
+ except OSError:
62
+ pass
63
+ rows, cols, xpixels, ypixels = output
64
+ return rows, cols, xpixels, ypixels
65
+
66
+
67
+ def passthrough(cmd: str, config: Config | None = None) -> str:
68
+ """Wrap an escape sequence for terminal passthrough."""
69
+ config = config or get_app().config
70
+ if config.multiplexer_passthrough:
71
+ if in_tmux():
72
+ cmd = cmd.replace("\x1b", "\x1b\x1b")
73
+ cmd = f"\x1bPtmux;{cmd}\x1b\\"
74
+ elif in_screen():
75
+ # Screen limits escape sequences to 768 bytes, so we have to chunk it
76
+ cmd = "".join(
77
+ f"\x1bP{cmd[i : i + 764]}\x1b\\" for i in range(0, len(cmd), 764)
78
+ )
79
+ return cmd
33
80
 
34
81
 
35
82
  class _IsPrefixOfLongerMatchCache(vt100_parser._IsPrefixOfLongerMatchCache):
83
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
84
+ super().__init__(*args, **kwargs)
85
+ # Pattern for any ANSI escape sequence
86
+ self._response_prefix_re = re.compile(
87
+ r"""^\x1b(
88
+ \][^\\\x07]* # Operating System Commands
89
+ |
90
+ _[^\\]* # Application Program Command
91
+ |
92
+ \[\?[\d;]* # Primary device attribute responses
93
+ |
94
+ P[ -~]*(\x1b|x1b\\)?
95
+ )\Z""",
96
+ re.VERBOSE,
97
+ )
98
+ # Generate prefix matches for all known ansi escape sequences
99
+ # This is faster than PTK's method
100
+ self._ansi_sequence_prefixes = {
101
+ seq[:i] for seq in ANSI_SEQUENCES for i in range(len(seq))
102
+ }
103
+
36
104
  def __missing__(self, prefix: str) -> bool:
37
105
  """Check if the response might match an OSC or APC code, or DA response."""
38
- result = super().__missing__(prefix)
39
- if not result and _response_prefix_re.match(prefix):
40
- result = True
41
- self[prefix] = result
106
+ result = bool(
107
+ # (hard coded) If this could be a prefix of a CPR response, return True.
108
+ vt100_parser._cpr_response_prefix_re.match(prefix)
109
+ # True if this could be a mouse event sequence
110
+ or vt100_parser._mouse_event_prefix_re.match(prefix)
111
+ # True if this could be the prefix of an expected escape sequence
112
+ or prefix in self._ansi_sequence_prefixes
113
+ # If this could be a prefix of any other escape sequence, return True
114
+ or self._response_prefix_re.match(prefix)
115
+ )
116
+ self[prefix] = result
42
117
  return result
43
118
 
44
119
 
@@ -51,14 +126,39 @@ class Vt100Parser(vt100_parser.Vt100Parser):
51
126
 
52
127
  def __init__(self, *args: Any, **kwargs: Any) -> None:
53
128
  """Create a new VT100 parser."""
129
+ from euporie.core.keys import MoreKeys
130
+
54
131
  super().__init__(*args, **kwargs)
55
- self.queries: dict[Keys, re.Pattern] = {}
132
+ self.patterns: dict[Keys | MoreKeys, re.Pattern] = {
133
+ MoreKeys.ColorsResponse: re.compile(
134
+ r"^\x1b\](?P<c>(\d+;)?\d+)+;rgb:"
135
+ r"(?P<r>[0-9A-Fa-f]{2,4})\/"
136
+ r"(?P<g>[0-9A-Fa-f]{2,4})\/"
137
+ r"(?P<b>[0-9A-Fa-f]{2,4})"
138
+ # Allow BEL or ST as terminator
139
+ r"(?:\x1b\\|\x9c|\x07)"
140
+ ),
141
+ MoreKeys.PixelSizeResponse: re.compile(r"^\x1b\[4;(?P<y>\d+);(?P<x>\d+)t"),
142
+ MoreKeys.KittyGraphicsStatusResponse: re.compile(
143
+ r"^\x1b_Gi=(4294967295|0);(?P<status>OK)\x1b\\"
144
+ ),
145
+ MoreKeys.SixelGraphicsStatusResponse: re.compile(
146
+ r"^\x1b\[\?(?:\d+;)*(?P<sixel>4)(?:;\d+)*c"
147
+ ),
148
+ MoreKeys.ItermGraphicsStatusResponse: re.compile(
149
+ r"^\x1bP>\|(?P<term>[^\x1b]+)\x1b\\"
150
+ ),
151
+ MoreKeys.SgrPixelStatusResponse: re.compile(r"^\x1b\[\?1016;(?P<Pm>\d)\$"),
152
+ MoreKeys.ClipboardDataResponse: re.compile(
153
+ r"^\x1b\]52;(?:c|p)?;(?P<data>[A-Za-z0-9+/=]+)\x1b\\"
154
+ ),
155
+ }
56
156
 
57
157
  def _get_match(self, prefix: str) -> None | Keys | tuple[Keys, ...]:
58
158
  """Check for additional key matches first."""
59
- for key, pattern in self.queries.items():
159
+ for key, pattern in self.patterns.items():
60
160
  if pattern.match(prefix):
61
- return key
161
+ return cast("Keys", key)
62
162
 
63
163
  return super()._get_match(prefix)
64
164
 
@@ -66,7 +166,9 @@ class Vt100Parser(vt100_parser.Vt100Parser):
66
166
  class IgnoredInput(DummyInput):
67
167
  """An input which ignores input but does not immediately close the app."""
68
168
 
69
- def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
169
+ def attach(
170
+ self, input_ready_callback: Callable[[], None]
171
+ ) -> AbstractContextManager[None]:
70
172
  """Do not call the callback, so the input is never closed."""
71
173
  return _dummy_context_manager()
72
174
 
@@ -74,14 +176,12 @@ class IgnoredInput(DummyInput):
74
176
  class Vt100_Output(PtkVt100_Output):
75
177
  """A Vt100 output which enables SGR pixel mouse positioning."""
76
178
 
77
- def enable_mouse_support(self) -> None:
78
- """Additionally enable SGR-pixel mouse positioning."""
79
- super().enable_mouse_support()
179
+ def enable_sgr_pixel(self) -> None:
180
+ """Enable SGR-pixel mouse positioning."""
80
181
  self.write_raw("\x1b[?1016h")
81
182
 
82
- def disable_mouse_support(self) -> None:
83
- """Additionally disable SGR-pixel mouse positioning."""
84
- super().disable_mouse_support()
183
+ def disable_sgr_pixel(self) -> None:
184
+ """Disable SGR-pixel mouse positioning."""
85
185
  self.write_raw("\x1b[?1016l")
86
186
 
87
187
  def enable_private_sixel_colors(self) -> None:
@@ -115,6 +215,44 @@ class Vt100_Output(PtkVt100_Output):
115
215
  """Get clipboard contents using OSC-52."""
116
216
  self.write_raw("\x1b]52;c;?\x1b\\")
117
217
 
218
+ def get_colors(self) -> None:
219
+ """Query terminal colors."""
220
+ self.write_raw(
221
+ passthrough(
222
+ ("\x1b]10;?\x1b\\\x1b]11;?\x1b\\")
223
+ + "".join(f"\x1b]4;{i};?\x1b\\" for i in range(16))
224
+ )
225
+ )
226
+
227
+ def get_pixel_size(self) -> None:
228
+ """Check the terminal's dimensions in pixels."""
229
+ self.write_raw("\x1b[14t")
230
+
231
+ def get_kitty_graphics_status(self) -> None:
232
+ """Query terminal to check for kitty graphics support."""
233
+ self.write_raw(
234
+ "\x1b[s"
235
+ + passthrough("\x1b_gi=4294967295,s=1,v=1,a=q,t=d,f=24;aaaa\x1b\\")
236
+ + "\x1b[u\x1b[2k"
237
+ )
238
+
239
+ def get_sixel_graphics_status(self) -> None:
240
+ """Query terminal for sixel graphics support."""
241
+ self.write_raw(passthrough("\x1b[c"))
242
+
243
+ def get_iterm_graphics_status(self) -> None:
244
+ """Query terminal for iTerm graphics support."""
245
+ self.write_raw(passthrough("\x1b[>q"))
246
+
247
+ def get_sgr_pixel_status(self) -> None:
248
+ """Query terminal to check for Pixel SGR support."""
249
+ # Enable, check, disable
250
+ self.write_raw("\x1b[?1016h\x1b[?1016$p\x1b[?1016l")
251
+
252
+ def get_csiu_status(self) -> None:
253
+ """Query terminal to check for CSI-u support."""
254
+ self.write_raw("\x1b[?u")
255
+
118
256
 
119
257
  class PseudoTTY:
120
258
  """Make an output stream look like a TTY."""
@@ -139,3 +277,44 @@ class PseudoTTY:
139
277
  def __getattr__(self, name: str) -> Any:
140
278
  """Return an attribute of the wrappeed stream."""
141
279
  return getattr(self._underlying, name)
280
+
281
+
282
+ def edit_in_editor(filename: str, line_number: int = 0) -> None:
283
+ """Suspend the current app and edit a file in an external editor."""
284
+ import os
285
+ import shlex
286
+ import subprocess
287
+
288
+ from prompt_toolkit.application.run_in_terminal import run_in_terminal
289
+
290
+ def _open_file_in_editor(filename: str) -> None:
291
+ """Call editor executable."""
292
+ # If the 'VISUAL' or 'EDITOR' environment variable has been set, use that.
293
+ # Otherwise, fall back to the first available editor that we can find.
294
+ for editor in [
295
+ os.environ.get("VISUAL"),
296
+ os.environ.get("EDITOR"),
297
+ "editor",
298
+ "micro",
299
+ "nano",
300
+ "pico",
301
+ "vi",
302
+ "emacs",
303
+ ]:
304
+ if editor:
305
+ try:
306
+ # Use 'shlex.split()' because $VISUAL can contain spaces and quotes
307
+ subprocess.call([*shlex.split(editor), filename])
308
+ return
309
+ except OSError:
310
+ # Executable does not exist, try the next one.
311
+ pass
312
+
313
+ async def run() -> None:
314
+ # Open in editor
315
+ # (We need to use `run_in_terminal`, because not all editors go to
316
+ # the alternate screen buffer, and some could influence the cursor
317
+ # position)
318
+ await run_in_terminal(lambda: _open_file_in_editor(filename), in_executor=True)
319
+
320
+ get_app().create_background_task(run())
@@ -0,0 +1 @@
1
+ """Concerns the interaction with Jupyter kernels."""