From f8b759cf0b90fe8fe2e05c70a343b503dc553df7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 28 Aug 2025 15:03:44 +0000 Subject: [PATCH] Add support for rich formatted text using the 'rich' library. This allows for using rich objects anywhere in prompt_toolkit where formatted text is expected. --- examples/rich/choices.py | 46 +++++++++++++++++ examples/rich/dialog.py | 21 ++++++++ examples/rich/multiline-prompt.py | 32 ++++++++++++ examples/rich/prompt-with-frame.py | 15 ++++++ examples/rich/prompt.py | 13 +++++ src/prompt_toolkit/formatted_text/__init__.py | 3 ++ src/prompt_toolkit/formatted_text/base.py | 13 +++++ src/prompt_toolkit/formatted_text/rich.py | 50 +++++++++++++++++++ 8 files changed, 193 insertions(+) create mode 100755 examples/rich/choices.py create mode 100755 examples/rich/dialog.py create mode 100755 examples/rich/multiline-prompt.py create mode 100755 examples/rich/prompt-with-frame.py create mode 100755 examples/rich/prompt.py create mode 100644 src/prompt_toolkit/formatted_text/rich.py diff --git a/examples/rich/choices.py b/examples/rich/choices.py new file mode 100755 index 000000000..65ec4a8df --- /dev/null +++ b/examples/rich/choices.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +from rich.markdown import Markdown +from rich.text import Text + +from prompt_toolkit import choice +from prompt_toolkit.formatted_text.rich import Rich + +# For the header, we wrap the `Markdown` object from `rich` in a `Rich` object +# from `prompt_toolkit`, so that we can explicitly set a width. +header = Rich( + Markdown( + """ +# Please select a dish + +Choose *one* item please. + +```python +def some_example_function() -> None: "test" +``` +""".strip() + ), + width=50, + style="black on blue", +) + + +def main(): + answer = choice( + message=header, + options=[ + ("pizza", "Pizza with mushrooms"), + ( + "salad", + Text.from_markup( + ":warning: [green]Salad[/green] with [red]tomatoes[/red]" + ), + ), + ("sushi", "Sushi"), + ], + show_frame=True, + ) + print(f"You said: {answer}") + + +if __name__ == "__main__": + main() diff --git a/examples/rich/dialog.py b/examples/rich/dialog.py new file mode 100755 index 000000000..7ad6cc21e --- /dev/null +++ b/examples/rich/dialog.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +""" +Example of an input box dialog. +""" + +from rich.text import Text + +from prompt_toolkit.shortcuts import input_dialog + + +def main(): + result = input_dialog( + title=Text.from_markup("[red]Input[/red] dialog [b]example[b]"), + text=Text.from_markup("Please type your [green]name[/green]:"), + ).run() + + print(f"Result = {result}") + + +if __name__ == "__main__": + main() diff --git a/examples/rich/multiline-prompt.py b/examples/rich/multiline-prompt.py new file mode 100755 index 000000000..321cf0ccf --- /dev/null +++ b/examples/rich/multiline-prompt.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +from rich.markdown import Markdown + +from prompt_toolkit import prompt +from prompt_toolkit.formatted_text import merge_formatted_text +from prompt_toolkit.formatted_text.rich import Rich + +# For the header, we wrap the `Markdown` object from `rich` in a `Rich` object +# from `prompt_toolkit`, so that we can explicitly set a width. +header = Rich( + Markdown( + """ +# Type the name of the following function: + +```python +def fibonacci(number: int) -> int: + "compute Fibonacci number" +``` + +""" + ), + width=50, +) + + +def main(): + answer = prompt(merge_formatted_text([header, "> "])) + print(f"You said: {answer}") + + +if __name__ == "__main__": + main() diff --git a/examples/rich/prompt-with-frame.py b/examples/rich/prompt-with-frame.py new file mode 100755 index 000000000..be35b3f15 --- /dev/null +++ b/examples/rich/prompt-with-frame.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +from rich.text import Text + +from prompt_toolkit import prompt + + +def main(): + answer = prompt( + Text.from_markup("[green]Say[/green] [b]something[/b] > "), show_frame=True + ) + print(f"You said: {answer}") + + +if __name__ == "__main__": + main() diff --git a/examples/rich/prompt.py b/examples/rich/prompt.py new file mode 100755 index 000000000..e61aaceab --- /dev/null +++ b/examples/rich/prompt.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +from rich.text import Text + +from prompt_toolkit import prompt + + +def main(): + answer = prompt(Text.from_markup("[green]Say[/green] [b]something[/b] > ")) + print(f"You said: {answer}") + + +if __name__ == "__main__": + main() diff --git a/src/prompt_toolkit/formatted_text/__init__.py b/src/prompt_toolkit/formatted_text/__init__.py index 0590c8158..8edaf3dc5 100644 --- a/src/prompt_toolkit/formatted_text/__init__.py +++ b/src/prompt_toolkit/formatted_text/__init__.py @@ -26,6 +26,7 @@ ) from .html import HTML from .pygments import PygmentsTokens +from .rich import Rich from .utils import ( fragment_list_len, fragment_list_to_text, @@ -50,6 +51,8 @@ "ANSI", # Pygments. "PygmentsTokens", + # Rich. + "Rich", # Utils. "fragment_list_len", "fragment_list_width", diff --git a/src/prompt_toolkit/formatted_text/base.py b/src/prompt_toolkit/formatted_text/base.py index 5fee1f862..e1d5ac619 100644 --- a/src/prompt_toolkit/formatted_text/base.py +++ b/src/prompt_toolkit/formatted_text/base.py @@ -40,10 +40,19 @@ class MagicFormattedText(Protocol): def __pt_formatted_text__(self) -> StyleAndTextTuples: ... + class RichFormattedText(Protocol): + """ + Any rich text object from the rich library that implements + ``__rich_console__``. + """ + + def __rich_console__(self, console: Any = ..., options: Any = ...) -> Any: ... + AnyFormattedText = Union[ str, "MagicFormattedText", + "RichFormattedText", StyleAndTextTuples, # Callable[[], 'AnyFormattedText'] # Recursive definition not supported by mypy. Callable[[], Any], @@ -78,6 +87,10 @@ def to_formatted_text( result = value # StyleAndTextTuples elif hasattr(value, "__pt_formatted_text__"): result = cast("MagicFormattedText", value).__pt_formatted_text__() + elif hasattr(value, "__rich_console__"): + from .rich import Rich + + result = Rich(value).__pt_formatted_text__() elif callable(value): return to_formatted_text(value(), style=style) elif auto_convert: diff --git a/src/prompt_toolkit/formatted_text/rich.py b/src/prompt_toolkit/formatted_text/rich.py new file mode 100644 index 000000000..f5bcf8e22 --- /dev/null +++ b/src/prompt_toolkit/formatted_text/rich.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from io import StringIO +from typing import TYPE_CHECKING, Any + +from .ansi import ANSI +from .base import StyleAndTextTuples + +if TYPE_CHECKING: + from rich.style import StyleType + +__all__ = [ + "Rich", +] + + +class Rich: + """ + Turn any rich text object from the `rich` library into prompt_toolkit + formatted text, so that it can be used in a prompt or anywhere else. + + Note that `to_formatted_text` automatically recognizes objects that have a + `__rich_console__` attribute and will wrap them in a `Rich` instance. + """ + + def __init__( + self, + rich_object: Any, + width: int | None = None, + style: StyleType | None = None, + ) -> None: + self.rich_object = rich_object + self.width = width + self.style = style + + def __pt_formatted_text__(self) -> StyleAndTextTuples: + from rich.console import Console + + file = StringIO() + + console = Console( + file=file, + force_terminal=True, + color_system="truecolor", + width=self.width, + style=self.style, + ) + console.print(self.rich_object, end="") + ansi = file.getvalue() + return ANSI(ansi).__pt_formatted_text__()