Getting a Prompt inside a Layout using Python Rich

Question:

Is it possible to get user input using a Prompt within a Layout element using Python Rich?

My aim is to use Rich’s Layout to build a full-screen window with 4 panes. The top 3, containing title, ingredients and method work fine, but I would like the bottom one to contain a Prompt for user input.

Desired output:

The text the user enters appears inside the bottom panel of the layout.

┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                                                   │
│ Chocolate cheesecake                                                                                              │
│                                                                                                                   │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────── 'ingredients' (58 x 7) ────────────────┐┌─────────────────── 'method' (59 x 7) ───────────────────┐
│                                                        ││                                                         │
│                                                        ││                                                         │
│               Layout(name='ingredients')               ││                  Layout(name='method')                  │
│                                                        ││                                                         │
│                                                        ││                                                         │
└────────────────────────────────────────────────────────┘└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────── Search for a recipe ───────────────────────────────────────────────┐
│                                                                                                                   │
│  > :                                                                                                              │
│                                                                                                                   │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

My attempt:

from rich import print
from rich.panel import Panel
from rich.layout import Layout
from rich.prompt import Prompt

def rich_ui():
    while True:
        layout = Layout()
        layout.split_column(
            Layout(name="banner"),
            Layout(name="recipe"),
            Layout(name="search")
        )

        layout['banner'].update(Panel('Chocolate cheesecake', padding=1))
        layout['banner'].size = 5

        layout['recipe'].split_row(
            Layout(name="ingredients"),
            Layout(name="method")
        )

        layout['search'].update(Panel(Prompt.ask('> '), title='Search for a recipe'))
        layout['search'].size = 5
        print(layout)

if __name__ == '__main__':
    rich_ui()

Actual output:

Notice the prompt’s >: is outside the layout section.

┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                                                   │
│ Chocolate cheesecake                                                                                              │
│                                                                                                                   │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────── 'ingredients' (58 x 7) ────────────────┐┌─────────────────── 'method' (59 x 7) ───────────────────┐
│                                                        ││                                                         │
│                                                        ││                                                         │
│               Layout(name='ingredients')               ││                  Layout(name='method')                  │
│                                                        ││                                                         │
│                                                        ││                                                         │
└────────────────────────────────────────────────────────┘└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────── Search for a recipe ───────────────────────────────────────────────┐
│                                                                                                                   │
│                                                                                                                   │
│                                                                                                                   │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
> :
Asked By: PangolinPaws

||

Answers:

It is possible, but Rich doesn’t have a built in way of doing this. You would have to capture keys, possibly with the keyboard module (on PyPi), and update the layout accordingly.

Answered By: Will McGugan

It is also possible, and arguably easier to do with Textual if you’re willing to adopt it for your text based GUI. I like it for my needs but @Will McGugan may have something to say about the future of Textual.

You just have to implement a new widget as your text input control, but it is not that much code for a simple one. You’d use the keyboard input mechanisms for Textual itself.

Something like this:

class InputBox(Widget):
    """takes typed input mostly for debugging"""
    has_focus: Reactive[bool] = Reactive(False)
    style: Reactive[str] = Reactive("")
    height: Reactive[int or None] = Reactive(None)
    text: Reactive[str] = Reactive("")

    def __init__(self, *, name: str or None = None, height: int or None = None, callback: Callable[[str], None] = None) -> None:
        super().__init__(name=name)
        self.height = height
        self.callback = callback

    def render(self) -> Panel:
        return Panel(
            self.text,
            title=self.name,
            box=box.HEAVY if self.has_focus else box.ROUNDED,
            style="cyan" if self.has_focus else "dim white",
            height=self.height,
            highlight=True
        )

    async def on_focus(self, event: events.Focus) -> None:
        self.has_focus = True

    async def on_blur(self, event: events.Blur) -> None:
        self.has_focus = False

    async def on_key(self, event: events.Key) -> None:
        """Handle key presses."""
        self.log(event)
        if event.key == "ctrl+h":
            self.text = self.text[:-1]
        elif event.key == "enter":
            # process input
            if self.callback:
                self.callback(self.text)
            self.text = ""
        elif len(event.key) == 1:
            self.text += str(event.key)
Answered By: Warped Pixel
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.