euporie 2.8.3__py3-none-any.whl → 2.8.4__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 (48) hide show
  1. euporie/console/tabs/console.py +227 -104
  2. euporie/core/__init__.py +1 -1
  3. euporie/core/__main__.py +1 -1
  4. euporie/core/app.py +20 -18
  5. euporie/core/clipboard.py +1 -1
  6. euporie/core/comm/ipywidgets.py +5 -5
  7. euporie/core/commands.py +1 -1
  8. euporie/core/config.py +4 -4
  9. euporie/core/convert/datum.py +4 -1
  10. euporie/core/convert/registry.py +7 -2
  11. euporie/core/filters.py +3 -1
  12. euporie/core/ft/html.py +2 -4
  13. euporie/core/graphics.py +6 -6
  14. euporie/core/kernel.py +56 -32
  15. euporie/core/key_binding/bindings/__init__.py +2 -1
  16. euporie/core/key_binding/bindings/mouse.py +24 -22
  17. euporie/core/key_binding/bindings/vi.py +46 -0
  18. euporie/core/layout/cache.py +33 -23
  19. euporie/core/layout/containers.py +235 -73
  20. euporie/core/layout/decor.py +3 -3
  21. euporie/core/layout/print.py +14 -2
  22. euporie/core/layout/scroll.py +15 -21
  23. euporie/core/margins.py +59 -30
  24. euporie/core/style.py +7 -5
  25. euporie/core/tabs/base.py +32 -0
  26. euporie/core/tabs/notebook.py +6 -3
  27. euporie/core/terminal.py +12 -17
  28. euporie/core/utils.py +2 -4
  29. euporie/core/widgets/cell.py +64 -109
  30. euporie/core/widgets/dialog.py +25 -20
  31. euporie/core/widgets/file_browser.py +3 -3
  32. euporie/core/widgets/forms.py +8 -7
  33. euporie/core/widgets/layout.py +5 -5
  34. euporie/core/widgets/status.py +3 -3
  35. euporie/hub/app.py +7 -3
  36. euporie/notebook/app.py +59 -46
  37. euporie/notebook/tabs/log.py +1 -1
  38. euporie/notebook/tabs/notebook.py +5 -3
  39. euporie/preview/app.py +3 -0
  40. euporie/preview/tabs/notebook.py +9 -14
  41. euporie/web/tabs/web.py +0 -1
  42. {euporie-2.8.3.dist-info → euporie-2.8.4.dist-info}/METADATA +5 -5
  43. {euporie-2.8.3.dist-info → euporie-2.8.4.dist-info}/RECORD +48 -47
  44. {euporie-2.8.3.data → euporie-2.8.4.data}/data/share/applications/euporie-console.desktop +0 -0
  45. {euporie-2.8.3.data → euporie-2.8.4.data}/data/share/applications/euporie-notebook.desktop +0 -0
  46. {euporie-2.8.3.dist-info → euporie-2.8.4.dist-info}/WHEEL +0 -0
  47. {euporie-2.8.3.dist-info → euporie-2.8.4.dist-info}/entry_points.txt +0 -0
  48. {euporie-2.8.3.dist-info → euporie-2.8.4.dist-info}/licenses/LICENSE +0 -0
euporie/core/margins.py CHANGED
@@ -9,9 +9,11 @@ from typing import TYPE_CHECKING, cast
9
9
 
10
10
  from prompt_toolkit.data_structures import Point
11
11
  from prompt_toolkit.filters import FilterOrBool, to_filter
12
+ from prompt_toolkit.layout.containers import ScrollOffsets, WindowRenderInfo
12
13
  from prompt_toolkit.layout.controls import FormattedTextControl
13
14
  from prompt_toolkit.layout.dimension import Dimension
14
15
  from prompt_toolkit.layout.margins import Margin
16
+ from prompt_toolkit.layout.screen import WritePosition
15
17
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType
16
18
  from prompt_toolkit.mouse_events import MouseEvent as PtkMouseEvent
17
19
 
@@ -27,10 +29,10 @@ if TYPE_CHECKING:
27
29
  KeyBindingsBase,
28
30
  NotImplementedOrNone,
29
31
  )
30
- from prompt_toolkit.layout.containers import Container, WindowRenderInfo
32
+ from prompt_toolkit.layout.containers import Container
31
33
  from prompt_toolkit.layout.controls import UIContent
32
34
  from prompt_toolkit.layout.mouse_handlers import MouseHandlers
33
- from prompt_toolkit.layout.screen import Screen, WritePosition
35
+ from prompt_toolkit.layout.screen import Screen
34
36
 
35
37
  from euporie.core.diagnostics import Report
36
38
 
@@ -49,9 +51,9 @@ class ClickableMargin(Margin, metaclass=ABCMeta):
49
51
 
50
52
  write_position: WritePosition | None
51
53
 
52
- def set_write_position(self, write_position: WritePosition) -> None:
54
+ def set_margin_window(self, margin_window: Window) -> None:
53
55
  """Set the write position of the menu."""
54
- self.write_position = write_position
56
+ self.margin_window = margin_window
55
57
 
56
58
 
57
59
  class MarginContainer(Window):
@@ -65,6 +67,9 @@ class MarginContainer(Window):
65
67
  self.content = FormattedTextControl(self.create_fragments)
66
68
  self.always_hide_cursor = to_filter(True)
67
69
 
70
+ if isinstance(self.margin, ClickableMargin):
71
+ self.margin.set_margin_window(self)
72
+
68
73
  def create_fragments(self) -> StyleAndTextTuples:
69
74
  """Generate text fragments to display."""
70
75
  return self.margin.create_margin(
@@ -102,8 +107,6 @@ class MarginContainer(Window):
102
107
  ) -> None:
103
108
  """Write the actual content to the screen."""
104
109
  self.write_position = write_position
105
- if isinstance(self.margin, ClickableMargin):
106
- self.margin.set_write_position(write_position)
107
110
 
108
111
  margin_content: UIContent = self.content.create_content(
109
112
  write_position.width + 1, write_position.height
@@ -112,6 +115,21 @@ class MarginContainer(Window):
112
115
  margin_content, screen, write_position, 0, write_position.width
113
116
  )
114
117
 
118
+ self.render_info = WindowRenderInfo(
119
+ window=self,
120
+ ui_content=margin_content,
121
+ horizontal_scroll=0,
122
+ vertical_scroll=0,
123
+ window_width=write_position.width,
124
+ window_height=write_position.height,
125
+ configured_scroll_offsets=ScrollOffsets(),
126
+ visible_line_to_row_col=visible_line_to_row_col,
127
+ rowcol_to_yx=rowcol_to_yx,
128
+ x_offset=write_position.xpos,
129
+ y_offset=write_position.ypos,
130
+ wrap_lines=False,
131
+ )
132
+
115
133
  # Set mouse handlers.
116
134
  def mouse_handler(mouse_event: PtkMouseEvent) -> NotImplementedOrNone:
117
135
  """Turn screen coordinates into line coordinates."""
@@ -164,6 +182,8 @@ class MarginContainer(Window):
164
182
  handler=mouse_handler,
165
183
  )
166
184
 
185
+ screen.visible_windows_to_write_positions[self] = write_position
186
+
167
187
  def is_modal(self) -> bool:
168
188
  """When this container is modal."""
169
189
  return False
@@ -214,8 +234,7 @@ class ScrollbarMargin(ClickableMargin):
214
234
  self.thumb_top = 0.0
215
235
  self.thumb_size = 0.0
216
236
 
217
- self.window_render_info: WindowRenderInfo | None = None
218
- self.write_position: WritePosition | None = None
237
+ self.target_render_info: WindowRenderInfo | None = None
219
238
 
220
239
  def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
221
240
  """Return the scrollbar width: always 1."""
@@ -226,13 +245,11 @@ class ScrollbarMargin(ClickableMargin):
226
245
  window_render_info: WindowRenderInfo | None,
227
246
  width: int,
228
247
  height: int,
229
- margin_render_info: WindowRenderInfo | None = None,
230
248
  ) -> StyleAndTextTuples:
231
249
  """Create the margin's formatted text."""
232
250
  result: StyleAndTextTuples = []
233
251
 
234
- self.window_render_info = window_render_info
235
- self.margin_render_info = margin_render_info
252
+ self.target_render_info = window_render_info
236
253
 
237
254
  # If this is the first time the target is being drawn, it may not yet have a
238
255
  # render_info yet. Thus, invalidate the app so we can immediately redraw the
@@ -400,11 +417,11 @@ class ScrollbarMargin(ClickableMargin):
400
417
  :py:const:`None` is returned when the mouse event is handled successfully
401
418
 
402
419
  """
403
- render_info = self.window_render_info
404
- if render_info is None:
420
+ target_render_info = self.target_render_info
421
+ if target_render_info is None:
405
422
  return NotImplemented
406
423
 
407
- content_height = render_info.content_height
424
+ content_height = target_render_info.content_height
408
425
  if isinstance(mouse_event, MouseEvent):
409
426
  cell_position = mouse_event.cell_position
410
427
  else:
@@ -413,9 +430,9 @@ class ScrollbarMargin(ClickableMargin):
413
430
 
414
431
  # Handle scroll events on the scrollbar
415
432
  if mouse_event.event_type == MouseEventType.SCROLL_UP:
416
- render_info.window._scroll_up()
433
+ target_render_info.window._scroll_up()
417
434
  elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
418
- render_info.window._scroll_down()
435
+ target_render_info.window._scroll_down()
419
436
 
420
437
  # Mouse drag events
421
438
  elif self.dragging and mouse_event.event_type == MouseEventType.MOUSE_MOVE:
@@ -425,8 +442,8 @@ class ScrollbarMargin(ClickableMargin):
425
442
  + 0.5
426
443
  )
427
444
  # Use the window's current vertical scroll as it may have changed since the
428
- # window_render_info was generated
429
- window = render_info.window
445
+ # target_render_info was generated
446
+ window = target_render_info.window
430
447
  delta = window.vertical_scroll - target_scroll
431
448
 
432
449
  if isinstance(window, Window):
@@ -435,7 +452,7 @@ class ScrollbarMargin(ClickableMargin):
435
452
  func()
436
453
  # Hack to speed up scrolling on the :py:`ScrollingContainer`
437
454
  elif hasattr(window, "scrolling"):
438
- setattr(render_info.window, "scrolling", delta) # noqa: B010
455
+ setattr(target_render_info.window, "scrolling", delta) # noqa: B010
439
456
 
440
457
  # Mouse down events
441
458
  elif mouse_event.event_type == MouseEventType.MOUSE_DOWN:
@@ -443,20 +460,30 @@ class ScrollbarMargin(ClickableMargin):
443
460
  arrows = self.display_arrows()
444
461
  if arrows and int(row) == 0:
445
462
  offset = -1
446
- elif arrows and int(row) == render_info.window_height - 1:
463
+ elif arrows and int(row) == target_render_info.window_height - 1:
447
464
  offset = 1
448
465
  # Scroll up or down one page if clicking on the background
449
466
  elif row < self.thumb_top + 1 or self.thumb_top + 1 + self.thumb_size < row:
450
467
  direction = (row < (self.thumb_top + self.thumb_size // 2)) * -2 + 1
451
- offset = direction * render_info.window_height
468
+ offset = direction * target_render_info.window_height
452
469
  # We are on the scroll button - start a drag event if this is not a
453
470
  # repeated mouse event
454
471
  elif not repeated:
455
- self.dragging = True
456
- self.drag_start_scroll = render_info.vertical_scroll
457
- self.drag_start_offset = row - self.thumb_top
458
- # Restrict mouse events to this area
459
- get_app().mouse_limits = self.write_position
472
+ # Restrict mouse events to the scrollbar's area. Recalculate the area
473
+ # based on the margin's window's render_info, in case this is not the
474
+ # main screen
475
+ if margin_render_info := self.margin_window.render_info:
476
+ y_min, x_min = min(margin_render_info._rowcol_to_yx.values())
477
+ y_max, x_max = max(margin_render_info._rowcol_to_yx.values())
478
+ get_app().mouse_limits = WritePosition(
479
+ xpos=x_min,
480
+ ypos=y_min,
481
+ width=x_max - x_min + 1,
482
+ height=y_max - y_min + 1,
483
+ )
484
+ self.dragging = True
485
+ self.drag_start_scroll = target_render_info.vertical_scroll
486
+ self.drag_start_offset = row - self.thumb_top
460
487
  return None
461
488
  # Otherwise this is a click on the centre scroll button - do nothing
462
489
  else:
@@ -465,9 +492,9 @@ class ScrollbarMargin(ClickableMargin):
465
492
  if mouse_event.button == MouseButton.LEFT:
466
493
  func = None
467
494
  if offset < 0:
468
- func = render_info.window._scroll_up
495
+ func = target_render_info.window._scroll_up
469
496
  elif offset > 0:
470
- func = render_info.window._scroll_down
497
+ func = target_render_info.window._scroll_down
471
498
  if func is not None:
472
499
  # Scroll the window multiple times to scroll by the offset
473
500
  for _ in range(abs(int(offset))):
@@ -517,7 +544,7 @@ class NumberedMargin(Margin):
517
544
  def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
518
545
  """Return the width of the margin."""
519
546
  line_count = get_ui_content().line_count
520
- return len("%s" % line_count) + 2
547
+ return len(f"{line_count}") + 2
521
548
 
522
549
  def create_margin(
523
550
  self, window_render_info: WindowRenderInfo, width: int, height: int
@@ -561,8 +588,10 @@ class NumberedMargin(Margin):
561
588
  style = f"{self_style} class:line-number.current"
562
589
  else:
563
590
  style = self_style
591
+ if last_lineno is None and lineno == 0 and not multiline:
592
+ linestr = ">"
564
593
  # Only display line number if this line is not a continuation of the previous line.
565
- if lineno != last_lineno:
594
+ elif lineno != last_lineno:
566
595
  linestr = str(lineno + 1).rjust(width - 2)
567
596
  else:
568
597
  linestr = " " * (width - 2)
euporie/core/style.py CHANGED
@@ -207,9 +207,9 @@ IPYWIDGET_STYLE = [
207
207
  class ColorPaletteColor:
208
208
  """A representation of a color with adjustment methods."""
209
209
 
210
- _cache: SimpleCache[
211
- tuple[str, float, float, float, bool], ColorPaletteColor
212
- ] = SimpleCache()
210
+ _cache: SimpleCache[tuple[str, float, float, float, bool], ColorPaletteColor] = (
211
+ SimpleCache()
212
+ )
213
213
 
214
214
  def __init__(self, base: str, _base_override: str = "") -> None:
215
215
  """Create a new color.
@@ -496,8 +496,10 @@ def build_style(
496
496
  "cell border edit": f"fg:{cp.hl.adjust(hue=-0.3333, rel=False)}",
497
497
  "cell input prompt": "fg:blue",
498
498
  "cell output prompt": "fg:red",
499
- "cell show outputs": "bg:#888",
500
- "cell show inputs": "bg:#888",
499
+ "cell show outputs": f"fg:{cp.fg.more(0.5)} bg:{cp.bg.more(0.05)}",
500
+ "cell show inputs": f"fg:{cp.fg.more(0.5)} bg:{cp.bg.more(0.05)}",
501
+ "cell show inputs border": f"fg:{cp.bg.darker(0.1)}",
502
+ "cell show outputs border": f"fg:{cp.bg.darker(0.1)}",
501
503
  # Scrollbars
502
504
  "scrollbar": f"fg:{cp.bg.more(0.75)} bg:{cp.bg.more(0.15)}",
503
505
  "scrollbar.background": f"fg:{cp.bg.more(0.75)} bg:{cp.bg.more(0.15)}",
euporie/core/tabs/base.py CHANGED
@@ -584,3 +584,35 @@ class KernelTab(Tab, metaclass=ABCMeta):
584
584
  When set, execution timing data will be recorded in cell metadata.
585
585
  """,
586
586
  )
587
+
588
+ add_setting(
589
+ name="show_remote_inputs",
590
+ flags=["--show-remote-inputs"],
591
+ type_=bool,
592
+ help_="Display inputs sent to the kernel by other clients",
593
+ default=True,
594
+ description="""
595
+ If set to `True`, all code input sent to the kernel by any client will be
596
+ displayed.
597
+
598
+ If set to `False`, only inputs sent to the kernel by the current instance
599
+ of euporie will be displayed, and all other inputs will be ignored.
600
+
601
+ """,
602
+ )
603
+
604
+ add_setting(
605
+ name="show_remote_outputs",
606
+ flags=["--show-remote-outputs"],
607
+ type_=bool,
608
+ help_="Display kernel outputs triggered by other clients",
609
+ default=True,
610
+ description="""
611
+ If set to `False`, only outputs generated by code input from the current
612
+ instance of euporie will be displayed, and all other outputs will be
613
+ ignored.
614
+
615
+ If set to `True`, all outputs generated by the kernel will be
616
+ displayed.
617
+ """,
618
+ )
@@ -75,7 +75,7 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
75
75
  prompt, password
76
76
  ),
77
77
  "set_execution_count": lambda n: self.cell.set_execution_count(n),
78
- "add_output": lambda output_json: self.cell.add_output(output_json),
78
+ "add_output": self.new_output_default,
79
79
  "clear_output": lambda wait: self.cell.clear_output(wait),
80
80
  "set_metadata": lambda path, data: self.cell.set_metadata(path, data),
81
81
  "set_status": self.set_status,
@@ -116,12 +116,10 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
116
116
  prev = self.container
117
117
  self.container = self.load_container()
118
118
  self.loaded = True
119
-
120
119
  # Update the focus if the old container had focus
121
120
  if self.app.layout.has_focus(prev):
122
121
  self.focus()
123
122
 
124
- self.app.invalidate()
125
123
  # Load widgets
126
124
  self.load_widgets_from_metadata()
127
125
 
@@ -385,6 +383,11 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
385
383
  done=cell.ran,
386
384
  )
387
385
 
386
+ def new_output_default(self, output_json: dict[str, Any], own: bool) -> None:
387
+ """Add a new output without a cell to the currently selected cell."""
388
+ if self.app.config.show_remote_outputs:
389
+ self.cell.add_output(output_json, own)
390
+
388
391
  def load_widgets_from_metadata(self) -> None:
389
392
  """Load widgets from state saved in notebook metadata."""
390
393
  for comm_id, comm_data in (
euporie/core/terminal.py CHANGED
@@ -75,6 +75,7 @@ class TerminalQuery:
75
75
  cache = False
76
76
  cmd = ""
77
77
  pattern: re.Pattern | None = None
78
+ run_at_startup: bool = True
78
79
 
79
80
  def __init__(self, input_: Input, output: Output, config: Config) -> None:
80
81
  """Create a new instance of the terminal query."""
@@ -257,13 +258,11 @@ class KittyGraphicsStatus(TerminalQuery):
257
258
 
258
259
  def verify(self, data: str) -> bool:
259
260
  """Verify the terminal response means kitty graphics are supported."""
260
- if (
261
+ return bool(
261
262
  (match := self.pattern.match(data))
262
263
  and (values := match.groupdict())
263
264
  and values.get("status") == "OK"
264
- ):
265
- return True
266
- return False
265
+ )
267
266
 
268
267
 
269
268
  class SixelGraphicsStatus(TerminalQuery):
@@ -279,13 +278,11 @@ class SixelGraphicsStatus(TerminalQuery):
279
278
 
280
279
  def verify(self, data: str) -> bool:
281
280
  """Verify the terminal response means sixel graphics are supported."""
282
- if (
281
+ return bool(
283
282
  (match := self.pattern.match(data))
284
283
  and (values := match.groupdict())
285
284
  and values.get("sixel")
286
- ):
287
- return True
288
- return False
285
+ )
289
286
 
290
287
 
291
288
  class ItermGraphicsStatus(TerminalQuery):
@@ -301,14 +298,12 @@ class ItermGraphicsStatus(TerminalQuery):
301
298
 
302
299
  def verify(self, data: str) -> bool:
303
300
  """Verify iterm graphics are supported by the terminal."""
304
- if (
301
+ return bool(
305
302
  (match := self.pattern.match(data))
306
303
  and (values := match.groupdict())
307
304
  and (term := values.get("term"))
308
- and (term.startswith(("WezTerm", "Konsole", "mlterm")))
309
- ):
310
- return True
311
- return False
305
+ and term.startswith(("WezTerm", "Konsole", "mlterm"))
306
+ )
312
307
 
313
308
 
314
309
  class DepthOfColor(TerminalQuery):
@@ -362,9 +357,7 @@ class CsiUStatus(TerminalQuery):
362
357
 
363
358
  def verify(self, data: str) -> bool:
364
359
  """Verify the terminal responds."""
365
- if (match := self.pattern.match(data)) and match:
366
- return True
367
- return False
360
+ return bool((match := self.pattern.match(data)) and match)
368
361
 
369
362
 
370
363
  class ClipboardData(TerminalQuery):
@@ -374,6 +367,7 @@ class ClipboardData(TerminalQuery):
374
367
  cache = False
375
368
  cmd = "\x1b]52;c;?\x1b\\"
376
369
  pattern = re.compile(r"^\x1b\]52;(?:c|p)?;(?P<data>[A-Za-z0-9+/=]+)\x1b\\\Z")
370
+ run_at_startup = False
377
371
 
378
372
  def verify(self, data: str) -> str:
379
373
  """Verify the terminal responds."""
@@ -453,7 +447,8 @@ class TerminalInfo:
453
447
  # Ensure line wrapping is off before sending queries
454
448
  self.output.disable_autowrap()
455
449
  for query in self._queries.values():
456
- query.send(flush=False)
450
+ if query.run_at_startup:
451
+ query.send(flush=False)
457
452
  self.output.flush()
458
453
 
459
454
  def _tiocgwnsz(self) -> tuple[int, int, int, int]:
euporie/core/utils.py CHANGED
@@ -32,12 +32,10 @@ class ChainedList(Sequence[T]):
32
32
  return list(chain.from_iterable(self.lists))
33
33
 
34
34
  @overload
35
- def __getitem__(self, i: int) -> T:
36
- ...
35
+ def __getitem__(self, i: int) -> T: ...
37
36
 
38
37
  @overload
39
- def __getitem__(self, i: slice) -> list[T]:
40
- ...
38
+ def __getitem__(self, i: slice) -> list[T]: ...
41
39
 
42
40
  def __getitem__(self, i):
43
41
  """Get an item from the chained lists."""