euporie 2.8.5__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 (38) hide show
  1. euporie/core/__init__.py +1 -1
  2. euporie/core/__main__.py +2 -2
  3. euporie/core/_settings.py +7 -2
  4. euporie/core/app/_commands.py +26 -1
  5. euporie/core/app/_settings.py +34 -4
  6. euporie/core/app/app.py +18 -11
  7. euporie/core/bars/command.py +44 -21
  8. euporie/core/commands.py +0 -16
  9. euporie/core/filters.py +32 -9
  10. euporie/core/format.py +2 -3
  11. euporie/core/graphics.py +191 -31
  12. euporie/core/layout/scroll.py +35 -33
  13. euporie/core/log.py +1 -4
  14. euporie/core/path.py +61 -13
  15. euporie/core/tabs/__init__.py +2 -4
  16. euporie/core/tabs/base.py +73 -7
  17. euporie/core/tabs/kernel.py +2 -3
  18. euporie/core/tabs/notebook.py +14 -54
  19. euporie/core/utils.py +1 -18
  20. euporie/core/widgets/cell.py +1 -1
  21. euporie/core/widgets/dialog.py +32 -3
  22. euporie/core/widgets/display.py +2 -2
  23. euporie/core/widgets/menu.py +1 -1
  24. euporie/notebook/tabs/display.py +2 -2
  25. euporie/notebook/tabs/edit.py +8 -43
  26. euporie/notebook/tabs/json.py +2 -2
  27. euporie/web/__init__.py +1 -0
  28. euporie/web/tabs/__init__.py +14 -0
  29. euporie/web/tabs/web.py +10 -4
  30. euporie/web/widgets/__init__.py +1 -0
  31. euporie/web/widgets/webview.py +2 -4
  32. {euporie-2.8.5.dist-info → euporie-2.8.6.dist-info}/METADATA +4 -2
  33. {euporie-2.8.5.dist-info → euporie-2.8.6.dist-info}/RECORD +38 -35
  34. {euporie-2.8.5.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
  35. {euporie-2.8.5.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
  36. {euporie-2.8.5.dist-info → euporie-2.8.6.dist-info}/WHEEL +0 -0
  37. {euporie-2.8.5.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +0 -0
  38. {euporie-2.8.5.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +0 -0
euporie/core/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """This package defines the euporie application and its components."""
2
2
 
3
3
  __app_name__ = "euporie"
4
- __version__ = "2.8.5"
4
+ __version__ = "2.8.6"
5
5
  __logo__ = "⚈"
6
6
  __strapline__ = "Jupyter in the terminal"
7
7
  __author__ = "Josiah Outram Halstead"
euporie/core/__main__.py CHANGED
@@ -7,13 +7,13 @@ from importlib.metadata import entry_points
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  if TYPE_CHECKING:
10
- from importlib.metadata import EntryPoint, EntryPoints, SelectableGroups
10
+ from importlib.metadata import EntryPoint, EntryPoints
11
11
 
12
12
 
13
13
  @cache
14
14
  def available_apps() -> dict[str, EntryPoint]:
15
15
  """Return a list of loadable euporie apps."""
16
- eps: dict | SelectableGroups | EntryPoints
16
+ eps: dict | EntryPoints
17
17
  try:
18
18
  eps = entry_points(group="euporie.apps")
19
19
  except TypeError:
euporie/core/_settings.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Defines core settings."""
2
2
 
3
+ import json
4
+
3
5
  from euporie.core import __version__
4
6
  from euporie.core.config import add_setting
5
7
 
@@ -73,8 +75,11 @@ add_setting(
73
75
  name="log_config",
74
76
  group="euporie.core.log",
75
77
  flags=["--log-config"],
76
- type_=str,
77
- default="{}",
78
+ type_=json.loads,
79
+ default={},
80
+ schema={
81
+ "type": "object",
82
+ },
78
83
  title="additional logging configuration",
79
84
  help_="Additional logging configuration",
80
85
  description="""
@@ -8,7 +8,7 @@ from prompt_toolkit.filters import buffer_has_focus
8
8
 
9
9
  from euporie.core.app.current import get_app
10
10
  from euporie.core.commands import add_cmd
11
- from euporie.core.filters import tab_has_focus
11
+ from euporie.core.filters import tab_has_focus, tab_type_has_focus
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from prompt_toolkit.key_binding.key_processor import KeyPressEvent
@@ -20,6 +20,14 @@ def _quit() -> None:
20
20
  get_app().exit()
21
21
 
22
22
 
23
+ @add_cmd(aliases=["q!"])
24
+ def _force_quit() -> None:
25
+ """Quit euporie without saving any changes."""
26
+ from prompt_toolkit.application.application import Application
27
+
28
+ Application.exit(get_app())
29
+
30
+
23
31
  @add_cmd(aliases=["wq", "x"])
24
32
  def _save_and_quit(event: KeyPressEvent) -> None:
25
33
  """Save the current tab then quits euporie."""
@@ -68,3 +76,20 @@ def _focus_previous() -> None:
68
76
  def _clear_screen() -> None:
69
77
  """Clear the screen."""
70
78
  get_app().renderer.clear()
79
+
80
+
81
+ @add_cmd(hidden=True, aliases=[""])
82
+ def _go_to(event: KeyPressEvent) -> None:
83
+ """Go to a line or cell by number."""
84
+ try:
85
+ idx = int(event._arg or "") - 1
86
+ except (ValueError, TypeError):
87
+ return
88
+ if buffer_has_focus():
89
+ buffer = get_app().current_buffer
90
+ buffer.cursor_position = len("".join(buffer.text.splitlines(True)[:idx]))
91
+ elif tab_type_has_focus("euporie.notebook.tabs.notebook:Notebook")():
92
+ from euporie.notebook.tabs.notebook import Notebook
93
+
94
+ if isinstance(nb := get_app().tab, Notebook):
95
+ nb.select(idx)
@@ -140,6 +140,8 @@ add_setting(
140
140
 
141
141
  e.g.
142
142
 
143
+ .. code-block:: json
144
+
143
145
  [
144
146
  {"command": ["ruff", "format", "-"], "languages": ["python"]},
145
147
  {"command": ["black", "-"], "languages": ["python"]},
@@ -281,6 +283,30 @@ add_setting(
281
283
  """,
282
284
  )
283
285
 
286
+ add_setting(
287
+ name="custom_styles",
288
+ group="euporie.core.style",
289
+ flags=["--custom-styles"],
290
+ type_=json.loads,
291
+ default={},
292
+ schema={
293
+ "type": "object",
294
+ },
295
+ help_="Additional style settings",
296
+ description="""
297
+ A JSON object mapping style names to prompt-toolkit style values.
298
+
299
+ The style keys used in euporie can be found in :py:func:`euporie.core.style.build_style`.
300
+
301
+ e.g.:
302
+
303
+ .. code-block:: json
304
+
305
+ { "cell input prompt":"fg:purple", "cell output prompt": "fg:green" }
306
+
307
+ """,
308
+ )
309
+
284
310
  add_setting(
285
311
  name="key_bindings",
286
312
  group="euporie.core.app.app",
@@ -288,19 +314,19 @@ add_setting(
288
314
  type_=json.loads,
289
315
  help_="Additional key binding definitions",
290
316
  default={},
291
- description="""
292
- A mapping of component names to mappings of command name to key-binding lists.
293
- """,
294
317
  schema={
295
318
  "type": "object",
296
319
  },
320
+ description="""
321
+ A mapping of component names to mappings of command name to key-binding lists.
322
+ """,
297
323
  )
298
324
 
299
325
  add_setting(
300
326
  name="graphics",
301
327
  group="euporie.core.app.app",
302
328
  flags=["--graphics"],
303
- choices=["none", "sixel", "kitty", "iterm"],
329
+ choices=["none", "sixel", "kitty", "kitty-unicode", "iterm"],
304
330
  type_=str,
305
331
  default=None,
306
332
  help_="The preferred graphics protocol",
@@ -375,6 +401,8 @@ add_setting(
375
401
  description="""
376
402
  Additional language servers can be defined here, e.g.:
377
403
 
404
+ .. code-block:: json
405
+
378
406
  {
379
407
  "ruff": {"command": ["ruff-lsp"], "languages": ["python"]},
380
408
  "pylsp": {"command": ["pylsp"], "languages": ["python"]},
@@ -392,6 +420,8 @@ add_setting(
392
420
  empty dictionary. For example, the following would disable the awk language
393
421
  server:
394
422
 
423
+ .. code-block:: json
424
+
395
425
  {
396
426
  "awk-language-server": {},
397
427
  }
euporie/core/app/app.py CHANGED
@@ -791,6 +791,13 @@ class BaseApp(ConfigurableApp, Application, ABC):
791
791
  syntax_theme = "tango" if self.color_palette.bg.is_light else "euporie"
792
792
  return syntax_theme
793
793
 
794
+ base_styles = (
795
+ Style(MIME_STYLE),
796
+ Style(HTML_STYLE),
797
+ Style(LOG_STYLE),
798
+ Style(IPYWIDGET_STYLE),
799
+ )
800
+
794
801
  def create_merged_style(self) -> BaseStyle:
795
802
  """Generate a new merged style for the application.
796
803
 
@@ -801,6 +808,11 @@ class BaseApp(ConfigurableApp, Application, ABC):
801
808
  Return a combined style to use for the application
802
809
 
803
810
  """
811
+ styles: list[BaseStyle] = [
812
+ style_from_pygments_cls(get_style_by_name(self.syntax_theme)),
813
+ *self.base_styles,
814
+ ]
815
+
804
816
  # Get foreground and background colors based on the configured colour scheme
805
817
  theme_colors: dict[str, dict[str, str]] = {
806
818
  "default": {},
@@ -844,7 +856,7 @@ class BaseApp(ConfigurableApp, Application, ABC):
844
856
  )
845
857
 
846
858
  # Build app style
847
- app_style = build_style(cp)
859
+ styles.append(build_style(cp))
848
860
 
849
861
  # Apply style transformations based on the configured color scheme
850
862
  self.style_transformation = merge_style_transformations(
@@ -862,16 +874,11 @@ class BaseApp(ConfigurableApp, Application, ABC):
862
874
  ]
863
875
  )
864
876
 
865
- return merge_styles(
866
- [
867
- style_from_pygments_cls(get_style_by_name(self.syntax_theme)),
868
- Style(MIME_STYLE),
869
- Style(HTML_STYLE),
870
- Style(LOG_STYLE),
871
- Style(IPYWIDGET_STYLE),
872
- app_style,
873
- ]
874
- )
877
+ # Add user style customizations
878
+ if custom_style_dict := self.config.custom_styles:
879
+ styles.append(Style.from_dict(custom_style_dict))
880
+
881
+ return merge_styles(styles)
875
882
 
876
883
  def update_style(self, query: Setting | None = None) -> None:
877
884
  """Update the application's style when the syntax theme is changed."""
@@ -3,14 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import re
7
+ from functools import lru_cache
6
8
  from typing import TYPE_CHECKING
7
9
 
8
10
  from prompt_toolkit.buffer import Buffer
9
11
  from prompt_toolkit.completion.base import Completer, Completion
10
- from prompt_toolkit.filters import (
11
- buffer_has_focus,
12
- has_focus,
13
- )
12
+ from prompt_toolkit.filters import buffer_has_focus, has_focus, vi_navigation_mode
13
+ from prompt_toolkit.key_binding.vi_state import InputMode
14
14
  from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window
15
15
  from prompt_toolkit.layout.controls import (
16
16
  BufferControl,
@@ -39,6 +39,23 @@ if TYPE_CHECKING:
39
39
  log = logging.getLogger(__name__)
40
40
 
41
41
 
42
+ @lru_cache
43
+ def _parse_cmd(text: str) -> tuple[Command | None, str]:
44
+ """Parse a command line to command and arguments.
45
+
46
+ Command names cannot start with digits, so lines staring with digits have an empty
47
+ command string (this is used for the go-to-cell/go-to-line shortcuts).
48
+ """
49
+ if match := re.fullmatch(r"^(?P<cmd>[^\d][^\s]*|)\s*(?P<args>.*)$", text):
50
+ cmd, args = match.groups()
51
+ else:
52
+ cmd, args = "", ""
53
+ try:
54
+ return get_cmd(cmd), args
55
+ except KeyError:
56
+ return None, args
57
+
58
+
42
59
  class CommandCompleter(Completer):
43
60
  """Completer of commands."""
44
61
 
@@ -49,7 +66,11 @@ class CommandCompleter(Completer):
49
66
  prefix = document.text
50
67
  found_so_far: set[Command] = set()
51
68
  for alias, command in commands.items():
52
- if alias.startswith(prefix) and command not in found_so_far:
69
+ if (
70
+ alias.startswith(prefix)
71
+ and command not in found_so_far
72
+ and not command.hidden()
73
+ ):
53
74
  yield Completion(
54
75
  command.name,
55
76
  start_position=-len(prefix),
@@ -105,21 +126,18 @@ class CommandBar:
105
126
 
106
127
  def _validate(self, text: str) -> bool:
107
128
  """Verify that a valid command has been entered."""
108
- cmd, _, args = text.partition(" ")
109
- try:
110
- get_cmd(cmd)
111
- except KeyError:
112
- return False
113
- else:
114
- return True
129
+ cmd, _args = _parse_cmd(text)
130
+ return bool(cmd)
115
131
 
116
132
  def _accept(self, buffer: Buffer) -> bool:
117
133
  """Return value determines if the text is kept."""
118
- # TODO - lookup and run command with args
119
- get_app().layout.focus_last()
134
+ app = get_app()
135
+ app.vi_state.input_mode = InputMode.NAVIGATION
136
+ app.layout.focus_last()
120
137
  text = buffer.text.strip()
121
- cmd, _, args = text.partition(" ")
122
- get_cmd(cmd).run(args)
138
+ cmd, args = _parse_cmd(text)
139
+ if cmd:
140
+ cmd.run(args)
123
141
  return False
124
142
 
125
143
  def __pt_container__(self) -> Container:
@@ -135,25 +153,28 @@ class CommandBar:
135
153
  "activate-command-bar-shell-alt": "A-!",
136
154
  },
137
155
  "euporie.core.bars.command:CommandBar": {
138
- "deactivate-command-bar": "escape",
156
+ "deactivate-command-bar": ["escape", "c-c"],
139
157
  },
140
158
  }
141
159
  )
142
160
 
143
161
  @staticmethod
144
162
  @add_cmd(name="activate-command-bar-alt", hidden=True)
145
- @add_cmd(filter=~buffer_has_focus)
163
+ @add_cmd(filter=~buffer_has_focus | vi_navigation_mode)
146
164
  def _activate_command_bar(event: KeyPressEvent) -> None:
147
165
  """Enter command mode."""
148
166
  event.app.layout.focus(COMMAND_BAR_BUFFER)
167
+ event.app.vi_state.input_mode = InputMode.INSERT
149
168
 
150
169
  @staticmethod
151
170
  @add_cmd(filter=~buffer_has_focus)
152
171
  @add_cmd(name="activate-command-bar-shell-alt", hidden=True)
153
172
  def _activate_command_bar_shell(event: KeyPressEvent) -> None:
154
173
  """Enter command mode."""
155
- layout = event.app.layout
174
+ app = event.app
175
+ layout = app.layout
156
176
  layout.focus(COMMAND_BAR_BUFFER)
177
+ app.vi_state.input_mode = InputMode.INSERT
157
178
  if isinstance(control := layout.current_control, BufferControl):
158
179
  buffer = control.buffer
159
180
  buffer.text = "shell "
@@ -163,12 +184,14 @@ class CommandBar:
163
184
  @add_cmd(hidden=True)
164
185
  def _deactivate_command_bar(event: KeyPressEvent) -> None:
165
186
  """Exit command mode."""
166
- layout = event.app.layout
187
+ app = event.app
188
+ layout = app.layout
167
189
  layout.focus(COMMAND_BAR_BUFFER)
168
190
  if isinstance(control := layout.current_control, BufferControl):
191
+ app.vi_state.input_mode = InputMode.NAVIGATION
169
192
  buffer = control.buffer
170
193
  buffer.reset()
171
- event.app.layout.focus_previous()
194
+ app.layout.focus_previous()
172
195
 
173
196
  @staticmethod
174
197
  @add_cmd(aliases=["shell"])
euporie/core/commands.py CHANGED
@@ -191,22 +191,6 @@ class Command:
191
191
  return format_keys([self.keys[0]])[0]
192
192
  return ""
193
193
 
194
- @property
195
- def menu_handler(self) -> Callable[[], None]:
196
- """Return a menu handler for the command."""
197
- handler = self.handler
198
- if isawaitable(handler):
199
-
200
- def _menu_handler() -> None:
201
- task = cast("CommandHandlerNoArgs", handler)()
202
- task = cast("Coroutine[Any, Any, None]", task)
203
- if task is not None:
204
- get_app().create_background_task(task)
205
-
206
- return _menu_handler
207
- else:
208
- return cast("Callable[[], None]", handler)
209
-
210
194
  @property
211
195
  def menu(self) -> MenuItem:
212
196
  """Return a menu item for the command."""
euporie/core/filters.py CHANGED
@@ -133,6 +133,38 @@ def tab_has_focus() -> bool:
133
133
  return get_app().tab is not None
134
134
 
135
135
 
136
+ @Condition
137
+ def kernel_tab_has_focus() -> bool:
138
+ """Determine if there is a focused kernel tab."""
139
+ from euporie.core.app.current import get_app
140
+ from euporie.core.tabs.kernel import KernelTab
141
+
142
+ return isinstance(get_app().tab, KernelTab)
143
+
144
+
145
+ @cache
146
+ def tab_type_has_focus(tab_class_path: str) -> Condition:
147
+ """Determine if the focused tab is of a particular type."""
148
+ from pkgutil import resolve_name
149
+
150
+ from euporie.core.app.current import get_app
151
+
152
+ tab_class = cache(resolve_name)
153
+
154
+ return Condition(lambda: isinstance(get_app().tab, tab_class(tab_class_path)))
155
+
156
+
157
+ @Condition
158
+ def tab_can_save() -> bool:
159
+ """Determine if the current tab can save it's contents."""
160
+ from euporie.core.app.current import get_app
161
+ from euporie.core.tabs.base import Tab
162
+
163
+ return (
164
+ tab := get_app().tab
165
+ ) is not None and tab.__class__.write_file != Tab.write_file
166
+
167
+
136
168
  @Condition
137
169
  def pager_has_focus() -> bool:
138
170
  """Determine if there is a currently focused notebook."""
@@ -321,15 +353,6 @@ def multiple_cells_selected() -> bool:
321
353
  return False
322
354
 
323
355
 
324
- @Condition
325
- def kernel_tab_has_focus() -> bool:
326
- """Determine if there is a focused kernel tab."""
327
- from euporie.core.app.current import get_app
328
- from euporie.core.tabs.kernel import KernelTab
329
-
330
- return isinstance(get_app().tab, KernelTab)
331
-
332
-
333
356
  def scrollable(window: Window) -> Filter:
334
357
  """Return a filter which indicates if a window is scrollable."""
335
358
  return Condition(
euporie/core/format.py CHANGED
@@ -110,11 +110,10 @@ class LspFormatter(Formatter):
110
110
  range_ = change.get("range", {})
111
111
  start = range_.get("start", {})
112
112
  start_line = start.get("line", 0)
113
- start_char = start.get("char", 0)
113
+ start_char = start.get("character", 0)
114
114
  end = range_.get("end", {})
115
115
  end_line = end.get("line", 0)
116
- end_char = end.get("char", 0)
117
-
116
+ end_char = end.get("character", 0)
118
117
  segment = range_to_slice(
119
118
  start_line, start_char, end_line, end_char, text
120
119
  )