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,10 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import json
7
6
  import logging
7
+ import os
8
8
  import signal
9
9
  import sys
10
+ from abc import ABC, abstractmethod
11
+ from enum import Enum
10
12
  from functools import partial
11
13
  from pathlib import PurePath
12
14
  from typing import TYPE_CHECKING, cast
@@ -14,8 +16,8 @@ from weakref import WeakSet, WeakValueDictionary
14
16
 
15
17
  from prompt_toolkit.application.application import Application, _CombinedRegistry
16
18
  from prompt_toolkit.application.current import create_app_session, set_app
17
- from prompt_toolkit.cursor_shapes import CursorShape, CursorShapeConfig
18
19
  from prompt_toolkit.data_structures import Point
20
+ from prompt_toolkit.enums import EditingMode
19
21
  from prompt_toolkit.filters import Condition, buffer_has_focus, to_filter
20
22
  from prompt_toolkit.input.defaults import create_input
21
23
  from prompt_toolkit.key_binding.bindings.basic import (
@@ -30,10 +32,7 @@ from prompt_toolkit.key_binding.bindings.emacs import (
30
32
  from prompt_toolkit.key_binding.bindings.mouse import (
31
33
  load_mouse_bindings as load_ptk_mouse_bindings,
32
34
  )
33
- from prompt_toolkit.key_binding.bindings.vi import (
34
- load_vi_bindings,
35
- load_vi_search_bindings,
36
- )
35
+ from prompt_toolkit.key_binding.bindings.vi import load_vi_search_bindings
37
36
  from prompt_toolkit.key_binding.key_bindings import (
38
37
  ConditionalKeyBindings,
39
38
  merge_key_bindings,
@@ -54,18 +53,15 @@ from prompt_toolkit.styles import (
54
53
  merge_styles,
55
54
  style_from_pygments_cls,
56
55
  )
57
- from pygments.styles import STYLE_MAP as pygments_styles
58
- from pygments.styles import get_style_by_name
59
- from upath import UPath
56
+ from prompt_toolkit.utils import Event
60
57
 
61
- from euporie.core.clipboard import ConfiguredClipboard
62
- from euporie.core.commands import add_cmd
63
- from euporie.core.config import Config, add_setting
58
+ from euporie.core.app.base import ConfigurableApp
59
+ from euporie.core.app.cursor import CursorConfig
60
+ from euporie.core.clipboard import CONFIGURED_CLIPBOARDS
64
61
  from euporie.core.convert.mime import get_mime
65
- from euporie.core.current import get_app
66
- from euporie.core.filters import in_mplex, insert_mode, replace_mode, tab_has_focus
62
+ from euporie.core.filters import has_toolbar
67
63
  from euporie.core.format import CliFormatter
68
- from euporie.core.io import Vt100_Output, Vt100Parser
64
+ from euporie.core.io import COLOR_DEPTHS, Vt100_Output, Vt100Parser
69
65
  from euporie.core.key_binding.key_processor import KeyProcessor
70
66
  from euporie.core.key_binding.micro_state import MicroState
71
67
  from euporie.core.key_binding.registry import (
@@ -86,8 +82,8 @@ from euporie.core.style import (
86
82
  MIME_STYLE,
87
83
  ColorPalette,
88
84
  build_style,
85
+ get_style_by_name,
89
86
  )
90
- from euporie.core.terminal import TerminalInfo
91
87
  from euporie.core.utils import ChainedList
92
88
  from euporie.core.widgets.decor import Shadow
93
89
  from euporie.core.widgets.menu import CompletionsMenu
@@ -96,84 +92,38 @@ if TYPE_CHECKING:
96
92
  from asyncio import AbstractEventLoop
97
93
  from pathlib import Path
98
94
  from types import FrameType
99
- from typing import Any, Callable, TypeVar
95
+ from typing import Any, Callable, ClassVar, TypeVar
100
96
 
101
97
  # from prompt_toolkit.application import _AppResult
102
- from prompt_toolkit.clipboard import Clipboard
103
98
  from prompt_toolkit.contrib.ssh import PromptToolkitSSHSession
104
- from prompt_toolkit.enums import EditingMode
105
99
  from prompt_toolkit.filters import Filter, FilterOrBool
106
100
  from prompt_toolkit.input import Input
101
+ from prompt_toolkit.layout.containers import AnyContainer
107
102
  from prompt_toolkit.layout.layout import FocusableElement
108
103
  from prompt_toolkit.layout.screen import WritePosition
109
104
  from prompt_toolkit.output import Output
110
105
 
106
+ from euporie.core.bars.command import CommandBar
107
+ from euporie.core.bars.search import SearchBar
111
108
  from euporie.core.config import Setting
112
109
  from euporie.core.format import Formatter
110
+ from euporie.core.tabs import TabRegistryEntry
113
111
  from euporie.core.tabs.base import Tab
114
- from euporie.core.terminal import TerminalQuery
115
112
  from euporie.core.widgets.dialog import Dialog
116
113
  from euporie.core.widgets.pager import Pager
117
- from euporie.core.widgets.search import SearchBar
118
114
 
119
115
  _AppResult = TypeVar("_AppResult")
120
116
 
121
117
  log = logging.getLogger(__name__)
122
118
 
123
119
 
124
- _COLOR_DEPTHS = {
125
- 1: ColorDepth.DEPTH_1_BIT,
126
- 4: ColorDepth.DEPTH_4_BIT,
127
- 8: ColorDepth.DEPTH_8_BIT,
128
- 24: ColorDepth.DEPTH_24_BIT,
129
- }
130
-
131
-
132
- class CursorConfig(CursorShapeConfig):
133
- """Determine which cursor mode to use."""
134
-
135
- def get_cursor_shape(self, app: Application[Any]) -> CursorShape:
136
- """Return the cursor shape to be used in the current state."""
137
- if isinstance(app, BaseApp) and app.config.set_cursor_shape:
138
- if insert_mode():
139
- if app.config.cursor_blink:
140
- return CursorShape.BLINKING_BEAM
141
- else:
142
- return CursorShape.BEAM
143
- elif replace_mode():
144
- if app.config.cursor_blink:
145
- return CursorShape.BLINKING_UNDERLINE
146
- else:
147
- return CursorShape.UNDERLINE
148
- return CursorShape.BLOCK
149
-
150
- # ################################### Settings ####################################w
151
-
152
- add_setting(
153
- name="set_cursor_shape",
154
- flags=["--set-cursor-shape"],
155
- type_=bool,
156
- default=True,
157
- menu_title="Change cursor shape",
158
- help_="Whether to set the shape of the cursor depending on the editing mode",
159
- description="""
160
- When set to True, the euporie will set the shape of the terminal's cursor
161
- to a beam in insert mode and and underline in replace mode when editing.
162
- """,
163
- )
164
- add_setting(
165
- name="cursor_blink",
166
- flags=["--cursor-blink"],
167
- type_=bool,
168
- default=False,
169
- help_="Whether to blink the cursor",
170
- description="""
171
- When set to True, the cursor will blink.
172
- """,
173
- )
120
+ class ExtraEditingMode(str, Enum):
121
+ """Additional editing modes."""
174
122
 
123
+ MICRO = "MICRO"
175
124
 
176
- class BaseApp(Application):
125
+
126
+ class BaseApp(ConfigurableApp, Application, ABC):
177
127
  """All euporie apps.
178
128
 
179
129
  The base euporie application class.
@@ -182,12 +132,10 @@ class BaseApp(Application):
182
132
  wide methods can be easily added.
183
133
  """
184
134
 
185
- name: str
186
135
  color_palette: ColorPalette
187
136
  mouse_position: Point
188
137
 
189
- config = Config()
190
- log_stdout_level: str = "CRITICAL"
138
+ _config_defaults: ClassVar[dict[str, Any]] = {"log_level_stdout": "critical"}
191
139
 
192
140
  def __init__(
193
141
  self,
@@ -221,7 +169,10 @@ class BaseApp(Application):
221
169
  # Initialise the application
222
170
  super().__init__(
223
171
  **{
224
- "color_depth": self.config.color_depth,
172
+ "clipboard": CONFIGURED_CLIPBOARDS.get(
173
+ self.config.clipboard, lambda: None
174
+ )(),
175
+ "color_depth": COLOR_DEPTHS.get(self.config.color_depth),
225
176
  "editing_mode": self.get_edit_mode(),
226
177
  "mouse_support": True,
227
178
  "cursor": CursorConfig(),
@@ -241,14 +192,21 @@ class BaseApp(Application):
241
192
  )
242
193
  # Contains the opened tab containers
243
194
  self.tabs: list[Tab] = []
244
- # Holds the search bar to pass to cell inputs
195
+ self.on_tabs_change = Event(self)
196
+ # Holds the optional toolbars
245
197
  self.search_bar: SearchBar | None = None
198
+ self.command_bar: CommandBar | None = None
246
199
  # Holds the index of the current tab
247
200
  self._tab_idx = 0
248
201
  # Add state for micro key-bindings
249
202
  self.micro_state = MicroState()
250
- # Load the terminal information system
251
- self.term_info = TerminalInfo(self.input, self.output, self.config)
203
+ # Default terminal info values
204
+ self.term_colors = dict(DEFAULT_COLORS)
205
+ self.term_graphics_sixel = False
206
+ self.term_graphics_iterm = False
207
+ self.term_graphics_kitty = False
208
+ self.term_sgr_pixel = False
209
+ self._term_size_px: tuple[int, int]
252
210
  # Floats at the app level
253
211
  self.leave_graphics = to_filter(leave_graphics)
254
212
  self.graphics: WeakSet[Float] = WeakSet()
@@ -273,15 +231,16 @@ class BaseApp(Application):
273
231
  self.key_processor = KeyProcessor(_CombinedRegistry(self))
274
232
  # List of key-bindings groups to load
275
233
  self.bindings_to_load = [
276
- "euporie.core.app.BaseApp",
277
- "euporie.core.terminal.TerminalInfo",
234
+ "euporie.core.app.app:BaseApp",
235
+ "euporie.core.io.TerminalInfo",
278
236
  ]
279
237
 
280
- from euporie.core.key_binding.bindings.page_navigation import (
281
- load_page_navigation_bindings,
282
- )
238
+ if enable_page_navigation_bindings:
239
+ from euporie.core.key_binding.bindings.page_navigation import (
240
+ load_page_navigation_bindings,
241
+ )
283
242
 
284
- self._page_navigation_bindings = load_page_navigation_bindings(self.config)
243
+ self._page_navigation_bindings = load_page_navigation_bindings(self.config)
285
244
  # Allow hiding element when manually redrawing app
286
245
  self._redrawing = False
287
246
  self.redrawing = Condition(lambda: self._redrawing)
@@ -293,14 +252,17 @@ class BaseApp(Application):
293
252
  self.set_title = to_filter(set_title)
294
253
  self.title = title or self.__class__.__name__
295
254
  # Register config hooks
296
- self.config.get_item("edit_mode").event += self.update_edit_mode
297
- self.config.get_item("syntax_theme").event += self.update_style
298
- self.config.get_item("color_scheme").event += self.update_style
299
- self.config.get_item("log_level").event += lambda x: setup_logs(self.config)
300
- self.config.get_item("log_file").event += lambda x: setup_logs(self.config)
301
- self.config.get_item("log_config").event += lambda x: setup_logs(self.config)
302
- self.config.get_item("color_depth").event += lambda x: setattr(
303
- self, "_color_depth", _COLOR_DEPTHS[x.value]
255
+ self.config.events.edit_mode += self.update_edit_mode
256
+ self.config.events.syntax_theme += self.update_style
257
+ self.config.events.color_scheme += self.update_style
258
+ self.config.events.log_level += lambda x: setup_logs(self.config)
259
+ self.config.events.log_file += lambda x: setup_logs(self.config)
260
+ self.config.events.log_config += lambda x: setup_logs(self.config)
261
+ self.config.events.color_depth += lambda x: setattr(
262
+ self, "_color_depth", COLOR_DEPTHS[self.config.color_depth]
263
+ )
264
+ self.config.events.clipboard += lambda x: setattr(
265
+ self, "clipboard", CONFIGURED_CLIPBOARDS[self.config.clipboard]
304
266
  )
305
267
  # Set up the color palette
306
268
  self.color_palette = ColorPalette()
@@ -318,6 +280,30 @@ class BaseApp(Application):
318
280
  CliFormatter(**info) for info in self.config.formatters
319
281
  ]
320
282
 
283
+ @property
284
+ def term_size_px(self) -> tuple[int, int]:
285
+ """The dimensions of the terminal in pixels."""
286
+ try:
287
+ return self._term_size_px
288
+ except AttributeError:
289
+ from euporie.core.io import _tiocgwinsz
290
+
291
+ _rows, _cols, px, py = _tiocgwinsz()
292
+ self._term_size_px = (px, py)
293
+ return self._term_size_px
294
+
295
+ @term_size_px.setter
296
+ def term_size_px(self, value: tuple[int, int]) -> None:
297
+ self._term_size_px = value
298
+
299
+ @property
300
+ def cell_size_px(self) -> tuple[int, int]:
301
+ """Get the pixel size of a single terminal cell."""
302
+ px, py = self.term_size_px
303
+ rows, cols = self.output.get_size()
304
+ # If we can't get the pixel size, just guess wildly
305
+ return px // cols or 10, py // rows or 20
306
+
321
307
  @property
322
308
  def title(self) -> str:
323
309
  """The application's title."""
@@ -351,28 +337,32 @@ class BaseApp(Application):
351
337
 
352
338
  def pre_run(self, app: Application | None = None) -> None:
353
339
  """Call during the 'pre-run' stage of application loading."""
354
- # Determines which clipboard mechanism to use based on configuration
355
- self.clipboard: Clipboard = ConfiguredClipboard(self)
356
- # Determine what color depth to use
357
- self._color_depth = _COLOR_DEPTHS.get(
358
- self.config.color_depth, self.term_info.depth_of_color.value
359
- )
360
- # Set the application's style, and update it when the terminal responds
340
+ # Set the application's style
361
341
  self.update_style()
362
- self.term_info.colors.event += self.update_style
363
- # Load completions menu. This must be done after the app is set, because
364
- # :py:func:`get_app` is needed to access the config
342
+ # Load completions menu.
365
343
  self.menus["completions"] = Float(
366
- content=Shadow(CompletionsMenu()),
344
+ content=Shadow(CompletionsMenu(extra_filter=~has_toolbar)),
367
345
  xcursor=True,
368
346
  ycursor=True,
369
347
  )
370
- # Open any files we need to
371
- self.open_files()
372
348
  # Load the layout
373
349
  # We delay this until we have terminal responses to allow terminal graphics
374
350
  # support to be detected first
375
351
  self.layout = Layout(self.load_container(), self.focused_element)
352
+ # Open any files we need to
353
+ self.open_files()
354
+ # Start polling terminal style if configured
355
+ if self.config.terminal_polling_interval and hasattr(
356
+ self.input, "vt100_parser"
357
+ ):
358
+ self.create_background_task(self._poll_terminal_colors())
359
+
360
+ async def _poll_terminal_colors(self) -> None:
361
+ """Repeatedly query the terminal for its background and foreground colours."""
362
+ if isinstance(output := self.output, Vt100_Output):
363
+ while self.config.terminal_polling_interval:
364
+ await asyncio.sleep(self.config.terminal_polling_interval)
365
+ output.get_colors()
376
366
 
377
367
  async def run_async(
378
368
  self,
@@ -383,26 +373,50 @@ class BaseApp(Application):
383
373
  ) -> _AppResult:
384
374
  """Run the application."""
385
375
  with set_app(self):
376
+ # Use a custom vt100 parser to allow querying the terminal
377
+ if parser := getattr(self.input, "vt100_parser", None):
378
+ setattr( # noqa B010
379
+ self.input, "vt100_parser", Vt100Parser(parser.feed_key_callback)
380
+ )
381
+
386
382
  # Load key bindings
387
383
  self.load_key_bindings()
388
- # Send queries to the terminal
389
- self.term_info.send_all()
390
- # Read responses
391
- kp = self.key_processor
392
-
393
- def read_from_input() -> None:
394
- kp.feed_multiple(self.input.read_keys())
395
384
 
396
- with self.input.raw_mode(), self.input.attach(read_from_input):
397
- # Give the terminal time to respond and allow the event loop to read
398
- # the terminal responses from the input
399
- await asyncio.sleep(0.1)
400
- kp.process_keys()
385
+ if isinstance(self.output, Vt100_Output):
386
+ # Send terminal queries
387
+ self.output.get_colors()
388
+ self.output.get_pixel_size()
389
+ self.output.get_kitty_graphics_status()
390
+ self.output.get_sixel_graphics_status()
391
+ self.output.get_iterm_graphics_status()
392
+ self.output.get_sgr_pixel_status()
393
+ self.output.get_csiu_status()
394
+ self.output.flush()
395
+
396
+ # Read responses
397
+ kp = self.key_processor
398
+
399
+ def read_from_input() -> None:
400
+ kp.feed_multiple(self.input.read_keys())
401
+
402
+ with self.input.raw_mode(), self.input.attach(read_from_input):
403
+ # Give the terminal time to respond and allow the event loop to read
404
+ # the terminal responses from the input
405
+ await asyncio.sleep(0.1)
406
+ kp.process_keys()
401
407
 
402
408
  return await super().run_async(
403
409
  pre_run, set_exception_handler, handle_sigint, slow_callback_duration
404
410
  )
405
411
 
412
+ @classmethod
413
+ async def interact(cls, ssh_session: PromptToolkitSSHSession) -> None:
414
+ """Run the app asynchronously for the hub SSH server."""
415
+ try:
416
+ await cls().run_async()
417
+ except EOFError:
418
+ pass
419
+
406
420
  @classmethod
407
421
  def load_input(cls) -> Input:
408
422
  """Create the input for this application to use.
@@ -420,12 +434,6 @@ class BaseApp(Application):
420
434
 
421
435
  input_ = IgnoredInput()
422
436
 
423
- # Use a custom vt100 parser to allow querying the terminal
424
- if parser := getattr(input_, "vt100_parser", None):
425
- setattr( # noqa B010
426
- input_, "vt100_parser", Vt100Parser(parser.feed_key_callback)
427
- )
428
-
429
437
  return input_
430
438
 
431
439
  @classmethod
@@ -462,6 +470,8 @@ class BaseApp(Application):
462
470
  from euporie.core.key_binding.bindings.basic import load_basic_bindings
463
471
  from euporie.core.key_binding.bindings.micro import load_micro_bindings
464
472
  from euporie.core.key_binding.bindings.mouse import load_mouse_bindings
473
+ from euporie.core.key_binding.bindings.terminal import load_terminal_bindings
474
+ from euporie.core.key_binding.bindings.vi import load_vi_bindings
465
475
 
466
476
  self._default_bindings = merge_key_bindings(
467
477
  [
@@ -494,7 +504,7 @@ class BaseApp(Application):
494
504
  # Load extra mouse bindings
495
505
  load_mouse_bindings(),
496
506
  # Load terminal query response key bindings
497
- # load_command_bindings("terminal"),
507
+ load_terminal_bindings(),
498
508
  ]
499
509
  )
500
510
  self.key_bindings = load_registered_bindings(
@@ -503,40 +513,39 @@ class BaseApp(Application):
503
513
 
504
514
  def _on_resize(self) -> None:
505
515
  """Query the terminal dimensions on a resize event."""
506
- self.term_info.pixel_dimensions.send()
516
+ if isinstance(output := self.output, Vt100_Output):
517
+ output.get_pixel_size()
507
518
  super()._on_resize()
508
519
 
509
520
  @classmethod
510
521
  def launch(cls) -> None:
511
522
  """Launch the app."""
512
- # Load default logging
513
- setup_logs()
514
- # Load the app's configuration
515
- cls.config.load(cls)
523
+ super().launch()
516
524
  # Run the application
517
525
  with create_app_session(input=cls.load_input(), output=cls.load_output()):
518
526
  # Create an instance of the app and run it
519
527
  app = cls()
520
-
521
528
  # Handle SIGTERM while the app is running
522
529
  original_sigterm = signal.getsignal(signal.SIGTERM)
523
530
  signal.signal(signal.SIGTERM, app.cleanup)
524
- # Run the app
525
- try:
526
- result = app.run()
527
- except (EOFError, KeyboardInterrupt):
528
- result = None
529
- finally:
530
- signal.signal(signal.SIGTERM, original_sigterm)
531
- # Shut down any remaining LSP clients at exit
532
- app.shutdown_lsps()
531
+ # Set and run the app
532
+ with set_app(app):
533
+ try:
534
+ result = app.run()
535
+ except (EOFError, KeyboardInterrupt):
536
+ result = None
537
+ finally:
538
+ signal.signal(signal.SIGTERM, original_sigterm)
539
+ # Shut down any remaining LSP clients at exit
540
+ app.shutdown_lsps()
533
541
  return result
534
542
 
535
543
  def cleanup(self, signum: int, frame: FrameType | None) -> None:
536
544
  """Restore the state of the terminal on unexpected exit."""
537
545
  log.critical("Unexpected exit signal, restoring terminal")
538
546
  output = self.output
539
- self.exit()
547
+ if self.is_running:
548
+ self.exit()
540
549
  self.shutdown_lsps()
541
550
  # Reset terminal state
542
551
  output.reset_cursor_key_mode()
@@ -548,12 +557,8 @@ class BaseApp(Application):
548
557
  # Exit the main thread
549
558
  sys.exit(1)
550
559
 
551
- @classmethod
552
- async def interact(cls, ssh_session: PromptToolkitSSHSession) -> None:
553
- """Run the app asynchronously for the hub SSH server."""
554
- await cls().run_async()
555
-
556
- def load_container(self) -> FloatContainer:
560
+ @abstractmethod
561
+ def load_container(self) -> AnyContainer:
557
562
  """Load the root container for this application.
558
563
 
559
564
  Returns:
@@ -565,27 +570,32 @@ class BaseApp(Application):
565
570
  floats=cast("list[Float]", self.floats),
566
571
  )
567
572
 
568
- def get_file_tabs(self, path: Path) -> list[type[Tab]]:
569
- """Return the tab to use for a file path."""
570
- from euporie.core.tabs.base import Tab
573
+ @property
574
+ def tab_registry(self) -> list[TabRegistryEntry]:
575
+ """Return the tab registry."""
576
+ from euporie.core.tabs import _TAB_REGISTRY
577
+
578
+ return _TAB_REGISTRY
571
579
 
580
+ def get_file_tabs(self, path: Path) -> list[TabRegistryEntry]:
581
+ """Return the tab to use for a file path."""
572
582
  path_mime = get_mime(path) or "text/plain"
573
583
  log.debug("File %s has mime type: %s", path, path_mime)
574
584
 
575
- tab_options = set()
576
- for tab_cls in Tab._registry:
577
- for mime_type in tab_cls.mime_types:
585
+ tab_options: list[TabRegistryEntry] = []
586
+ for entry in self.tab_registry:
587
+ for mime_type in entry.mime_types:
578
588
  if PurePath(path_mime).match(mime_type):
579
- tab_options.add(tab_cls)
580
- if path.suffix in tab_cls.file_extensions:
581
- tab_options.add(tab_cls)
589
+ tab_options.append(entry)
590
+ if path.suffix in entry.file_extensions:
591
+ tab_options.append(entry)
582
592
 
583
- return sorted(tab_options, key=lambda x: x.weight, reverse=True)
593
+ return sorted(tab_options, reverse=True)
584
594
 
585
595
  def get_file_tab(self, path: Path) -> type[Tab] | None:
586
596
  """Return the tab to use for a file path."""
587
597
  if tabs := self.get_file_tabs(path):
588
- return tabs[0]
598
+ return tabs[0].tab_class
589
599
  return None
590
600
 
591
601
  def get_language_lsps(self, language: str) -> list[LspClient]:
@@ -648,7 +658,7 @@ class BaseApp(Application):
648
658
  log.error("Unable to display file %s", path)
649
659
  else:
650
660
  tab = tab_class(self, ppath)
651
- self.tabs.append(tab)
661
+ self.add_tab(tab)
652
662
  # Ensure the opened tab is focused at app start
653
663
  self.focused_element = tab
654
664
  # Ensure the newly opened tab is selected
@@ -688,7 +698,7 @@ class BaseApp(Application):
688
698
  try:
689
699
  self.layout.focus(container)
690
700
  except ValueError:
691
- self.to_focus = container
701
+ log.exception("Cannot focus tab")
692
702
 
693
703
  def focus_tab(self, tab: Tab) -> None:
694
704
  """Make a tab visible and focuses it."""
@@ -704,20 +714,22 @@ class BaseApp(Application):
704
714
  # Remove tab
705
715
  if tab in self.tabs:
706
716
  self.tabs.remove(tab)
707
- # Update body container to reflect new tab list
708
- # assert isinstance(self.body_container.body, HSplit)
709
- # self.body_container.body.children[0] = VSplit(self.tabs)
710
- # Focus another tab if one exists
711
- if self.tab:
712
- self.layout.focus(self.tab)
713
- # If a tab is not open, the status bar is not shown, so focus the logo, so
714
- # pressing tab focuses the menu
717
+ self.on_tabs_change()
718
+ # Focus the next active tab if one exists
719
+ if next_tab := self.tab:
720
+ next_tab.focus()
721
+ # If no tab is open, ensure something is focused
715
722
  else:
716
723
  try:
717
724
  self.layout.focus_next()
718
725
  except ValueError:
719
726
  pass
720
727
 
728
+ def add_tab(self, tab: Tab) -> None:
729
+ """Add a tab to the current tabs list."""
730
+ self.tabs.append(tab)
731
+ self.on_tabs_change()
732
+
721
733
  def close_tab(self, tab: Tab | None = None) -> None:
722
734
  """Close a notebook tab.
723
735
 
@@ -733,27 +745,49 @@ class BaseApp(Application):
733
745
 
734
746
  def get_edit_mode(self) -> EditingMode:
735
747
  """Return the editing mode enum defined in the configuration."""
736
- from euporie.core.key_binding.bindings.micro import EditingMode
748
+ micro_mode = cast("EditingMode", ExtraEditingMode.MICRO)
737
749
 
738
750
  return {
739
- "micro": EditingMode.MICRO, # type: ignore
751
+ "micro": micro_mode,
740
752
  "vi": EditingMode.VI,
741
753
  "emacs": EditingMode.EMACS,
742
- }.get(
743
- str(self.config.edit_mode),
744
- EditingMode.MICRO, # type: ignore
745
- )
754
+ }.get(str(self.config.edit_mode), micro_mode)
746
755
 
747
756
  def update_edit_mode(self, setting: Setting | None = None) -> None:
748
757
  """Set the keybindings for editing mode."""
749
758
  self.editing_mode = self.get_edit_mode()
750
759
  log.debug("Editing mode set to: %s", self.editing_mode)
751
760
 
761
+ @property
762
+ def color_depth(self) -> ColorDepth:
763
+ """The active :class:`.ColorDepth`.
764
+
765
+ The current value is determined as follows:
766
+
767
+ - If a color depth was given explicitly to this application, use that
768
+ value.
769
+ - Otherwise, fall back to the color depth that is reported by the
770
+ :class:`.Output` implementation. If the :class:`.Output` class was
771
+ created using `output.defaults.create_output`, then this value is
772
+ coming from the $PROMPT_TOOLKIT_COLOR_DEPTH environment variable.
773
+ """
774
+ # Detect terminal color depth
775
+ if self._color_depth is None:
776
+ if os.environ.get("NO_COLOR", "") or os.environ.get("TERM", "") == "dumb":
777
+ self._color_depth = ColorDepth.DEPTH_1_BIT
778
+ colorterm = os.environ.get("COLORTERM", "")
779
+ if "truecolor" in colorterm or "24bit" in colorterm:
780
+ self._color_depth = ColorDepth.DEPTH_24_BIT
781
+ elif "256" in os.environ.get("TERM", ""):
782
+ self._color_depth = ColorDepth.DEPTH_8_BIT
783
+
784
+ return super().color_depth
785
+
752
786
  @property
753
787
  def syntax_theme(self) -> str:
754
788
  """Calculate the current syntax theme."""
755
789
  syntax_theme = self.config.syntax_theme
756
- if syntax_theme == self.config.settings["syntax_theme"].default:
790
+ if syntax_theme == self.config.defaults.syntax_theme:
757
791
  syntax_theme = "tango" if self.color_palette.bg.is_light else "euporie"
758
792
  return syntax_theme
759
793
 
@@ -782,7 +816,7 @@ class BaseApp(Application):
782
816
  }
783
817
  base_colors: dict[str, str] = {
784
818
  **DEFAULT_COLORS,
785
- **self.term_info.colors.value,
819
+ **self.term_colors,
786
820
  **theme_colors.get(self.config.color_scheme, theme_colors["default"]),
787
821
  }
788
822
 
@@ -839,10 +873,7 @@ class BaseApp(Application):
839
873
  ]
840
874
  )
841
875
 
842
- def update_style(
843
- self,
844
- query: TerminalQuery | Setting | None = None,
845
- ) -> None:
876
+ def update_style(self, query: Setting | None = None) -> None:
846
877
  """Update the application's style when the syntax theme is changed."""
847
878
  self.renderer.style = self.create_merged_style()
848
879
 
@@ -893,406 +924,17 @@ class BaseApp(Application):
893
924
  # print(task.get_loop())
894
925
  # await asyncio.wait([task])
895
926
 
896
- # ################################### Commands ####################################
897
-
898
- @staticmethod
899
- @add_cmd()
900
- def _quit() -> None:
901
- """Quit euporie."""
902
- get_app().exit()
903
-
904
- @staticmethod
905
- @add_cmd(
906
- name="close-tab",
907
- filter=tab_has_focus,
908
- menu_title="Close File",
909
- )
910
- def _close_tab() -> None:
911
- """Close the current tab."""
912
- get_app().close_tab()
913
-
914
- @staticmethod
915
- @add_cmd(
916
- filter=tab_has_focus,
917
- )
918
- def _next_tab() -> None:
919
- """Switch to the next tab."""
920
- get_app().tab_idx += 1
921
-
922
- @staticmethod
923
- @add_cmd(
924
- filter=tab_has_focus,
925
- )
926
- def _previous_tab() -> None:
927
- """Switch to the previous tab."""
928
- get_app().tab_idx -= 1
929
-
930
- @staticmethod
931
- @add_cmd(
932
- filter=~buffer_has_focus,
933
- )
934
- def _focus_next() -> None:
935
- """Focus the next control."""
936
- get_app().layout.focus_next()
937
-
938
- @staticmethod
939
- @add_cmd(
940
- filter=~buffer_has_focus,
941
- )
942
- def _focus_previous() -> None:
943
- """Focus the previous control."""
944
- get_app().layout.focus_previous()
945
-
946
- @staticmethod
947
- @add_cmd()
948
- def _clear_screen() -> None:
949
- """Clear the screen."""
950
- get_app().renderer.clear()
951
-
952
- # ################################### Settings ####################################w
953
-
954
- add_setting(
955
- name="files",
956
- default=[],
957
- flags=["files"],
958
- nargs="*",
959
- type_=UPath,
960
- help_="List of file names to open",
961
- schema={
962
- "type": "array",
963
- "items": {
964
- "description": "File path",
965
- "type": "string",
966
- },
967
- },
968
- description="""
969
- A list of file paths to open when euporie is launched.
970
- """,
971
- )
972
-
973
- add_setting(
974
- name="edit_mode",
975
- flags=["--edit-mode"],
976
- type_=str,
977
- choices=["micro", "emacs", "vi"],
978
- title="Editor key bindings",
979
- help_="Key-binding mode for text editing",
980
- default="micro",
981
- description="""
982
- Key binding style to use when editing cells.
983
- """,
984
- )
985
-
986
- add_setting(
987
- name="tab_size",
988
- flags=["--tab-size"],
989
- type_=int,
990
- help_="Spaces per indentation level",
991
- default=4,
992
- schema={
993
- "minimum": 1,
994
- },
995
- description="""
996
- The number of spaces to use per indentation level. Should be set to 4.
997
- """,
998
- )
999
-
1000
- add_setting(
1001
- name="terminal_polling_interval",
1002
- flags=["--terminal-polling-interval"],
1003
- type_=float,
1004
- help_="Time between terminal colour queries",
1005
- default=0.0,
1006
- schema={
1007
- "min": 0.0,
1008
- },
1009
- description="""
1010
- Determine how frequently the terminal should be polled for changes to the
1011
- background / foreground colours. Set to zero to disable terminal polling.
1012
- """,
1013
- )
1014
-
1015
- add_setting(
1016
- name="formatters",
1017
- flags=["--formatters"],
1018
- type_=json.loads,
1019
- help_="List of external code formatters",
1020
- default=[
1021
- # {"command": ["ruff", "format", "-"], "languages": ["python"]},
1022
- # {"command": ["black", "-"], "languages": ["python"]},
1023
- # {"command": ["isort", "-"], "languages": ["python"]},
1024
- ],
1025
- action="append",
1026
- schema={
1027
- "type": "array",
1028
- "items": {
1029
- "type": "object",
1030
- "properties": {
1031
- "command": {
1032
- "type": "array",
1033
- "items": [{"type": "string"}],
1034
- },
1035
- "languages": {
1036
- "type": "array",
1037
- "items": [{"type": "string", "unique": True}],
1038
- },
1039
- },
1040
- "required": ["command", "languages"],
1041
- },
1042
- },
1043
- description="""
1044
- An array listing languages and commands of formatters to use for
1045
- reformatting code cells. The command is an array of the command any any
1046
- arguments. Code to be formatted is pass in via the standard input, and
1047
- replaced with the standard output.
1048
-
1049
- e.g.
1050
-
1051
- [
1052
- {"command": ["ruff", "format", "-"], "languages": ["python"]},
1053
- {"command": ["black", "-"], "languages": ["python"]},
1054
- {"command": ["isort", "-"], "languages": ["python"]}
1055
- ]
1056
- """,
1057
- )
1058
-
1059
- add_setting(
1060
- name="syntax_theme",
1061
- flags=["--syntax-theme"],
1062
- type_=str,
1063
- help_="Syntax highlighting theme",
1064
- default="euporie",
1065
- schema={
1066
- # Do not want to print all theme names in `--help` screen as it looks messy
1067
- # so we only add them in the scheme, not as setting choices
1068
- "enum": list(pygments_styles.keys()),
1069
- },
1070
- description="""
1071
- The name of the pygments style to use for syntax highlighting.
1072
- """,
1073
- )
1074
-
1075
- add_setting(
1076
- name="color_depth",
1077
- flags=["--color-depth"],
1078
- type_=int,
1079
- choices=[1, 4, 8, 24],
1080
- default=None,
1081
- help_="The color depth to use",
1082
- description="""
1083
- The number of bits to use to represent colors displayable on the screen.
1084
- If set to None, the supported color depth of the terminal will be detected
1085
- automatically.
1086
- """,
1087
- )
1088
-
1089
- add_setting(
1090
- name="multiplexer_passthrough",
1091
- flags=["--multiplexer-passthrough"],
1092
- type_=bool,
1093
- help_="Use passthrough from within terminal multiplexers",
1094
- default=False,
1095
- hidden=~in_mplex,
1096
- description="""
1097
- If set and euporie is running inside a terminal multiplexer
1098
- (:program:`screen` or :program:`tmux`), then certain escape sequences
1099
- will be passed-through the multiplexer directly to the terminal.
1100
-
1101
- This affects things such as terminal color detection and graphics display.
1102
-
1103
- for tmux, you will also need to ensure that ``allow-passthrough`` is set to
1104
- ``on`` in your :program:`tmux` configuration.
1105
-
1106
- .. warning::
1107
-
1108
- Terminal graphics in :program:`tmux` is experimental, and is not
1109
- guaranteed to work. Use at your own risk!
1110
-
1111
- .. note::
1112
- As of version :command:`tmux` version ``3.4`` sixel graphics are
1113
- supported, which may result in better terminal graphics then using
1114
- multiplexer passthrough.
1115
- """,
1116
- )
1117
-
1118
- add_setting(
1119
- name="color_scheme",
1120
- flags=["--color-scheme"],
1121
- type_=str,
1122
- choices=["default", "inverse", "light", "dark", "black", "white", "custom"],
1123
- help_="The color scheme to use",
1124
- default="default",
1125
- description="""
1126
- The color scheme to use: `auto` means euporie will try to use your
1127
- terminal's color scheme, `light` means black text on a white background,
1128
- and `dark` means white text on a black background.
1129
- """,
1130
- )
1131
-
1132
- add_setting(
1133
- name="custom_background_color",
1134
- flags=["--custom-background-color", "--custom-bg-color", "--bg"],
1135
- type_=str,
1136
- help_='Background color for "Custom" color theme',
1137
- default="#073642",
1138
- schema={
1139
- "maxLength": 7,
1140
- },
1141
- description="""
1142
- The hex code of the color to use for the background in the "Custom" color
1143
- scheme.
1144
- """,
1145
- )
1146
-
1147
- add_setting(
1148
- name="custom_foreground_color",
1149
- flags=["--custom-foreground-color", "--custom-fg-color", "--fg"],
1150
- type_=str,
1151
- help_='Foreground color for "Custom" color theme',
1152
- default="#839496",
1153
- schema={
1154
- "maxLength": 7,
1155
- },
1156
- description="""
1157
- The hex code of the color to use for the foreground in the "Custom" color
1158
- scheme.
1159
- """,
1160
- )
1161
-
1162
- add_setting(
1163
- name="accent_color",
1164
- flags=["--accent-color"],
1165
- type_=str,
1166
- help_="Accent color to use in the app",
1167
- default="ansiblue",
1168
- description="""
1169
- The hex code of a color to use for the accent color in the application.
1170
- """,
1171
- )
1172
-
1173
- add_setting(
1174
- name="key_bindings",
1175
- flags=["--key-bindings"],
1176
- type_=json.loads,
1177
- help_="Additional key binding definitions",
1178
- default={},
1179
- description="""
1180
- A mapping of component names to mappings of command name to key-binding lists.
1181
- """,
1182
- schema={
1183
- "type": "object",
1184
- },
1185
- )
1186
-
1187
- add_setting(
1188
- name="graphics",
1189
- flags=["--graphics"],
1190
- choices=["none", "sixel", "kitty", "iterm"],
1191
- type_=str,
1192
- default=None,
1193
- help_="The preferred graphics protocol",
1194
- description="""
1195
- The graphics protocol to use, if supported by the terminal.
1196
- If set to "none", terminal graphics will not be used.
1197
- """,
1198
- )
1199
-
1200
- add_setting(
1201
- name="force_graphics",
1202
- flags=["--force-graphics"],
1203
- type_=bool,
1204
- default=False,
1205
- help_="Force use of specified graphics protocol",
1206
- description="""
1207
- When set to :py:const:`True`, the graphics protocol specified by the
1208
- :option:`graphics` configuration option will be used even if the terminal
1209
- does not support it.
1210
-
1211
- This is also useful if you want to use graphics in :command:`euporie-hub`.
1212
- """,
1213
- )
1214
-
1215
- add_setting(
1216
- name="enable_language_servers",
1217
- flags=["--enable-language-servers", "--lsp"],
1218
- menu_title="Language servers",
1219
- type_=bool,
1220
- default=False,
1221
- help_="Enable language server support",
1222
- description="""
1223
- When set to :py:const:`True`, language servers will be used for liniting,
1224
- code inspection, and code formatting.
1225
-
1226
- Additional language servers can be added using the
1227
- :option:`language-servers` option.
1228
- """,
1229
- )
1230
-
1231
- add_setting(
1232
- name="language_servers",
1233
- flags=["--language-servers"],
1234
- type_=json.loads,
1235
- help_="Language server configurations",
1236
- default={},
1237
- schema={
1238
- "type": "object",
1239
- "items": {
1240
- "type": "object",
1241
- "patternProperties": {
1242
- "^[0-9]+$": {
1243
- "type": "object",
1244
- "properties": {
1245
- "command": {
1246
- "type": "array",
1247
- "items": [{"type": "string"}],
1248
- },
1249
- "language": {
1250
- "type": "array",
1251
- "items": [{"type": "string", "unique": True}],
1252
- },
1253
- },
1254
- "required": ["command"],
1255
- }
1256
- },
1257
- },
1258
- },
1259
- description="""
1260
- Additional language servers can be defined here, e.g.:
1261
-
1262
- {
1263
- "ruff": {"command": ["ruff-lsp"], "languages": ["python"]},
1264
- "pylsp": {"command": ["pylsp"], "languages": ["python"]},
1265
- "typos": {"command": ["typos-lsp"], "languages": []}
1266
- }
1267
-
1268
- The following properties are required:
1269
- - The name to be given to the the language server, must be unique
1270
- - The command list consists of the process to launch, followed by any
1271
- command line arguments
1272
- - A list of language the language server supports. If no languages are
1273
- given, the language server will be used for documents of any language.
1274
-
1275
- To disable one of the default language servers, its name can be set to an
1276
- empty dictionary. For example, the following would disable the awk language
1277
- server:
1278
-
1279
- {
1280
- "awk-language-server": {},
1281
- }
1282
- """,
1283
- )
1284
-
1285
927
  # ################################# Key Bindings ##################################
1286
928
 
1287
929
  register_bindings(
1288
930
  {
1289
- "euporie.core.app.BaseApp": {
931
+ "euporie.core.app.app:BaseApp": {
1290
932
  "quit": ["c-q", "<sigint>"],
1291
933
  "close-tab": "c-w",
1292
934
  "next-tab": "c-pagedown",
1293
935
  "previous-tab": "c-pageup",
1294
- "focus-next": "s-tab",
1295
- "focus-previous": "tab",
936
+ "focus-next": "tab",
937
+ "focus-previous": "s-tab",
1296
938
  "clear-screen": "c-l",
1297
939
  }
1298
940
  }