Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

207 Zeilen
5.3 KiB

  1. from __future__ import annotations
  2. from typing import TYPE_CHECKING, Generic, List, Optional, TypeVar
  3. import click
  4. from rich.console import RenderableType
  5. from rich.text import Text
  6. from typing_extensions import Any, Literal, TypedDict
  7. from ._input_handler import TextInputHandler
  8. from .element import CursorOffset, Element
  9. if TYPE_CHECKING:
  10. from .styles.base import BaseStyle
  11. ReturnValue = TypeVar("ReturnValue")
  12. class Option(TypedDict, Generic[ReturnValue]):
  13. name: str
  14. value: ReturnValue
  15. class Menu(Generic[ReturnValue], TextInputHandler, Element):
  16. DOWN_KEYS = [TextInputHandler.DOWN_KEY, "j"]
  17. UP_KEYS = [TextInputHandler.UP_KEY, "k"]
  18. LEFT_KEYS = [TextInputHandler.LEFT_KEY, "h"]
  19. RIGHT_KEYS = [TextInputHandler.RIGHT_KEY, "l"]
  20. current_selection_char = "●"
  21. selection_char = "○"
  22. filter_prompt = "Filter: "
  23. def __init__(
  24. self,
  25. label: str,
  26. options: List[Option[ReturnValue]],
  27. inline: bool = False,
  28. allow_filtering: bool = False,
  29. *,
  30. style: Optional[BaseStyle] = None,
  31. cursor_offset: int = 0,
  32. **metadata: Any,
  33. ):
  34. self.label = Text.from_markup(label)
  35. self.inline = inline
  36. self.allow_filtering = allow_filtering
  37. self.selected = 0
  38. self.metadata = metadata
  39. self._options = options
  40. self._padding_bottom = 1
  41. self.valid = None
  42. cursor_offset = cursor_offset + len(self.filter_prompt)
  43. Element.__init__(self, style=style, metadata=metadata)
  44. super().__init__()
  45. def get_key(self) -> Optional[str]:
  46. char = click.getchar()
  47. if char == "\r":
  48. return "enter"
  49. if self.allow_filtering:
  50. left_keys, right_keys = [[self.LEFT_KEY], [self.RIGHT_KEY]]
  51. down_keys, up_keys = [[self.DOWN_KEY], [self.UP_KEY]]
  52. else:
  53. left_keys, right_keys = self.LEFT_KEYS, self.RIGHT_KEYS
  54. down_keys, up_keys = self.DOWN_KEYS, self.UP_KEYS
  55. next_keys, prev_keys = (
  56. (right_keys, left_keys) if self.inline else (down_keys, up_keys)
  57. )
  58. if char in next_keys:
  59. return "next"
  60. if char in prev_keys:
  61. return "prev"
  62. if self.allow_filtering:
  63. return char
  64. return None
  65. @property
  66. def options(self) -> List[Option[ReturnValue]]:
  67. if self.allow_filtering:
  68. return [
  69. option
  70. for option in self._options
  71. if self.text.lower() in option["name"].lower()
  72. ]
  73. return self._options
  74. def _update_selection(self, key: Literal["next", "prev"]) -> None:
  75. if key == "next":
  76. self.selected += 1
  77. elif key == "prev":
  78. self.selected -= 1
  79. if self.selected < 0:
  80. self.selected = len(self.options) - 1
  81. if self.selected >= len(self.options):
  82. self.selected = 0
  83. def render_result(self) -> RenderableType:
  84. result_text = Text()
  85. result_text.append(self.label)
  86. result_text.append(" ")
  87. result_text.append(
  88. self.options[self.selected]["name"],
  89. style=self.console.get_style("result"),
  90. )
  91. return result_text
  92. def is_next_key(self, key: str) -> bool:
  93. keys = self.RIGHT_KEYS if self.inline else self.DOWN_KEYS
  94. if self.allow_filtering:
  95. keys = [keys[0]]
  96. return key in keys
  97. def is_prev_key(self, key: str) -> bool:
  98. keys = self.LEFT_KEYS if self.inline else self.UP_KEYS
  99. if self.allow_filtering:
  100. keys = [keys[0]]
  101. return key in keys
  102. def handle_key(self, key: str) -> None:
  103. current_selection: Optional[str] = None
  104. if self.is_next_key(key):
  105. self._update_selection("next")
  106. elif self.is_prev_key(key):
  107. self._update_selection("prev")
  108. else:
  109. if self.options:
  110. current_selection = self.options[self.selected]["name"]
  111. super().handle_key(key)
  112. if current_selection:
  113. matching_index = next(
  114. (
  115. index
  116. for index, option in enumerate(self.options)
  117. if option["name"] == current_selection
  118. ),
  119. 0,
  120. )
  121. self.selected = matching_index
  122. def _handle_enter(self) -> bool:
  123. if self.allow_filtering and self.text and len(self.options) == 0:
  124. return False
  125. return True
  126. @property
  127. def validation_message(self) -> Optional[str]:
  128. if self.valid is False:
  129. return "This field is required"
  130. return None
  131. def on_blur(self):
  132. self.on_validate()
  133. def on_validate(self):
  134. self.valid = len(self.options) > 0
  135. @property
  136. def should_show_cursor(self) -> bool:
  137. return self.allow_filtering
  138. def ask(self) -> ReturnValue:
  139. from .container import Container
  140. container = Container(style=self.style, metadata=self.metadata)
  141. container.elements = [self]
  142. container.run()
  143. return self.options[self.selected]["value"]
  144. @property
  145. def cursor_offset(self) -> CursorOffset:
  146. top = 2
  147. left_offset = len(self.filter_prompt) + self.cursor_left
  148. return CursorOffset(top=top, left=left_offset)