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
@@ -72,7 +72,7 @@ def crop(data: PilImage, bbox: DiInt) -> PilImage:
72
72
  rows=full_height,
73
73
  )
74
74
  if image is not None:
75
- cell_size_x, cell_size_y = self.app.term_info.cell_size_px
75
+ cell_size_x, cell_size_y = self.app.cell_size_px
76
76
  # Downscale image to fit target region for precise cropping
77
77
  image.thumbnail((full_width * cell_size_x, full_height * cell_size_y))
78
78
  image = image.crop(
@@ -13,7 +13,6 @@ from euporie.core.filters import command_exists, have_modules
13
13
  if TYPE_CHECKING:
14
14
  from euporie.core.convert.datum import Datum
15
15
 
16
-
17
16
  register(
18
17
  from_="base64-png",
19
18
  to="png",
@@ -126,9 +125,8 @@ async def latex_to_png_py_mpl(
126
125
  from matplotlib.backends import backend_agg
127
126
 
128
127
  # mpl mathtext doesn't support display math, force inline
129
- data = datum.data.strip().replace("$$", "$")
130
- if not data.startswith("$"):
131
- data = f"${data}$"
128
+ data = datum.data.replace("$", "").strip()
129
+ data = f"${data}$"
132
130
  buffer = BytesIO()
133
131
  prop = font_manager.FontProperties(size=12)
134
132
  parser = mathtext.MathTextParser("path")
@@ -143,7 +141,7 @@ async def latex_to_png_py_mpl(
143
141
  register(
144
142
  from_=("svg", "jpeg", "pdf", "gif"),
145
143
  to="png",
146
- filter_=command_exists("convert", "mogrify"),
144
+ filter_=command_exists("magick"),
147
145
  )(partial(imagemagick_convert, "PNG"))
148
146
 
149
147
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from functools import partial
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from euporie.core.app.current import get_app
8
9
  from euporie.core.convert.formats.common import (
9
10
  chafa_convert_cmd,
10
11
  chafa_convert_py,
@@ -12,7 +13,6 @@ from euporie.core.convert.formats.common import (
12
13
  )
13
14
  from euporie.core.convert.registry import register
14
15
  from euporie.core.convert.utils import call_subproc
15
- from euporie.core.current import get_app
16
16
  from euporie.core.filters import command_exists, have_modules
17
17
 
18
18
  if TYPE_CHECKING:
@@ -53,7 +53,7 @@ async def png_to_sixel_img2sixel(
53
53
  if bg:
54
54
  cmd += [f"--bgcolor={bg}"]
55
55
  if cols is not None:
56
- px, _ = get_app().term_info.cell_size_px
56
+ px, _ = get_app().cell_size_px
57
57
  cmd += [f"--width={int(cols * px)}"]
58
58
  return (await call_subproc(datum.data, cmd)).decode()
59
59
 
@@ -61,7 +61,7 @@ async def png_to_sixel_img2sixel(
61
61
  register(
62
62
  from_=("png", "jpeg", "svg", "pdf"),
63
63
  to="sixel",
64
- filter_=command_exists("convert", "mogrify"),
64
+ filter_=command_exists("magick"),
65
65
  )(partial(imagemagick_convert, "sixel"))
66
66
 
67
67
 
@@ -8,19 +8,15 @@ from typing import TYPE_CHECKING, NamedTuple
8
8
  from prompt_toolkit.cache import FastDictCache, SimpleCache
9
9
  from prompt_toolkit.filters import to_filter
10
10
 
11
- # from euporie.core.cache import cache
12
-
13
11
  if TYPE_CHECKING:
14
- from typing import Callable, Iterable
12
+ from collections.abc import Iterable
13
+ from typing import Callable
15
14
 
16
15
  from prompt_toolkit.filters import Filter, FilterOrBool
17
16
 
18
17
  log = logging.getLogger(__name__)
19
18
 
20
19
 
21
- BASE64_FORMATS = {"png", "jpeg", "pdf", "gif"}
22
-
23
-
24
20
  class Converter(NamedTuple):
25
21
  """Hold a conversion function and its weight."""
26
22
 
@@ -58,8 +54,10 @@ def register(
58
54
  return decorator
59
55
 
60
56
 
61
- def find_route(from_: str, to: str) -> list | None:
57
+ def _find_route(from_: str, to: str) -> list | None:
62
58
  """Find the shortest conversion path between two formats."""
59
+ from euporie.core.convert import formats # noqa: F401
60
+
63
61
  if from_ == to:
64
62
  return [from_]
65
63
 
@@ -100,5 +98,10 @@ def find_route(from_: str, to: str) -> list | None:
100
98
 
101
99
 
102
100
  _CONVERTOR_ROUTE_CACHE: FastDictCache[tuple[str, str], list | None] = FastDictCache(
103
- find_route
101
+ _find_route
104
102
  )
103
+
104
+
105
+ def find_route(from_: str, to: str) -> list | None:
106
+ """Find and cache conversion routes."""
107
+ return _CONVERTOR_ROUTE_CACHE[from_, to]
@@ -6,15 +6,49 @@ import asyncio
6
6
  import logging
7
7
  import subprocess # S404 - Security implications have been considered
8
8
  import tempfile
9
+ from math import ceil
9
10
  from pathlib import Path
10
11
  from typing import TYPE_CHECKING
11
12
 
13
+ from euporie.core.app.current import get_app
14
+
12
15
  if TYPE_CHECKING:
13
16
  from typing import Any
14
17
 
18
+ from euporie.core.convert.datum import Datum
19
+
15
20
  log = logging.getLogger(__name__)
16
21
 
17
22
 
23
+ async def scale_to_fit(
24
+ datum: Datum, cols: int | None, rows: int | None
25
+ ) -> tuple[int, int]:
26
+ """Calculate image size based on aspect ratio, and scale to fit."""
27
+ data = datum.data
28
+ px, py = get_app().cell_size_px
29
+
30
+ # Calculate rows based on image aspect ratio
31
+ w, h = data.size
32
+ if rows is None and cols is not None:
33
+ rows = ceil(cols / w * h)
34
+ elif cols is None and rows is not None:
35
+ cols = ceil(rows / h * w)
36
+ elif rows is None and cols is None:
37
+ cols = ceil(w / px)
38
+ rows = ceil(h / py)
39
+ assert rows is not None
40
+ assert cols is not None
41
+
42
+ # Scale to fit while maintaining aspect ratio
43
+ _width, aspect = await datum.cell_size_async()
44
+ if cols * aspect < rows:
45
+ rows = ceil(cols * aspect)
46
+ else:
47
+ cols = ceil(rows / aspect)
48
+
49
+ return cols, rows
50
+
51
+
18
52
  async def call_subproc(
19
53
  data: str | bytes,
20
54
  cmd: list[Any],
@@ -43,15 +77,18 @@ async def call_subproc(
43
77
  if use_tempfile:
44
78
  # If the command cannot read from stdin, create a temporary file to pass to
45
79
  # the command
46
- tfile = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
47
- tfile.write(data)
48
- tfile.close()
80
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tfile:
81
+ tfile.write(data)
49
82
  cmd.append(tfile.name)
50
83
  stdinput = None
51
84
  else:
52
85
  stdinput = data
53
86
 
54
- log.debug("Running external command `%s`", cmd)
87
+ if log.level <= 0:
88
+ import shlex
89
+
90
+ log.debug("Running external command `%s`", shlex.join(cmd))
91
+
55
92
  error: Exception | None = None
56
93
  try:
57
94
  proc = await asyncio.create_subprocess_exec(
@@ -61,28 +98,18 @@ async def call_subproc(
61
98
  stderr=asyncio.subprocess.DEVNULL,
62
99
  )
63
100
  output_bytes, _ = await proc.communicate(stdinput)
64
- except FileNotFoundError as error_:
101
+ except FileNotFoundError as error:
65
102
  log.error("Could not run external command `%s`", cmd)
66
- error = error_
67
- except subprocess.CalledProcessError as error_:
103
+ raise error
104
+ except subprocess.CalledProcessError as error:
68
105
  log.error("There was an error while running external command `%s`", cmd)
69
- error = error_
106
+ raise error
107
+ else:
108
+ if (proc.returncode or 0) > 0 or error:
109
+ # Raise an exception if the process failed so we can continue on the the
110
+ # next conversion method
111
+ raise subprocess.CalledProcessError(proc.returncode or 0, cmd)
70
112
  finally:
71
- if error is not None:
72
- # Generate an output stating there was an error
73
- output_bytes = (
74
- b"\x1b[33m" # Set fg to yellow
75
- b"\xee\x82\xb6" # Draw left pill side
76
- b"\x1b[43m\x1b[30m" # Set fg to black, bg to yellow
77
- b"\xe2\x9a\xa0" # Draw warning symbol
78
- b" Rendering Error"
79
- b"\x1b[33m\x1b[49m" # Set fg to yellow, reset bg
80
- b"\xee\x82\xb4" # Draw right pill side
81
- b"\x1b[n" # Reset style
82
- )
83
-
84
- # TODO Log any stderr
85
-
86
113
  # Clean up any temporary file
87
114
  if use_tempfile:
88
115
  tfile.close()
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from abc import ABCMeta
7
- from typing import TYPE_CHECKING, List, NamedTuple
7
+ from typing import TYPE_CHECKING, NamedTuple
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from typing import Literal
@@ -23,7 +23,7 @@ class Diagnostic(NamedTuple):
23
23
  chars: slice
24
24
 
25
25
 
26
- class Report(List[Diagnostic], metaclass=ABCMeta):
26
+ class Report(list[Diagnostic], metaclass=ABCMeta):
27
27
  """Class for storing a diagnostic report."""
28
28
 
29
29
  @classmethod
euporie/core/filters.py CHANGED
@@ -3,16 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from functools import lru_cache, partial, reduce
7
- from importlib import import_module
6
+ from functools import cache, partial, reduce
7
+ from importlib.util import find_spec
8
8
  from shutil import which
9
9
  from typing import TYPE_CHECKING
10
10
 
11
- from prompt_toolkit.enums import EditingMode
11
+ # from prompt_toolkit.enums import EditingMode
12
12
  from prompt_toolkit.filters import (
13
13
  Condition,
14
14
  emacs_insert_mode,
15
15
  emacs_mode,
16
+ has_completions,
16
17
  to_filter,
17
18
  vi_insert_mode,
18
19
  vi_mode,
@@ -26,6 +27,7 @@ if TYPE_CHECKING:
26
27
  from prompt_toolkit.layout.containers import Window
27
28
 
28
29
 
30
+ @cache
29
31
  def command_exists(*cmds: str) -> Filter:
30
32
  """Verify a list of external commands exist on the system."""
31
33
  filters = [
@@ -35,72 +37,18 @@ def command_exists(*cmds: str) -> Filter:
35
37
  return reduce(lambda a, b: a & b, filters, to_filter(True))
36
38
 
37
39
 
40
+ @cache
38
41
  def have_modules(*modules: str) -> Filter:
39
42
  """Verify a list of python modules are importable."""
40
43
 
41
44
  def try_import(module: str) -> bool:
42
- try:
43
- import_module(module)
44
- except ModuleNotFoundError:
45
- return False
46
- else:
47
- return True
45
+ loader = find_spec(module)
46
+ return loader is not None
48
47
 
49
48
  filters = [Condition(partial(try_import, module)) for module in modules]
50
49
  return reduce(lambda a, b: a & b, filters, to_filter(True))
51
50
 
52
51
 
53
- @Condition
54
- @lru_cache
55
- def have_ruff() -> bool:
56
- """Determine if ruff is available."""
57
- try:
58
- import ruff # noqa F401
59
- except ModuleNotFoundError:
60
- return False
61
- else:
62
- return True
63
-
64
-
65
- @Condition
66
- @lru_cache
67
- def have_black() -> bool:
68
- """Determine if black is available."""
69
- try:
70
- import black.const # noqa F401
71
- except ModuleNotFoundError:
72
- return False
73
- else:
74
- return True
75
-
76
-
77
- @Condition
78
- @lru_cache
79
- def have_isort() -> bool:
80
- """Determine if isort is available."""
81
- try:
82
- import isort # noqa F401
83
- except ModuleNotFoundError:
84
- return False
85
- else:
86
- return True
87
-
88
-
89
- @Condition
90
- @lru_cache
91
- def have_ssort() -> bool:
92
- """Determine if ssort is available."""
93
- try:
94
- import ssort # noqa F401
95
- except ModuleNotFoundError:
96
- return False
97
- else:
98
- return True
99
-
100
-
101
- # Determine if we have at least one formatter
102
- have_formatter = have_black | have_isort | have_ssort | have_ruff
103
-
104
52
  # Determine if euporie is running inside a multiplexer.
105
53
  in_screen = to_filter(os.environ.get("TERM", "").startswith("screen"))
106
54
  in_tmux = to_filter(os.environ.get("TMUX") is not None)
@@ -129,12 +77,20 @@ def has_suggestion() -> bool:
129
77
  )
130
78
 
131
79
 
80
+ @Condition
81
+ def has_tabs() -> bool:
82
+ """Filter to show if any tabs are open in an app."""
83
+ from euporie.core.app.current import get_app
84
+
85
+ return bool(get_app().tabs)
86
+
87
+
132
88
  @Condition
133
89
  def has_dialog() -> bool:
134
90
  """Determine if a dialog is being displayed."""
135
91
  from prompt_toolkit.layout.containers import ConditionalContainer
136
92
 
137
- from euporie.core.current import get_app
93
+ from euporie.core.app.current import get_app
138
94
 
139
95
  app = get_app()
140
96
  for dialog in app.dialogs.values():
@@ -157,10 +113,22 @@ def has_menus() -> bool:
157
113
  return False
158
114
 
159
115
 
116
+ has_float = has_dialog | has_menus | has_completions
117
+
118
+
119
+ @Condition
120
+ def has_toolbar() -> bool:
121
+ """Is there an active toolbar?"""
122
+ from euporie.core.app.current import get_app
123
+ from euporie.core.bars import BAR_BUFFERS
124
+
125
+ return get_app().current_buffer.name in BAR_BUFFERS
126
+
127
+
160
128
  @Condition
161
129
  def tab_has_focus() -> bool:
162
130
  """Determine if there is a currently focused tab."""
163
- from euporie.core.current import get_app
131
+ from euporie.core.app.current import get_app
164
132
 
165
133
  return get_app().tab is not None
166
134
 
@@ -168,7 +136,7 @@ def tab_has_focus() -> bool:
168
136
  @Condition
169
137
  def pager_has_focus() -> bool:
170
138
  """Determine if there is a currently focused notebook."""
171
- from euporie.core.current import get_app
139
+ from euporie.core.app.current import get_app
172
140
 
173
141
  app = get_app()
174
142
  pager = app.pager
@@ -180,7 +148,7 @@ def pager_has_focus() -> bool:
180
148
  @Condition
181
149
  def display_has_focus() -> bool:
182
150
  """Determine if there is a currently focused cell."""
183
- from euporie.core.current import get_app
151
+ from euporie.core.app.current import get_app
184
152
  from euporie.core.widgets.display import DisplayControl
185
153
 
186
154
  return isinstance(get_app().layout.current_control, DisplayControl)
@@ -189,7 +157,7 @@ def display_has_focus() -> bool:
189
157
  @Condition
190
158
  def buffer_is_empty() -> bool:
191
159
  """Determine if the current buffer contains nothing."""
192
- from euporie.core.current import get_app
160
+ from euporie.core.app.current import get_app
193
161
 
194
162
  return not get_app().current_buffer.text
195
163
 
@@ -197,7 +165,7 @@ def buffer_is_empty() -> bool:
197
165
  @Condition
198
166
  def buffer_is_code() -> bool:
199
167
  """Determine if the current buffer contains code."""
200
- from euporie.core.current import get_app
168
+ from euporie.core.app.current import get_app
201
169
 
202
170
  return get_app().current_buffer.name == "code"
203
171
 
@@ -205,7 +173,7 @@ def buffer_is_code() -> bool:
205
173
  @Condition
206
174
  def buffer_is_markdown() -> bool:
207
175
  """Determine if the current buffer contains markdown."""
208
- from euporie.core.current import get_app
176
+ from euporie.core.app.current import get_app
209
177
 
210
178
  return get_app().current_buffer.name == "markdown"
211
179
 
@@ -213,15 +181,16 @@ def buffer_is_markdown() -> bool:
213
181
  @Condition
214
182
  def micro_mode() -> bool:
215
183
  """When the micro key-bindings are active."""
216
- from euporie.core.current import get_app
184
+ from euporie.core.app.app import ExtraEditingMode
185
+ from euporie.core.app.current import get_app
217
186
 
218
- return get_app().editing_mode == EditingMode.MICRO # type: ignore
187
+ return get_app().editing_mode == ExtraEditingMode.MICRO
219
188
 
220
189
 
221
190
  @Condition
222
191
  def micro_replace_mode() -> bool:
223
192
  """Determine if the editor is in overwrite mode."""
224
- from euporie.core.current import get_app
193
+ from euporie.core.app.current import get_app
225
194
 
226
195
  app = get_app()
227
196
  return app.micro_state.input_mode == MicroInputMode.REPLACE
@@ -230,7 +199,7 @@ def micro_replace_mode() -> bool:
230
199
  @Condition
231
200
  def micro_insert_mode() -> bool:
232
201
  """Determine if the editor is in insert mode."""
233
- from euporie.core.current import get_app
202
+ from euporie.core.app.current import get_app
234
203
 
235
204
  app = get_app()
236
205
  return app.micro_state.input_mode == MicroInputMode.INSERT
@@ -239,7 +208,7 @@ def micro_insert_mode() -> bool:
239
208
  @Condition
240
209
  def micro_recording_macro() -> bool:
241
210
  """Determine if a micro macro is being recorded."""
242
- from euporie.core.current import get_app
211
+ from euporie.core.app.current import get_app
243
212
 
244
213
  return get_app().micro_state.current_recording is not None
245
214
 
@@ -247,7 +216,7 @@ def micro_recording_macro() -> bool:
247
216
  @Condition
248
217
  def is_returnable() -> bool:
249
218
  """Determine if the current buffer has an accept handler."""
250
- from euporie.core.current import get_app
219
+ from euporie.core.app.current import get_app
251
220
 
252
221
  return get_app().current_buffer.is_returnable
253
222
 
@@ -255,7 +224,7 @@ def is_returnable() -> bool:
255
224
  @Condition
256
225
  def cursor_at_start_of_line() -> bool:
257
226
  """Determine if the cursor is at the start of a line."""
258
- from euporie.core.current import get_app
227
+ from euporie.core.app.current import get_app
259
228
 
260
229
  return get_app().current_buffer.document.cursor_position_col == 0
261
230
 
@@ -263,7 +232,7 @@ def cursor_at_start_of_line() -> bool:
263
232
  @Condition
264
233
  def cursor_on_first_line() -> bool:
265
234
  """Determine if the cursor is on the first line of a buffer."""
266
- from euporie.core.current import get_app
235
+ from euporie.core.app.current import get_app
267
236
 
268
237
  return get_app().current_buffer.document.on_first_line
269
238
 
@@ -271,11 +240,32 @@ def cursor_on_first_line() -> bool:
271
240
  @Condition
272
241
  def cursor_on_last_line() -> bool:
273
242
  """Determine if the cursor is on the last line of a buffer."""
274
- from euporie.core.current import get_app
243
+ from euporie.core.app.current import get_app
275
244
 
276
245
  return get_app().current_buffer.document.on_last_line
277
246
 
278
247
 
248
+ @cache
249
+ def char_after_cursor(char: str) -> Condition:
250
+ """Generate a condition to check for a character after the cursor."""
251
+ from euporie.core.app.current import get_app
252
+
253
+ return Condition(
254
+ lambda: bool(
255
+ (post := get_app().current_buffer.document.text_after_cursor)
256
+ and post[0] == char
257
+ )
258
+ )
259
+
260
+
261
+ @Condition
262
+ def has_matching_bracket() -> bool:
263
+ """Determine if the bracket at the cursor has a matching pair."""
264
+ from euporie.core.app.current import get_app
265
+
266
+ return bool(get_app().current_buffer.document.find_matching_bracket_position())
267
+
268
+
279
269
  """Determine if any binding style is in insert mode."""
280
270
  insert_mode = (
281
271
  (vi_mode & vi_insert_mode)
@@ -290,7 +280,7 @@ replace_mode = micro_replace_mode | vi_replace_mode
290
280
  @Condition
291
281
  def is_searching() -> bool:
292
282
  """Determine if the app is in search mode."""
293
- from euporie.core.current import get_app
283
+ from euporie.core.app.current import get_app
294
284
 
295
285
  app = get_app()
296
286
  return (
@@ -310,8 +300,8 @@ def at_end_of_buffer() -> bool:
310
300
  @Condition
311
301
  def kernel_is_python() -> bool:
312
302
  """Determine if the current notebook has a python kernel."""
313
- from euporie.core.current import get_app
314
- from euporie.core.tabs.base import KernelTab
303
+ from euporie.core.app.current import get_app
304
+ from euporie.core.tabs.kernel import KernelTab
315
305
 
316
306
  kernel_tab = get_app().tab
317
307
  if isinstance(kernel_tab, KernelTab):
@@ -322,7 +312,7 @@ def kernel_is_python() -> bool:
322
312
  @Condition
323
313
  def multiple_cells_selected() -> bool:
324
314
  """Determine if there is more than one selected cell."""
325
- from euporie.core.current import get_app
315
+ from euporie.core.app.current import get_app
326
316
  from euporie.core.tabs.notebook import BaseNotebook
327
317
 
328
318
  nb = get_app().tab
@@ -334,8 +324,8 @@ def multiple_cells_selected() -> bool:
334
324
  @Condition
335
325
  def kernel_tab_has_focus() -> bool:
336
326
  """Determine if there is a focused kernel tab."""
337
- from euporie.core.current import get_app
338
- from euporie.core.tabs.base import KernelTab
327
+ from euporie.core.app.current import get_app
328
+ from euporie.core.tabs.kernel import KernelTab
339
329
 
340
330
  return isinstance(get_app().tab, KernelTab)
341
331
 
euporie/core/format.py CHANGED
@@ -67,11 +67,18 @@ class CliFormatter(Formatter):
67
67
  except Exception:
68
68
  return text
69
69
  try:
70
- output, _error = proc.communicate(text)
70
+ output, error = proc.communicate(text)
71
71
  except Exception:
72
72
  return text
73
73
  else:
74
- return output.rstrip("\r\n")
74
+ if output and not error:
75
+ return output.rstrip("\r\n")
76
+ else:
77
+ return text
78
+
79
+ def __repr__(self) -> str:
80
+ """Return representation of the formatter as a string."""
81
+ return f"{self.command[0].title()}Formatter()"
75
82
 
76
83
 
77
84
  class LspFormatter(Formatter):
@@ -115,3 +122,7 @@ class LspFormatter(Formatter):
115
122
  text = text.rstrip()
116
123
 
117
124
  return text
125
+
126
+ def __repr__(self) -> str:
127
+ """Return representation of the formatter as a string."""
128
+ return f"{self.lsp.name.title()}Formatter()"
euporie/core/ft/ansi.py CHANGED
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
9
9
  from prompt_toolkit.formatted_text import ANSI as PTANSI
10
10
 
11
11
  if TYPE_CHECKING:
12
- from typing import Generator
12
+ from collections.abc import Generator
13
13
 
14
14
  log = logging.getLogger(__name__)
15
15