Urwid Tutorial¶
Minimal Application¶
This program displays the string Hello World
in the top left corner of the
screen and will run until interrupted with CTRL+C (^C).
1from __future__ import annotations
2
3import urwid
4
5txt = urwid.Text("Hello World")
6fill = urwid.Filler(txt, "top")
7loop = urwid.MainLoop(fill)
8loop.run()
The txt
Text
widget handles formatting blocks of text, wrapping to the next line when necessary. Widgets like this are called “flow widgets” because their sizing can have a number of columns given, in this case the full screen width, then they will flow to fill as many rows as necessary.The fill
Filler
widget fills in blank lines above or below flow widgets so that they can be displayed in a fixed number of rows. This Filler will align our Text to the top of the screen, filling all the rows below with blank lines. Widgets which are given both the number of columns and number of rows they must be displayed in are called “box widgets”.The
MainLoop
class handles displaying our widgets as well as accepting input from the user. The widget passed toMainLoop
is called the “topmost” widget. The topmost widget is used to render the whole screen and so it must be a box widget. In this case our widgets can’t handle any user input so we need to interrupt the program to exit with ^C.
Global Input¶
This program initially displays the string Hello World
, then it displays
each key pressed, exiting when the user presses Q.
1from __future__ import annotations
2
3import urwid
4
5
6def show_or_exit(key: str) -> None:
7 if key in {"q", "Q"}:
8 raise urwid.ExitMainLoop()
9 txt.set_text(repr(key))
10
11
12txt = urwid.Text("Hello World")
13fill = urwid.Filler(txt, "top")
14loop = urwid.MainLoop(fill, unhandled_input=show_or_exit)
15loop.run()
The
MainLoop
class has an optional function parameter unhandled_input. This function will be called once for each keypress that is not handled by the widgets being displayed. Since none of the widgets being displayed here handle input, every key the user presses will be passed to the show_or_exit function.The
ExitMainLoop
exception is used to exit cleanly from theMainLoop.run()
function when the user presses Q. All other input is displayed by replacing the current Text widget’s content.
Display Attributes¶
This program displays the string Hello World
in the center of the screen.
It uses different attributes for the text, the space on either side of the text
and the space above and below the text. It waits for a keypress before exiting.
The screenshots above show how these widgets react to being resized.
1from __future__ import annotations
2
3import urwid
4
5
6def exit_on_q(key: str) -> None:
7 if key in {"q", "Q"}:
8 raise urwid.ExitMainLoop()
9
10
11palette = [
12 ("banner", "black", "light gray"),
13 ("streak", "black", "dark red"),
14 ("bg", "black", "dark blue"),
15]
16
17txt = urwid.Text(("banner", " Hello World "), align="center")
18map1 = urwid.AttrMap(txt, "streak")
19fill = urwid.Filler(map1)
20map2 = urwid.AttrMap(fill, "bg")
21loop = urwid.MainLoop(map2, palette, unhandled_input=exit_on_q)
22loop.run()
Display attributes are defined as part of a palette. Valid foreground, background and setting values are documented in Foreground and Background Settings A palette is a list of tuples containing:
Name of the display attribute, typically a string
Foreground color and settings for 16-color (normal) mode
Background color for normal mode
Settings for monochrome mode (optional)
Foreground color and settings for 88 and 256-color modes (optional, see next example)
Background color for 88 and 256-color modes (optional)
A
Text
widget is created containing the string" Hello World "
with display attribute'banner'
. The attributes of text in a Text widget is set by using a (attribute, text) tuple instead of a simple text string. Display attributes will flow with the text, and multiple display attributes may be specified by combining tuples into a list. This format is called Text Markup.An
AttrMap
widget is created to wrap the text widget with display attribute'streak'
.AttrMap
widgets allow you to map any display attribute to any other display attribute, but by default they will set the display attribute of everything that does not already have a display attribute. In this case the text has an attribute, so only the areas around the text used for alignment will have the new attribute.A second
AttrMap
widget is created to wrap theFiller
widget with attribute'bg'
.
When this program is run you can now clearly see the separation of the text, the alignment around the text, and the filler above and below the text.
See also
High Color Modes¶
This program displays the string Hello World
in the center of the screen.
It uses a number of 256-color-mode colors to decorate the text,
and will work in any terminal that supports 256-color mode. It will exit when
Q is pressed.
1from __future__ import annotations
2
3import urwid
4
5
6def exit_on_q(key):
7 if key in {"q", "Q"}:
8 raise urwid.ExitMainLoop()
9
10
11palette = [
12 ("banner", "", "", "", "#ffa", "#60d"),
13 ("streak", "", "", "", "g50", "#60a"),
14 ("inside", "", "", "", "g38", "#808"),
15 ("outside", "", "", "", "g27", "#a06"),
16 ("bg", "", "", "", "g7", "#d06"),
17]
18
19placeholder = urwid.SolidFill()
20loop = urwid.MainLoop(placeholder, palette, unhandled_input=exit_on_q)
21loop.screen.set_terminal_properties(colors=256)
22loop.widget = urwid.AttrMap(placeholder, "bg")
23loop.widget.original_widget = urwid.Filler(urwid.Pile([]))
24
25div = urwid.Divider()
26outside = urwid.AttrMap(div, "outside")
27inside = urwid.AttrMap(div, "inside")
28txt = urwid.Text(("banner", " Hello World "), align="center")
29streak = urwid.AttrMap(txt, "streak")
30pile = loop.widget.base_widget # .base_widget skips the decorations
31for item in (outside, inside, streak, inside, outside):
32 pile.contents.append((item, pile.options()))
33
34loop.run()
This palette only defines values for the high color foreground and backgrounds, because only the high colors will be used. A real application should define values for all the modes in their palette. Valid foreground, background and setting values are documented in Foreground and Background Settings.
Behind the scenes our
MainLoop
class has created araw_display.Screen
object for drawing the screen. The program is put into 256-color mode by using the screen object’sset_terminal_properties()
method.
This example also demonstrates how you can build the widgets to display in a top-down order instead of the usual bottom-up order. In some places we need to use a placeholder widget because we must provide a widget before the correct one has been created.
We change the topmost widget used by the
MainLoop
by assigning to itsMainLoop.widget
property.Decoration Widgets like
AttrMap
have anoriginal_widget
property that we can assign to change the widget they wrap.Divider
widgets are used to create blank lines, colored withAttrMap
.Container Widgets like
Pile
have acontents
property that we can treat like a list of (widget, options) tuples.Pile.contents
supports normal list operations includingappend()
to add child widgets.Pile.options()
is used to generate the default options for the new child widgets.
Question and Answer¶
This program asks for your name then responds Nice to meet you, (your
name).
1from __future__ import annotations
2
3import urwid
4
5
6def exit_on_q(key: str) -> None:
7 if key in {"q", "Q"}:
8 raise urwid.ExitMainLoop()
9
10
11class QuestionBox(urwid.Filler):
12 def keypress(self, size, key: str) -> str | None:
13 if key != "enter":
14 return super().keypress(size, key)
15 self.original_widget = urwid.Text(
16 f"Nice to meet you,\n{edit.edit_text}.\n\nPress Q to exit.",
17 )
18 return None
19
20
21edit = urwid.Edit("What is your name?\n")
22fill = QuestionBox(edit)
23loop = urwid.MainLoop(fill, unhandled_input=exit_on_q)
24loop.run()
The Edit
widget is based on the Text
widget but it accepts
keyboard input for entering text, making corrections and
moving the cursor around with the HOME, END and arrow keys.
Here we are customizing the Filler
decoration widget that is holding
our Edit
widget by subclassing it and defining a new keypress()
method. Customizing decoration or container widgets to handle input this way
is a common pattern in Urwid applications. This pattern is easier to maintain
and extend than handling all special input in an unhandled_input function.
In QuestionBox.keypress() all keypresses except ENTER are passed along to the default
Filler.keypress()
which sends them to the childEdit.keypress()
method.Note that names containing Q can be entered into the
Edit
widget without causing the program to exit becauseEdit.keypress()
indicates that it has handled the key by returningNone
. SeeWidget.keypress()
for more information.When ENTER is pressed the child widget
original_widget
is changed to aText
widget.Text
widgets don’t handle any keyboard input so all input ends up in the unhandled_input function exit_on_q, allowing the user to exit the program.
Signal Handlers¶
This program asks for your name and responds Nice to meet you, (your name)
while you type your name. Press DOWN then SPACE or ENTER to exit.
1from __future__ import annotations
2
3import typing
4
5import urwid
6
7palette = [("I say", "default,bold", "default", "bold")]
8ask = urwid.Edit(("I say", "What is your name?\n"))
9reply = urwid.Text("")
10button_inst = urwid.Button("Exit")
11div = urwid.Divider()
12pile = urwid.Pile([ask, div, reply, div, button_inst])
13top = urwid.Filler(pile, valign="top")
14
15
16def on_ask_change(_edit: urwid.Edit, new_edit_text: str) -> None:
17 reply.set_text(("I say", f"Nice to meet you, {new_edit_text}"))
18
19
20def on_exit_clicked(_button: urwid.Button) -> typing.NoReturn:
21 raise urwid.ExitMainLoop()
22
23
24urwid.connect_signal(ask, "change", on_ask_change)
25urwid.connect_signal(button_inst, "click", on_exit_clicked)
26
27urwid.MainLoop(top, palette).run()
An
Edit
widget and aText
reply widget are created, like in the previous example.The
connect_signal()
function is used to attach our on_ask_change() function to ourEdit
widget’s'change'
signal. Now any time the content of theEdit
widget changes on_ask_change() will be called and passed the new content.Finally we attach our on_exit_clicked() function to our exit
Button
’s'click'
signal.on_ask_change() updates the reply text as the user enters their name and on_exit_click() exits.
Multiple Questions¶
This program asks for your name and responds Nice to meet you, (your name).
It then asks again, and again. Old values may be changed and the responses will
be updated when you press ENTER. ENTER on a blank line exits.
1from __future__ import annotations
2
3import urwid
4
5
6def question():
7 return urwid.Pile([urwid.Edit(("I say", "What is your name?\n"))])
8
9
10def answer(name):
11 return urwid.Text(("I say", f"Nice to meet you, {name}\n"))
12
13
14class ConversationListBox(urwid.ListBox):
15 def __init__(self) -> None:
16 body = urwid.SimpleFocusListWalker([question()])
17 super().__init__(body)
18
19 def keypress(self, size: tuple[int, int], key: str) -> str | None:
20 key = super().keypress(size, key)
21 if key != "enter":
22 return key
23 name = self.focus[0].edit_text
24 if not name:
25 raise urwid.ExitMainLoop()
26 # replace or add response
27 self.focus.contents[1:] = [(answer(name), self.focus.options())]
28 pos = self.focus_position
29 # add a new question
30 self.body.insert(pos + 1, question())
31 self.focus_position = pos + 1
32 return None
33
34
35palette = [("I say", "default,bold", "default")]
36urwid.MainLoop(ConversationListBox(), palette).run()
ListBox
widgets let you scroll through a number of flow widgets
vertically. It handles UP, DOWN, PAGE UP and PAGE DOWN keystrokes
and changing the focus for you. ListBox Contents are managed by
a “list walker”, one of the list walkers that is easiest to use is
SimpleFocusListWalker
.
SimpleFocusListWalker
is like a normal python list of widgets, but
any time you insert or remove widgets the focus position is updated
automatically.
Here we are customizing our ListBox
’s keypress handling by overriding
it in a subclass.
The question() function is used to build widgets to communicate with the user. Here we return a
Pile
widget with a singleEdit
widget to start.We retrieve the name entered with
ListBox.focus
to get thePile
in focus, the standard container widget method[0]
to get the first child of the pile andEdit.edit_text
to get the user-entered text.For the response we use the fact that we can treat
Pile.contents
like a list of (widget, options) tuples to create or replace any existing response by assigning a one-tuple list to contents[1:]. We create the default options usingPile.options()
.To add another question after the current one we treat our
SimpleFocusListWalker
stored asListBox.body
like a normal list of widgets by calling insert(), then update the focus position to the widget we just created.
Adventure Game¶
We can use the same sort of code to build a simple adventure game. Instead of menus we have “places” and instead of submenus and parent menus we just have “exits”. This example scrolls previous places off the top of the screen, allowing you to scroll back to view but not interact with previous places.
1from __future__ import annotations
2
3import typing
4
5import urwid
6
7if typing.TYPE_CHECKING:
8 from collections.abc import Callable, Hashable, MutableSequence
9
10
11class ActionButton(urwid.Button):
12 def __init__(
13 self,
14 caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
15 callback: Callable[[ActionButton], typing.Any],
16 ) -> None:
17 super().__init__("", on_press=callback)
18 self._w = urwid.AttrMap(urwid.SelectableIcon(caption, 1), None, focus_map="reversed")
19
20
21class Place(urwid.WidgetWrap[ActionButton]):
22 def __init__(self, name: str, choices: MutableSequence[urwid.Widget]) -> None:
23 super().__init__(ActionButton([" > go to ", name], self.enter_place))
24 self.heading = urwid.Text(["\nLocation: ", name, "\n"])
25 self.choices = choices
26 # create links back to ourself
27 for child in choices:
28 getattr(child, "choices", []).insert(0, self)
29
30 def enter_place(self, button: ActionButton) -> None:
31 game.update_place(self)
32
33
34class Thing(urwid.WidgetWrap[ActionButton]):
35 def __init__(self, name: str) -> None:
36 super().__init__(ActionButton([" * take ", name], self.take_thing))
37 self.name = name
38
39 def take_thing(self, button: ActionButton) -> None:
40 self._w = urwid.Text(f" - {self.name} (taken)")
41 game.take_thing(self)
42
43
44def exit_program(button: ActionButton) -> typing.NoReturn:
45 raise urwid.ExitMainLoop()
46
47
48map_top = Place(
49 "porch",
50 [
51 Place(
52 "kitchen",
53 [
54 Place("refrigerator", []),
55 Place("cupboard", [Thing("jug")]),
56 ],
57 ),
58 Place(
59 "garden",
60 [
61 Place(
62 "tree",
63 [
64 Thing("lemon"),
65 Thing("bird"),
66 ],
67 ),
68 ],
69 ),
70 Place(
71 "street",
72 [
73 Place("store", [Thing("sugar")]),
74 Place(
75 "lake",
76 [Place("beach", [])],
77 ),
78 ],
79 ),
80 ],
81)
82
83
84class AdventureGame:
85 def __init__(self) -> None:
86 self.log = urwid.SimpleFocusListWalker([])
87 self.top = urwid.ListBox(self.log)
88 self.inventory = set()
89 self.update_place(map_top)
90
91 def update_place(self, place: Place) -> None:
92 if self.log: # disable interaction with previous place
93 self.log[-1] = urwid.WidgetDisable(self.log[-1])
94 self.log.append(urwid.Pile([place.heading, *place.choices]))
95 self.top.focus_position = len(self.log) - 1
96 self.place = place
97
98 def take_thing(self, thing: Thing) -> None:
99 self.inventory.add(thing.name)
100 if self.inventory >= {"sugar", "lemon", "jug"}:
101 response = urwid.Text("You can make lemonade!\n")
102 done = ActionButton(" - Joy", exit_program)
103 self.log[:] = [response, done]
104 else:
105 self.update_place(self.place)
106
107
108game = AdventureGame()
109urwid.MainLoop(game.top, palette=[("reversed", "standout", "")]).run()
This example starts to show some separation between the application logic and the widgets that have been created. The AdventureGame class is responsible for all the changes that happen through the game and manages the topmost widget, but isn’t a widget itself. This is a good pattern to follow as your application grows larger.