diff --git a/questionary/prompts/checkbox.py b/questionary/prompts/checkbox.py index 95234652..f7d89a5b 100644 --- a/questionary/prompts/checkbox.py +++ b/questionary/prompts/checkbox.py @@ -38,6 +38,7 @@ def checkbox( use_jk_keys: bool = True, use_emacs_keys: bool = True, instruction: Optional[str] = None, + custom_key_bindings: Optional[Dict[Union[str, Keys], Callable]] = None, **kwargs: Any, ) -> Question: """Ask the user to select from a list of items. @@ -104,8 +105,28 @@ def checkbox( use_emacs_keys: Allow the user to select items from the list using `Ctrl+N` (down) and `Ctrl+P` (up) keys. + instruction: A message describing how to navigate the menu. + custom_key_bindings: A dictionary specifying custom key bindings for the + prompt. The dictionary should have key-value pairs, + where the key represents the key combination or key + code, and the value is a callable that will be + executed when the key is pressed. The callable + should take an ``event`` object as its argument, + which will provide information about the key event. + + Examples: + + - Exit with a result of ``custom`` when the user + presses :kbd:`c`:: + + {"c": lambda event: event.app.exit(result="custom")} + + - Exit with a result of ``ctrl-q`` when the user + presses :kbd:`Ctrl` + :kbd:`q`:: + + {Keys.ControlQ: lambda event: event.app.exit(result="ctrl-q")} Returns: :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). """ @@ -202,6 +223,9 @@ def perform_validation(selected_values: List[str]) -> bool: layout = common.create_inquirer_layout(ic, get_prompt_tokens, **kwargs) bindings = KeyBindings() + if custom_key_bindings is not None: + for key, func in custom_key_bindings.items(): + bindings.add(key, eager=True)(func) @bindings.add(Keys.ControlQ, eager=True) @bindings.add(Keys.ControlC, eager=True) diff --git a/questionary/prompts/confirm.py b/questionary/prompts/confirm.py index cbb5347e..72ba6a9b 100644 --- a/questionary/prompts/confirm.py +++ b/questionary/prompts/confirm.py @@ -1,5 +1,8 @@ from typing import Any +from typing import Callable +from typing import Dict from typing import Optional +from typing import Union from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import to_formatted_text @@ -22,6 +25,7 @@ def confirm( qmark: str = DEFAULT_QUESTION_PREFIX, style: Optional[Style] = None, auto_enter: bool = True, + custom_key_bindings: Optional[Dict[Union[str, Keys], Callable]] = None, instruction: Optional[str] = None, **kwargs: Any, ) -> Question: @@ -59,6 +63,26 @@ def confirm( accept their answer. If set to `True`, a valid input will be accepted without the need to press 'Enter'. + custom_key_bindings: A dictionary specifying custom key bindings for the + prompt. The dictionary should have key-value pairs, + where the key represents the key combination or key + code, and the value is a callable that will be + executed when the key is pressed. The callable + should take an ``event`` object as its argument, + which will provide information about the key event. + + Examples: + + - Exit with a result of ``custom`` when the user + presses :kbd:`c`:: + + {"c": lambda event: event.app.exit(result="custom")} + + - Exit with a result of ``ctrl-q`` when the user + presses :kbd:`Ctrl` + :kbd:`q`:: + + {Keys.ControlQ: lambda event: event.app.exit(result="ctrl-q")} + instruction: A message describing how to proceed through the confirmation prompt. Returns: @@ -75,7 +99,7 @@ def get_prompt_tokens(): tokens.append(("class:question", " {} ".format(message))) if instruction is not None: - tokens.append(("class:instruction", instruction)) + tokens.append(("class:instruction", "{} ".format(instruction))) elif not status["complete"]: _instruction = YES_OR_NO if default else NO_OR_YES tokens.append(("class:instruction", "{} ".format(_instruction))) @@ -91,6 +115,9 @@ def exit_with_result(event): event.app.exit(result=status["answer"]) bindings = KeyBindings() + if custom_key_bindings is not None: + for key, func in custom_key_bindings.items(): + bindings.add(key, eager=True)(func) @bindings.add(Keys.ControlQ, eager=True) @bindings.add(Keys.ControlC, eager=True) diff --git a/questionary/prompts/select.py b/questionary/prompts/select.py index d6d41c5d..791d5a6d 100644 --- a/questionary/prompts/select.py +++ b/questionary/prompts/select.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from typing import Any +from typing import Callable from typing import Dict from typing import Optional from typing import Sequence @@ -36,6 +37,7 @@ def select( use_emacs_keys: bool = True, show_selected: bool = False, instruction: Optional[str] = None, + custom_key_bindings: Optional[Dict[Union[str, Keys], Callable]] = None, **kwargs: Any, ) -> Question: """A list of items to select **one** option from. @@ -110,6 +112,26 @@ def select( show_selected: Display current selection choice at the bottom of list. + custom_key_bindings: A dictionary specifying custom key bindings for the + prompt. The dictionary should have key-value pairs, + where the key represents the key combination or key + code, and the value is a callable that will be + executed when the key is pressed. The callable + should take an ``event`` object as its argument, + which will provide information about the key event. + + Examples: + + - Exit with a result of ``custom`` when the user + presses :kbd:`c`:: + + {"c": lambda event: event.app.exit(result="custom")} + + - Exit with a result of ``ctrl-q`` when the user + presses :kbd:`Ctrl` + :kbd:`q`:: + + {Keys.ControlQ: lambda event: event.app.exit(result="ctrl-q")} + Returns: :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). """ @@ -186,6 +208,10 @@ def get_prompt_tokens(): bindings = KeyBindings() + if custom_key_bindings is not None: + for key, func in custom_key_bindings.items(): + bindings.add(key, eager=True)(func) + @bindings.add(Keys.ControlQ, eager=True) @bindings.add(Keys.ControlC, eager=True) def _(event): diff --git a/questionary/prompts/text.py b/questionary/prompts/text.py index 03452055..f3d47634 100644 --- a/questionary/prompts/text.py +++ b/questionary/prompts/text.py @@ -1,9 +1,14 @@ from typing import Any +from typing import Callable +from typing import Dict from typing import List from typing import Optional from typing import Tuple +from typing import Union from prompt_toolkit.document import Document +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.keys import Keys from prompt_toolkit.lexers import Lexer from prompt_toolkit.lexers import SimpleLexer from prompt_toolkit.shortcuts.prompt import PromptSession @@ -25,6 +30,7 @@ def text( multiline: bool = False, instruction: Optional[str] = None, lexer: Optional[Lexer] = None, + custom_key_bindings: Optional[Dict[Union[str, Keys], Callable]] = None, **kwargs: Any, ) -> Question: """Prompt the user to enter a free text message. @@ -70,6 +76,26 @@ def text( lexer: Supply a valid lexer to style the answer. Leave empty to use a simple one by default. + custom_key_bindings: A dictionary specifying custom key bindings for the + prompt. The dictionary should have key-value pairs, + where the key represents the key combination or key + code, and the value is a callable that will be + executed when the key is pressed. The callable + should take an ``event`` object as its argument, + which will provide information about the key event. + + Examples: + + - Exit with a result of ``custom`` when the user + presses :kbd:`c`:: + + {"c": lambda event: event.app.exit(result="custom")} + + - Exit with a result of ``ctrl-q`` when the user + presses :kbd:`Ctrl` + :kbd:`q`:: + + {Keys.ControlQ: lambda event: event.app.exit(result="ctrl-q")} + kwargs: Additional arguments, they will be passed to prompt toolkit. Returns: @@ -88,8 +114,14 @@ def get_prompt_tokens() -> List[Tuple[str, str]]: result.append(("class:instruction", " {} ".format(instruction))) return result + bindings = KeyBindings() + if custom_key_bindings is not None: + for key, func in custom_key_bindings.items(): + bindings.add(key, eager=True)(func) + p: PromptSession = PromptSession( get_prompt_tokens, + key_bindings=bindings, style=merged_style, validator=validator, lexer=lexer, diff --git a/tests/prompts/test_checkbox.py b/tests/prompts/test_checkbox.py index 4b7c8404..e88d0dbc 100644 --- a/tests/prompts/test_checkbox.py +++ b/tests/prompts/test_checkbox.py @@ -34,6 +34,20 @@ def test_select_with_instruction(): assert result == ["foo"] +def test_select_with_custom_key_bindings(): + message = "Foo message" + kwargs = { + "choices": ["foo", "bar", "bazz"], + "custom_key_bindings": { + KeyInputs.ONE: lambda event: event.app.exit(result="1-pressed") + }, + } + text = KeyInputs.ONE + "\r" + + result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) + assert result == "1-pressed" + + def test_select_first_choice_with_token_title(): message = "Foo message" kwargs = { diff --git a/tests/prompts/test_confirm.py b/tests/prompts/test_confirm.py index 7345c431..11c78be7 100644 --- a/tests/prompts/test_confirm.py +++ b/tests/prompts/test_confirm.py @@ -93,6 +93,21 @@ def test_confirm_not_autoenter_backspace(): assert result is True +def test_confirm_with_custom_key_bindings(): + message = "Foo message" + kwargs = { + "custom_key_bindings": { + KeyInputs.ONE: lambda event: event.app.exit(result="1-pressed") + } + } + text = KeyInputs.ONE + "\r" + + result, cli = feed_cli_with_input( + "confirm", message, text, auto_enter=False, **kwargs + ) + assert result == "1-pressed" + + def test_confirm_instruction(): message = "Foo message" text = "Y" + "\r" diff --git a/tests/prompts/test_select.py b/tests/prompts/test_select.py index 1d3eaa57..4a02a360 100644 --- a/tests/prompts/test_select.py +++ b/tests/prompts/test_select.py @@ -103,6 +103,20 @@ def test_select_with_instruction(): assert result == "foo" +def test_select_with_custom_key_bindings(): + message = "Foo message" + kwargs = { + "choices": ["foo", "bar", "bazz"], + "custom_key_bindings": { + KeyInputs.ONE: lambda event: event.app.exit(result="1-pressed") + }, + } + text = KeyInputs.ONE + "\r" + + result, cli = feed_cli_with_input("select", message, text, **kwargs) + assert result == "1-pressed" + + def test_cycle_to_first_choice(): message = "Foo message" kwargs = {"choices": ["foo", "bar", "bazz"]} diff --git a/tests/prompts/test_text.py b/tests/prompts/test_text.py index efe3c850..54c18e51 100644 --- a/tests/prompts/test_text.py +++ b/tests/prompts/test_text.py @@ -4,6 +4,7 @@ from prompt_toolkit.validation import ValidationError from prompt_toolkit.validation import Validator +from tests.utils import KeyInputs from tests.utils import feed_cli_with_input @@ -36,6 +37,19 @@ def test_text_validate(): assert result == "Doe" +def test_text_with_custom_key_bindings(): + message = "What is your name" + kwargs = { + "custom_key_bindings": { + KeyInputs.ONE: lambda event: event.app.exit(result="1-pressed") + } + } + text = KeyInputs.ONE + "\r" + + result, cli = feed_cli_with_input("text", message, text, **kwargs) + assert result == "1-pressed" + + def test_text_validate_with_class(): class SimpleValidator(Validator): def validate(self, document):