Spaces:
Sleeping
Sleeping
File size: 10,314 Bytes
2d876d1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
"""Contains the interface class :class:`.BaseComplexPrompt` for more complex prompts and the mocked document class :class:`.FakeDocument`."""
import shutil
from dataclasses import dataclass
from typing import Any, Callable, List, Optional, Tuple, Union
from prompt_toolkit.application import Application
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.filters.base import Condition, FilterOrBool
from prompt_toolkit.key_binding.key_bindings import KeyHandlerCallable
from prompt_toolkit.keys import Keys
from InquirerPy.base.simple import BaseSimplePrompt
from InquirerPy.enum import INQUIRERPY_KEYBOARD_INTERRUPT
from InquirerPy.utils import (
InquirerPySessionResult,
InquirerPyStyle,
InquirerPyValidate,
)
@dataclass
class FakeDocument:
"""A fake `prompt_toolkit` document class.
Work around to allow non-buffer type :class:`~prompt_toolkit.layout.UIControl` to use
:class:`~prompt_toolkit.validation.Validator`.
Args:
text: Content to be validated.
cursor_position: Fake cursor position.
"""
text: str
cursor_position: int = 0
class BaseComplexPrompt(BaseSimplePrompt):
"""A base class to create a more complex prompt that will involve :class:`~prompt_toolkit.application.Application`.
Note:
This class does not create :class:`~prompt_toolkit.layout.Layout` nor :class:`~prompt_toolkit.application.Application`,
it only contains the necessary attributes and helper functions to be consumed.
Note:
Use :class:`~InquirerPy.base.BaseListPrompt` to create a complex list prompt which involves multiple choices. It has
more methods and helper function implemented.
See Also:
:class:`~InquirerPy.base.BaseListPrompt`
:class:`~InquirerPy.prompts.fuzzy.FuzzyPrompt`
"""
def __init__(
self,
message: Union[str, Callable[[InquirerPySessionResult], str]],
style: Optional[InquirerPyStyle] = None,
border: bool = False,
vi_mode: bool = False,
qmark: str = "?",
amark: str = "?",
instruction: str = "",
long_instruction: str = "",
transformer: Optional[Callable[[Any], Any]] = None,
filter: Optional[Callable[[Any], Any]] = None,
validate: Optional[InquirerPyValidate] = None,
invalid_message: str = "Invalid input",
wrap_lines: bool = True,
raise_keyboard_interrupt: bool = True,
mandatory: bool = True,
mandatory_message: str = "Mandatory prompt",
session_result: Optional[InquirerPySessionResult] = None,
) -> None:
super().__init__(
message=message,
style=style,
vi_mode=vi_mode,
qmark=qmark,
amark=amark,
instruction=instruction,
transformer=transformer,
filter=filter,
invalid_message=invalid_message,
validate=validate,
wrap_lines=wrap_lines,
raise_keyboard_interrupt=raise_keyboard_interrupt,
mandatory=mandatory,
mandatory_message=mandatory_message,
session_result=session_result,
)
self._invalid_message = invalid_message
self._rendered = False
self._invalid = False
self._loading = False
self._application: Application
self._long_instruction = long_instruction
self._border = border
self._height_offset = 2 # prev prompt result + current prompt question
if self._border:
self._height_offset += 2
if self._long_instruction:
self._height_offset += 1
self._validation_window_bottom_offset = 0 if not self._long_instruction else 1
if self._wrap_lines:
self._validation_window_bottom_offset += (
self.extra_long_instruction_line_count
)
self._is_vim_edit = Condition(lambda: self._editing_mode == EditingMode.VI)
self._is_invalid = Condition(lambda: self._invalid)
self._is_displaying_long_instruction = Condition(
lambda: self._long_instruction != ""
)
def _redraw(self) -> None:
"""Redraw the application UI."""
self._application.invalidate()
def register_kb(
self, *keys: Union[Keys, str], filter: FilterOrBool = True
) -> Callable[[KeyHandlerCallable], KeyHandlerCallable]:
"""Decorate keybinding registration function.
Ensure that the `invalid` state is cleared on next keybinding entered.
"""
kb_dec = super().register_kb(*keys, filter=filter)
def decorator(func: KeyHandlerCallable) -> KeyHandlerCallable:
@kb_dec
def executable(event):
if self._invalid:
self._invalid = False
func(event)
return executable
return decorator
def _exception_handler(self, _, context) -> None:
"""Set exception handler for the event loop.
Skip the question and raise exception.
Args:
loop: Current event loop.
context: Exception context.
"""
self._status["answered"] = True
self._status["result"] = INQUIRERPY_KEYBOARD_INTERRUPT
self._status["skipped"] = True
self._application.exit(exception=context["exception"])
def _after_render(self, app: Optional[Application]) -> None:
"""Run after the :class:`~prompt_toolkit.application.Application` is rendered/updated.
Since this function is fired up on each render, adding a check on `self._rendered` to
process logics that should only run once.
Set event loop exception handler here, since its guaranteed that the event loop is running
in `_after_render`.
"""
if not self._rendered:
self._rendered = True
self._keybinding_factory()
self._on_rendered(app)
def _set_error(self, message: str) -> None:
"""Set error message and set invalid state.
Args:
message: Error message to display.
"""
self._invalid_message = message
self._invalid = True
def _get_error_message(self) -> List[Tuple[str, str]]:
"""Obtain the error message dynamically.
Returns:
FormattedText in list of tuple format.
"""
return [
(
"class:validation-toolbar",
self._invalid_message,
)
]
def _on_rendered(self, _: Optional[Application]) -> None:
"""Run once after the UI is rendered. Acts like `ComponentDidMount`."""
pass
def _get_prompt_message(self) -> List[Tuple[str, str]]:
"""Get the prompt message to display.
Returns:
Formatted text in list of tuple format.
"""
pre_answer = (
"class:instruction",
" %s " % self.instruction if self.instruction else " ",
)
post_answer = ("class:answer", " %s" % self.status["result"])
return super()._get_prompt_message(pre_answer, post_answer)
def _run(self) -> Any:
"""Run the application."""
return self.application.run()
async def _run_async(self) -> None:
"""Run the application asynchronously."""
return await self.application.run_async()
@property
def application(self) -> Application:
"""Get the application.
:class:`.BaseComplexPrompt` requires :attr:`.BaseComplexPrompt._application` to be defined since this class
doesn't implement :class:`~prompt_toolkit.layout.Layout` and :class:`~prompt_toolkit.application.Application`.
Raises:
NotImplementedError: When `self._application` is not defined.
"""
if not self._application:
raise NotImplementedError
return self._application
@application.setter
def application(self, value: Application) -> None:
self._application = value
@property
def height_offset(self) -> int:
"""int: Height offset to apply."""
if not self._wrap_lines:
return self._height_offset
return self.extra_line_count + self._height_offset
@property
def total_message_length(self) -> int:
"""int: Total length of the message."""
total_message_length = 0
if self._qmark:
total_message_length += len(self._qmark)
total_message_length += 1 # Extra space if qmark is present
total_message_length += len(str(self._message))
total_message_length += 1 # Extra space between message and instruction
total_message_length += len(str(self._instruction))
if self._instruction:
total_message_length += 1 # Extra space behind the instruction
return total_message_length
@property
def extra_message_line_count(self) -> int:
"""int: Get the extra lines created caused by line wrapping.
Minus 1 on the totoal message length as we only want the extra line.
24 // 24 will equal to 1 however we only want the value to be 1 when we have 25 char
which will create an extra line.
"""
term_width, _ = shutil.get_terminal_size()
return (self.total_message_length - 1) // term_width
@property
def extra_long_instruction_line_count(self) -> int:
"""int: Get the extra lines created caused by line wrapping.
See Also:
:attr:`.BaseComplexPrompt.extra_message_line_count`
"""
if self._long_instruction:
term_width, _ = shutil.get_terminal_size()
return (len(self._long_instruction) - 1) // term_width
else:
return 0
@property
def extra_line_count(self) -> int:
"""Get the extra lines created caused by line wrapping.
Used mainly to calculate how much additional offset should be applied when getting
the height.
Returns:
Total extra lines created due to line wrapping.
"""
result = 0
# message wrap
result += self.extra_message_line_count
# long instruction wrap
result += self.extra_long_instruction_line_count
return result
|