Python urwid
Urwid is a higher level abstraction to the curses library. It introduces a layout and widget system to curses, while still providing access to curses core.
Basic Concepts
Use the Source
Urwid's documentation is useful, but not as complete as I would like it to be. For greater understanding, I recommend installing it into a python virtualenv, and using a debugger to step through the code. As always,
grep -rli
is always useful.Widget Types
http://urwid.org/manual/widgets.html#included-widgets
urwid has three primary types of widgets. Flow Widgets, BoxWidgets,and FixedWidgets. Fixed Widgets are not well defined, and currently only encompass 'LargeText' Widgets.
Not all widgets can contain other widget types. If you are receiving the error Too Many Values to Unpack, you might want to try putting your layout in one of the conversion layouts like a Filler.
Flow Widgets widgets whose contents will overlap onto the next line if there is not enough room. Box Widgets widgets with a fixed height/width. The top-level widget is always a box-widget. Fixed Widgets ?? pile = urwid.Pile([ ## Flow Widget urwid.Buton('A'), urwid.Button('B'), ]) filler = urwid.Filler( pile ) ## Box Widget MainLoop( filler ).run() ## (mainloop expects box widgets)layout.contents
This is how you add/inspect the contents of layouts in urwid. It is universal across all container types. After the object has been created, appending objects requires a tuple of the container's options. These are specific to the layout type.
pile = urwid.Pile([]) ## Empty Pile pile.contents.append(( urwid.Text('A'), pile.options() )) ## Append Single Item pile.contents.append(( urwid.Text('B'), ('weight', 10) )) ## Append Single Item (manually specifying pile.options() rather than using default ) print pile.contents ## List of Widgets print len(pile.contents) ## List Number of Widets in layout print pile.focus ## Get name of Selected Widget. (Read Only) print pile.focus_position ## Get/Set selected widget by it's index in it's parent layout. print main_loop._topmost_widget ## (I believe this is the top layout) print main_loop.base_widget ## (I believe this is the selected widget)keypress methods
Every widget/layout contains a keypress method you can override for binding custom actions to keypresses. If subclassing widgets and calling the keypress method using
super
, make sure to capture the return value as keys, and return it.
The order that keypresses are evaluated are from the top-most layout, to the bottom-most widget. Before processing each layout, if it has contents, it's focused child's keypress is evaluated. The key is then compared against the widget's_command_map._command
dict, if a command is bound to it, the command is run (and typically, the keypress returns None). If a command is not bound to it, the key is returned, and the parent layout tries to interpret the key. Occasionally (notably with cursor-movements), a widget returns a different value to be interpreted by the parent. For example: 'cursor down' returns 'down'. You can bind these values directly if you know that is your desired behaviour.Keypress Life-Cycle
+Startup+ | -+ main_loop._reset_input_descriptors | | | row_display.Screen.hook_event_loop | Subscribe main_loop._update to each keypress | | ( keypresses appear to be logged to a file ) row_display._make_legacy_input_wrapper | -+ +Key Event (from FIFO?)+ | main_loop._update -+ | | Process each key event +-main_loop.input_filter | | | +-main_loop.process_input | | | | | +-main_loop._topmost_widget.keypress( key, ord ) | -+ | | | | Starting from topmost layout, navigate to | +-next_widget.keypress | | bottom widget, try to process key, | | | | and then each consecutive parent layout | +-next_widget.keypress | | processes the return value of it's child. | | -+ +-main_loop.unhandled_input | -+Example Subclass
import urwid class MyButton(urwid.Button): def __init__(self, *args, **kwds): super(MyButton, self).__init__( *args, **kwds ) def keypress( self, size, key ): """ ___________________________________________________________________________________________________________________ size | (30, 40) | | The Container's size. One of the dimensions | | | is allowed to be undefined. ex: (30,) | | | key | 'meta a', 'ctrl z', 'b' | | The name of the button pressed. Certain | | | ctrl+key combos are problematic. """ if key in ('a','b'): print 'a or b' elif key == 'esc': raise urwid.ExitMainLoop() else: key = super( MyButton, self).keypress( size, key ) return keyurwid.command_map
This builds a dictionary that links keypresses to functions. Most keypress functions use something similar to:
if self._command_map[key] == 'cursor up':
to process input. All of the default bindings use a variableCURSOR_LEFT
, but when parsed they end up as lower-case stringscursor up
. I have no idea why this is.button = urwid.Button('A') button._command_map['z'] = 'do something'
Access Curses!
urwid not man enough for you? Fall back onto curses and do it yourself. You can access urwid's curses display. (see Curses).
urwid.curses_display.clear() ...
First Examples
Simple EventLoop
Here's a simple example of a program that processes a user's input using urwid.
import urwid class MyUrwid( object ): def __init__(self): self.main() def main(self): pile = urwid.Pile([ urwid.Button('A'), urwid.Button('B'), urwid.Button('C'), ]) main = urwid.Filler( pile ) urwid.MainLoop( widget = main , unhandled_input = self.keypress , ).run() def keypress(self, key): if key in ('q','Q'): raise urwid.ExitMainLoop() ## Exit else: return key
Widgets
Text
Rather than curses where text is just that, inrwid Text() is a textblock.
import urwid txt = urwid.Text() txt.set_text('Hello')
Edit/IntEdit
Edit is an urwid.Text(), that accepts input.
Signals changeEmitted every time the text is modified qstn = urwid.Edit('How Old Are you?') ## Form, Requesting Input print qstn.edit_text() ## Form's (current) User-Input print qstn.text() ## Form's (label) textButton
Buttons
Signals clickEmitted whenever a button is clicked on with a mouse, or a button bound to 'activate' is pressed while the button is selected. ## Constructor okbtn = urwid.Button( ## All Button's Flags are optional label = 'Okay' , on_press = self.runFunc , user_data = 'send this string with signal' ) ## Signal Sample urwid.connect_signal( okbtn, 'click', self.function, user_data=None ) ## connect signal ## Functions label = okbtn.get_label() okbtn.set_label('newLabel')
CheckBox
Signals changeEmitted every time the text is modified Divider
A Horizontal Rule used as space between objects. You can choose the character to repeat.
Linebox
A Widget with a border. You can assign a title that will be shown on the top left of the border.
btn = urwid.Button('Test') urwid.Linebox( btn, title='My Title' )SolidFill
Fills widget with a single repeated character.
BarGraph
ProgressBar
Terminal
Best For Last. Yes, a fully featured terminal. In your GUI. sexy sexy sexy sexy sexy sexy sexy sexy.
Layouts
General Info
#### Focus col = urwid.Columns([]) col.focus ## Get selected widget (cannot set) col.focus_position ## Get/Set the selected widget within a layout#### Contents ## Every layout also has an attribute called ''contents'', which can be used as a list ## that stores a layout's contents. When appending to contents, each layout type requires ## a different set of options(). This is a tuple that typically consists of <code>('weight', 10)</code>. col = urwid.Columns([]) col.contents.append(( urwid.Button('A'), col.options() )) ## Append to Layout print col.contents ## List of Contents print len(col.contents) ## NumItems in layout#### Switching Containers ## Switching Containers is fairly simple. The keypress is sent to the ## parent container, and the next selection is determined by the distance ## from sel.focus_position to either len(self.contents), or 0. +-TopLayout---------------------------------+ |+--ColumnA--------------------------------+| || +--Column1--+-Column2-----+-Column3---+ || self.focus_position is 1 || | | | | || ( 2nd index in ColumnA ) || | | (self) | | || || | | | | || || | | | | || || +-----------+-------------+-----------+ || |+-----------------------------------------+| +-------------------------------------------+ ## left count down from self.focus_position to 0 ## right count from self.focus_position to len(self.contents) #### How Parent Layouts are selected ## Each keypress either triggers a function, or returns a key ## when a key is returned, it is passed to the parent layout ## this continues until there is no parent layout. widget in Column2 is passed to Column2 Column2 intercepts key, or passes to ColumnA ColumnA intercepts key, or passes to TopLayoutFrame/Pile (Vertical)
Frames/Piles are your QVBoxLayouts. Piles will allow you to change focus with keystrokes, Frames will not. Frames and Piles can be assigned header and footer widgets that are tied to the top/bottom of the frame/pile.
## Adding Widgets pile = urwid.Pile([ urwid.Button('A'), urwid.Button('B'), urwid.Button('C'), ])## Appending Widgets pile = urwid.Pile([]) pile.contents.append( urwid.Text('AAA'), pile.options() ) pile.contents.append( urwid.Text('AAA'), ('weight', 10) )## Weighted Sizing of Widgets btnA = urwid.Button('A') btnB = urwid.Button('B') btnC = urwid.Button('C') urwid.Pile([ ('weight', 10, btnA ), ('weight', 2, btnB), ('weight', 5, btnC) ])## Resizing Layout ## ##! I can't figure out how to dynamically rezize these yet #### Attributes index = pile.focus_position() ## Index of Widget Under Focus widget = pile.focus_item() ## Widget under Focus widgetList = pile.contents() ## list of pile widgets numWidgets = len(pile.contents) ## number of pile widgets
Columns (Horizontal)
(QHBoxLayout). Arranges Widgets in Horizontal Columns. Used Interchangeably with Pile.
#### Append to Column col = urwid.Columns([]) col.contents.append(( urwid.Text('test'), col.options() ))#### Add List of Widgets colWidgets = [ urwid.Button('A'), urwid.Button('B'), urwid.Button('C'), ] column = urwid.Columns( colWidgets )#### Size-Weighted Columns btnA = urwid.Button('A') btnB = urwid.Button('B') btnC = urwid.Button('C') urwid.Columns([ ('weight', 10, btnA ), ('weight', 2, btnB), ('weight', 5, btnC) ])## ##! I can't figure out how to dynamically rezize these yet ##GridFlow
Creates a Flow Grid of items in your database. Based on your set
cell_width
, your items will be pushed onwards to the next row if there isn't enough room in your terminal window. Arranges so heights/widths match. This IS NOT analogous to a gridLayout in Qt.
## IMPORTANT ## # GridFlow must be placed in a FlowWidget (like Filler) in order to work. items = [ ## Create List of items to add to ListBox urwid.Text('monkey' ) , urwid.Text('aardvark' ) , urwid.Text('toast' ) , urwid.Text('monkey' ) , urwid.Text('aardvark' ) , urwid.Text('toast' ) ] grid = urwid.GridFlow( ## Add Items to GridFlow cells = ([ txt for txt in items ]), cell_width = 20 , h_sep = 3 , v_sep = 1 , align = 'left' ) main = urwid.Filler( grid ) ## Add GridFlow to Filler # (so can be main layout in MainLoop() urwid.MainLoop( main, palette, unhandled_input=keypress ).run()ListBox
Manages Lists of Objects. Note That ListBoxes are Box Widgets which means they must have a fixed height. In order to add a listBox to a layout, you must first add it to a BoxAdapter.
Signals modified(listWalker) Emitted every time the selected widget changes Attributes
listbox = urwid.ListBox( listWalker ) print listbox.body ## all children of contained listWalker as iterable print lisbox.body[-1] ## the last object in the listbox print listbox.contents ## listWalker object itself.Complete Example
ListBoxes are both important, and can be a little obscure in the official documentation. Here is a simple example of how they actually work. Fortunately, they are MUCH easier to use than my own first-stab at in curses.
import urwid import pdb palette = [ ('normal' , 'black' , 'light gray' ) , ('selected' , 'light gray' , 'black' ) , ] items = [ ## Create List of items to add to ListBox urwid.Text('monkey' ) , urwid.Text('aardvark' ) , urwid.Text('toast' ) ] content = urwid.SimpleListWalker([ ## The AttrMap is applied when objects are urwid.AttrMap(w, None, 'selected') for w in items ## marked as selected. SimpleListWalkers manage ]) ## the contents of a list listbox = urwid.ListBox( content ) ## Assign the SimpleListWalker to the listbox def keypress(input): ## keypress will be our main eventloop pdb.set_trace() if input in ('q','Q'): raise urwid.ExitMainLoop() elif input in ('up', 'k'): focus_widget, idx = listbox.get_focus() # getFocus returns (widget, index) if idx > 0: idx = idx-1 listbox.set_focus(idx) elif input in ('down','j'): focus_widget, idx = listbox.get_focus() if idx < len(listbox.body) -1: # listbox's body attribute is a list of all idx = idx+1 # top-level widgets in listWalker (in this case AttrMaps) listbox.set_focus(idx) urwid.MainLoop( ## Run the mainloop, like anywhere else! listbox, palette, unhandled_input=keypress ).run()Example with BoxAdapter:
## List listConts = [ urwid.Button('a'), urwid.Button('b'), urwid.Button('c'), urwid.Button('d'), ] listWalk = urwid.SimpleListWalker( listConts ) listbox = urwid.ListBox( listWalk ) listBoxAdapt = urwid.BoxAdapter( listbox, height=2 ) main = urwid.Filler( listbox )
Columns of Listboxes# ListboxA listboxAConts = [urwid.Text("Some widgets here:"),urwid.Edit("Edits","")] listboxA = urwid.ListBox( listboxAConts ) # ListboxB listConts = [] for i in xrange(0, 1000): listConts.append( urwid.Button(str(i)) ) listWalk = urwid.SimpleListWalker( listConts ) listboxB = vimctl.ListBox( listWalk ) # ListboxC listConts = [ urwid.Button('a'), urwid.Button('b'), urwid.Button('c') ] listWalk = urwid.SimpleListWalker( listConts ) listboxC = vimctl.ListBox( listWalk ) ## All Together now! body = urwid.Columns([ ('weight',1,listboxA), ('weight',3,listboxB), ('weight',1,listboxC) ])Filler
Filler Widgets are boxlayouts. They can contain other widgets, and they can be weighted to one side or another.
txt = urwid.Text('Child Text') fill = urwid.Filler( child, 'top' )
Signals and Events
Ian is pretty explicit about users not needing to create their own signal instances. They are not even present in the glossary, you need to search to get a description. Regardless, here is how to use existing ones, search signal to find more information on creating your own.
Signals are not well documented in urwid, but they are simple enough to find by searching the source code for
signals =
btn = urwid.Button('Exit') def on_button_click( button ): raise urwid.ExitMainLoop() urwid.connect_signal( btn, 'click', on_button_click ) ## Standard Signal urwid.connect_signal( btn, 'click', one_button_click, listSentOnClick ) ## Signal Can be sent with an additional variable to carry data
Colours
Colours can be applied by creating 'palettes', and applying them to the mainloop's constructor. These are essentially named pairs of fg/bg colours. urwid palettes allow you to create fallback colours when you're using more than 16x colours, so that you have a palette for 8 colours, for 88 colours, and for 256 colours.
Named Colours (16x)
## Palette: NAME FG BG palette = [ ('banner' , 'black' , 'light gray' ) , ('streak' , 'black' , 'dark red' ) , ('bg' , 'black' , 'dark blue' ) ,] txt = urwid.Text(( 'banner', 'Text to be replaced'), align='center') map1 = urwid.AttrMap(txt, 'streak') ## AttrMaps work like QSS in Qt. Anything parented under fill = urwid.Filler( map1 ) ## an Attrmap will inherit it's colours, unless they have colours ## chosen for them. map2 = urwid.AttrMap(fill, 'bg') loop = urwid.MainLoop( map2, palette, unhandled_input=show_or_exit) loop.run()
Hex Colours
Now we're talking!
# Palette: NAME 16fg 16bg mono 256-fg 256-bg palette = [ ('banner' , '', '', '', '#000' , '#333' ) , ## I don't understand mono's role yet ('streak' , '', '', '', '#000' , '#777' ) , ('bg' , '', '', '', '#000' , '#888' ) ,] loop = urwid.MainLoop( widget, palette, unhandled_input=show_or_exit) loop.screen.set_terminal_properties(colors=256)Nested Colours within line
Say for example you were doing syntaxhighlighting, and needed some words to be formatted differently than other words. This is how you would do it:
## name fg bg palette = [ ('red', 'red', '' ), ## An empty string means to leave a colour alone. ('blue', 'blue','' ),] txt = urwid.Text( 'red' 'MyText') ## Entire Line is Red txt = urwid.Text([ 'My', ('red', 'Text') ]) ## My is default, Text is RedText Formatting
Text Formatting is also done in the palette, and is applied by adding a comma after the colourName, with a text attribute.
palette = [ ('red_bold', 'red,bold' ), ## Note that ONLY STANDOUT is available ('blue_und', 'blue,underline' ), ## in a TTY. ('green_stnd', 'dark green,standout'), ]
Alignment
This is going to cover broadly anything related to positioning widgets, both within layouts, and their attributes themselves.
## Widget Alignment urwid.Text('test', align='top' ) ## center and position at top urwid.Text('test', valign='top' ) ## Widget vertical alignment urwid.Text('test', halign='left') ## Widget horizontal alignment
Threaded GUI Loading
urwid's main_loop has a hook
watch_pipe
that listens over aos.pipe()
for changes during each iteration of the event loop. This can be used to load a GUI in a separate thread while still maintaining the ability to interact with it.
The following class is broken up by it's methods for readability.
class ThreadedGui( object ): def __init__( self ): """ Start GUI """ import threading self.pile = urwid.Pile([]) self.pile.contents.append(( urwid.Button('start'), self.pile.options() )) self.main = urwid.Filler( self.pile ) self.loading_btn = threading.Event() self.quitEvent = threading.Event() self.quitEvent.clear() self.loop = urwid.MainLoop( widget = self.main , unhandled_input = self.unhandled_input , ) self.loop.run()def unhandled_input( self, input ): """ keypress func, press 'q' to quit, press 'i' to add buttons to the list """ import threading if input in ('q','Q'): raise urwid.ExitMainLoop() elif input == 'i': self.write_fd = self.loop.watch_pipe( self.add_buttons ) ## any data written/flushed into write_fd will be passed as # an argument to self.add_buttons threading.Thread( target = self.thread_generate_buttons, args = ( self.write_fd, ), ).start()def add_buttons( self, data ): """ Adds a Button Widget to the pile every time data is received on write_fd. Button is named after the value sent. """ if data: self.loading_btn.set() self.pile.contents.append(( urwid.Button( data ), self.pile.options() )) self.loop.draw_screen() self.loading_btn.clear() else: return Falsedef thread_generate_buttons(self, write_fd ): """ Run in separate thread, every 'write' to the file descriptor is passed as an argument to add_buttons(), which adds a new button to the list. """ import time import os for i in xrange(10000): while not self.loading_btn.is_set(): if self.quitEvent.is_set(): break os.write( write_fd, str(i) ) time.sleep(0.005) if self.quitEvent.is_set(): break ## PoisonPill, once data is received, the file descriptor is ## continuously read until data of '' is sent. os.write( write_fd, '' )