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
@@ -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 (
@@ -52,18 +54,14 @@ from prompt_toolkit.styles import (
52
54
  style_from_pygments_cls,
53
55
  )
54
56
  from prompt_toolkit.utils import Event
55
- from pygments.styles import STYLE_MAP as pygments_styles
56
- from pygments.styles import get_style_by_name
57
- from upath import UPath
58
57
 
59
- from euporie.core.clipboard import ConfiguredClipboard
60
- from euporie.core.commands import add_cmd
61
- 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
62
61
  from euporie.core.convert.mime import get_mime
63
- from euporie.core.current import get_app
64
- from euporie.core.filters import in_mplex, insert_mode, replace_mode, tab_has_focus
62
+ from euporie.core.filters import has_toolbar
65
63
  from euporie.core.format import CliFormatter
66
- from euporie.core.io import Vt100_Output, Vt100Parser
64
+ from euporie.core.io import COLOR_DEPTHS, Vt100_Output, Vt100Parser
67
65
  from euporie.core.key_binding.key_processor import KeyProcessor
68
66
  from euporie.core.key_binding.micro_state import MicroState
69
67
  from euporie.core.key_binding.registry import (
@@ -84,8 +82,8 @@ from euporie.core.style import (
84
82
  MIME_STYLE,
85
83
  ColorPalette,
86
84
  build_style,
85
+ get_style_by_name,
87
86
  )
88
- from euporie.core.terminal import TerminalInfo
89
87
  from euporie.core.utils import ChainedList
90
88
  from euporie.core.widgets.decor import Shadow
91
89
  from euporie.core.widgets.menu import CompletionsMenu
@@ -94,84 +92,38 @@ if TYPE_CHECKING:
94
92
  from asyncio import AbstractEventLoop
95
93
  from pathlib import Path
96
94
  from types import FrameType
97
- from typing import Any, Callable, TypeVar
95
+ from typing import Any, Callable, ClassVar, TypeVar
98
96
 
99
97
  # from prompt_toolkit.application import _AppResult
100
- from prompt_toolkit.clipboard import Clipboard
101
98
  from prompt_toolkit.contrib.ssh import PromptToolkitSSHSession
102
- from prompt_toolkit.enums import EditingMode
103
99
  from prompt_toolkit.filters import Filter, FilterOrBool
104
100
  from prompt_toolkit.input import Input
101
+ from prompt_toolkit.layout.containers import AnyContainer
105
102
  from prompt_toolkit.layout.layout import FocusableElement
106
103
  from prompt_toolkit.layout.screen import WritePosition
107
104
  from prompt_toolkit.output import Output
108
105
 
106
+ from euporie.core.bars.command import CommandBar
107
+ from euporie.core.bars.search import SearchBar
109
108
  from euporie.core.config import Setting
110
109
  from euporie.core.format import Formatter
110
+ from euporie.core.tabs import TabRegistryEntry
111
111
  from euporie.core.tabs.base import Tab
112
- from euporie.core.terminal import TerminalQuery
113
112
  from euporie.core.widgets.dialog import Dialog
114
113
  from euporie.core.widgets.pager import Pager
115
- from euporie.core.widgets.search import SearchBar
116
114
 
117
115
  _AppResult = TypeVar("_AppResult")
118
116
 
119
117
  log = logging.getLogger(__name__)
120
118
 
121
119
 
122
- _COLOR_DEPTHS = {
123
- 1: ColorDepth.DEPTH_1_BIT,
124
- 4: ColorDepth.DEPTH_4_BIT,
125
- 8: ColorDepth.DEPTH_8_BIT,
126
- 24: ColorDepth.DEPTH_24_BIT,
127
- }
128
-
129
-
130
- class CursorConfig(CursorShapeConfig):
131
- """Determine which cursor mode to use."""
132
-
133
- def get_cursor_shape(self, app: Application[Any]) -> CursorShape:
134
- """Return the cursor shape to be used in the current state."""
135
- if isinstance(app, BaseApp) and app.config.set_cursor_shape:
136
- if insert_mode():
137
- if app.config.cursor_blink:
138
- return CursorShape.BLINKING_BEAM
139
- else:
140
- return CursorShape.BEAM
141
- elif replace_mode():
142
- if app.config.cursor_blink:
143
- return CursorShape.BLINKING_UNDERLINE
144
- else:
145
- return CursorShape.UNDERLINE
146
- return CursorShape.BLOCK
147
-
148
- # ################################### Settings ####################################w
149
-
150
- add_setting(
151
- name="set_cursor_shape",
152
- flags=["--set-cursor-shape"],
153
- type_=bool,
154
- default=True,
155
- menu_title="Change cursor shape",
156
- help_="Whether to set the shape of the cursor depending on the editing mode",
157
- description="""
158
- When set to True, the euporie will set the shape of the terminal's cursor
159
- to a beam in insert mode and and underline in replace mode when editing.
160
- """,
161
- )
162
- add_setting(
163
- name="cursor_blink",
164
- flags=["--cursor-blink"],
165
- type_=bool,
166
- default=False,
167
- help_="Whether to blink the cursor",
168
- description="""
169
- When set to True, the cursor will blink.
170
- """,
171
- )
120
+ class ExtraEditingMode(str, Enum):
121
+ """Additional editing modes."""
172
122
 
123
+ MICRO = "MICRO"
173
124
 
174
- class BaseApp(Application):
125
+
126
+ class BaseApp(ConfigurableApp, Application, ABC):
175
127
  """All euporie apps.
176
128
 
177
129
  The base euporie application class.
@@ -180,12 +132,10 @@ class BaseApp(Application):
180
132
  wide methods can be easily added.
181
133
  """
182
134
 
183
- name: str
184
135
  color_palette: ColorPalette
185
136
  mouse_position: Point
186
137
 
187
- config = Config()
188
- log_stdout_level: str = "CRITICAL"
138
+ _config_defaults: ClassVar[dict[str, Any]] = {"log_level_stdout": "critical"}
189
139
 
190
140
  def __init__(
191
141
  self,
@@ -219,7 +169,10 @@ class BaseApp(Application):
219
169
  # Initialise the application
220
170
  super().__init__(
221
171
  **{
222
- "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),
223
176
  "editing_mode": self.get_edit_mode(),
224
177
  "mouse_support": True,
225
178
  "cursor": CursorConfig(),
@@ -240,14 +193,20 @@ class BaseApp(Application):
240
193
  # Contains the opened tab containers
241
194
  self.tabs: list[Tab] = []
242
195
  self.on_tabs_change = Event(self)
243
- # Holds the search bar to pass to cell inputs
196
+ # Holds the optional toolbars
244
197
  self.search_bar: SearchBar | None = None
198
+ self.command_bar: CommandBar | None = None
245
199
  # Holds the index of the current tab
246
200
  self._tab_idx = 0
247
201
  # Add state for micro key-bindings
248
202
  self.micro_state = MicroState()
249
- # Load the terminal information system
250
- 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]
251
210
  # Floats at the app level
252
211
  self.leave_graphics = to_filter(leave_graphics)
253
212
  self.graphics: WeakSet[Float] = WeakSet()
@@ -272,15 +231,16 @@ class BaseApp(Application):
272
231
  self.key_processor = KeyProcessor(_CombinedRegistry(self))
273
232
  # List of key-bindings groups to load
274
233
  self.bindings_to_load = [
275
- "euporie.core.app.BaseApp",
276
- "euporie.core.terminal.TerminalInfo",
234
+ "euporie.core.app.app:BaseApp",
235
+ "euporie.core.io.TerminalInfo",
277
236
  ]
278
237
 
279
- from euporie.core.key_binding.bindings.page_navigation import (
280
- load_page_navigation_bindings,
281
- )
238
+ if enable_page_navigation_bindings:
239
+ from euporie.core.key_binding.bindings.page_navigation import (
240
+ load_page_navigation_bindings,
241
+ )
282
242
 
283
- self._page_navigation_bindings = load_page_navigation_bindings(self.config)
243
+ self._page_navigation_bindings = load_page_navigation_bindings(self.config)
284
244
  # Allow hiding element when manually redrawing app
285
245
  self._redrawing = False
286
246
  self.redrawing = Condition(lambda: self._redrawing)
@@ -292,14 +252,17 @@ class BaseApp(Application):
292
252
  self.set_title = to_filter(set_title)
293
253
  self.title = title or self.__class__.__name__
294
254
  # Register config hooks
295
- self.config.get_item("edit_mode").event += self.update_edit_mode
296
- self.config.get_item("syntax_theme").event += self.update_style
297
- self.config.get_item("color_scheme").event += self.update_style
298
- self.config.get_item("log_level").event += lambda x: setup_logs(self.config)
299
- self.config.get_item("log_file").event += lambda x: setup_logs(self.config)
300
- self.config.get_item("log_config").event += lambda x: setup_logs(self.config)
301
- self.config.get_item("color_depth").event += lambda x: setattr(
302
- 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]
303
266
  )
304
267
  # Set up the color palette
305
268
  self.color_palette = ColorPalette()
@@ -317,6 +280,30 @@ class BaseApp(Application):
317
280
  CliFormatter(**info) for info in self.config.formatters
318
281
  ]
319
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
+
320
307
  @property
321
308
  def title(self) -> str:
322
309
  """The application's title."""
@@ -350,19 +337,11 @@ class BaseApp(Application):
350
337
 
351
338
  def pre_run(self, app: Application | None = None) -> None:
352
339
  """Call during the 'pre-run' stage of application loading."""
353
- # Determines which clipboard mechanism to use based on configuration
354
- self.clipboard: Clipboard = ConfiguredClipboard(self)
355
- # Determine what color depth to use
356
- self._color_depth = _COLOR_DEPTHS.get(
357
- self.config.color_depth, self.term_info.depth_of_color.value
358
- )
359
- # Set the application's style, and update it when the terminal responds
340
+ # Set the application's style
360
341
  self.update_style()
361
- self.term_info.colors.event += self.update_style
362
- # Load completions menu. This must be done after the app is set, because
363
- # :py:func:`get_app` is needed to access the config
342
+ # Load completions menu.
364
343
  self.menus["completions"] = Float(
365
- content=Shadow(CompletionsMenu()),
344
+ content=Shadow(CompletionsMenu(extra_filter=~has_toolbar)),
366
345
  xcursor=True,
367
346
  ycursor=True,
368
347
  )
@@ -372,6 +351,18 @@ class BaseApp(Application):
372
351
  self.layout = Layout(self.load_container(), self.focused_element)
373
352
  # Open any files we need to
374
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()
375
366
 
376
367
  async def run_async(
377
368
  self,
@@ -382,26 +373,50 @@ class BaseApp(Application):
382
373
  ) -> _AppResult:
383
374
  """Run the application."""
384
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
+
385
382
  # Load key bindings
386
383
  self.load_key_bindings()
387
- # Send queries to the terminal
388
- self.term_info.send_all()
389
- # Read responses
390
- kp = self.key_processor
391
-
392
- def read_from_input() -> None:
393
- kp.feed_multiple(self.input.read_keys())
394
384
 
395
- with self.input.raw_mode(), self.input.attach(read_from_input):
396
- # Give the terminal time to respond and allow the event loop to read
397
- # the terminal responses from the input
398
- await asyncio.sleep(0.1)
399
- 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()
400
407
 
401
408
  return await super().run_async(
402
409
  pre_run, set_exception_handler, handle_sigint, slow_callback_duration
403
410
  )
404
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
+
405
420
  @classmethod
406
421
  def load_input(cls) -> Input:
407
422
  """Create the input for this application to use.
@@ -419,12 +434,6 @@ class BaseApp(Application):
419
434
 
420
435
  input_ = IgnoredInput()
421
436
 
422
- # Use a custom vt100 parser to allow querying the terminal
423
- if parser := getattr(input_, "vt100_parser", None):
424
- setattr( # noqa B010
425
- input_, "vt100_parser", Vt100Parser(parser.feed_key_callback)
426
- )
427
-
428
437
  return input_
429
438
 
430
439
  @classmethod
@@ -461,6 +470,7 @@ class BaseApp(Application):
461
470
  from euporie.core.key_binding.bindings.basic import load_basic_bindings
462
471
  from euporie.core.key_binding.bindings.micro import load_micro_bindings
463
472
  from euporie.core.key_binding.bindings.mouse import load_mouse_bindings
473
+ from euporie.core.key_binding.bindings.terminal import load_terminal_bindings
464
474
  from euporie.core.key_binding.bindings.vi import load_vi_bindings
465
475
 
466
476
  self._default_bindings = merge_key_bindings(
@@ -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]:
@@ -735,30 +745,59 @@ class BaseApp(Application):
735
745
 
736
746
  def get_edit_mode(self) -> EditingMode:
737
747
  """Return the editing mode enum defined in the configuration."""
738
- from euporie.core.key_binding.bindings.micro import EditingMode
748
+ micro_mode = cast("EditingMode", ExtraEditingMode.MICRO)
739
749
 
740
750
  return {
741
- "micro": EditingMode.MICRO, # type: ignore
751
+ "micro": micro_mode,
742
752
  "vi": EditingMode.VI,
743
753
  "emacs": EditingMode.EMACS,
744
- }.get(
745
- str(self.config.edit_mode),
746
- EditingMode.MICRO, # type: ignore
747
- )
754
+ }.get(str(self.config.edit_mode), micro_mode)
748
755
 
749
756
  def update_edit_mode(self, setting: Setting | None = None) -> None:
750
757
  """Set the keybindings for editing mode."""
751
758
  self.editing_mode = self.get_edit_mode()
752
759
  log.debug("Editing mode set to: %s", self.editing_mode)
753
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
+
754
786
  @property
755
787
  def syntax_theme(self) -> str:
756
788
  """Calculate the current syntax theme."""
757
789
  syntax_theme = self.config.syntax_theme
758
- if syntax_theme == self.config.settings["syntax_theme"].default:
790
+ if syntax_theme == self.config.defaults.syntax_theme:
759
791
  syntax_theme = "tango" if self.color_palette.bg.is_light else "euporie"
760
792
  return syntax_theme
761
793
 
794
+ base_styles = (
795
+ Style(MIME_STYLE),
796
+ Style(HTML_STYLE),
797
+ Style(LOG_STYLE),
798
+ Style(IPYWIDGET_STYLE),
799
+ )
800
+
762
801
  def create_merged_style(self) -> BaseStyle:
763
802
  """Generate a new merged style for the application.
764
803
 
@@ -769,6 +808,11 @@ class BaseApp(Application):
769
808
  Return a combined style to use for the application
770
809
 
771
810
  """
811
+ styles: list[BaseStyle] = [
812
+ style_from_pygments_cls(get_style_by_name(self.syntax_theme)),
813
+ *self.base_styles,
814
+ ]
815
+
772
816
  # Get foreground and background colors based on the configured colour scheme
773
817
  theme_colors: dict[str, dict[str, str]] = {
774
818
  "default": {},
@@ -784,7 +828,7 @@ class BaseApp(Application):
784
828
  }
785
829
  base_colors: dict[str, str] = {
786
830
  **DEFAULT_COLORS,
787
- **self.term_info.colors.value,
831
+ **self.term_colors,
788
832
  **theme_colors.get(self.config.color_scheme, theme_colors["default"]),
789
833
  }
790
834
 
@@ -812,7 +856,7 @@ class BaseApp(Application):
812
856
  )
813
857
 
814
858
  # Build app style
815
- app_style = build_style(cp)
859
+ styles.append(build_style(cp))
816
860
 
817
861
  # Apply style transformations based on the configured color scheme
818
862
  self.style_transformation = merge_style_transformations(
@@ -830,21 +874,13 @@ class BaseApp(Application):
830
874
  ]
831
875
  )
832
876
 
833
- return merge_styles(
834
- [
835
- style_from_pygments_cls(get_style_by_name(self.syntax_theme)),
836
- Style(MIME_STYLE),
837
- Style(HTML_STYLE),
838
- Style(LOG_STYLE),
839
- Style(IPYWIDGET_STYLE),
840
- app_style,
841
- ]
842
- )
877
+ # Add user style customizations
878
+ if custom_style_dict := self.config.custom_styles:
879
+ styles.append(Style.from_dict(custom_style_dict))
843
880
 
844
- def update_style(
845
- self,
846
- query: TerminalQuery | Setting | None = None,
847
- ) -> None:
881
+ return merge_styles(styles)
882
+
883
+ def update_style(self, query: Setting | None = None) -> None:
848
884
  """Update the application's style when the syntax theme is changed."""
849
885
  self.renderer.style = self.create_merged_style()
850
886
 
@@ -895,411 +931,11 @@ class BaseApp(Application):
895
931
  # print(task.get_loop())
896
932
  # await asyncio.wait([task])
897
933
 
898
- # ################################### Commands ####################################
899
-
900
- @staticmethod
901
- @add_cmd()
902
- def _quit() -> None:
903
- """Quit euporie."""
904
- get_app().exit()
905
-
906
- @staticmethod
907
- @add_cmd(
908
- name="close-tab",
909
- filter=tab_has_focus,
910
- menu_title="Close File",
911
- )
912
- def _close_tab() -> None:
913
- """Close the current tab."""
914
- get_app().close_tab()
915
-
916
- @staticmethod
917
- @add_cmd(
918
- filter=tab_has_focus,
919
- )
920
- def _next_tab() -> None:
921
- """Switch to the next tab."""
922
- get_app().tab_idx += 1
923
-
924
- @staticmethod
925
- @add_cmd(
926
- filter=tab_has_focus,
927
- )
928
- def _previous_tab() -> None:
929
- """Switch to the previous tab."""
930
- get_app().tab_idx -= 1
931
-
932
- @staticmethod
933
- @add_cmd(
934
- filter=~buffer_has_focus,
935
- )
936
- def _focus_next() -> None:
937
- """Focus the next control."""
938
- get_app().layout.focus_next()
939
-
940
- @staticmethod
941
- @add_cmd(
942
- filter=~buffer_has_focus,
943
- )
944
- def _focus_previous() -> None:
945
- """Focus the previous control."""
946
- get_app().layout.focus_previous()
947
-
948
- @staticmethod
949
- @add_cmd()
950
- def _clear_screen() -> None:
951
- """Clear the screen."""
952
- get_app().renderer.clear()
953
-
954
- # ################################### Settings ####################################w
955
-
956
- add_setting(
957
- name="files",
958
- default=[],
959
- flags=["files"],
960
- nargs="*",
961
- type_=UPath,
962
- help_="List of file names to open",
963
- schema={
964
- "type": "array",
965
- "items": {
966
- "description": "File path",
967
- "type": "string",
968
- },
969
- },
970
- description="""
971
- A list of file paths to open when euporie is launched.
972
- """,
973
- )
974
-
975
- add_setting(
976
- name="edit_mode",
977
- flags=["--edit-mode"],
978
- type_=str,
979
- choices=["micro", "emacs", "vi"],
980
- title="Editor key bindings",
981
- help_="Key-binding mode for text editing",
982
- default="micro",
983
- description="""
984
- Key binding style to use when editing cells.
985
- """,
986
- )
987
-
988
- add_setting(
989
- name="tab_size",
990
- flags=["--tab-size"],
991
- type_=int,
992
- help_="Spaces per indentation level",
993
- default=4,
994
- schema={
995
- "minimum": 1,
996
- },
997
- description="""
998
- The number of spaces to use per indentation level. Should be set to 4.
999
- """,
1000
- )
1001
-
1002
- add_setting(
1003
- name="terminal_polling_interval",
1004
- flags=["--terminal-polling-interval"],
1005
- type_=float,
1006
- help_="Time between terminal colour queries",
1007
- default=0.0,
1008
- schema={
1009
- "min": 0.0,
1010
- },
1011
- description="""
1012
- Determine how frequently the terminal should be polled for changes to the
1013
- background / foreground colours. Set to zero to disable terminal polling.
1014
- """,
1015
- )
1016
-
1017
- add_setting(
1018
- name="formatters",
1019
- flags=["--formatters"],
1020
- type_=json.loads,
1021
- help_="List of external code formatters",
1022
- default=[
1023
- # {"command": ["ruff", "format", "-"], "languages": ["python"]},
1024
- # {"command": ["black", "-"], "languages": ["python"]},
1025
- # {"command": ["isort", "-"], "languages": ["python"]},
1026
- ],
1027
- action="append",
1028
- schema={
1029
- "type": "array",
1030
- "items": {
1031
- "type": "object",
1032
- "properties": {
1033
- "command": {
1034
- "type": "array",
1035
- "items": [{"type": "string"}],
1036
- },
1037
- "languages": {
1038
- "type": "array",
1039
- "items": [{"type": "string", "unique": True}],
1040
- },
1041
- },
1042
- "required": ["command", "languages"],
1043
- },
1044
- },
1045
- description="""
1046
- An array listing languages and commands of formatters to use for
1047
- reformatting code cells. The command is an array of the command any any
1048
- arguments. Code to be formatted is pass in via the standard input, and
1049
- replaced with the standard output.
1050
-
1051
- e.g.
1052
-
1053
- [
1054
- {"command": ["ruff", "format", "-"], "languages": ["python"]},
1055
- {"command": ["black", "-"], "languages": ["python"]},
1056
- {"command": ["isort", "-"], "languages": ["python"]}
1057
- ]
1058
- """,
1059
- )
1060
-
1061
- add_setting(
1062
- name="syntax_highlighting",
1063
- flags=["--syntax-highlighting"],
1064
- type_=bool,
1065
- help_="Syntax highlighting",
1066
- default=True,
1067
- description="""
1068
- Enable or disable syntax highlighting in code input fields.
1069
- """,
1070
- )
1071
-
1072
- add_setting(
1073
- name="syntax_theme",
1074
- flags=["--syntax-theme"],
1075
- type_=str,
1076
- help_="Syntax highlighting theme",
1077
- default="euporie",
1078
- schema={
1079
- # Do not want to print all theme names in `--help` screen as it looks messy
1080
- # so we only add them in the scheme, not as setting choices
1081
- "enum": list(pygments_styles.keys()),
1082
- },
1083
- description="""
1084
- The name of the pygments style to use for syntax highlighting.
1085
- """,
1086
- )
1087
-
1088
- add_setting(
1089
- name="color_depth",
1090
- flags=["--color-depth"],
1091
- type_=int,
1092
- choices=[1, 4, 8, 24],
1093
- default=None,
1094
- help_="The color depth to use",
1095
- description="""
1096
- The number of bits to use to represent colors displayable on the screen.
1097
- If set to None, the supported color depth of the terminal will be detected
1098
- automatically.
1099
- """,
1100
- )
1101
-
1102
- add_setting(
1103
- name="multiplexer_passthrough",
1104
- flags=["--multiplexer-passthrough"],
1105
- type_=bool,
1106
- help_="Use passthrough from within terminal multiplexers",
1107
- default=False,
1108
- hidden=~in_mplex,
1109
- description="""
1110
- If set and euporie is running inside a terminal multiplexer
1111
- (:program:`screen` or :program:`tmux`), then certain escape sequences
1112
- will be passed-through the multiplexer directly to the terminal.
1113
-
1114
- This affects things such as terminal color detection and graphics display.
1115
-
1116
- for tmux, you will also need to ensure that ``allow-passthrough`` is set to
1117
- ``on`` in your :program:`tmux` configuration.
1118
-
1119
- .. warning::
1120
-
1121
- Terminal graphics in :program:`tmux` is experimental, and is not
1122
- guaranteed to work. Use at your own risk!
1123
-
1124
- .. note::
1125
- As of version :command:`tmux` version ``3.4`` sixel graphics are
1126
- supported, which may result in better terminal graphics then using
1127
- multiplexer passthrough.
1128
- """,
1129
- )
1130
-
1131
- add_setting(
1132
- name="color_scheme",
1133
- flags=["--color-scheme"],
1134
- type_=str,
1135
- choices=["default", "inverse", "light", "dark", "black", "white", "custom"],
1136
- help_="The color scheme to use",
1137
- default="default",
1138
- description="""
1139
- The color scheme to use: `auto` means euporie will try to use your
1140
- terminal's color scheme, `light` means black text on a white background,
1141
- and `dark` means white text on a black background.
1142
- """,
1143
- )
1144
-
1145
- add_setting(
1146
- name="custom_background_color",
1147
- flags=["--custom-background-color", "--custom-bg-color", "--bg"],
1148
- type_=str,
1149
- help_='Background color for "Custom" color theme',
1150
- default="#073642",
1151
- schema={
1152
- "maxLength": 7,
1153
- },
1154
- description="""
1155
- The hex code of the color to use for the background in the "Custom" color
1156
- scheme.
1157
- """,
1158
- )
1159
-
1160
- add_setting(
1161
- name="custom_foreground_color",
1162
- flags=["--custom-foreground-color", "--custom-fg-color", "--fg"],
1163
- type_=str,
1164
- help_='Foreground color for "Custom" color theme',
1165
- default="#839496",
1166
- schema={
1167
- "maxLength": 7,
1168
- },
1169
- description="""
1170
- The hex code of the color to use for the foreground in the "Custom" color
1171
- scheme.
1172
- """,
1173
- )
1174
-
1175
- add_setting(
1176
- name="accent_color",
1177
- flags=["--accent-color"],
1178
- type_=str,
1179
- help_="Accent color to use in the app",
1180
- default="ansiblue",
1181
- description="""
1182
- The hex code of a color to use for the accent color in the application.
1183
- """,
1184
- )
1185
-
1186
- add_setting(
1187
- name="key_bindings",
1188
- flags=["--key-bindings"],
1189
- type_=json.loads,
1190
- help_="Additional key binding definitions",
1191
- default={},
1192
- description="""
1193
- A mapping of component names to mappings of command name to key-binding lists.
1194
- """,
1195
- schema={
1196
- "type": "object",
1197
- },
1198
- )
1199
-
1200
- add_setting(
1201
- name="graphics",
1202
- flags=["--graphics"],
1203
- choices=["none", "sixel", "kitty", "iterm"],
1204
- type_=str,
1205
- default=None,
1206
- help_="The preferred graphics protocol",
1207
- description="""
1208
- The graphics protocol to use, if supported by the terminal.
1209
- If set to "none", terminal graphics will not be used.
1210
- """,
1211
- )
1212
-
1213
- add_setting(
1214
- name="force_graphics",
1215
- flags=["--force-graphics"],
1216
- type_=bool,
1217
- default=False,
1218
- help_="Force use of specified graphics protocol",
1219
- description="""
1220
- When set to :py:const:`True`, the graphics protocol specified by the
1221
- :option:`graphics` configuration option will be used even if the terminal
1222
- does not support it.
1223
-
1224
- This is also useful if you want to use graphics in :command:`euporie-hub`.
1225
- """,
1226
- )
1227
-
1228
- add_setting(
1229
- name="enable_language_servers",
1230
- flags=["--enable-language-servers", "--lsp"],
1231
- menu_title="Language servers",
1232
- type_=bool,
1233
- default=False,
1234
- help_="Enable language server support",
1235
- description="""
1236
- When set to :py:const:`True`, language servers will be used for liniting,
1237
- code inspection, and code formatting.
1238
-
1239
- Additional language servers can be added using the
1240
- :option:`language-servers` option.
1241
- """,
1242
- )
1243
-
1244
- add_setting(
1245
- name="language_servers",
1246
- flags=["--language-servers"],
1247
- type_=json.loads,
1248
- help_="Language server configurations",
1249
- default={},
1250
- schema={
1251
- "type": "object",
1252
- "items": {
1253
- "type": "object",
1254
- "patternProperties": {
1255
- "^[0-9]+$": {
1256
- "type": "object",
1257
- "properties": {
1258
- "command": {
1259
- "type": "array",
1260
- "items": [{"type": "string"}],
1261
- },
1262
- "language": {
1263
- "type": "array",
1264
- "items": [{"type": "string", "unique": True}],
1265
- },
1266
- },
1267
- "required": ["command"],
1268
- }
1269
- },
1270
- },
1271
- },
1272
- description="""
1273
- Additional language servers can be defined here, e.g.:
1274
-
1275
- {
1276
- "ruff": {"command": ["ruff-lsp"], "languages": ["python"]},
1277
- "pylsp": {"command": ["pylsp"], "languages": ["python"]},
1278
- "typos": {"command": ["typos-lsp"], "languages": []}
1279
- }
1280
-
1281
- The following properties are required:
1282
- - The name to be given to the the language server, must be unique
1283
- - The command list consists of the process to launch, followed by any
1284
- command line arguments
1285
- - A list of language the language server supports. If no languages are
1286
- given, the language server will be used for documents of any language.
1287
-
1288
- To disable one of the default language servers, its name can be set to an
1289
- empty dictionary. For example, the following would disable the awk language
1290
- server:
1291
-
1292
- {
1293
- "awk-language-server": {},
1294
- }
1295
- """,
1296
- )
1297
-
1298
934
  # ################################# Key Bindings ##################################
1299
935
 
1300
936
  register_bindings(
1301
937
  {
1302
- "euporie.core.app.BaseApp": {
938
+ "euporie.core.app.app:BaseApp": {
1303
939
  "quit": ["c-q", "<sigint>"],
1304
940
  "close-tab": "c-w",
1305
941
  "next-tab": "c-pagedown",