1from __future__ import annotations
2
3from abc import ABC, abstractmethod
4from collections.abc import Callable
5from enum import Enum
6from typing import TYPE_CHECKING, Any
7
8from prompt_toolkit.enums import EditingMode
9from prompt_toolkit.key_binding.vi_state import InputMode
10
11if TYPE_CHECKING:
12 from .application import Application
13
14__all__ = [
15 "CursorShape",
16 "CursorShapeConfig",
17 "SimpleCursorShapeConfig",
18 "ModalCursorShapeConfig",
19 "DynamicCursorShapeConfig",
20 "to_cursor_shape_config",
21]
22
23
24class CursorShape(Enum):
25 # Default value that should tell the output implementation to never send
26 # cursor shape escape sequences. This is the default right now, because
27 # before this `CursorShape` functionality was introduced into
28 # prompt_toolkit itself, people had workarounds to send cursor shapes
29 # escapes into the terminal, by monkey patching some of prompt_toolkit's
30 # internals. We don't want the default prompt_toolkit implementation to
31 # interfere with that. E.g., IPython patches the `ViState.input_mode`
32 # property. See: https://github.com/ipython/ipython/pull/13501/files
33 _NEVER_CHANGE = "_NEVER_CHANGE"
34
35 BLOCK = "BLOCK"
36 BEAM = "BEAM"
37 UNDERLINE = "UNDERLINE"
38 BLINKING_BLOCK = "BLINKING_BLOCK"
39 BLINKING_BEAM = "BLINKING_BEAM"
40 BLINKING_UNDERLINE = "BLINKING_UNDERLINE"
41
42
43class CursorShapeConfig(ABC):
44 @abstractmethod
45 def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
46 """
47 Return the cursor shape to be used in the current state.
48 """
49
50
51AnyCursorShapeConfig = CursorShape | CursorShapeConfig | None
52
53
54class SimpleCursorShapeConfig(CursorShapeConfig):
55 """
56 Always show the given cursor shape.
57 """
58
59 def __init__(self, cursor_shape: CursorShape = CursorShape._NEVER_CHANGE) -> None:
60 self.cursor_shape = cursor_shape
61
62 def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
63 return self.cursor_shape
64
65
66class ModalCursorShapeConfig(CursorShapeConfig):
67 """
68 Show cursor shape according to the current input mode.
69 """
70
71 def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
72 if application.editing_mode == EditingMode.VI:
73 if application.vi_state.input_mode in {
74 InputMode.NAVIGATION,
75 }:
76 return CursorShape.BLOCK
77 if application.vi_state.input_mode in {
78 InputMode.INSERT,
79 InputMode.INSERT_MULTIPLE,
80 }:
81 return CursorShape.BEAM
82 if application.vi_state.input_mode in {
83 InputMode.REPLACE,
84 InputMode.REPLACE_SINGLE,
85 }:
86 return CursorShape.UNDERLINE
87 elif application.editing_mode == EditingMode.EMACS:
88 # like vi's INSERT
89 return CursorShape.BEAM
90
91 # Default
92 return CursorShape.BLOCK
93
94
95class DynamicCursorShapeConfig(CursorShapeConfig):
96 def __init__(
97 self, get_cursor_shape_config: Callable[[], AnyCursorShapeConfig]
98 ) -> None:
99 self.get_cursor_shape_config = get_cursor_shape_config
100
101 def get_cursor_shape(self, application: Application[Any]) -> CursorShape:
102 return to_cursor_shape_config(self.get_cursor_shape_config()).get_cursor_shape(
103 application
104 )
105
106
107def to_cursor_shape_config(value: AnyCursorShapeConfig) -> CursorShapeConfig:
108 """
109 Take a `CursorShape` instance or `CursorShapeConfig` and turn it into a
110 `CursorShapeConfig`.
111 """
112 if value is None:
113 return SimpleCursorShapeConfig()
114
115 if isinstance(value, CursorShape):
116 return SimpleCursorShapeConfig(value)
117
118 return value