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 ───────────────────────────────────────────────┐
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
> :
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)