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