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
@@ -153,7 +153,10 @@ class Datum(Generic[T], metaclass=_MetaDatum):
153
153
  for key, (datum_ref, _size) in list(size_instances.items()):
154
154
  datum = datum_ref()
155
155
  if not datum or datum.hash == data_hash:
156
- del size_instances[key]
156
+ try:
157
+ del size_instances[key]
158
+ except KeyError:
159
+ pass
157
160
  del datum
158
161
 
159
162
  def to_bytes(self) -> bytes:
@@ -58,7 +58,7 @@ def register(
58
58
  return decorator
59
59
 
60
60
 
61
- def find_route(from_: str, to: str) -> list | None:
61
+ def _find_route(from_: str, to: str) -> list | None:
62
62
  """Find the shortest conversion path between two formats."""
63
63
  if from_ == to:
64
64
  return [from_]
@@ -100,5 +100,10 @@ def find_route(from_: str, to: str) -> list | None:
100
100
 
101
101
 
102
102
  _CONVERTOR_ROUTE_CACHE: FastDictCache[tuple[str, str], list | None] = FastDictCache(
103
- find_route
103
+ _find_route
104
104
  )
105
+
106
+
107
+ def find_route(from_: str, to: str) -> list | None:
108
+ """Find and cache conversion routes."""
109
+ return _CONVERTOR_ROUTE_CACHE[from_, to]
euporie/core/filters.py CHANGED
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from functools import partial, reduce
6
+ from functools import lru_cache, partial, reduce
7
7
  from importlib import import_module
8
8
  from shutil import which
9
9
  from typing import TYPE_CHECKING
@@ -27,6 +27,7 @@ if TYPE_CHECKING:
27
27
  from prompt_toolkit.layout.containers import Window
28
28
 
29
29
 
30
+ @lru_cache(maxsize=None)
30
31
  def command_exists(*cmds: str) -> Filter:
31
32
  """Verify a list of external commands exist on the system."""
32
33
  filters = [
@@ -36,6 +37,7 @@ def command_exists(*cmds: str) -> Filter:
36
37
  return reduce(lambda a, b: a & b, filters, to_filter(True))
37
38
 
38
39
 
40
+ @lru_cache(maxsize=None)
39
41
  def have_modules(*modules: str) -> Filter:
40
42
  """Verify a list of python modules are importable."""
41
43
 
euporie/core/ft/html.py CHANGED
@@ -4318,14 +4318,12 @@ class HTML:
4318
4318
  @overload
4319
4319
  async def _render_image(
4320
4320
  self, data: bytes, format_: str, theme: Theme, path: Path | None = None
4321
- ) -> StyleAndTextTuples:
4322
- ...
4321
+ ) -> StyleAndTextTuples: ...
4323
4322
 
4324
4323
  @overload
4325
4324
  async def _render_image(
4326
4325
  self, data: str, format_: str, theme: Theme, path: Path | None = None
4327
- ) -> StyleAndTextTuples:
4328
- ...
4326
+ ) -> StyleAndTextTuples: ...
4329
4327
 
4330
4328
  async def _render_image(self, data, format_, theme, path=None):
4331
4329
  """Render an image and prepare graphic representation."""
euporie/core/graphics.py CHANGED
@@ -335,9 +335,9 @@ class KittyGraphicControl(GraphicControl):
335
335
  super().__init__(datum=datum, scale=scale, bbox=bbox)
336
336
  self.kitty_image_id = 0
337
337
  self.loaded = False
338
- self._datum_pad_cache: FastDictCache[
339
- tuple[Datum, int, int], Datum
340
- ] = FastDictCache(get_value=self._pad_datum, size=1)
338
+ self._datum_pad_cache: FastDictCache[tuple[Datum, int, int], Datum] = (
339
+ FastDictCache(get_value=self._pad_datum, size=1)
340
+ )
341
341
 
342
342
  def _pad_datum(self, datum: Datum, cell_size_x: int, cell_size_y: int) -> Datum:
343
343
  from PIL import ImageOps
@@ -704,9 +704,9 @@ class GraphicProcessor:
704
704
  self.control = control
705
705
 
706
706
  self.positions: dict[str, Point] = {}
707
- self._position_cache: FastDictCache[
708
- tuple[UIContent], dict[str, Point]
709
- ] = FastDictCache(self._load_positions, size=1_000)
707
+ self._position_cache: FastDictCache[tuple[UIContent], dict[str, Point]] = (
708
+ FastDictCache(self._load_positions, size=1_000)
709
+ )
710
710
  self._float_cache: FastDictCache[tuple[str], Float | None] = FastDictCache(
711
711
  self.get_graphic_float, size=1_000
712
712
  )
euporie/core/kernel.py CHANGED
@@ -9,13 +9,13 @@ import os
9
9
  import re
10
10
  import sys
11
11
  import threading
12
+ from _frozen_importlib import _DeadlockError
12
13
  from collections import defaultdict
13
14
  from subprocess import PIPE, STDOUT # S404 - Security implications considered
14
15
  from typing import TYPE_CHECKING, TypedDict
15
16
  from uuid import uuid4
16
17
 
17
18
  import nbformat
18
- from _frozen_importlib import _DeadlockError
19
19
  from jupyter_client import AsyncKernelManager, KernelManager
20
20
  from jupyter_client.kernelspec import NATIVE_KERNEL_NAME, NoSuchKernel
21
21
  from jupyter_client.provisioning import KernelProvisionerFactory as KPF
@@ -130,7 +130,8 @@ class MsgCallbacks(TypedDict, total=False):
130
130
 
131
131
  get_input: Callable[[str, bool], None] | None
132
132
  set_execution_count: Callable[[int], None] | None
133
- add_output: Callable[[dict[str, Any]], None] | None
133
+ add_output: Callable[[dict[str, Any], bool], None] | None
134
+ add_input: Callable[[dict[str, Any], bool], None] | None
134
135
  clear_output: Callable[[bool], None] | None
135
136
  done: Callable[[dict[str, Any]], None] | None
136
137
  set_metadata: Callable[[tuple[str, ...], Any], None] | None
@@ -151,6 +152,8 @@ class Kernel:
151
152
  Has the ability to run itself in it's own thread.
152
153
  """
153
154
 
155
+ _CLIENT_ID = f"euporie-{os.getpid()}"
156
+
154
157
  def __init__(
155
158
  self,
156
159
  kernel_tab: KernelTab,
@@ -457,6 +460,9 @@ class Kernel:
457
460
  ]
458
461
  self.dead = False
459
462
 
463
+ # Set username so we can identify our own messages
464
+ self.kc.session.username = self._CLIENT_ID
465
+
460
466
  # Start monitoring the kernel status
461
467
  if self.monitor_task is not None:
462
468
  self.monitor_task.cancel()
@@ -507,22 +513,24 @@ class Kernel:
507
513
  rsp = await msg_getter_coro()
508
514
  # Run msg type handler
509
515
  msg_type = rsp.get("header", {}).get("msg_type")
516
+ own = rsp.get("parent_header", {}).get("username") == self._CLIENT_ID
510
517
  if callable(handler := getattr(self, f"on_{channel}_{msg_type}", None)):
511
- handler(rsp)
518
+ handler(rsp, own)
512
519
  else:
513
- self.on_unhandled(channel, rsp)
520
+ self.on_unhandled(channel, rsp, own)
514
521
 
515
- def on_unhandled(self, channel: str, rsp: dict[str, Any]) -> None:
522
+ def on_unhandled(self, channel: str, rsp: dict[str, Any], own: bool) -> None:
516
523
  """Report unhandled messages to the debug log."""
517
524
  log.debug(
518
- "Unhandled %s message:\nparent_id = '%s'\ntype = '%s'\ncontent='%s'",
525
+ "Unhandled %s message:\nparent_id = '%s'\ntype = '%s'\ncontent='%s'\nown: %s",
519
526
  channel,
520
527
  rsp.get("parent_header", {}).get("msg_id"),
521
528
  rsp["header"]["msg_type"],
522
529
  rsp.get("content"),
530
+ own,
523
531
  )
524
532
 
525
- def on_stdin_input_request(self, rsp: dict[str, Any]) -> None:
533
+ def on_stdin_input_request(self, rsp: dict[str, Any], own: bool) -> None:
526
534
  """Call ``get_input`` callback for a stdin input request message."""
527
535
  msg_id = rsp.get("parent_header", {}).get("msg_id")
528
536
  content = rsp.get("content", {})
@@ -532,7 +540,7 @@ class Kernel:
532
540
  content.get("password", False),
533
541
  )
534
542
 
535
- def on_shell_status(self, rsp: dict[str, Any]) -> None:
543
+ def on_shell_status(self, rsp: dict[str, Any], own: bool) -> None:
536
544
  """Call ``set_execution_count`` callback for a shell status response."""
537
545
  msg_id = rsp.get("parent_header", {}).get("msg_id")
538
546
  content = rsp.get("content", {})
@@ -548,7 +556,7 @@ class Kernel:
548
556
  ):
549
557
  set_execution_count(execution_count)
550
558
 
551
- def on_shell_execute_reply(self, rsp: dict[str, Any]) -> None:
559
+ def on_shell_execute_reply(self, rsp: dict[str, Any], own: bool) -> None:
552
560
  """Call callbacks for a shell execute reply response."""
553
561
  msg_id = rsp.get("parent_header", {}).get("msg_id")
554
562
  content = rsp.get("content", {})
@@ -579,7 +587,8 @@ class Kernel:
579
587
  nbformat.v4.new_output(
580
588
  "execute_result",
581
589
  data=data,
582
- )
590
+ ),
591
+ own,
583
592
  )
584
593
  elif source == "set_next_input":
585
594
  if callable(
@@ -605,7 +614,7 @@ class Kernel:
605
614
  ):
606
615
  done(content)
607
616
 
608
- def on_shell_kernel_info_reply(self, rsp: dict[str, Any]) -> None:
617
+ def on_shell_kernel_info_reply(self, rsp: dict[str, Any], own: bool) -> None:
609
618
  """Call callbacks for a shell kernel info response."""
610
619
  msg_id = rsp.get("parent_header", {}).get("msg_id")
611
620
  if callable(
@@ -613,25 +622,25 @@ class Kernel:
613
622
  ):
614
623
  set_kernel_info(rsp.get("content", {}))
615
624
 
616
- def on_shell_complete_reply(self, rsp: dict[str, Any]) -> None:
625
+ def on_shell_complete_reply(self, rsp: dict[str, Any], own: bool) -> None:
617
626
  """Call callbacks for a shell completion reply response."""
618
627
  msg_id = rsp.get("parent_header", {}).get("msg_id")
619
628
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
620
629
  done(rsp.get("content", {}))
621
630
 
622
- def on_shell_history_reply(self, rsp: dict[str, Any]) -> None:
631
+ def on_shell_history_reply(self, rsp: dict[str, Any], own: bool) -> None:
623
632
  """Call callbacks for a shell history reply response."""
624
633
  msg_id = rsp.get("parent_header", {}).get("msg_id")
625
634
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
626
635
  done(rsp.get("content", {}))
627
636
 
628
- def on_shell_inspect_reply(self, rsp: dict[str, Any]) -> None:
637
+ def on_shell_inspect_reply(self, rsp: dict[str, Any], own: bool) -> None:
629
638
  """Call callbacks for a shell inspection reply response."""
630
639
  msg_id = rsp.get("parent_header", {}).get("msg_id")
631
640
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
632
641
  done(rsp.get("content", {}))
633
642
 
634
- def on_shell_is_complete_reply(self, rsp: dict[str, Any]) -> None:
643
+ def on_shell_is_complete_reply(self, rsp: dict[str, Any], own: bool) -> None:
635
644
  """Call callbacks for a shell completeness reply response."""
636
645
  msg_id = rsp.get("parent_header", {}).get("msg_id")
637
646
  if callable(
@@ -641,7 +650,7 @@ class Kernel:
641
650
  ):
642
651
  completeness_status(rsp.get("content", {}))
643
652
 
644
- def on_iopub_status(self, rsp: dict[str, Any]) -> None:
653
+ def on_iopub_status(self, rsp: dict[str, Any], own: bool) -> None:
645
654
  """Call callbacks for an iopub status response."""
646
655
  msg_id = rsp.get("parent_header", {}).get("msg_id")
647
656
  status = rsp.get("content", {}).get("execution_state")
@@ -670,9 +679,11 @@ class Kernel:
670
679
  rsp["header"]["date"].isoformat(),
671
680
  )
672
681
 
673
- def on_iopub_execute_input(self, rsp: dict[str, Any]) -> None:
682
+ def on_iopub_execute_input(self, rsp: dict[str, Any], own: bool) -> None:
674
683
  """Call callbacks for an iopub execute input response."""
675
684
  msg_id = rsp.get("parent_header", {}).get("msg_id")
685
+ content = rsp.get("content", {})
686
+
676
687
  if self.kernel_tab.app.config.record_cell_timing and callable(
677
688
  set_metadata := self.msg_id_callbacks[msg_id]["set_metadata"]
678
689
  ):
@@ -681,23 +692,36 @@ class Kernel:
681
692
  rsp["header"]["date"].isoformat(),
682
693
  )
683
694
 
684
- def on_iopub_display_data(self, rsp: dict[str, Any]) -> None:
695
+ execution_count: int | None = None
696
+ if (execution_count := content.get("execution_count")) and (
697
+ callable(
698
+ set_execution_count := self.msg_id_callbacks[msg_id][
699
+ "set_execution_count"
700
+ ]
701
+ )
702
+ ):
703
+ set_execution_count(execution_count)
704
+
705
+ if callable(add_input := self.msg_id_callbacks[msg_id].get("add_input")):
706
+ add_input(content, own)
707
+
708
+ def on_iopub_display_data(self, rsp: dict[str, Any], own: bool) -> None:
685
709
  """Call callbacks for an iopub display data response."""
686
710
  msg_id = rsp.get("parent_header", {}).get("msg_id")
687
711
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
688
- add_output(nbformat.v4.output_from_msg(rsp))
712
+ add_output(nbformat.v4.output_from_msg(rsp), own)
689
713
 
690
- def on_iopub_update_display_data(self, rsp: dict[str, Any]) -> None:
714
+ def on_iopub_update_display_data(self, rsp: dict[str, Any], own: bool) -> None:
691
715
  """Call callbacks for an iopub update display data response."""
692
716
  msg_id = rsp.get("parent_header", {}).get("msg_id")
693
717
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
694
- add_output(nbformat.v4.output_from_msg(rsp))
718
+ add_output(nbformat.v4.output_from_msg(rsp), own)
695
719
 
696
- def on_iopub_execute_result(self, rsp: dict[str, Any]) -> None:
720
+ def on_iopub_execute_result(self, rsp: dict[str, Any], own: bool) -> None:
697
721
  """Call callbacks for an iopub execute result response."""
698
722
  msg_id = rsp.get("parent_header", {}).get("msg_id")
699
723
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
700
- add_output(nbformat.v4.output_from_msg(rsp))
724
+ add_output(nbformat.v4.output_from_msg(rsp), own)
701
725
 
702
726
  if (execution_count := rsp.get("content", {}).get("execution_count")) and (
703
727
  callable(
@@ -708,21 +732,21 @@ class Kernel:
708
732
  ):
709
733
  set_execution_count(execution_count)
710
734
 
711
- def on_iopub_error(self, rsp: dict[str, dict[str, Any]]) -> None:
735
+ def on_iopub_error(self, rsp: dict[str, Any], own: bool) -> None:
712
736
  """Call callbacks for an iopub error response."""
713
737
  msg_id = rsp.get("parent_header", {}).get("msg_id", "")
714
738
  if callable(add_output := self.msg_id_callbacks[msg_id].get("add_output")):
715
- add_output(nbformat.v4.output_from_msg(rsp))
739
+ add_output(nbformat.v4.output_from_msg(rsp), own)
716
740
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
717
741
  done(rsp.get("content", {}))
718
742
 
719
- def on_iopub_stream(self, rsp: dict[str, Any]) -> None:
743
+ def on_iopub_stream(self, rsp: dict[str, Any], own: bool) -> None:
720
744
  """Call callbacks for an iopub stream response."""
721
745
  msg_id = rsp.get("parent_header", {}).get("msg_id")
722
746
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
723
- add_output(nbformat.v4.output_from_msg(rsp))
747
+ add_output(nbformat.v4.output_from_msg(rsp), own)
724
748
 
725
- def on_iopub_clear_output(self, rsp: dict[str, Any]) -> None:
749
+ def on_iopub_clear_output(self, rsp: dict[str, Any], own: bool) -> None:
726
750
  """Call callbacks for an iopub clear output response."""
727
751
  # Clear cell output, either now or when we get the next output
728
752
  msg_id = rsp.get("parent_header", {}).get("msg_id")
@@ -737,7 +761,7 @@ class Kernel:
737
761
  )
738
762
  '''
739
763
 
740
- def on_iopub_comm_open(self, rsp: dict[str, Any]) -> None:
764
+ def on_iopub_comm_open(self, rsp: dict[str, Any], own: bool) -> None:
741
765
  """Call callbacks for an comm open response."""
742
766
  # TODO
743
767
  # "If the target_name key is not found on the receiving side, then it should
@@ -747,13 +771,13 @@ class Kernel:
747
771
  content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
748
772
  )
749
773
 
750
- def on_iopub_comm_msg(self, rsp: dict[str, Any]) -> None:
774
+ def on_iopub_comm_msg(self, rsp: dict[str, Any], own: bool) -> None:
751
775
  """Call callbacks for an iopub comm message response."""
752
776
  self.kernel_tab.comm_msg(
753
777
  content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
754
778
  )
755
779
 
756
- def on_iopub_comm_close(self, rsp: dict[str, Any]) -> None:
780
+ def on_iopub_comm_close(self, rsp: dict[str, Any], own: bool) -> None:
757
781
  """Call callbacks for an iopub comm close response."""
758
782
  self.kernel_tab.comm_close(
759
783
  content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
@@ -795,7 +819,7 @@ class Kernel:
795
819
  source: str,
796
820
  get_input: Callable[[str, bool], None] | None = None,
797
821
  set_execution_count: Callable[[int], None] | None = None,
798
- add_output: Callable[[dict[str, Any]], None] | None = None,
822
+ add_output: Callable[[dict[str, Any], bool], None] | None = None,
799
823
  clear_output: Callable[[bool], None] | None = None,
800
824
  done: Callable[[dict[str, Any]], None] | None = None,
801
825
  set_metadata: Callable[[tuple[str, ...], Any], None] | None = None,
@@ -6,6 +6,7 @@ from euporie.core.key_binding.bindings import (
6
6
  micro,
7
7
  mouse,
8
8
  page_navigation,
9
+ vi,
9
10
  )
10
11
 
11
- __all__ = ["basic", "completion", "micro", "mouse", "page_navigation"]
12
+ __all__ = ["basic", "completion", "micro", "mouse", "page_navigation", "vi"]
@@ -22,6 +22,7 @@ from prompt_toolkit.key_binding.bindings.mouse import (
22
22
  from prompt_toolkit.keys import Keys
23
23
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType, MouseModifier
24
24
  from prompt_toolkit.mouse_events import MouseEvent as PtkMouseEvent
25
+ from prompt_toolkit.renderer import HeightIsUnknownError
25
26
 
26
27
  from euporie.core.app import BaseApp
27
28
 
@@ -102,29 +103,23 @@ def _parse_mouse_data(
102
103
  # Parse event type.
103
104
  if sgr:
104
105
  if sgr_pixels:
105
- # Calculate cell position
106
+ # Scale down pixel-wise mouse position to cell based, and calculate
107
+ # relative position of mouse within the cell
106
108
  cell_x, cell_y = cell_size_xy
107
109
  px, py = x, y
108
110
  fx, fy = px / cell_x + 1, py / cell_y + 1
109
111
  x, y = int(fx), int(fy)
110
112
  rx, ry = fx - x, fy - y
111
-
112
113
  try:
113
- (
114
- mouse_button,
115
- mouse_event_type,
116
- mouse_modifiers,
117
- ) = xterm_sgr_mouse_events[mouse_event, m]
114
+ (mouse_button, mouse_event_type, mouse_modifiers) = (
115
+ xterm_sgr_mouse_events[mouse_event, m]
116
+ )
118
117
  except KeyError:
119
118
  return None
120
119
 
121
120
  else:
122
121
  # Some other terminals, like urxvt, Hyper terminal, ...
123
- (
124
- mouse_button,
125
- mouse_event_type,
126
- mouse_modifiers,
127
- ) = urxvt_mouse_events.get(
122
+ (mouse_button, mouse_event_type, mouse_modifiers) = urxvt_mouse_events.get(
128
123
  mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER)
129
124
  )
130
125
 
@@ -153,9 +148,10 @@ def load_mouse_bindings() -> KeyBindings:
153
148
  def _(event: KeyPressEvent) -> NotImplementedOrNone:
154
149
  """Handle incoming mouse event, include SGR-pixel mode."""
155
150
  # Ensure mypy knows this would only run in a euporie appo
156
- assert isinstance(app := event.app, BaseApp)
151
+ app = event.app
152
+ assert isinstance(app, BaseApp)
157
153
 
158
- if not event.app.renderer.height_is_known:
154
+ if not app.renderer.height_is_known:
159
155
  return NotImplemented
160
156
 
161
157
  mouse_event = _MOUSE_EVENT_CACHE[
@@ -169,10 +165,9 @@ def load_mouse_bindings() -> KeyBindings:
169
165
  if mouse_event.event_type is not None:
170
166
  # Take region above the layout into account. The reported
171
167
  # coordinates are absolute to the visible part of the terminal.
172
- from prompt_toolkit.renderer import HeightIsUnknownError
173
-
174
168
  x, y = mouse_event.position
175
169
 
170
+ # Adjust position to take into account space above non-full screen apps
176
171
  try:
177
172
  rows_above = app.renderer.rows_above_layout
178
173
  except HeightIsUnknownError:
@@ -180,28 +175,35 @@ def load_mouse_bindings() -> KeyBindings:
180
175
  else:
181
176
  y -= rows_above
182
177
 
183
- # Save global mouse position
184
- app.mouse_position = mouse_event.position
178
+ # Save mouse position within the app
179
+ app.mouse_position = Point(x=x, y=y)
185
180
 
186
181
  # Apply limits to mouse position if enabled
187
182
  if (mouse_limits := app.mouse_limits) is not None:
188
183
  x = max(
189
184
  mouse_limits.xpos,
190
- min(x, mouse_limits.xpos + (mouse_limits.width - 1)),
185
+ min(x, mouse_limits.xpos + (mouse_limits.width) - 1),
191
186
  )
192
187
  y = max(
193
188
  mouse_limits.ypos,
194
- min(y, mouse_limits.ypos + (mouse_limits.height - 1)),
189
+ min(y, mouse_limits.ypos + (mouse_limits.height) - 1),
195
190
  )
196
191
 
197
- mouse_event.position = Point(x=x, y=y)
192
+ # Do not modify the mouse event in the cache, instead create a new instance
193
+ mouse_event = MouseEvent(
194
+ position=Point(x=x, y=y),
195
+ event_type=mouse_event.event_type,
196
+ button=mouse_event.button,
197
+ modifiers=mouse_event.modifiers,
198
+ cell_position=mouse_event.cell_position,
199
+ )
198
200
 
199
201
  # Call the mouse handler from the renderer.
200
202
  # Note: This can return `NotImplemented` if no mouse handler was
201
203
  # found for this position, or if no repainting needs to
202
204
  # happen. this way, we avoid excessive repaints during mouse
203
205
  # movements.
204
- handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
206
+ handler = app.renderer.mouse_handlers.mouse_handlers[y][x]
205
207
 
206
208
  return handler(mouse_event)
207
209
 
@@ -0,0 +1,46 @@
1
+ """Add additional keys to the prompt_toolkit vi key-bindings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ from prompt_toolkit.buffer import indent, unindent
8
+ from prompt_toolkit.filters.app import vi_insert_mode
9
+ from prompt_toolkit.key_binding.bindings.vi import (
10
+ load_vi_bindings as load_ptk_vi_bindings,
11
+ )
12
+
13
+ from euporie.core.filters import cursor_in_leading_ws
14
+
15
+ if TYPE_CHECKING:
16
+ from prompt_toolkit.key_binding.key_bindings import (
17
+ ConditionalKeyBindings,
18
+ KeyBindings,
19
+ KeyBindingsBase,
20
+ )
21
+ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
22
+
23
+
24
+ def load_vi_bindings() -> KeyBindingsBase:
25
+ """Load vi keybindings from PTK, adding additional bindings."""
26
+ # We know the type of the vi bindings
27
+ vi_bindings = cast(
28
+ "KeyBindings",
29
+ cast("ConditionalKeyBindings", load_ptk_vi_bindings()).key_bindings,
30
+ )
31
+ handle = vi_bindings.add
32
+
33
+ @handle("c-i", filter=vi_insert_mode & cursor_in_leading_ws)
34
+ def _indent(event: KeyPressEvent) -> None:
35
+ """Indent lines."""
36
+ buffer = event.current_buffer
37
+ current_row = buffer.document.cursor_position_row
38
+ indent(buffer, current_row, current_row + event.arg)
39
+
40
+ @handle("s-tab", filter=vi_insert_mode & cursor_in_leading_ws)
41
+ def _unindent(event: KeyPressEvent) -> None:
42
+ """Unindent lines."""
43
+ current_row = event.current_buffer.document.cursor_position_row
44
+ unindent(event.current_buffer, current_row, current_row + event.arg)
45
+
46
+ return vi_bindings
@@ -256,7 +256,10 @@ class CachedContainer(Container):
256
256
  cols: The columns to copy
257
257
  rows: The rows to copy
258
258
  .
259
- """ # Copy write positions
259
+ """
260
+ # Copy write positions
261
+ new_wps = {}
262
+
260
263
  for win, wp in self.screen.visible_windows_to_write_positions.items():
261
264
  new_wp = BoundedWritePosition(
262
265
  xpos=wp.xpos + left,
@@ -270,10 +273,9 @@ class CachedContainer(Container):
270
273
  left=max(0, wp.width - (cols.stop - wp.xpos)),
271
274
  ),
272
275
  )
273
- screen.visible_windows_to_write_positions[win] = new_wp
274
- screen.height = max(screen.height, self.screen.height)
276
+ new_wps[win] = new_wp
275
277
 
276
- # # Modify render info
278
+ # Modify render info
277
279
  info = win.render_info
278
280
  if info is not None:
279
281
  visible_line_to_row_col = {
@@ -308,7 +310,10 @@ class CachedContainer(Container):
308
310
  win.render_info, "horizontal_scroll", horizontal_scroll
309
311
  )
310
312
 
311
- @lru_cache
313
+ screen.visible_windows_to_write_positions.update(new_wps)
314
+ screen.height = max(screen.height, self.screen.height)
315
+
316
+ @lru_cache(maxsize=None)
312
317
  def _wrap_mouse_handler(handler: Callable) -> MouseHandler:
313
318
  def _wrapped(mouse_event: MouseEvent) -> NotImplementedOrNone:
314
319
  # Modify mouse events to reflect position of content
@@ -334,14 +339,17 @@ class CachedContainer(Container):
334
339
  output_db = screen.data_buffer
335
340
  output_zwes = screen.zero_width_escapes
336
341
  output_mhs = mouse_handlers.mouse_handlers
337
- for y in range(max(0, rows.start), rows.stop):
342
+
343
+ rows_range = range(max(0, rows.start), rows.stop)
344
+ cols_range = range(max(0, cols.start), cols.stop)
345
+ for y in rows_range:
338
346
  input_db_row = input_db[y]
339
347
  input_zwes_row = input_zwes[y]
340
348
  input_mhs_row = input_mhs[y]
341
349
  output_dbs_row = output_db[top + y]
342
350
  output_zwes_row = output_zwes[top + y]
343
351
  output_mhs_row = output_mhs[top + y]
344
- for x in range(max(0, cols.start), cols.stop):
352
+ for x in cols_range:
345
353
  # Data
346
354
  output_dbs_row[left + x] = input_db_row[x]
347
355
  # Escape sequences
@@ -353,24 +361,26 @@ class CachedContainer(Container):
353
361
  layout = get_app().layout
354
362
  if self.screen.show_cursor:
355
363
  for window, point in self.screen.cursor_positions.items():
356
- if layout.current_control == window.content:
357
- assert window.render_info is not None
358
- if (
359
- (
360
- window.render_info.ui_content.show_cursor
361
- and not window.always_hide_cursor()
362
- )
363
- and point.x in range(cols.start, cols.stop)
364
- and point.y in range(rows.start, rows.stop)
365
- ):
366
- screen.cursor_positions[window] = Point(
367
- x=left + point.x, y=top + point.y
368
- )
369
- screen.show_cursor = True
364
+ if (
365
+ layout.current_control == window.content
366
+ and window.render_info is not None
367
+ and window.render_info.ui_content.show_cursor
368
+ and not window.always_hide_cursor()
369
+ and cols.start <= point.x < cols.stop
370
+ and rows.start <= point.y < rows.stop
371
+ ):
372
+ screen.cursor_positions[window] = Point(
373
+ x=left + point.x, y=top + point.y
374
+ )
375
+ screen.show_cursor = True
370
376
 
371
377
  # Copy menu positions
372
- for window, point in self.screen.menu_positions.items():
373
- screen.menu_positions[window] = Point(x=left + point.x, y=top + point.y)
378
+ screen.menu_positions.update(
379
+ {
380
+ window: Point(x=left + point.x, y=top + point.y)
381
+ for window, point in self.screen.menu_positions.items()
382
+ }
383
+ )
374
384
 
375
385
  def get_children(self) -> list[Container]:
376
386
  """Return a list of all child containers."""