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).
1 2 3 4 5 6 | import urwid
txt = urwid.Text(u"Hello World")
fill = urwid.Filler(txt, 'top')
loop = urwid.MainLoop(fill)
loop.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.
1 2 3 4 5 6 7 8 9 10 11 | import urwid
def show_or_exit(key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()
txt.set_text(repr(key))
txt = urwid.Text(u"Hello World")
fill = urwid.Filler(txt, 'top')
loop = urwid.MainLoop(fill, unhandled_input=show_or_exit)
loop.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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import urwid
def exit_on_q(key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()
palette = [
('banner', 'black', 'light gray'),
('streak', 'black', 'dark red'),
('bg', 'black', 'dark blue'),]
txt = urwid.Text(('banner', u" Hello World "), align='center')
map1 = urwid.AttrMap(txt, 'streak')
fill = urwid.Filler(map1)
map2 = urwid.AttrMap(fill, 'bg')
loop = urwid.MainLoop(map2, palette, unhandled_input=exit_on_q)
loop.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 be 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import urwid
def exit_on_q(key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()
palette = [
('banner', '', '', '', '#ffa', '#60d'),
('streak', '', '', '', 'g50', '#60a'),
('inside', '', '', '', 'g38', '#808'),
('outside', '', '', '', 'g27', '#a06'),
('bg', '', '', '', 'g7', '#d06'),]
placeholder = urwid.SolidFill()
loop = urwid.MainLoop(placeholder, palette, unhandled_input=exit_on_q)
loop.screen.set_terminal_properties(colors=256)
loop.widget = urwid.AttrMap(placeholder, 'bg')
loop.widget.original_widget = urwid.Filler(urwid.Pile([]))
div = urwid.Divider()
outside = urwid.AttrMap(div, 'outside')
inside = urwid.AttrMap(div, 'inside')
txt = urwid.Text(('banner', u" Hello World "), align='center')
streak = urwid.AttrMap(txt, 'streak')
pile = loop.widget.base_widget # .base_widget skips the decorations
for item in [outside, inside, streak, inside, outside]:
pile.contents.append((item, pile.options()))
loop.run()
|
This palette only defines values for the high color foregroundand 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 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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import urwid
def exit_on_q(key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()
class QuestionBox(urwid.Filler):
def keypress(self, size, key):
if key != 'enter':
return super(QuestionBox, self).keypress(size, key)
self.original_widget = urwid.Text(
u"Nice to meet you,\n%s.\n\nPress Q to exit." %
edit.edit_text)
edit = urwid.Edit(u"What is your name?\n")
fill = QuestionBox(edit)
loop = urwid.MainLoop(fill, unhandled_input=exit_on_q)
loop.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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import urwid
palette = [('I say', 'default,bold', 'default', 'bold'),]
ask = urwid.Edit(('I say', u"What is your name?\n"))
reply = urwid.Text(u"")
button = urwid.Button(u'Exit')
div = urwid.Divider()
pile = urwid.Pile([ask, div, reply, div, button])
top = urwid.Filler(pile, valign='top')
def on_ask_change(edit, new_edit_text):
reply.set_text(('I say', u"Nice to meet you, %s" % new_edit_text))
def on_exit_clicked(button):
raise urwid.ExitMainLoop()
urwid.connect_signal(ask, 'change', on_ask_change)
urwid.connect_signal(button, 'click', on_exit_clicked)
urwid.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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import urwid
def question():
return urwid.Pile([urwid.Edit(('I say', u"What is your name?\n"))])
def answer(name):
return urwid.Text(('I say', u"Nice to meet you, " + name + "\n"))
class ConversationListBox(urwid.ListBox):
def __init__(self):
body = urwid.SimpleFocusListWalker([question()])
super(ConversationListBox, self).__init__(body)
def keypress(self, size, key):
key = super(ConversationListBox, self).keypress(size, key)
if key != 'enter':
return key
name = self.focus[0].edit_text
if not name:
raise urwid.ExitMainLoop()
# replace or add response
self.focus.contents[1:] = [(answer(name), self.focus.options())]
pos = self.focus_position
# add a new question
self.body.insert(pos + 1, question())
self.focus_position = pos + 1
palette = [('I say', 'default,bold', 'default'),]
urwid.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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | import urwid
class ActionButton(urwid.Button):
def __init__(self, caption, callback):
super(ActionButton, self).__init__("")
urwid.connect_signal(self, 'click', callback)
self._w = urwid.AttrMap(urwid.SelectableIcon(caption, 1),
None, focus_map='reversed')
class Place(urwid.WidgetWrap):
def __init__(self, name, choices):
super(Place, self).__init__(
ActionButton([u" > go to ", name], self.enter_place))
self.heading = urwid.Text([u"\nLocation: ", name, "\n"])
self.choices = choices
# create links back to ourself
for child in choices:
getattr(child, 'choices', []).insert(0, self)
def enter_place(self, button):
game.update_place(self)
class Thing(urwid.WidgetWrap):
def __init__(self, name):
super(Thing, self).__init__(
ActionButton([u" * take ", name], self.take_thing))
self.name = name
def take_thing(self, button):
self._w = urwid.Text(u" - %s (taken)" % self.name)
game.take_thing(self)
def exit_program(button):
raise urwid.ExitMainLoop()
map_top = Place(u'porch', [
Place(u'kitchen', [
Place(u'refrigerator', []),
Place(u'cupboard', [
Thing(u'jug'),
]),
]),
Place(u'garden', [
Place(u'tree', [
Thing(u'lemon'),
Thing(u'bird'),
]),
]),
Place(u'street', [
Place(u'store', [
Thing(u'sugar'),
]),
Place(u'lake', [
Place(u'beach', []),
]),
]),
])
class AdventureGame(object):
def __init__(self):
self.log = urwid.SimpleFocusListWalker([])
self.top = urwid.ListBox(self.log)
self.inventory = set()
self.update_place(map_top)
def update_place(self, place):
if self.log: # disable interaction with previous place
self.log[-1] = urwid.WidgetDisable(self.log[-1])
self.log.append(urwid.Pile([place.heading] + place.choices))
self.top.focus_position = len(self.log) - 1
self.place = place
def take_thing(self, thing):
self.inventory.add(thing.name)
if self.inventory >= set([u'sugar', u'lemon', u'jug']):
response = urwid.Text(u'You can make lemonade!\n')
done = ActionButton(u' - Joy', exit_program)
self.log[:] = [response, done]
else:
self.update_place(self.place)
game = AdventureGame()
urwid.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.