border.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. from typing import Any, Optional, Tuple, Union
  2. from rich import box
  3. from rich.color import Color
  4. from rich.console import Group, RenderableType
  5. from rich.style import Style
  6. from rich.text import Text
  7. from rich_toolkit._rich_components import Panel
  8. from rich_toolkit.container import Container
  9. from rich_toolkit.element import CursorOffset, Element
  10. from rich_toolkit.form import Form
  11. from rich_toolkit.input import Input
  12. from rich_toolkit.menu import Menu
  13. from rich_toolkit.progress import Progress
  14. from .base import BaseStyle
  15. class BorderedStyle(BaseStyle):
  16. box = box.SQUARE
  17. def empty_line(self) -> RenderableType:
  18. return ""
  19. def _box(
  20. self,
  21. content: RenderableType,
  22. title: Union[str, Text, None],
  23. is_active: bool,
  24. border_color: Color,
  25. after: Tuple[str, ...] = (),
  26. ) -> RenderableType:
  27. return Group(
  28. Panel(
  29. content,
  30. title=title,
  31. title_align="left",
  32. highlight=is_active,
  33. width=50,
  34. box=self.box,
  35. border_style=Style(color=border_color),
  36. ),
  37. *after,
  38. )
  39. def render_container(
  40. self,
  41. element: Container,
  42. is_active: bool = False,
  43. done: bool = False,
  44. parent: Optional[Element] = None,
  45. ) -> RenderableType:
  46. content = super().render_container(element, is_active, done, parent)
  47. if isinstance(element, Form):
  48. return self._box(content, element.title, is_active, Color.parse("white"))
  49. return content
  50. def render_input(
  51. self,
  52. element: Input,
  53. is_active: bool = False,
  54. done: bool = False,
  55. parent: Optional[Element] = None,
  56. **metadata: Any,
  57. ) -> RenderableType:
  58. validation_message: Tuple[str, ...] = ()
  59. if isinstance(parent, Form):
  60. return super().render_input(element, is_active, done, parent, **metadata)
  61. if message := self.render_validation_message(element):
  62. validation_message = (message,)
  63. title = self.render_input_label(
  64. element,
  65. is_active=is_active,
  66. parent=parent,
  67. )
  68. # Determine border color based on validation state
  69. if element.valid is False:
  70. try:
  71. border_color = self.console.get_style("error").color or Color.parse(
  72. "red"
  73. )
  74. except Exception:
  75. # Fallback if error style is not defined
  76. border_color = Color.parse("red")
  77. else:
  78. border_color = Color.parse("white")
  79. return self._box(
  80. self.render_input_value(element, is_active=is_active, parent=parent),
  81. title,
  82. is_active,
  83. border_color,
  84. after=validation_message,
  85. )
  86. def render_menu(
  87. self,
  88. element: Menu,
  89. is_active: bool = False,
  90. done: bool = False,
  91. parent: Optional[Element] = None,
  92. **metadata: Any,
  93. ) -> RenderableType:
  94. validation_message: Tuple[str, ...] = ()
  95. menu = Text(justify="left")
  96. selected_prefix = Text(element.current_selection_char + " ")
  97. not_selected_prefix = Text(element.selection_char + " ")
  98. separator = Text("\t" if element.inline else "\n")
  99. content: list[RenderableType] = []
  100. if done:
  101. content.append(
  102. Text(
  103. element.options[element.selected]["name"],
  104. style=self.console.get_style("result"),
  105. )
  106. )
  107. else:
  108. for id_, option in enumerate(element.options):
  109. if id_ == element.selected:
  110. prefix = selected_prefix
  111. style = self.console.get_style("selected")
  112. else:
  113. prefix = not_selected_prefix
  114. style = self.console.get_style("text")
  115. is_last = id_ == len(element.options) - 1
  116. menu.append(
  117. Text.assemble(
  118. prefix,
  119. option["name"],
  120. separator if not is_last else "",
  121. style=style,
  122. )
  123. )
  124. if not element.options:
  125. menu = Text("No results found", style=self.console.get_style("text"))
  126. filter = (
  127. [
  128. Text.assemble(
  129. (element.filter_prompt, self.console.get_style("text")),
  130. (element.text, self.console.get_style("text")),
  131. "\n",
  132. )
  133. ]
  134. if element.allow_filtering
  135. else []
  136. )
  137. content.extend(filter)
  138. content.append(menu)
  139. if message := self.render_validation_message(element):
  140. validation_message = (message,)
  141. result = Group(*content)
  142. return self._box(
  143. result,
  144. self.render_input_label(element),
  145. is_active,
  146. Color.parse("white"),
  147. after=validation_message,
  148. )
  149. def render_progress(
  150. self,
  151. element: Progress,
  152. is_active: bool = False,
  153. done: bool = False,
  154. parent: Optional[Element] = None,
  155. ) -> RenderableType:
  156. content: str | Group | Text = element.current_message
  157. title: Union[str, Text, None] = None
  158. title = element.title
  159. if element.logs and element._inline_logs:
  160. lines_to_show = (
  161. element.logs[-element.lines_to_show :]
  162. if element.lines_to_show > 0
  163. else element.logs
  164. )
  165. content = Group(
  166. *[
  167. self.render_element(
  168. line,
  169. index=index,
  170. max_lines=element.lines_to_show,
  171. total_lines=len(element.logs),
  172. )
  173. for index, line in enumerate(lines_to_show)
  174. ]
  175. )
  176. border_color = Color.parse("white")
  177. if not done:
  178. colors = self._get_animation_colors(
  179. steps=10, animation_status="started", breathe=True
  180. )
  181. border_color = colors[self.animation_counter % 10]
  182. return self._box(content, title, is_active, border_color=border_color)
  183. def get_cursor_offset_for_element(
  184. self, element: Element, parent: Optional[Element] = None
  185. ) -> CursorOffset:
  186. top_offset = element.cursor_offset.top
  187. left_offset = element.cursor_offset.left + 2
  188. if isinstance(element, Input) and element.inline:
  189. # we don't support inline inputs yet in border style
  190. top_offset += 1
  191. inline_left_offset = (len(element.label) - 1) if element.label else 0
  192. left_offset = element.cursor_offset.left - inline_left_offset
  193. if isinstance(parent, Form):
  194. top_offset += 1
  195. return CursorOffset(top=top_offset, left=left_offset)