summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorIan Ward <ian@excess.org>2011-11-29 00:20:13 -0500
committerIan Ward <ian@excess.org>2011-11-29 00:20:13 -0500
commit001d8940531111f34297943d7e75b34ffa02c8d2 (patch)
treeda7076988d9816e675de6731192cc57a2dda74fb /examples
parentb16c4b3269339f657a97be383770c54730626d12 (diff)
downloadurwid-001d8940531111f34297943d7e75b34ffa02c8d2.tar.gz
move examples to examples/
--HG-- rename : bigtext.py => examples/bigtext.py rename : browse.py => examples/browse.py rename : calc.py => examples/calc.py rename : dialog.py => examples/dialog.py rename : edit.py => examples/edit.py rename : fib.py => examples/fib.py rename : graph.py => examples/graph.py rename : input_test.py => examples/input_test.py rename : lcd_cf635.py => examples/lcd_cf635.py rename : palette_test.py => examples/palette_test.py rename : pop_up.py => examples/pop_up.py rename : subproc.py => examples/subproc.py rename : subproc2.py => examples/subproc2.py rename : terminal.py => examples/terminal.py rename : tour.py => examples/tour.py rename : treesample.py => examples/treesample.py rename : twisted_serve_ssh.py => examples/twisted_serve_ssh.py rename : twisted_serve_ssh.tac => examples/twisted_serve_ssh.tac
Diffstat (limited to 'examples')
-rwxr-xr-xexamples/bigtext.py161
-rwxr-xr-xexamples/browse.py388
-rwxr-xr-xexamples/calc.py819
-rwxr-xr-xexamples/dialog.py343
-rwxr-xr-xexamples/edit.py255
-rwxr-xr-xexamples/fib.py112
-rwxr-xr-xexamples/graph.py356
-rwxr-xr-xexamples/input_test.py102
-rwxr-xr-xexamples/lcd_cf635.py291
-rwxr-xr-xexamples/palette_test.py252
-rwxr-xr-xexamples/pop_up.py41
-rwxr-xr-xexamples/subproc.py30
-rw-r--r--examples/subproc2.py8
-rwxr-xr-xexamples/terminal.py56
-rwxr-xr-xexamples/tour.py333
-rwxr-xr-xexamples/treesample.py138
-rw-r--r--examples/twisted_serve_ssh.py456
-rw-r--r--examples/twisted_serve_ssh.tac41
18 files changed, 4182 insertions, 0 deletions
diff --git a/examples/bigtext.py b/examples/bigtext.py
new file mode 100755
index 0000000..aa5c3a1
--- /dev/null
+++ b/examples/bigtext.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+#
+# Urwid BigText example program
+# Copyright (C) 2004-2009 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid example demonstrating use of the BigText widget.
+"""
+
+import urwid
+import urwid.raw_display
+
+
+class SwitchingPadding(urwid.Padding):
+ def padding_values(self, size, focus):
+ maxcol = size[0]
+ width, ignore = self.original_widget.pack(size, focus=focus)
+ if maxcol > width:
+ self.align = "left"
+ else:
+ self.align = "right"
+ return urwid.Padding.padding_values(self, size, focus)
+
+
+class BigTextDisplay:
+ palette = [
+ ('body', 'black', 'light gray', 'standout'),
+ ('header', 'white', 'dark red', 'bold'),
+ ('button normal','light gray', 'dark blue', 'standout'),
+ ('button select','white', 'dark green'),
+ ('button disabled','dark gray','dark blue'),
+ ('edit', 'light gray', 'dark blue'),
+ ('bigtext', 'white', 'black'),
+ ('chars', 'light gray', 'black'),
+ ('exit', 'white', 'dark cyan'),
+ ]
+
+ def create_radio_button(self, g, name, font, fn):
+ w = urwid.RadioButton(g, name, False, on_state_change=fn)
+ w.font = font
+ w = urwid.AttrWrap(w, 'button normal', 'button select')
+ return w
+
+ def create_disabled_radio_button(self, name):
+ w = urwid.Text(" " + name + " (UTF-8 mode required)")
+ w = urwid.AttrWrap(w, 'button disabled')
+ return w
+
+ def create_edit(self, label, text, fn):
+ w = urwid.Edit(label, text)
+ urwid.connect_signal(w, 'change', fn)
+ fn(w, text)
+ w = urwid.AttrWrap(w, 'edit')
+ return w
+
+ def set_font_event(self, w, state):
+ if state:
+ self.bigtext.set_font(w.font)
+ self.chars_avail.set_text(w.font.characters())
+
+ def edit_change_event(self, widget, text):
+ self.bigtext.set_text(text)
+
+ def setup_view(self):
+ fonts = urwid.get_all_fonts()
+ # setup mode radio buttons
+ self.font_buttons = []
+ group = []
+ utf8 = urwid.get_encoding_mode() == "utf8"
+ for name, fontcls in fonts:
+ font = fontcls()
+ if font.utf8_required and not utf8:
+ rb = self.create_disabled_radio_button(name)
+ else:
+ rb = self.create_radio_button(group, name, font,
+ self.set_font_event)
+ if fontcls == urwid.Thin6x6Font:
+ chosen_font_rb = rb
+ exit_font = font
+ self.font_buttons.append( rb )
+
+ # Create BigText
+ self.bigtext = urwid.BigText("", None)
+ bt = SwitchingPadding(self.bigtext, 'left', None)
+ bt = urwid.AttrWrap(bt, 'bigtext')
+ bt = urwid.Filler(bt, 'bottom', None, 7)
+ bt = urwid.BoxAdapter(bt, 7)
+
+ # Create chars_avail
+ cah = urwid.Text("Characters Available:")
+ self.chars_avail = urwid.Text("", wrap='any')
+ ca = urwid.AttrWrap(self.chars_avail, 'chars')
+
+ chosen_font_rb.set_state(True) # causes set_font_event call
+
+ # Create Edit widget
+ edit = self.create_edit("", "Urwid "+urwid.__version__,
+ self.edit_change_event)
+
+ # ListBox
+ chars = urwid.Pile([cah, ca])
+ fonts = urwid.Pile([urwid.Text("Fonts:")] + self.font_buttons,
+ focus_item=1)
+ col = urwid.Columns([('fixed',16,chars), fonts], 3,
+ focus_column=1)
+ bt = urwid.Pile([bt, edit], focus_item=1)
+ l = [bt, urwid.Divider(), col]
+ w = urwid.ListBox(urwid.SimpleListWalker(l))
+
+ # Frame
+ w = urwid.AttrWrap(w, 'body')
+ hdr = urwid.Text("Urwid BigText example program - F8 exits.")
+ hdr = urwid.AttrWrap(hdr, 'header')
+ w = urwid.Frame(header=hdr, body=w)
+
+ # Exit message
+ exit = urwid.BigText(('exit'," Quit? "), exit_font)
+ exit = urwid.Overlay(exit, w, 'center', None, 'middle', None)
+ return w, exit
+
+
+ def main(self):
+ self.view, self.exit_view = self.setup_view()
+ self.loop = urwid.MainLoop(self.view, self.palette,
+ unhandled_input=self.unhandled_input)
+ self.loop.run()
+
+ def unhandled_input(self, key):
+ if key == 'f8':
+ self.loop.widget = self.exit_view
+ return True
+ if self.loop.widget != self.exit_view:
+ return
+ if key in ('y', 'Y'):
+ raise urwid.ExitMainLoop()
+ if key in ('n', 'N'):
+ self.loop.widget = self.view
+ return True
+
+
+def main():
+ BigTextDisplay().main()
+
+if '__main__'==__name__:
+ main()
diff --git a/examples/browse.py b/examples/browse.py
new file mode 100755
index 0000000..e182cde
--- /dev/null
+++ b/examples/browse.py
@@ -0,0 +1,388 @@
+#!/usr/bin/python
+#
+# Urwid example lazy directory browser / tree view
+# Copyright (C) 2004-2011 Ian Ward
+# Copyright (C) 2010 Kirk McDonald
+# Copyright (C) 2010 Rob Lanphier
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid example lazy directory browser / tree view
+
+Features:
+- custom selectable widgets for files and directories
+- custom message widgets to identify access errors and empty directories
+- custom list walker for displaying widgets in a tree fashion
+- outputs a quoted list of files and directories "selected" on exit
+"""
+
+import itertools
+import re
+import os
+
+import urwid
+
+
+class FlagFileWidget(urwid.TreeWidget):
+ # apply an attribute to the expand/unexpand icons
+ unexpanded_icon = urwid.AttrMap(urwid.TreeWidget.unexpanded_icon,
+ 'dirmark')
+ expanded_icon = urwid.AttrMap(urwid.TreeWidget.expanded_icon,
+ 'dirmark')
+
+ def __init__(self, node):
+ self.__super.__init__(node)
+ # insert an extra AttrWrap for our own use
+ self._w = urwid.AttrWrap(self._w, None)
+ self.flagged = False
+ self.update_w()
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ """allow subclasses to intercept keystrokes"""
+ key = self.__super.keypress(size, key)
+ if key:
+ key = self.unhandled_keys(size, key)
+ return key
+
+ def unhandled_keys(self, size, key):
+ """
+ Override this method to intercept keystrokes in subclasses.
+ Default behavior: Toggle flagged on space, ignore other keys.
+ """
+ if key == " ":
+ self.flagged = not self.flagged
+ self.update_w()
+ else:
+ return key
+
+ def update_w(self):
+ """Update the attributes of self.widget based on self.flagged.
+ """
+ if self.flagged:
+ self._w.attr = 'flagged'
+ self._w.focus_attr = 'flagged focus'
+ else:
+ self._w.attr = 'body'
+ self._w.focus_attr = 'focus'
+
+
+class FileTreeWidget(FlagFileWidget):
+ """Widget for individual files."""
+ def __init__(self, node):
+ self.__super.__init__(node)
+ path = node.get_value()
+ add_widget(path, self)
+
+ def get_display_text(self):
+ return self.get_node().get_key()
+
+
+
+class EmptyWidget(urwid.TreeWidget):
+ """A marker for expanded directories with no contents."""
+ def get_display_text(self):
+ return ('flag', '(empty directory)')
+
+
+class ErrorWidget(urwid.TreeWidget):
+ """A marker for errors reading directories."""
+
+ def get_display_text(self):
+ return ('error', "(error/permission denied)")
+
+
+class DirectoryWidget(FlagFileWidget):
+ """Widget for a directory."""
+ def __init__(self, node):
+ self.__super.__init__(node)
+ path = node.get_value()
+ add_widget(path, self)
+ self.expanded = starts_expanded(path)
+ self.update_expanded_icon()
+
+ def get_display_text(self):
+ node = self.get_node()
+ if node.get_depth() == 0:
+ return "/"
+ else:
+ return node.get_key()
+
+
+class FileNode(urwid.TreeNode):
+ """Metadata storage for individual files"""
+
+ def __init__(self, path, parent=None):
+ depth = path.count(dir_sep())
+ key = os.path.basename(path)
+ urwid.TreeNode.__init__(self, path, key=key, parent=parent, depth=depth)
+
+ def load_parent(self):
+ parentname, myname = os.path.split(self.get_value())
+ parent = DirectoryNode(parentname)
+ parent.set_child_node(self.get_key(), self)
+ return parent
+
+ def load_widget(self):
+ return FileTreeWidget(self)
+
+
+class EmptyNode(urwid.TreeNode):
+ def load_widget(self):
+ return EmptyWidget(self)
+
+
+class ErrorNode(urwid.TreeNode):
+ def load_widget(self):
+ return ErrorWidget(self)
+
+
+class DirectoryNode(urwid.ParentNode):
+ """Metadata storage for directories"""
+
+ def __init__(self, path, parent=None):
+ if path == dir_sep():
+ depth = 0
+ key = None
+ else:
+ depth = path.count(dir_sep())
+ key = os.path.basename(path)
+ urwid.ParentNode.__init__(self, path, key=key, parent=parent,
+ depth=depth)
+
+ def load_parent(self):
+ parentname, myname = os.path.split(self.get_value())
+ parent = DirectoryNode(parentname)
+ parent.set_child_node(self.get_key(), self)
+ return parent
+
+ def load_child_keys(self):
+ dirs = []
+ files = []
+ try:
+ path = self.get_value()
+ # separate dirs and files
+ for a in os.listdir(path):
+ if os.path.isdir(os.path.join(path,a)):
+ dirs.append(a)
+ else:
+ files.append(a)
+ except OSError, e:
+ depth = self.get_depth() + 1
+ self._children[None] = ErrorNode(self, parent=self, key=None,
+ depth=depth)
+ return [None]
+
+ # sort dirs and files
+ dirs.sort(key=alphabetize)
+ files.sort(key=alphabetize)
+ # store where the first file starts
+ self.dir_count = len(dirs)
+ # collect dirs and files together again
+ keys = dirs + files
+ if len(keys) == 0:
+ depth=self.get_depth() + 1
+ self._children[None] = EmptyNode(self, parent=self, key=None,
+ depth=depth)
+ keys = [None]
+ return keys
+
+ def load_child_node(self, key):
+ """Return either a FileNode or DirectoryNode"""
+ index = self.get_child_index(key)
+ if key is None:
+ return EmptyNode(None)
+ else:
+ path = os.path.join(self.get_value(), key)
+ if index < self.dir_count:
+ return DirectoryNode(path, parent=self)
+ else:
+ path = os.path.join(self.get_value(), key)
+ return FileNode(path, parent=self)
+
+ def load_widget(self):
+ return DirectoryWidget(self)
+
+
+class DirectoryBrowser:
+ palette = [
+ ('body', 'black', 'light gray'),
+ ('flagged', 'black', 'dark green', ('bold','underline')),
+ ('focus', 'light gray', 'dark blue', 'standout'),
+ ('flagged focus', 'yellow', 'dark cyan',
+ ('bold','standout','underline')),
+ ('head', 'yellow', 'black', 'standout'),
+ ('foot', 'light gray', 'black'),
+ ('key', 'light cyan', 'black','underline'),
+ ('title', 'white', 'black', 'bold'),
+ ('dirmark', 'black', 'dark cyan', 'bold'),
+ ('flag', 'dark gray', 'light gray'),
+ ('error', 'dark red', 'light gray'),
+ ]
+
+ footer_text = [
+ ('title', "Directory Browser"), " ",
+ ('key', "UP"), ",", ('key', "DOWN"), ",",
+ ('key', "PAGE UP"), ",", ('key', "PAGE DOWN"),
+ " ",
+ ('key', "SPACE"), " ",
+ ('key', "+"), ",",
+ ('key', "-"), " ",
+ ('key', "LEFT"), " ",
+ ('key', "HOME"), " ",
+ ('key', "END"), " ",
+ ('key', "Q"),
+ ]
+
+
+ def __init__(self):
+ cwd = os.getcwd()
+ store_initial_cwd(cwd)
+ self.header = urwid.Text("")
+ self.listbox = urwid.TreeListBox(urwid.TreeWalker(DirectoryNode(cwd)))
+ self.listbox.offset_rows = 1
+ self.footer = urwid.AttrWrap(urwid.Text(self.footer_text),
+ 'foot')
+ self.view = urwid.Frame(
+ urwid.AttrWrap(self.listbox, 'body'),
+ header=urwid.AttrWrap(self.header, 'head'),
+ footer=self.footer)
+
+ def main(self):
+ """Run the program."""
+
+ self.loop = urwid.MainLoop(self.view, self.palette,
+ unhandled_input=self.unhandled_input)
+ self.loop.run()
+
+ # on exit, write the flagged filenames to the console
+ names = [escape_filename_sh(x) for x in get_flagged_names()]
+ print " ".join(names)
+
+ def unhandled_input(self, k):
+ # update display of focus directory
+ if k in ('q','Q'):
+ raise urwid.ExitMainLoop()
+
+
+def main():
+ DirectoryBrowser().main()
+
+
+
+
+#######
+# global cache of widgets
+_widget_cache = {}
+
+def add_widget(path, widget):
+ """Add the widget for a given path"""
+
+ _widget_cache[path] = widget
+
+def get_flagged_names():
+ """Return a list of all filenames marked as flagged."""
+
+ l = []
+ for w in _widget_cache.values():
+ if w.flagged:
+ l.append(w.get_node().get_value())
+ return l
+
+
+
+######
+# store path components of initial current working directory
+_initial_cwd = []
+
+def store_initial_cwd(name):
+ """Store the initial current working directory path components."""
+
+ global _initial_cwd
+ _initial_cwd = name.split(dir_sep())
+
+def starts_expanded(name):
+ """Return True if directory is a parent of initial cwd."""
+
+ if name is '/':
+ return True
+
+ l = name.split(dir_sep())
+ if len(l) > len(_initial_cwd):
+ return False
+
+ if l != _initial_cwd[:len(l)]:
+ return False
+
+ return True
+
+
+def escape_filename_sh(name):
+ """Return a hopefully safe shell-escaped version of a filename."""
+
+ # check whether we have unprintable characters
+ for ch in name:
+ if ord(ch) < 32:
+ # found one so use the ansi-c escaping
+ return escape_filename_sh_ansic(name)
+
+ # all printable characters, so return a double-quoted version
+ name.replace('\\','\\\\')
+ name.replace('"','\\"')
+ name.replace('`','\\`')
+ name.replace('$','\\$')
+ return '"'+name+'"'
+
+
+def escape_filename_sh_ansic(name):
+ """Return an ansi-c shell-escaped version of a filename."""
+
+ out =[]
+ # gather the escaped characters into a list
+ for ch in name:
+ if ord(ch) < 32:
+ out.append("\\x%02x"% ord(ch))
+ elif ch == '\\':
+ out.append('\\\\')
+ else:
+ out.append(ch)
+
+ # slap them back together in an ansi-c quote $'...'
+ return "$'" + "".join(out) + "'"
+
+SPLIT_RE = re.compile(r'[a-zA-Z]+|\d+')
+def alphabetize(s):
+ L = []
+ for isdigit, group in itertools.groupby(SPLIT_RE.findall(s), key=lambda x: x.isdigit()):
+ if isdigit:
+ for n in group:
+ L.append(('', int(n)))
+ else:
+ L.append((''.join(group).lower(), 0))
+ return L
+
+def dir_sep():
+ """Return the separator used in this os."""
+ return getattr(os.path,'sep','/')
+
+
+if __name__=="__main__":
+ main()
+
diff --git a/examples/calc.py b/examples/calc.py
new file mode 100755
index 0000000..a40259f
--- /dev/null
+++ b/examples/calc.py
@@ -0,0 +1,819 @@
+#!/usr/bin/python
+#
+# Urwid advanced example column calculator application
+# Copyright (C) 2004-2009 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid advanced example column calculator application
+
+Features:
+- multiple separate list boxes within columns
+- custom edit widget for editing calculator cells
+- custom parent widget for links to other columns
+- custom list walker to show and hide cell results as required
+- custom wrap and align modes for editing right-1 aligned numbers
+- outputs commands that may be used to recreate expression on exit
+"""
+
+import urwid
+import urwid.raw_display
+import urwid.web_display
+
+# use appropriate Screen class
+if urwid.web_display.is_web_request():
+ Screen = urwid.web_display.Screen
+else:
+ Screen = urwid.raw_display.Screen
+
+
+def div_or_none(a,b):
+ """Divide a by b. Return result or None on divide by zero."""
+ if b == 0:
+ return None
+ return a/b
+
+# operators supported and the functions used to calculate a result
+OPERATORS = {
+ '+': (lambda a, b: a+b),
+ '-': (lambda a, b: a-b),
+ '*': (lambda a, b: a*b),
+ '/': div_or_none,
+ }
+
+# the uppercase versions of keys used to switch columns
+COLUMN_KEYS = list( "?ABCDEF" )
+
+# these lists are used to determine when to display errors
+EDIT_KEYS = OPERATORS.keys() + COLUMN_KEYS + ['backspace','delete']
+MOVEMENT_KEYS = ['up','down','left','right','page up','page down']
+
+# Event text
+E_no_such_column = "Column %s does not exist."
+E_no_more_columns = "Maxumum number of columns reached."
+E_new_col_cell_not_empty = "Column must be started from an empty cell."
+E_invalid_key = "Invalid key '%s'."
+E_no_parent_column = "There is no parent column to return to."
+E_cant_combine = "Cannot combine cells with sub-expressions."
+E_invalid_in_parent_cell = "Cannot enter numbers into parent cell."
+E_invalid_in_help_col = [
+ "Help Column is in focus. Press ",
+ ('key',COLUMN_KEYS[1]),"-",('key',COLUMN_KEYS[-1]),
+ " to select another column."]
+
+# Shared layout object
+CALC_LAYOUT = None
+
+
+class CalcEvent(Exception):
+ """Events triggered by user input."""
+
+ attr = 'event'
+
+ def __init__(self, message):
+ self.message = message
+
+ def widget(self):
+ """Return a widget containing event information"""
+ text = urwid.Text( self.message, 'center' )
+ return urwid.AttrWrap( text, self.attr )
+
+class ColumnDeleteEvent(CalcEvent):
+ """Sent when user wants to delete a column"""
+
+ attr = 'confirm'
+
+ def __init__(self, letter, from_parent=0):
+ self.message = ["Press ", ('key',"BACKSPACE"),
+ " again to confirm column removal."]
+ self.letter = letter
+
+class UpdateParentEvent(Exception):
+ """Sent when parent columns may need to be updated."""
+ pass
+
+
+class Cell:
+ def __init__(self, op ):
+ self.op = op
+ self.is_top = op is None
+ self.child = None
+ self.setup_edit()
+ self.result = urwid.Text("", layout=CALC_LAYOUT)
+
+ def show_result(self, next_cell):
+ """Return whether this widget should display its result.
+
+ next_cell -- the cell following self or None"""
+
+ if self.is_top:
+ return False
+ if next_cell is None:
+ return True
+ if self.op == "+" and next_cell.op == "+":
+ return False
+ return True
+
+
+ def setup_edit(self):
+ """Create the standard edit widget for this cell."""
+
+ self.edit = urwid.IntEdit()
+ if not self.is_top:
+ self.edit.set_caption( self.op + " " )
+ self.edit.set_layout( None, None, CALC_LAYOUT )
+
+ def get_value(self):
+ """Return the numeric value of the cell."""
+
+ if self.child is not None:
+ return self.child.get_result()
+ else:
+ return long("0"+self.edit.edit_text)
+
+ def get_result(self):
+ """Return the numeric result of this cell's operation."""
+
+ if self.is_top:
+ return self.get_value()
+ if self.result.text == "":
+ return None
+ return long(self.result.text)
+
+ def set_result(self, result):
+ """Set the numeric result for this cell."""
+
+ if result == None:
+ self.result.set_text("")
+ else:
+ self.result.set_text( "%d" %result )
+
+ def become_parent(self, column, letter):
+ """Change the edit widget to a parent cell widget."""
+
+ self.child = column
+ self.edit = ParentEdit( self.op, letter )
+
+ def remove_child(self):
+ """Change the edit widget back to a standard edit widget."""
+
+ self.child = None
+ self.setup_edit()
+
+ def is_empty( self ):
+ """Return True if the cell is "empty"."""
+
+ return self.child is None and self.edit.edit_text == ""
+
+
+class ParentEdit(urwid.Edit):
+ """Edit widget modified to link to a child column"""
+
+ def __init__(self, op, letter):
+ """Use the operator and letter of the child column as caption
+
+ op -- operator or None
+ letter -- letter of child column
+ remove_fn -- function to call when user wants to remove child
+ function takes no parameters
+ """
+
+ urwid.Edit.__init__(self, layout=CALC_LAYOUT)
+ self.op = op
+ self.set_letter( letter )
+
+ def set_letter(self, letter):
+ """Set the letter of the child column for display."""
+
+ self.letter = letter
+ caption = "("+letter+")"
+ if self.op is not None:
+ caption = self.op+" "+caption
+ self.set_caption(caption)
+
+ def keypress(self, size, key):
+ """Disable usual editing, allow only removing of child"""
+
+ if key == "backspace":
+ raise ColumnDeleteEvent(self.letter, from_parent=True)
+ elif key in list("0123456789"):
+ raise CalcEvent, E_invalid_in_parent_cell
+ else:
+ return key
+
+
+class CellWalker(urwid.ListWalker):
+ def __init__(self, content):
+ self.content = urwid.MonitoredList(content)
+ self.content.modified = self._modified
+ self.focus = (0,0)
+ # everyone can share the same divider widget
+ self.div = urwid.Divider("-")
+
+ def get_cell(self, i):
+ if i < 0 or i >= len(self.content):
+ return None
+ else:
+ return self.content[i]
+
+ def _get_at_pos(self, pos):
+ i, sub = pos
+ assert sub in (0,1,2)
+ if i < 0 or i >= len(self.content):
+ return None, None
+ if sub == 0:
+ edit = self.content[i].edit
+ return urwid.AttrWrap(edit, 'edit', 'editfocus'), pos
+ elif sub == 1:
+ return self.div, pos
+ else:
+ return self.content[i].result, pos
+
+ def get_focus(self):
+ return self._get_at_pos(self.focus)
+
+ def set_focus(self, focus):
+ self.focus = focus
+
+ def get_next(self, start_from):
+ i, sub = start_from
+ assert sub in (0,1,2)
+ if sub == 0:
+ show_result = self.content[i].show_result(
+ self.get_cell(i+1))
+ if show_result:
+ return self._get_at_pos( (i, 1) )
+ else:
+ return self._get_at_pos( (i+1, 0) )
+ elif sub == 1:
+ return self._get_at_pos( (i, 2) )
+ else:
+ return self._get_at_pos( (i+1, 0) )
+
+ def get_prev(self, start_from):
+ i, sub = start_from
+ assert sub in (0,1,2)
+ if sub == 0:
+ if i == 0: return None, None
+ show_result = self.content[i-1].show_result(
+ self.content[i])
+ if show_result:
+ return self._get_at_pos( (i-1, 2) )
+ else:
+ return self._get_at_pos( (i-1, 0) )
+ elif sub == 1:
+ return self._get_at_pos( (i, 0) )
+ else:
+ return self._get_at_pos( (i, 1) )
+
+
+class CellColumn( urwid.WidgetWrap ):
+ def __init__(self, letter):
+ self.walker = CellWalker([Cell(None)])
+ self.content = self.walker.content
+ self.listbox = urwid.ListBox( self.walker )
+ self.set_letter( letter )
+ urwid.WidgetWrap.__init__(self, self.frame)
+
+ def set_letter(self, letter):
+ """Set the column header with letter."""
+
+ self.letter = letter
+ header = urwid.AttrWrap(
+ urwid.Text( ["Column ",('key',letter)],
+ layout = CALC_LAYOUT), 'colhead' )
+ self.frame = urwid.Frame( self.listbox, header )
+
+ def keypress(self, size, key):
+ key = self.frame.keypress( size, key)
+ if key is None:
+ changed = self.update_results()
+ if changed:
+ raise UpdateParentEvent()
+ return
+
+ f, (i, sub) = self.walker.get_focus()
+ if sub != 0:
+ # f is not an edit widget
+ return key
+ if OPERATORS.has_key(key):
+ # move trailing text to new cell below
+ edit = self.walker.get_cell(i).edit
+ cursor_pos = edit.edit_pos
+ tail = edit.edit_text[cursor_pos:]
+ edit.set_edit_text( edit.edit_text[:cursor_pos] )
+
+ new_cell = Cell( key )
+ new_cell.edit.set_edit_text( tail )
+ self.content[i+1:i+1] = [new_cell]
+
+ changed = self.update_results()
+ self.move_focus_next( size )
+ self.content[i+1].edit.set_edit_pos(0)
+ if changed:
+ raise UpdateParentEvent()
+ return
+
+ elif key == 'backspace':
+ # unhandled backspace, we're at beginning of number
+ # append current number to cell above, removing operator
+ above = self.walker.get_cell(i-1)
+ if above is None:
+ # we're the first cell
+ raise ColumnDeleteEvent( self.letter,
+ from_parent=False )
+
+ edit = self.walker.get_cell(i).edit
+ # check that we can combine
+ if above.child is not None:
+ # cell above is parent
+ if edit.edit_text:
+ # ..and current not empty, no good
+ raise CalcEvent, E_cant_combine
+ above_pos = 0
+ else:
+ # above is normal number cell
+ above_pos = len(above.edit.edit_text)
+ above.edit.set_edit_text( above.edit.edit_text +
+ edit.edit_text )
+
+ self.move_focus_prev( size )
+ self.content[i-1].edit.set_edit_pos(above_pos)
+ del self.content[i]
+ changed = self.update_results()
+ if changed:
+ raise UpdateParentEvent()
+ return
+
+ elif key == 'delete':
+ # pull text from next cell into current
+ cell = self.walker.get_cell(i)
+ below = self.walker.get_cell(i+1)
+ if cell.child is not None:
+ # this cell is a parent
+ raise CalcEvent, E_cant_combine
+ if below is None:
+ # nothing below
+ return key
+ if below.child is not None:
+ # cell below is a parent
+ raise CalcEvent, E_cant_combine
+
+ edit = self.walker.get_cell(i).edit
+ edit.set_edit_text( edit.edit_text +
+ below.edit.edit_text )
+
+ del self.content[i+1]
+ changed = self.update_results()
+ if changed:
+ raise UpdateParentEvent()
+ return
+ return key
+
+
+ def move_focus_next(self, size):
+ f, (i, sub) = self.walker.get_focus()
+ assert i<len(self.content)-1
+
+ ni = i
+ while ni == i:
+ self.frame.keypress(size, 'down')
+ nf, (ni, nsub) = self.walker.get_focus()
+
+ def move_focus_prev(self, size):
+ f, (i, sub) = self.walker.get_focus()
+ assert i>0
+
+ ni = i
+ while ni == i:
+ self.frame.keypress(size, 'up')
+ nf, (ni, nsub) = self.walker.get_focus()
+
+
+ def update_results( self, start_from=None ):
+ """Update column. Return True if final result changed.
+
+ start_from -- Cell to start updating from or None to start from
+ the current focus (default None)
+ """
+
+ if start_from is None:
+ f, (i, sub) = self.walker.get_focus()
+ else:
+ i = self.content.index(start_from)
+ if i == None: return False
+
+ focus_cell = self.walker.get_cell(i)
+
+ if focus_cell.is_top:
+ x = focus_cell.get_value()
+ last_op = None
+ else:
+ last_cell = self.walker.get_cell(i-1)
+ x = last_cell.get_result()
+
+ if x is not None and focus_cell.op is not None:
+ x = OPERATORS[focus_cell.op]( x,
+ focus_cell.get_value() )
+ focus_cell.set_result(x)
+
+ for cell in self.content[i+1:]:
+ if cell.op is None:
+ x = None
+ if x is not None:
+ x = OPERATORS[cell.op]( x, cell.get_value() )
+ if cell.get_result() == x:
+ return False
+ cell.set_result(x)
+
+ return True
+
+
+ def create_child( self, letter ):
+ """Return (parent cell,child column) or None,None on failure."""
+ f, (i, sub) = self.walker.get_focus()
+ if sub != 0:
+ # f is not an edit widget
+ return None, None
+
+ cell = self.walker.get_cell(i)
+ if cell.child is not None:
+ raise CalcEvent, E_new_col_cell_not_empty
+ if cell.edit.edit_text:
+ raise CalcEvent, E_new_col_cell_not_empty
+
+ child = CellColumn( letter )
+ cell.become_parent( child, letter )
+
+ return cell, child
+
+ def is_empty( self ):
+ """Return True if this column is empty."""
+
+ return len(self.content)==1 and self.content[0].is_empty()
+
+
+ def get_expression(self):
+ """Return the expression as a printable string."""
+
+ l = []
+ for c in self.content:
+ if c.op is not None: # only applies to first cell
+ l.append(c.op)
+ if c.child is not None:
+ l.append("("+c.child.get_expression()+")")
+ else:
+ l.append("%d"%c.get_value())
+
+ return "".join(l)
+
+ def get_result(self):
+ """Return the result of the last cell in the column."""
+
+ return self.content[-1].get_result()
+
+
+
+
+class HelpColumn(urwid.BoxWidget):
+ help_text = [
+ ('title', "Column Calculator"),
+ "",
+ [ "Numbers: ", ('key', "0"), "-", ('key', "9") ],
+ "" ,
+ [ "Operators: ",('key', "+"), ", ", ('key', "-"), ", ",
+ ('key', "*"), " and ", ('key', "/")],
+ "",
+ [ "Editing: ", ('key', "BACKSPACE"), " and ",('key', "DELETE")],
+ "",
+ [ "Movement: ", ('key', "UP"), ", ", ('key', "DOWN"), ", ",
+ ('key', "LEFT"), ", ", ('key', "RIGHT"), ", ",
+ ('key', "PAGE UP"), " and ", ('key', "PAGE DOWN") ],
+ "",
+ [ "Sub-expressions: ", ('key', "("), " and ", ('key', ")") ],
+ "",
+ [ "Columns: ", ('key', COLUMN_KEYS[0]), " and ",
+ ('key',COLUMN_KEYS[1]), "-",
+ ('key',COLUMN_KEYS[-1]) ],
+ "",
+ [ "Exit: ", ('key', "Q") ],
+ "",
+ "",
+ ["Column Calculator does operations in the order they are ",
+ "typed, not by following usual precedence rules. ",
+ "If you want to calculate ", ('key', "12 - 2 * 3"),
+ " with the multiplication happening before the ",
+ "subtraction you must type ",
+ ('key', "12 - (2 * 3)"), " instead."],
+ ]
+
+ def __init__(self):
+ self.head = urwid.AttrWrap(
+ urwid.Text(["Help Column ", ('key',"?")],
+ layout = CALC_LAYOUT),
+ 'help')
+ self.foot = urwid.AttrWrap(
+ urwid.Text(["[text continues.. press ",
+ ('key',"?"), " then scroll]"]), 'helpnote' )
+ self.items = [urwid.Text(x) for x in self.help_text]
+ self.listbox = urwid.ListBox(urwid.SimpleListWalker(self.items))
+ self.body = urwid.AttrWrap( self.listbox, 'help' )
+ self.frame = urwid.Frame( self.body, header=self.head)
+
+ def render(self, size, focus=False):
+ maxcol, maxrow = size
+ head_rows = self.head.rows((maxcol,))
+ if "bottom" in self.listbox.ends_visible(
+ (maxcol, maxrow-head_rows) ):
+ self.frame.footer = None
+ else:
+ self.frame.footer = self.foot
+
+ return self.frame.render( (maxcol, maxrow), focus)
+
+ def keypress( self, size, key ):
+ return self.frame.keypress( size, key )
+
+
+class CalcDisplay:
+ palette = [
+ ('body','white', 'dark blue'),
+ ('edit','yellow', 'dark blue'),
+ ('editfocus','yellow','dark cyan', 'bold'),
+ ('key','dark cyan', 'light gray', ('standout','underline')),
+ ('title', 'white', 'light gray', ('bold','standout')),
+ ('help', 'black', 'light gray', 'standout'),
+ ('helpnote', 'dark green', 'light gray'),
+ ('colhead', 'black', 'light gray', 'standout'),
+ ('event', 'light red', 'black', 'standout'),
+ ('confirm', 'yellow', 'black', 'bold'),
+ ]
+
+ def __init__(self):
+ self.columns = urwid.Columns([HelpColumn(), CellColumn("A")], 1)
+ self.col_list = self.columns.widget_list
+ self.columns.set_focus_column( 1 )
+ view = urwid.AttrWrap(self.columns, 'body')
+ self.view = urwid.Frame(view) # for showing messages
+ self.col_link = {}
+
+ def main(self):
+ self.loop = urwid.MainLoop(self.view, self.palette, screen=Screen(),
+ input_filter=self.input_filter)
+ self.loop.run()
+
+ # on exit write the formula and the result to the console
+ expression, result = self.get_expression_result()
+ print "Paste this expression into a new Column Calculator session to continue editing:"
+ print expression
+ print "Result:", result
+
+ def input_filter(self, input, raw_input):
+ if 'q' in input or 'Q' in input:
+ raise urwid.ExitMainLoop()
+
+ # handle other keystrokes
+ for k in input:
+ try:
+ self.wrap_keypress(k)
+ self.event = None
+ self.view.footer = None
+ except CalcEvent, e:
+ # display any message
+ self.event = e
+ self.view.footer = e.widget()
+
+ # remove all input from further processing by MainLoop
+ return []
+
+ def wrap_keypress(self, key):
+ """Handle confirmation and throw event on bad input."""
+
+ try:
+ key = self.keypress(key)
+
+ except ColumnDeleteEvent, e:
+ if e.letter == COLUMN_KEYS[1]:
+ # cannot delete the first column, ignore key
+ return
+
+ if not self.column_empty( e.letter ):
+ # need to get two in a row, so check last event
+ if not isinstance(self.event,ColumnDeleteEvent):
+ # ask for confirmation
+ raise e
+ self.delete_column(e.letter)
+
+ except UpdateParentEvent, e:
+ self.update_parent_columns()
+ return
+
+ if key is None:
+ return
+
+ if self.columns.get_focus_column() == 0:
+ if key not in ('up','down','page up','page down'):
+ raise CalcEvent, E_invalid_in_help_col
+
+ if key not in EDIT_KEYS and key not in MOVEMENT_KEYS:
+ raise CalcEvent, E_invalid_key % key.upper()
+
+ def keypress(self, key):
+ """Handle a keystroke."""
+
+ self.loop.process_input([key])
+
+ if key.upper() in COLUMN_KEYS:
+ # column switch
+ i = COLUMN_KEYS.index(key.upper())
+ if i >= len( self.col_list ):
+ raise CalcEvent, E_no_such_column % key.upper()
+ self.columns.set_focus_column( i )
+ return
+ elif key == "(":
+ # open a new column
+ if len( self.col_list ) >= len(COLUMN_KEYS):
+ raise CalcEvent, E_no_more_columns
+ i = self.columns.get_focus_column()
+ if i == 0:
+ # makes no sense in help column
+ return key
+ col = self.col_list[i]
+ new_letter = COLUMN_KEYS[len(self.col_list)]
+ parent, child = col.create_child( new_letter )
+ if child is None:
+ # something invalid in focus
+ return key
+ self.col_list.append(child)
+ self.set_link( parent, col, child )
+ self.columns.set_focus_column(len(self.col_list)-1)
+
+ elif key == ")":
+ i = self.columns.get_focus_column()
+ if i == 0:
+ # makes no sense in help column
+ return key
+ col = self.col_list[i]
+ parent, pcol = self.get_parent( col )
+ if parent is None:
+ # column has no parent
+ raise CalcEvent, E_no_parent_column
+
+ new_i = self.col_list.index( pcol )
+ self.columns.set_focus_column( new_i )
+ else:
+ return key
+
+ def set_link( self, parent, pcol, child ):
+ """Store the link between a parent cell and child column.
+
+ parent -- parent Cell object
+ pcol -- CellColumn where parent resides
+ child -- child CellColumn object"""
+
+ self.col_link[ child ] = parent, pcol
+
+ def get_parent( self, child ):
+ """Return the parent and parent column for a given column."""
+
+ return self.col_link.get( child, (None,None) )
+
+ def column_empty(self, letter):
+ """Return True if the column passed is empty."""
+
+ i = COLUMN_KEYS.index(letter)
+ col = self.col_list[i]
+ return col.is_empty()
+
+
+ def delete_column(self, letter):
+ """Delete the column with the given letter."""
+
+ i = COLUMN_KEYS.index(letter)
+ col = self.col_list[i]
+
+ parent, pcol = self.get_parent( col )
+
+ f = self.columns.get_focus_column()
+ if f == i:
+ # move focus to the parent column
+ f = self.col_list.index(pcol)
+ self.columns.set_focus_column(f)
+
+ parent.remove_child()
+ pcol.update_results(parent)
+ del self.col_list[i]
+
+ # delete children of this column
+ keep_right_cols = []
+ remove_cols = [col]
+ for rcol in self.col_list[i:]:
+ parent, pcol = self.get_parent( rcol )
+ if pcol in remove_cols:
+ remove_cols.append( rcol )
+ else:
+ keep_right_cols.append( rcol )
+ for rc in remove_cols:
+ # remove the links
+ del self.col_link[rc]
+ # keep only the non-children
+ self.col_list[i:] = keep_right_cols
+
+ # fix the letter assigmnents
+ for j in range(i, len(self.col_list)):
+ col = self.col_list[j]
+ # fix the column heading
+ col.set_letter( COLUMN_KEYS[j] )
+ parent, pcol = self.get_parent( col )
+ # fix the parent cell
+ parent.edit.set_letter( COLUMN_KEYS[j] )
+
+ def update_parent_columns(self):
+ "Update the parent columns of the current focus column."
+
+ f = self.columns.get_focus_column()
+ col = self.col_list[f]
+ while 1:
+ parent, pcol = self.get_parent(col)
+ if pcol is None:
+ return
+
+ changed = pcol.update_results( start_from = parent )
+ if not changed:
+ return
+ col = pcol
+
+
+ def get_expression_result(self):
+ """Return (expression, result) as strings."""
+
+ col = self.col_list[1]
+ return col.get_expression(), "%d"%col.get_result()
+
+
+
+class CalcNumLayout(urwid.TextLayout):
+ """
+ TextLayout class for bottom-right aligned numbers with a space on
+ the last line for the cursor.
+ """
+ def layout( self, text, width, align, wrap ):
+ """
+ Return layout structure for calculator number display.
+ """
+ lt = len(text) + 1 # extra space for cursor
+ r = (lt) % width # remaining segment not full width wide
+ linestarts = range( r, lt, width )
+ l = []
+ if linestarts:
+ if r:
+ # right-align the remaining segment on 1st line
+ l.append( [(width-r,None),(r, 0, r)] )
+ # fill all but the last line
+ for x in linestarts[:-1]:
+ l.append( [(width, x, x+width)] )
+ s = linestarts[-1]
+ # add the last line with a cursor hint
+ l.append( [(width-1, s, lt-1), (0, lt-1)] )
+ elif lt-1:
+ # all fits on one line, so right align the text
+ # with a cursor hint at the end
+ l.append( [(width-lt,None),(lt-1,0,lt-1), (0,lt-1)] )
+ else:
+ # nothing on the line, right align a cursor hint
+ l.append( [(width-1,None),(0,0)] )
+
+ return l
+
+
+
+
+def main():
+ """Launch Column Calculator."""
+ global CALC_LAYOUT
+ CALC_LAYOUT = CalcNumLayout()
+
+ urwid.web_display.set_preferences("Column Calculator")
+ # try to handle short web requests quickly
+ if urwid.web_display.handle_short_request():
+ return
+
+ CalcDisplay().main()
+
+if '__main__'==__name__ or urwid.web_display.is_web_request():
+ main()
diff --git a/examples/dialog.py b/examples/dialog.py
new file mode 100755
index 0000000..7de86d5
--- /dev/null
+++ b/examples/dialog.py
@@ -0,0 +1,343 @@
+#!/usr/bin/python
+#
+# Urwid example similar to dialog(1) program
+# Copyright (C) 2004-2009 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid example similar to dialog(1) program
+
+"""
+
+import sys
+
+import urwid
+
+
+class DialogExit(Exception):
+ pass
+
+
+class DialogDisplay:
+ palette = [
+ ('body','black','light gray', 'standout'),
+ ('border','black','dark blue'),
+ ('shadow','white','black'),
+ ('selectable','black', 'dark cyan'),
+ ('focus','white','dark blue','bold'),
+ ('focustext','light gray','dark blue'),
+ ]
+
+ def __init__(self, text, height, width, body=None):
+ width = int(width)
+ if width <= 0:
+ width = ('relative', 80)
+ height = int(height)
+ if height <= 0:
+ height = ('relative', 80)
+
+ self.body = body
+ if body is None:
+ # fill space with nothing
+ body = urwid.Filler(urwid.Divider(),'top')
+
+ self.frame = urwid.Frame( body, focus_part='footer')
+ if text is not None:
+ self.frame.header = urwid.Pile( [urwid.Text(text),
+ urwid.Divider()] )
+ w = self.frame
+
+ # pad area around listbox
+ w = urwid.Padding(w, ('fixed left',2), ('fixed right',2))
+ w = urwid.Filler(w, ('fixed top',1), ('fixed bottom',1))
+ w = urwid.AttrWrap(w, 'body')
+
+ # "shadow" effect
+ w = urwid.Columns( [w,('fixed', 2, urwid.AttrWrap(
+ urwid.Filler(urwid.Text(('border',' ')), "top")
+ ,'shadow'))])
+ w = urwid.Frame( w, footer =
+ urwid.AttrWrap(urwid.Text(('border',' ')),'shadow'))
+
+ # outermost border area
+ w = urwid.Padding(w, 'center', width )
+ w = urwid.Filler(w, 'middle', height )
+ w = urwid.AttrWrap( w, 'border' )
+
+ self.view = w
+
+
+ def add_buttons(self, buttons):
+ l = []
+ for name, exitcode in buttons:
+ b = urwid.Button( name, self.button_press )
+ b.exitcode = exitcode
+ b = urwid.AttrWrap( b, 'selectable','focus' )
+ l.append( b )
+ self.buttons = urwid.GridFlow(l, 10, 3, 1, 'center')
+ self.frame.footer = urwid.Pile( [ urwid.Divider(),
+ self.buttons ], focus_item = 1)
+
+ def button_press(self, button):
+ raise DialogExit(button.exitcode)
+
+ def main(self):
+ self.loop = urwid.MainLoop(self.view, self.palette)
+ try:
+ self.loop.run()
+ except DialogExit, e:
+ return self.on_exit( e.args[0] )
+
+ def on_exit(self, exitcode):
+ return exitcode, ""
+
+
+
+class InputDialogDisplay(DialogDisplay):
+ def __init__(self, text, height, width):
+ self.edit = urwid.Edit()
+ body = urwid.ListBox([self.edit])
+ body = urwid.AttrWrap(body, 'selectable','focustext')
+
+ DialogDisplay.__init__(self, text, height, width, body)
+
+ self.frame.set_focus('body')
+
+ def unhandled_key(self, size, k):
+ if k in ('up','page up'):
+ self.frame.set_focus('body')
+ if k in ('down','page down'):
+ self.frame.set_focus('footer')
+ if k == 'enter':
+ # pass enter to the "ok" button
+ self.frame.set_focus('footer')
+ self.view.keypress( size, k )
+
+ def on_exit(self, exitcode):
+ return exitcode, self.edit.get_edit_text()
+
+
+class TextDialogDisplay(DialogDisplay):
+ def __init__(self, file, height, width):
+ l = []
+ # read the whole file (being slow, not lazy this time)
+ for line in open(file).readlines():
+ l.append( urwid.Text( line.rstrip() ))
+ body = urwid.ListBox(l)
+ body = urwid.AttrWrap(body, 'selectable','focustext')
+
+ DialogDisplay.__init__(self, None, height, width, body)
+
+
+ def unhandled_key(self, size, k):
+ if k in ('up','page up','down','page down'):
+ self.frame.set_focus('body')
+ self.view.keypress( size, k )
+ self.frame.set_focus('footer')
+
+
+class ListDialogDisplay(DialogDisplay):
+ def __init__(self, text, height, width, constr, items, has_default):
+ j = []
+ if has_default:
+ k, tail = 3, ()
+ else:
+ k, tail = 2, ("no",)
+ while items:
+ j.append( items[:k] + tail )
+ items = items[k:]
+
+ l = []
+ self.items = []
+ for tag, item, default in j:
+ w = constr( tag, default=="on" )
+ self.items.append(w)
+ w = urwid.Columns( [('fixed', 12, w),
+ urwid.Text(item)], 2 )
+ w = urwid.AttrWrap(w, 'selectable','focus')
+ l.append(w)
+
+ lb = urwid.ListBox(l)
+ lb = urwid.AttrWrap( lb, "selectable" )
+ DialogDisplay.__init__(self, text, height, width, lb )
+
+ self.frame.set_focus('body')
+
+ def unhandled_key(self, size, k):
+ if k in ('up','page up'):
+ self.frame.set_focus('body')
+ if k in ('down','page down'):
+ self.frame.set_focus('footer')
+ if k == 'enter':
+ # pass enter to the "ok" button
+ self.frame.set_focus('footer')
+ self.buttons.set_focus(0)
+ self.view.keypress( size, k )
+
+ def on_exit(self, exitcode):
+ """Print the tag of the item selected."""
+ if exitcode != 0:
+ return exitcode, ""
+ s = ""
+ for i in self.items:
+ if i.get_state():
+ s = i.get_label()
+ break
+ return exitcode, s
+
+
+
+
+class CheckListDialogDisplay(ListDialogDisplay):
+ def on_exit(self, exitcode):
+ """
+ Mimick dialog(1)'s --checklist exit.
+ Put each checked item in double quotes with a trailing space.
+ """
+ if exitcode != 0:
+ return exitcode, ""
+ l = []
+ for i in self.items:
+ if i.get_state():
+ l.append(i.get_label())
+ return exitcode, "".join(['"'+tag+'" ' for tag in l])
+
+
+
+
+class MenuItem(urwid.Text):
+ """A custom widget for the --menu option"""
+ def __init__(self, label):
+ urwid.Text.__init__(self, label)
+ self.state = False
+ def selectable(self):
+ return True
+ def keypress(self,size,key):
+ if key == "enter":
+ self.state = True
+ raise DialogExit, 0
+ return key
+ def mouse_event(self,size,event,button,col,row,focus):
+ if event=='mouse release':
+ self.state = True
+ raise DialogExit, 0
+ return False
+ def get_state(self):
+ return self.state
+ def get_label(self):
+ text, attr = self.get_text()
+ return text
+
+
+def do_checklist(text, height, width, list_height, *items):
+ def constr(tag, state):
+ return urwid.CheckBox(tag, state)
+ d = CheckListDialogDisplay( text, height, width, constr, items, True)
+ d.add_buttons([ ("OK", 0), ("Cancel", 1) ])
+ return d
+
+def do_inputbox(text, height, width):
+ d = InputDialogDisplay( text, height, width )
+ d.add_buttons([ ("Exit", 0) ])
+ return d
+
+def do_menu(text, height, width, menu_height, *items):
+ def constr(tag, state ):
+ return MenuItem(tag)
+ d = ListDialogDisplay(text, height, width, constr, items, False)
+ d.add_buttons([ ("OK", 0), ("Cancel", 1) ])
+ return d
+
+def do_msgbox(text, height, width):
+ d = DialogDisplay( text, height, width )
+ d.add_buttons([ ("OK", 0) ])
+ return d
+
+def do_radiolist(text, height, width, list_height, *items):
+ radiolist = []
+ def constr(tag, state, radiolist=radiolist):
+ return urwid.RadioButton(radiolist, tag, state)
+ d = ListDialogDisplay( text, height, width, constr, items, True )
+ d.add_buttons([ ("OK", 0), ("Cancel", 1) ])
+ return d
+
+def do_textbox(file, height, width):
+ d = TextDialogDisplay( file, height, width )
+ d.add_buttons([ ("Exit", 0) ])
+ return d
+
+def do_yesno(text, height, width):
+ d = DialogDisplay( text, height, width )
+ d.add_buttons([ ("Yes", 0), ("No", 1) ])
+ return d
+
+MODES={ '--checklist': (do_checklist,
+ "text height width list-height [ tag item status ] ..."),
+ '--inputbox': (do_inputbox,
+ "text height width"),
+ '--menu': (do_menu,
+ "text height width menu-height [ tag item ] ..."),
+ '--msgbox': (do_msgbox,
+ "text height width"),
+ '--radiolist': (do_radiolist,
+ "text height width list-height [ tag item status ] ..."),
+ '--textbox': (do_textbox,
+ "file height width"),
+ '--yesno': (do_yesno,
+ "text height width"),
+ }
+
+
+def show_usage():
+ """
+ Display a helpful usage message.
+ """
+ modelist = [(mode, help) for (mode, (fn, help)) in MODES.items()]
+ modelist.sort()
+ sys.stdout.write(
+ __doc__ +
+ "\n".join(["%-15s %s"%(mode,help) for (mode,help) in modelist])
+ + """
+
+height and width may be set to 0 to auto-size.
+list-height and menu-height are currently ignored.
+status may be either on or off.
+""" )
+
+
+def main():
+ if len(sys.argv) < 2 or not MODES.has_key(sys.argv[1]):
+ show_usage()
+ return
+
+ # Create a DialogDisplay instance
+ fn, help = MODES[sys.argv[1]]
+ d = fn( * sys.argv[2:] )
+
+ # Run it
+ exitcode, exitstring = d.main()
+
+ # Exit
+ if exitstring:
+ sys.stderr.write(exitstring+"\n")
+
+ sys.exit(exitcode)
+
+
+if __name__=="__main__":
+ main()
diff --git a/examples/edit.py b/examples/edit.py
new file mode 100755
index 0000000..eed7970
--- /dev/null
+++ b/examples/edit.py
@@ -0,0 +1,255 @@
+#!/usr/bin/python
+#
+# Urwid example lazy text editor suitable for tabbed and format=flowed text
+# Copyright (C) 2004-2009 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid example lazy text editor suitable for tabbed and flowing text
+
+Features:
+- custom list walker for lazily loading text file
+
+Usage:
+edit.py <filename>
+
+"""
+
+import sys
+
+import urwid
+
+
+class LineWalker(urwid.ListWalker):
+ """ListWalker-compatible class for lazily reading file contents."""
+
+ def __init__(self, name):
+ self.file = open(name)
+ self.lines = []
+ self.focus = 0
+
+ def get_focus(self):
+ return self._get_at_pos(self.focus)
+
+ def set_focus(self, focus):
+ self.focus = focus
+ self._modified()
+
+ def get_next(self, start_from):
+ return self._get_at_pos(start_from + 1)
+
+ def get_prev(self, start_from):
+ return self._get_at_pos(start_from - 1)
+
+ def read_next_line(self):
+ """Read another line from the file."""
+
+ next_line = self.file.readline()
+
+ if not next_line or next_line[-1:] != '\n':
+ # no newline on last line of file
+ self.file = None
+ else:
+ # trim newline characters
+ next_line = next_line[:-1]
+
+ expanded = next_line.expandtabs()
+
+ edit = urwid.Edit("", expanded, allow_tab=True)
+ edit.set_edit_pos(0)
+ edit.original_text = next_line
+ self.lines.append(edit)
+
+ return next_line
+
+
+ def _get_at_pos(self, pos):
+ """Return a widget for the line number passed."""
+
+ if pos < 0:
+ # line 0 is the start of the file, no more above
+ return None, None
+
+ if len(self.lines) > pos:
+ # we have that line so return it
+ return self.lines[pos], pos
+
+ if self.file is None:
+ # file is closed, so there are no more lines
+ return None, None
+
+ assert pos == len(self.lines), "out of order request?"
+
+ self.read_next_line()
+
+ return self.lines[-1], pos
+
+ def split_focus(self):
+ """Divide the focus edit widget at the cursor location."""
+
+ focus = self.lines[self.focus]
+ pos = focus.edit_pos
+ edit = urwid.Edit("",focus.edit_text[pos:], allow_tab=True)
+ edit.original_text = ""
+ focus.set_edit_text(focus.edit_text[:pos])
+ edit.set_edit_pos(0)
+ self.lines.insert(self.focus+1, edit)
+
+ def combine_focus_with_prev(self):
+ """Combine the focus edit widget with the one above."""
+
+ above, ignore = self.get_prev(self.focus)
+ if above is None:
+ # already at the top
+ return
+
+ focus = self.lines[self.focus]
+ above.set_edit_pos(len(above.edit_text))
+ above.set_edit_text(above.edit_text + focus.edit_text)
+ del self.lines[self.focus]
+ self.focus -= 1
+
+ def combine_focus_with_next(self):
+ """Combine the focus edit widget with the one below."""
+
+ below, ignore = self.get_next(self.focus)
+ if below is None:
+ # already at bottom
+ return
+
+ focus = self.lines[self.focus]
+ focus.set_edit_text(focus.edit_text + below.edit_text)
+ del self.lines[self.focus+1]
+
+
+class EditDisplay:
+ palette = [
+ ('body','default', 'default'),
+ ('foot','dark cyan', 'dark blue', 'bold'),
+ ('key','light cyan', 'dark blue', 'underline'),
+ ]
+
+ footer_text = ('foot', [
+ "Text Editor ",
+ ('key', "F5"), " save ",
+ ('key', "F8"), " quit",
+ ])
+
+ def __init__(self, name):
+ self.save_name = name
+ self.walker = LineWalker(name)
+ self.listbox = urwid.ListBox(self.walker)
+ self.footer = urwid.AttrWrap(urwid.Text(self.footer_text),
+ "foot")
+ self.view = urwid.Frame(urwid.AttrWrap(self.listbox, 'body'),
+ footer=self.footer)
+
+ def main(self):
+ self.loop = urwid.MainLoop(self.view, self.palette,
+ unhandled_input=self.unhandled_keypress)
+ self.loop.run()
+
+ def unhandled_keypress(self, k):
+ """Last resort for keypresses."""
+
+ if k == "f5":
+ self.save_file()
+ elif k == "f8":
+ raise urwid.ExitMainLoop()
+ elif k == "delete":
+ # delete at end of line
+ self.walker.combine_focus_with_next()
+ elif k == "backspace":
+ # backspace at beginning of line
+ self.walker.combine_focus_with_prev()
+ elif k == "enter":
+ # start new line
+ self.walker.split_focus()
+ # move the cursor to the new line and reset pref_col
+ self.loop.process_input(["down", "home"])
+ elif k == "right":
+ w, pos = self.walker.get_focus()
+ w, pos = self.walker.get_next(pos)
+ if w:
+ self.listbox.set_focus(pos, 'above')
+ self.loop.process_input(["home"])
+ elif k == "left":
+ w, pos = self.walker.get_focus()
+ w, pos = self.walker.get_prev(pos)
+ if w:
+ self.listbox.set_focus(pos, 'below')
+ self.loop.process_input(["end"])
+ else:
+ return
+ return True
+
+
+ def save_file(self):
+ """Write the file out to disk."""
+
+ l = []
+ walk = self.walker
+ for edit in walk.lines:
+ # collect the text already stored in edit widgets
+ if edit.original_text.expandtabs() == edit.edit_text:
+ l.append(edit.original_text)
+ else:
+ l.append(re_tab(edit.edit_text))
+
+ # then the rest
+ while walk.file is not None:
+ l.append(walk.read_next_line())
+
+ # write back to disk
+ outfile = open(self.save_name, "w")
+
+ prefix = ""
+ for line in l:
+ outfile.write(prefix + line)
+ prefix = "\n"
+
+def re_tab(s):
+ """Return a tabbed string from an expanded one."""
+ l = []
+ p = 0
+ for i in range(8, len(s), 8):
+ if s[i-2:i] == " ":
+ # collapse two or more spaces into a tab
+ l.append(s[p:i].rstrip() + "\t")
+ p = i
+
+ if p == 0:
+ return s
+ else:
+ l.append(s[p:])
+ return "".join(l)
+
+
+
+def main():
+ try:
+ name = sys.argv[1]
+ assert open(name, "a")
+ except:
+ sys.stderr.write(__doc__)
+ return
+ EditDisplay(name).main()
+
+
+if __name__=="__main__":
+ main()
diff --git a/examples/fib.py b/examples/fib.py
new file mode 100755
index 0000000..4439129
--- /dev/null
+++ b/examples/fib.py
@@ -0,0 +1,112 @@
+#!/usr/bin/python
+#
+# Urwid example fibonacci sequence viewer / unbounded data demo
+# Copyright (C) 2004-2007 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid example fibonacci sequence viewer / unbounded data demo
+
+Features:
+- custom list walker class for browsing infinite set
+- custom wrap mode "numeric" for wrapping numbers to right and bottom
+"""
+
+import urwid
+
+class FibonacciWalker(urwid.ListWalker):
+ """ListWalker-compatible class for browsing fibonacci set.
+
+ positions returned are (value at position-1, value at poistion) tuples.
+ """
+ def __init__(self):
+ self.focus = (0L,1L)
+ self.numeric_layout = NumericLayout()
+
+ def _get_at_pos(self, pos):
+ """Return a widget and the position passed."""
+ return urwid.Text("%d"%pos[1], layout=self.numeric_layout), pos
+
+ def get_focus(self):
+ return self._get_at_pos(self.focus)
+
+ def set_focus(self, focus):
+ self.focus = focus
+ self._modified()
+
+ def get_next(self, start_from):
+ a, b = start_from
+ focus = b, a+b
+ return self._get_at_pos(focus)
+
+ def get_prev(self, start_from):
+ a, b = start_from
+ focus = b-a, a
+ return self._get_at_pos(focus)
+
+def main():
+ palette = [
+ ('body','black','dark cyan', 'standout'),
+ ('foot','light gray', 'black'),
+ ('key','light cyan', 'black', 'underline'),
+ ('title', 'white', 'black',),
+ ]
+
+ footer_text = [
+ ('title', "Fibonacci Set Viewer"), " ",
+ ('key', "UP"), ", ", ('key', "DOWN"), ", ",
+ ('key', "PAGE UP"), " and ", ('key', "PAGE DOWN"),
+ " move view ",
+ ('key', "Q"), " exits",
+ ]
+
+ def exit_on_q(input):
+ if input in ('q', 'Q'):
+ raise urwid.ExitMainLoop()
+
+ listbox = urwid.ListBox(FibonacciWalker())
+ footer = urwid.AttrMap(urwid.Text(footer_text), 'foot')
+ view = urwid.Frame(urwid.AttrWrap(listbox, 'body'), footer=footer)
+ loop = urwid.MainLoop(view, palette, unhandled_input=exit_on_q)
+ loop.run()
+
+
+class NumericLayout(urwid.TextLayout):
+ """
+ TextLayout class for bottom-right aligned numbers
+ """
+ def layout( self, text, width, align, wrap ):
+ """
+ Return layout structure for right justified numbers.
+ """
+ lt = len(text)
+ r = lt % width # remaining segment not full width wide
+ if r:
+ linestarts = range( r, lt, width )
+ return [
+ # right-align the remaining segment on 1st line
+ [(width-r,None),(r, 0, r)]
+ # fill the rest of the lines
+ ] + [[(width, x, x+width)] for x in linestarts]
+ else:
+ linestarts = range( 0, lt, width )
+ return [[(width, x, x+width)] for x in linestarts]
+
+
+if __name__=="__main__":
+ main()
diff --git a/examples/graph.py b/examples/graph.py
new file mode 100755
index 0000000..a882599
--- /dev/null
+++ b/examples/graph.py
@@ -0,0 +1,356 @@
+#!/usr/bin/python
+#
+# Urwid graphics example program
+# Copyright (C) 2004-2011 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid example demonstrating use of the BarGraph widget and creating a
+floating-window appearance. Also shows use of alarms to create timed
+animation.
+"""
+
+import urwid
+
+import math
+import time
+
+UPDATE_INTERVAL = 0.2
+
+def sin100( x ):
+ """
+ A sin function that returns values between 0 and 100 and repeats
+ after x == 100.
+ """
+ return 50 + 50 * math.sin( x * math.pi / 50 )
+
+class GraphModel:
+ """
+ A class responsible for storing the data that will be displayed
+ on the graph, and keeping track of which mode is enabled.
+ """
+
+ data_max_value = 100
+
+ def __init__(self):
+ data = [ ('Saw', range(0,100,2)*2),
+ ('Square', [0]*30 + [100]*30),
+ ('Sine 1', [sin100(x) for x in range(100)] ),
+ ('Sine 2', [(sin100(x) + sin100(x*2))/2
+ for x in range(100)] ),
+ ('Sine 3', [(sin100(x) + sin100(x*3))/2
+ for x in range(100)] ),
+ ]
+ self.modes = []
+ self.data = {}
+ for m, d in data:
+ self.modes.append(m)
+ self.data[m] = d
+
+ def get_modes(self):
+ return self.modes
+
+ def set_mode(self, m):
+ self.current_mode = m
+
+ def get_data(self, offset, r):
+ """
+ Return the data in [offset:offset+r], the maximum value
+ for items returned, and the offset at which the data
+ repeats.
+ """
+ l = []
+ d = self.data[self.current_mode]
+ while r:
+ offset = offset % len(d)
+ segment = d[offset:offset+r]
+ r -= len(segment)
+ offset += len(segment)
+ l += segment
+ return l, self.data_max_value, len(d)
+
+
+class GraphView(urwid.WidgetWrap):
+ """
+ A class responsible for providing the application's interface and
+ graph display.
+ """
+ palette = [
+ ('body', 'black', 'light gray', 'standout'),
+ ('header', 'white', 'dark red', 'bold'),
+ ('screen edge', 'light blue', 'dark cyan'),
+ ('main shadow', 'dark gray', 'black'),
+ ('line', 'black', 'light gray', 'standout'),
+ ('bg background','light gray', 'black'),
+ ('bg 1', 'black', 'dark blue', 'standout'),
+ ('bg 1 smooth', 'dark blue', 'black'),
+ ('bg 2', 'black', 'dark cyan', 'standout'),
+ ('bg 2 smooth', 'dark cyan', 'black'),
+ ('button normal','light gray', 'dark blue', 'standout'),
+ ('button select','white', 'dark green'),
+ ('line', 'black', 'light gray', 'standout'),
+ ('pg normal', 'white', 'black', 'standout'),
+ ('pg complete', 'white', 'dark magenta'),
+ ('pg smooth', 'dark magenta','black')
+ ]
+
+ graph_samples_per_bar = 10
+ graph_num_bars = 5
+ graph_offset_per_second = 5
+
+ def __init__(self, controller):
+ self.controller = controller
+ self.started = True
+ self.start_time = None
+ self.offset = 0
+ self.last_offset = None
+ urwid.WidgetWrap.__init__(self, self.main_window())
+
+ def get_offset_now(self):
+ if self.start_time is None:
+ return 0
+ if not self.started:
+ return self.offset
+ tdelta = time.time() - self.start_time
+ return int(self.offset + (tdelta*self.graph_offset_per_second))
+
+ def update_graph(self, force_update=False):
+ o = self.get_offset_now()
+ if o == self.last_offset and not force_update:
+ return False
+ self.last_offset = o
+ gspb = self.graph_samples_per_bar
+ r = gspb * self.graph_num_bars
+ d, max_value, repeat = self.controller.get_data( o, r )
+ l = []
+ for n in range(self.graph_num_bars):
+ value = sum(d[n*gspb:(n+1)*gspb])/gspb
+ # toggle between two bar types
+ if n & 1:
+ l.append([0,value])
+ else:
+ l.append([value,0])
+ self.graph.set_data(l,max_value)
+
+ # also update progress
+ if (o//repeat)&1:
+ # show 100% for first half, 0 for second half
+ if o%repeat > repeat//2:
+ prog = 0
+ else:
+ prog = 1
+ else:
+ prog = float(o%repeat) / repeat
+ self.animate_progress.set_completion( prog )
+ return True
+
+ def on_animate_button(self, button):
+ """Toggle started state and button text."""
+ if self.started: # stop animation
+ button.set_label("Start")
+ self.offset = self.get_offset_now()
+ self.started = False
+ self.controller.stop_animation()
+ else:
+ button.set_label("Stop")
+ self.started = True
+ self.start_time = time.time()
+ self.controller.animate_graph()
+
+
+ def on_reset_button(self, w):
+ self.offset = 0
+ self.start_time = time.time()
+ self.update_graph(True)
+
+ def on_mode_button(self, button, state):
+ """Notify the controller of a new mode setting."""
+ if state:
+ # The new mode is the label of the button
+ self.controller.set_mode( button.get_label() )
+ self.last_offset = None
+
+ def on_mode_change(self, m):
+ """Handle external mode change by updating radio buttons."""
+ for rb in self.mode_buttons:
+ if rb.get_label() == m:
+ rb.set_state(True, do_callback=False)
+ break
+ self.last_offset = None
+
+ def on_unicode_checkbox(self, w, state):
+ self.graph = self.bar_graph( state )
+ self.graph_wrap._w = self.graph
+ self.animate_progress = self.progress_bar( state )
+ self.animate_progress_wrap._w = self.animate_progress
+ self.update_graph( True )
+
+
+ def main_shadow(self, w):
+ """Wrap a shadow and background around widget w."""
+ bg = urwid.AttrWrap(urwid.SolidFill(u"\u2592"), 'screen edge')
+ shadow = urwid.AttrWrap(urwid.SolidFill(u" "), 'main shadow')
+
+ bg = urwid.Overlay( shadow, bg,
+ ('fixed left', 3), ('fixed right', 1),
+ ('fixed top', 2), ('fixed bottom', 1))
+ w = urwid.Overlay( w, bg,
+ ('fixed left', 2), ('fixed right', 3),
+ ('fixed top', 1), ('fixed bottom', 2))
+ return w
+
+ def bar_graph(self, smooth=False):
+ satt = None
+ if smooth:
+ satt = {(1,0): 'bg 1 smooth', (2,0): 'bg 2 smooth'}
+ w = urwid.BarGraph(['bg background','bg 1','bg 2'], satt=satt)
+ return w
+
+ def button(self, t, fn):
+ w = urwid.Button(t, fn)
+ w = urwid.AttrWrap(w, 'button normal', 'button select')
+ return w
+
+ def radio_button(self, g, l, fn):
+ w = urwid.RadioButton(g, l, False, on_state_change=fn)
+ w = urwid.AttrWrap(w, 'button normal', 'button select')
+ return w
+
+ def progress_bar(self, smooth=False):
+ if smooth:
+ return urwid.ProgressBar('pg normal', 'pg complete',
+ 0, 1, 'pg smooth')
+ else:
+ return urwid.ProgressBar('pg normal', 'pg complete',
+ 0, 1)
+
+ def exit_program(self, w):
+ raise urwid.ExitMainLoop()
+
+ def graph_controls(self):
+ modes = self.controller.get_modes()
+ # setup mode radio buttons
+ self.mode_buttons = []
+ group = []
+ for m in modes:
+ rb = self.radio_button( group, m, self.on_mode_button )
+ self.mode_buttons.append( rb )
+ # setup animate button
+ self.animate_button = self.button( "", self.on_animate_button)
+ self.on_animate_button( self.animate_button )
+ self.offset = 0
+ self.animate_progress = self.progress_bar()
+ animate_controls = urwid.GridFlow( [
+ self.animate_button,
+ self.button("Reset", self.on_reset_button),
+ ], 9, 2, 0, 'center')
+
+ if urwid.get_encoding_mode() == "utf8":
+ unicode_checkbox = urwid.CheckBox(
+ "Enable Unicode Graphics",
+ on_state_change=self.on_unicode_checkbox)
+ else:
+ unicode_checkbox = urwid.Text(
+ "UTF-8 encoding not detected")
+
+ self.animate_progress_wrap = urwid.WidgetWrap(
+ self.animate_progress)
+
+ l = [ urwid.Text("Mode",align="center"),
+ ] + self.mode_buttons + [
+ urwid.Divider(),
+ urwid.Text("Animation",align="center"),
+ animate_controls,
+ self.animate_progress_wrap,
+ urwid.Divider(),
+ urwid.LineBox( unicode_checkbox ),
+ urwid.Divider(),
+ self.button("Quit", self.exit_program ),
+ ]
+ w = urwid.ListBox(urwid.SimpleListWalker(l))
+ return w
+
+ def main_window(self):
+ self.graph = self.bar_graph()
+ self.graph_wrap = urwid.WidgetWrap( self.graph )
+ vline = urwid.AttrWrap( urwid.SolidFill(u'\u2502'), 'line')
+ c = self.graph_controls()
+ w = urwid.Columns([('weight',2,self.graph_wrap),
+ ('fixed',1,vline), c],
+ dividechars=1, focus_column=2)
+ w = urwid.Padding(w,('fixed left',1),('fixed right',0))
+ w = urwid.AttrWrap(w,'body')
+ w = urwid.LineBox(w)
+ w = urwid.AttrWrap(w,'line')
+ w = self.main_shadow(w)
+ return w
+
+
+class GraphController:
+ """
+ A class responsible for setting up the model and view and running
+ the application.
+ """
+ def __init__(self):
+ self.animate_alarm = None
+ self.model = GraphModel()
+ self.view = GraphView( self )
+ # use the first mode as the default
+ mode = self.get_modes()[0]
+ self.model.set_mode( mode )
+ # update the view
+ self.view.on_mode_change( mode )
+ self.view.update_graph(True)
+
+ def get_modes(self):
+ """Allow our view access to the list of modes."""
+ return self.model.get_modes()
+
+ def set_mode(self, m):
+ """Allow our view to set the mode."""
+ rval = self.model.set_mode( m )
+ self.view.update_graph(True)
+ return rval
+
+ def get_data(self, offset, range):
+ """Provide data to our view for the graph."""
+ return self.model.get_data( offset, range )
+
+
+ def main(self):
+ self.loop = urwid.MainLoop(self.view, self.view.palette)
+ self.loop.run()
+
+ def animate_graph(self, loop=None, user_data=None):
+ """update the graph and schedule the next update"""
+ self.view.update_graph()
+ self.animate_alarm = self.loop.set_alarm_in(
+ UPDATE_INTERVAL, self.animate_graph)
+
+ def stop_animation(self):
+ """stop animating the graph"""
+ if self.animate_alarm:
+ self.loop.remove_alarm(self.animate_alarm)
+ self.animate_alarm = None
+
+
+def main():
+ GraphController().main()
+
+if '__main__'==__name__:
+ main()
diff --git a/examples/input_test.py b/examples/input_test.py
new file mode 100755
index 0000000..e202162
--- /dev/null
+++ b/examples/input_test.py
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+#
+# Urwid keyboard input test app
+# Copyright (C) 2004-2009 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Keyboard test application
+"""
+
+import urwid.curses_display
+import urwid.raw_display
+import urwid.web_display
+import urwid
+
+import sys
+
+if urwid.web_display.is_web_request():
+ Screen = urwid.web_display.Screen
+else:
+ if len(sys.argv)>1 and sys.argv[1][:1] == "r":
+ Screen = urwid.raw_display.Screen
+ else:
+ Screen = urwid.curses_display.Screen
+
+def key_test():
+ screen = Screen()
+ header = urwid.Text("Values from get_input(). Q exits.")
+ header = urwid.AttrWrap(header,'header')
+ lw = urwid.SimpleListWalker([])
+ listbox = urwid.ListBox(lw)
+ listbox = urwid.AttrWrap(listbox, 'listbox')
+ top = urwid.Frame(listbox, header)
+
+ def input_filter(keys, raw):
+ if 'q' in keys or 'Q' in keys:
+ raise urwid.ExitMainLoop
+
+ t = []
+ a = []
+ for k in keys:
+ if type(k) == tuple:
+ out = []
+ for v in k:
+ if out:
+ out += [', ']
+ out += [('key',repr(v))]
+ t += ["("] + out + [")"]
+ else:
+ t += ["'",('key',k),"' "]
+
+ rawt = urwid.Text(", ".join(["%d"%r for r in raw]))
+
+ if t:
+ lw.append(
+ urwid.Columns([
+ ('weight',2,urwid.Text(t)),
+ rawt])
+ )
+ listbox.set_focus(len(lw)-1,'above')
+ return keys
+
+ loop = urwid.MainLoop(top, [
+ ('header', 'black', 'dark cyan', 'standout'),
+ ('key', 'yellow', 'dark blue', 'bold'),
+ ('listbox', 'light gray', 'black' ),
+ ], screen, input_filter=input_filter)
+
+ try:
+ old = screen.tty_signal_keys('undefined','undefined',
+ 'undefined','undefined','undefined')
+ loop.run()
+ finally:
+ screen.tty_signal_keys(*old)
+
+
+
+
+def main():
+ urwid.web_display.set_preferences('Input Test')
+ if urwid.web_display.handle_short_request():
+ return
+ key_test()
+
+
+if '__main__'==__name__ or urwid.web_display.is_web_request():
+ main()
diff --git a/examples/lcd_cf635.py b/examples/lcd_cf635.py
new file mode 100755
index 0000000..e01ead2
--- /dev/null
+++ b/examples/lcd_cf635.py
@@ -0,0 +1,291 @@
+#!/usr/bin/python
+
+"""
+The crystalfontz 635 has these characters in ROM:
+
+....X. ...... ......
+...XX. .XXXXX ..XXX.
+..XXX. .XXXXX .XXXXX
+.XXXX. .XXXXX .XXXXX
+..XXX. .XXXXX .XXXXX
+...XX. .XXXXX ..XXX.
+....X. ...... ......
+...... ...... ......
+ 0x11 0xd0 0xbb
+
+By adding the characters in CGRAM below we can use them as part of a
+horizontal slider control, selected check box and selected radio button
+respectively.
+"""
+
+import sys
+import urwid.lcd_display
+
+CGRAM = """
+...... ...... ...... ...... ..X... ...... ...... ......
+XXXXXX XXXXXX XXXXXX XXXXXX X.XX.. .XXXXX ..XXX. .....X
+...... XX.... XXXX.. XXXXXX X.XXX. .X...X .X...X ....XX
+...... XX.... XXXX.. XXXXXX X.XXXX .X...X .X...X .X.XX.
+...... XX.... XXXX.. XXXXXX X.XXX. .X...X .X...X .XXX..
+XXXXXX XXXXXX XXXXXX XXXXXX X.XX.. .XXXXX ..XXX. ..X...
+...... ...... ...... ...... ..X... ...... ...... ......
+...... ...... ...... ...... ...... ...... ...... ......
+"""
+
+def program_cgram(screen):
+ """
+ Load the character data
+ """
+ # convert .'s and X's above into integer data
+ cbuf = [list() for x in range(8)]
+ for row in CGRAM.strip().split('\n'):
+ rowsegments = row.strip().split()
+ for num, r in enumerate(rowsegments):
+ accum = 0
+ for c in r:
+ accum = (accum << 1) + (c == 'X')
+ cbuf[num].append(accum)
+
+ for num, cdata in enumerate(cbuf):
+ screen.program_cgram(num, cdata)
+
+class LCDCheckBox(urwid.CheckBox):
+ """
+ A check box+label that uses only one character for the check box,
+ including custom CGRAM character
+ """
+ states = {
+ True: urwid.SelectableIcon('\xd0', cursor_position=0),
+ False: urwid.SelectableIcon('\x05', cursor_position=0),
+ }
+ reserve_columns = 1
+
+class LCDRadioButton(urwid.RadioButton):
+ """
+ A radio button+label that uses only one character for the radio button,
+ including custom CGRAM character
+ """
+ states = {
+ True: urwid.SelectableIcon('\xbb', cursor_position=0),
+ False: urwid.SelectableIcon('\x06', cursor_position=0),
+ }
+ reserve_columns = 1
+
+class LCDProgressBar(urwid.FlowWidget):
+ """
+ The "progress bar" used by the horizontal slider for this device,
+ using custom CGRAM characters
+ """
+ segments = '\x00\x01\x02\x03'
+ def __init__(self, range, value):
+ self.range = range
+ self.value = value
+
+ def rows(self, size, focus=False):
+ return 1
+
+ def render(self, size, focus=False):
+ """
+ Draw the bar with self.segments where [0] is empty and [-1]
+ is completely full
+ """
+ (maxcol,) = size
+ steps = self.get_steps(size)
+ filled = urwid.int_scale(self.value, self.range, steps)
+ full_segments = int(filled / (len(self.segments) - 1))
+ last_char = filled % (len(self.segments) - 1) + 1
+ s = (self.segments[-1] * full_segments +
+ self.segments[last_char] +
+ self.segments[0] * (maxcol -full_segments - 1))
+ return urwid.Text(s).render(size)
+
+ def move_position(self, size, direction):
+ """
+ Update and return the value one step +ve or -ve, based on
+ the size of the displayed bar.
+
+ directon -- 1 for +ve, 0 for -ve
+ """
+ steps = self.get_steps(size)
+ filled = urwid.int_scale(self.value, self.range, steps)
+ filled += 2 * direction - 1
+ value = urwid.int_scale(filled, steps, self.range)
+ value = max(0, min(self.range - 1, value))
+ if value != self.value:
+ self.value = value
+ self._invalidate()
+ return value
+
+ def get_steps(self, size):
+ """
+ Return the number of steps available given size for rendering
+ the bar and number of segments we can draw.
+ """
+ (maxcol,) = size
+ return maxcol * (len(self.segments) - 1)
+
+
+class LCDHorizontalSlider(urwid.WidgetWrap):
+ """
+ A slider control using custom CGRAM characters
+ """
+ def __init__(self, range, value, callback):
+ self.bar = LCDProgressBar(range, value)
+ cols = urwid.Columns([
+ ('fixed', 1, urwid.SelectableIcon('\x11', cursor_position=0)),
+ self.bar,
+ ('fixed', 1, urwid.SelectableIcon('\x04', cursor_position=0)),
+ ])
+ self.__super.__init__(cols)
+ self.callback = callback
+
+ def keypress(self, size, key):
+ # move the slider based on which arrow is focused
+ if key == 'enter':
+ # use the correct size for adjusting the bar
+ self.bar.move_position((self._w.column_widths(size)[1],),
+ self._w.get_focus_column() != 0)
+ self.callback(self.bar.value)
+ else:
+ return self.__super.keypress(size, key)
+
+
+
+class MenuOption(urwid.Button):
+ """
+ A menu option, indicated with a single arrow character
+ """
+ def __init__(self, label, submenu):
+ self.__super.__init__("")
+ # use a Text widget for label, we want the cursor
+ # on the arrow not the label
+ self._label = urwid.Text("")
+ self.set_label(label)
+
+ self._w = urwid.Columns([
+ ('fixed', 1, urwid.SelectableIcon('\xdf', cursor_position=0)),
+ self._label])
+
+ urwid.connect_signal(self, 'click',
+ lambda option: show_menu(submenu))
+
+ def keypress(self, size, key):
+ if key == 'right':
+ key = 'enter'
+ return self.__super.keypress(size, key)
+
+
+class Menu(urwid.ListBox):
+ def __init__(self, widgets):
+ self.menu_parent = None
+ self.__super.__init__(urwid.SimpleListWalker(widgets))
+
+ def keypress(self, size, key):
+ """
+ Go back to the previous menu on cancel button (mapped to esc)
+ """
+ key = self.__super.keypress(size, key)
+ if key in ('left', 'esc') and self.menu_parent:
+ show_menu(self.menu_parent)
+ else:
+ return key
+
+def build_menus():
+ cursor_option_group = []
+ def cursor_option(label, style):
+ "a radio button that sets the cursor style"
+ def on_change(b, state):
+ if state: screen.set_cursor_style(style)
+ b = LCDRadioButton(cursor_option_group, label,
+ screen.cursor_style == style)
+ urwid.connect_signal(b, 'change', on_change)
+ return b
+
+ def display_setting(label, range, fn):
+ slider = LCDHorizontalSlider(range, range/2, fn)
+ return urwid.Columns([
+ urwid.Text(label),
+ ('fixed', 10, slider),
+ ])
+
+ def led_custom(index):
+ def exp_scale_led(rg):
+ """
+ apply an exponential transformation to values sent so
+ that apparent brightness increases in a natural way.
+ """
+ return lambda value: screen.set_led_pin(index, rg,
+ [0, 1, 2, 3, 4, 5, 6, 8, 11, 14, 18,
+ 23, 29, 38, 48, 61, 79, 100][value])
+
+ return urwid.Columns([
+ ('fixed', 2, urwid.Text('%dR' % index)),
+ LCDHorizontalSlider(18, 0, exp_scale_led(0)),
+ ('fixed', 2, urwid.Text(' G')),
+ LCDHorizontalSlider(18, 0, exp_scale_led(1)),
+ ])
+
+ menu_structure = [
+ ('Display Settings', [
+ display_setting('Brightness', 101, screen.set_backlight),
+ display_setting('Contrast', 76,
+ lambda x: screen.set_lcd_contrast(x + 75)),
+ ]),
+ ('Cursor Settings', [
+ cursor_option('Block', screen.CURSOR_BLINKING_BLOCK),
+ cursor_option('Underscore', screen.CURSOR_UNDERSCORE),
+ cursor_option('Block + Underscore',
+ screen.CURSOR_BLINKING_BLOCK_UNDERSCORE),
+ cursor_option('Inverting Block',
+ screen.CURSOR_INVERTING_BLINKING_BLOCK),
+ ]),
+ ('LEDs', [
+ led_custom(0),
+ led_custom(1),
+ led_custom(2),
+ led_custom(3),
+ ]),
+ ('About this Demo', [
+ urwid.Text("This is a demo of Urwid's CF635Display "
+ "module. If you need an interface for a limited "
+ "character display device this should serve as a "
+ "good example for implmenting your own display "
+ "module and menu-driven application."),
+ ])
+ ]
+
+ def build_submenu(ms):
+ """
+ Recursive menu building from structure above
+ """
+ options = []
+ submenus = []
+ for opt in ms:
+ # shortform for MenuOptions
+ if type(opt) == tuple:
+ name, sub = opt
+ submenu = build_submenu(sub)
+ opt = MenuOption(name, submenu)
+ submenus.append(submenu)
+ options.append(opt)
+ menu = Menu(options)
+ for s in submenus:
+ s.menu_parent = menu
+ return menu
+ return build_submenu(menu_structure)
+
+
+screen = urwid.lcd_display.CF635Screen(sys.argv[1])
+# set up our font
+program_cgram(screen)
+loop = urwid.MainLoop(build_menus(), screen=screen)
+# FIXME: want screen to know it is in narrow mode, or better yet,
+# do the unicode conversion for us
+urwid.set_encoding('narrow')
+
+
+def show_menu(menu):
+ loop.widget = menu
+
+loop.run()
+
diff --git a/examples/palette_test.py b/examples/palette_test.py
new file mode 100755
index 0000000..271dd51
--- /dev/null
+++ b/examples/palette_test.py
@@ -0,0 +1,252 @@
+#!/usr/bin/python
+#
+# Urwid Palette Test. Showing off highcolor support
+# Copyright (C) 2004-2009 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Palette test. Shows the available foreground and background settings
+in monochrome, 16 color, 88 color and 256 color modes.
+"""
+
+import re
+import sys
+
+import urwid
+import urwid.raw_display
+
+CHART_256 = """
+brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_
+yellow_ light_red light_magenta light_blue light_cyan light_green
+
+ #00f#06f#08f#0af#0df#0ff black_______ dark_gray___
+ #60f#00d#06d#08d#0ad#0dd#0fd light_gray__ white_______
+ #80f#60d#00a#06a#08a#0aa#0da#0fa
+ #a0f#80d#60a#008#068#088#0a8#0d8#0f8
+ #d0f#a0d#80d#608#006#066#086#0a6#0d6#0f6
+ #f0f#d0d#a0a#808#606#000#060#080#0a0#0d0#0f0#0f6#0f8#0fa#0fd#0ff
+ #f0d#d0a#a08#806#600#660#680#6a0#6d0#6f0#6f6#6f8#6fa#6fd#6ff#0df
+ #f0a#d08#a06#800#860#880#8a0#8d0#8f0#8f6#8f8#8fa#8fd#8ff#6df#0af
+ #f08#d06#a00#a60#a80#aa0#ad0#af0#af6#af8#afa#afd#aff#8df#6af#08f
+ #f06#d00#d60#d80#da0#dd0#df0#df6#df8#dfa#dfd#dff#adf#8af#68f#06f
+ #f00#f60#f80#fa0#fd0#ff0#ff6#ff8#ffa#ffd#fff#ddf#aaf#88f#66f#00f
+ #fd0#fd6#fd8#fda#fdd#fdf#daf#a8f#86f#60f
+ #66d#68d#6ad#6dd #fa0#fa6#fa8#faa#fad#faf#d8f#a6f#80f
+ #86d#66a#68a#6aa#6da #f80#f86#f88#f8a#f8d#f8f#d6f#a0f
+ #a6d#86a#668#688#6a8#6d8 #f60#f66#f68#f6a#f6d#f6f#d0f
+#d6d#a6a#868#666#686#6a6#6d6#6d8#6da#6dd #f00#f06#f08#f0a#f0d#f0f
+ #d6a#a68#866#886#8a6#8d6#8d8#8da#8dd#6ad
+ #d68#a66#a86#aa6#ad6#ad8#ada#add#8ad#68d
+ #d66#d86#da6#dd6#dd8#dda#ddd#aad#88d#66d g78_g82_g85_g89_g93_g100
+ #da6#da8#daa#dad#a8d#86d g52_g58_g62_g66_g70_g74_
+ #88a#8aa #d86#d88#d8a#d8d#a6d g27_g31_g35_g38_g42_g46_g50_
+ #a8a#888#8a8#8aa #d66#d68#d6a#d6d g0__g3__g7__g11_g15_g19_g23_
+ #a88#aa8#aaa#88a
+ #a88#a8a
+"""
+
+CHART_88 = """
+brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_
+yellow_ light_red light_magenta light_blue light_cyan light_green
+
+ #00f#08f#0cf#0ff black_______ dark_gray___
+ #80f#00c#08c#0cc#0fc light_gray__ white_______
+ #c0f#80c#008#088#0c8#0f8
+#f0f#c0c#808#000#080#0c0#0f0#0f8#0fc#0ff #88c#8cc
+ #f0c#c08#800#880#8c0#8f0#8f8#8fc#8ff#0cf #c8c#888#8c8#8cc
+ #f08#c00#c80#cc0#cf0#cf8#cfc#cff#8cf#08f #c88#cc8#ccc#88c
+ #f00#f80#fc0#ff0#ff8#ffc#fff#ccf#88f#00f #c88#c8c
+ #fc0#fc8#fcc#fcf#c8f#80f
+ #f80#f88#f8c#f8f#c0f g62_g74_g82_g89_g100
+ #f00#f08#f0c#f0f g0__g19_g35_g46_g52
+"""
+
+CHART_16 = """
+brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_
+yellow_ light_red light_magenta light_blue light_cyan light_green
+
+black_______ dark_gray___ light_gray__ white_______
+"""
+
+ATTR_RE = re.compile("(?P<whitespace>[ \n]*)(?P<entry>[^ \n]+)")
+SHORT_ATTR = 4 # length of short high-colour descriptions which may
+# be packed one after the next
+
+def parse_chart(chart, convert):
+ """
+ Convert string chart into text markup with the correct attributes.
+
+ chart -- palette chart as a string
+ convert -- function that converts a single palette entry to an
+ (attr, text) tuple, or None if no match is found
+ """
+ out = []
+ for match in re.finditer(ATTR_RE, chart):
+ if match.group('whitespace'):
+ out.append(match.group('whitespace'))
+ entry = match.group('entry')
+ entry = entry.replace("_", " ")
+ while entry:
+ # try the first four characters
+ attrtext = convert(entry[:SHORT_ATTR])
+ if attrtext:
+ elen = SHORT_ATTR
+ entry = entry[SHORT_ATTR:].strip()
+ else: # try the whole thing
+ attrtext = convert(entry.strip())
+ assert attrtext, "Invalid palette entry: %r" % entry
+ elen = len(entry)
+ entry = ""
+ attr, text = attrtext
+ out.append((attr, text.ljust(elen)))
+ return out
+
+def foreground_chart(chart, background, colors):
+ """
+ Create text markup for a foreground colour chart
+
+ chart -- palette chart as string
+ background -- colour to use for background of chart
+ colors -- number of colors (88 or 256)
+ """
+ def convert_foreground(entry):
+ try:
+ attr = urwid.AttrSpec(entry, background, colors)
+ except urwid.AttrSpecError:
+ return None
+ return attr, entry
+ return parse_chart(chart, convert_foreground)
+
+def background_chart(chart, foreground, colors):
+ """
+ Create text markup for a background colour chart
+
+ chart -- palette chart as string
+ foreground -- colour to use for foreground of chart
+ colors -- number of colors (88 or 256)
+
+ This will remap 8 <= colour < 16 to high-colour versions
+ in the hopes of greater compatibility
+ """
+ def convert_background(entry):
+ try:
+ attr = urwid.AttrSpec(foreground, entry, colors)
+ except urwid.AttrSpecError:
+ return None
+ # fix 8 <= colour < 16
+ if colors > 16 and attr.background_basic and \
+ attr.background_number >= 8:
+ # use high-colour with same number
+ entry = 'h%d'%attr.background_number
+ attr = urwid.AttrSpec(foreground, entry, colors)
+ return attr, entry
+ return parse_chart(chart, convert_background)
+
+
+def main():
+ palette = [
+ ('header', 'black,underline', 'light gray', 'standout,underline',
+ 'black,underline', '#88a'),
+ ('panel', 'light gray', 'dark blue', '',
+ '#ffd', '#00a'),
+ ('focus', 'light gray', 'dark cyan', 'standout',
+ '#ff8', '#806'),
+ ]
+
+ screen = urwid.raw_display.Screen()
+ screen.register_palette(palette)
+
+ lb = urwid.SimpleListWalker([])
+ chart_offset = None # offset of chart in lb list
+
+ mode_radio_buttons = []
+ chart_radio_buttons = []
+
+ def fcs(widget):
+ # wrap widgets that can take focus
+ return urwid.AttrMap(widget, None, 'focus')
+
+ def set_mode(colors, is_foreground_chart):
+ # set terminal mode and redraw chart
+ screen.set_terminal_properties(colors)
+ screen.reset_default_terminal_palette()
+
+ chart_fn = (background_chart, foreground_chart)[is_foreground_chart]
+ if colors == 1:
+ lb[chart_offset] = urwid.Divider()
+ else:
+ chart = {16: CHART_16, 88: CHART_88, 256: CHART_256}[colors]
+ txt = chart_fn(chart, 'default', colors)
+ lb[chart_offset] = urwid.Text(txt, wrap='clip')
+
+ def on_mode_change(rb, state, colors):
+ # if this radio button is checked
+ if state:
+ is_foreground_chart = chart_radio_buttons[0].state
+ set_mode(colors, is_foreground_chart)
+
+ def mode_rb(text, colors, state=False):
+ # mode radio buttons
+ rb = urwid.RadioButton(mode_radio_buttons, text, state)
+ urwid.connect_signal(rb, 'change', on_mode_change, colors)
+ return fcs(rb)
+
+ def on_chart_change(rb, state):
+ # handle foreground check box state change
+ set_mode(screen.colors, state)
+
+ def click_exit(button):
+ raise urwid.ExitMainLoop()
+
+ lb.extend([
+ urwid.AttrMap(urwid.Text("Urwid Palette Test"), 'header'),
+ urwid.AttrMap(urwid.Columns([
+ urwid.Pile([
+ mode_rb("Monochrome", 1),
+ mode_rb("16-Color", 16, True),
+ mode_rb("88-Color", 88),
+ mode_rb("256-Color", 256),]),
+ urwid.Pile([
+ fcs(urwid.RadioButton(chart_radio_buttons,
+ "Foreground Colors", True, on_chart_change)),
+ fcs(urwid.RadioButton(chart_radio_buttons,
+ "Background Colors")),
+ urwid.Divider(),
+ fcs(urwid.Button("Exit", click_exit)),
+ ]),
+ ]),'panel')
+ ])
+
+ chart_offset = len(lb)
+ lb.extend([
+ urwid.Divider() # placeholder for the chart
+ ])
+
+ set_mode(16, True) # displays the chart
+
+ def unhandled_input(key):
+ if key in ('Q','q','esc'):
+ raise urwid.ExitMainLoop()
+
+ urwid.MainLoop(urwid.ListBox(lb), screen=screen,
+ unhandled_input=unhandled_input).run()
+
+if __name__ == "__main__":
+ main()
+
+
diff --git a/examples/pop_up.py b/examples/pop_up.py
new file mode 100755
index 0000000..37e2258
--- /dev/null
+++ b/examples/pop_up.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+
+import urwid
+
+class PopUpDialog(urwid.WidgetWrap):
+ """A dialog that appears with nothing but a close button """
+ signals = ['close']
+ def __init__(self):
+ close_button = urwid.Button("that's pretty cool")
+ urwid.connect_signal(close_button, 'click',
+ lambda button:self._emit("close"))
+ pile = urwid.Pile([urwid.Text(
+ "^^ I'm attached to the widget that opened me. "
+ "Try resizing the window!\n"), close_button])
+ fill = urwid.Filler(pile)
+ self.__super.__init__(urwid.AttrWrap(fill, 'popbg'))
+
+
+class ThingWithAPopUp(urwid.PopUpLauncher):
+ def __init__(self):
+ self.__super.__init__(urwid.Button("click-me"))
+ urwid.connect_signal(self.original_widget, 'click',
+ lambda button: self.open_pop_up())
+
+ def create_pop_up(self):
+ pop_up = PopUpDialog()
+ urwid.connect_signal(pop_up, 'close',
+ lambda button: self.close_pop_up())
+ return pop_up
+
+ def get_pop_up_parameters(self):
+ return {'left':0, 'top':1, 'overlay_width':32, 'overlay_height':7}
+
+
+fill = urwid.Filler(urwid.Padding(ThingWithAPopUp(), 'center', 15))
+loop = urwid.MainLoop(
+ fill,
+ [('popbg', 'white', 'dark blue')],
+ pop_ups=True)
+loop.run()
+
diff --git a/examples/subproc.py b/examples/subproc.py
new file mode 100755
index 0000000..28f5497
--- /dev/null
+++ b/examples/subproc.py
@@ -0,0 +1,30 @@
+#!/usr/bin/python
+
+import subprocess
+import urwid
+
+factor_me = 362923067964327863989661926737477737673859044111968554257667
+
+output_widget = urwid.Text("Factors of %d:\n" % factor_me)
+edit_widget = urwid.Edit("Type anything or press enter to exit:")
+frame_widget = urwid.Frame(
+ header=edit_widget,
+ body=urwid.Filler(output_widget, valign='bottom'),
+ focus_part='header')
+
+def exit_on_enter(key):
+ if key == 'enter': raise urwid.ExitMainLoop()
+
+loop = urwid.MainLoop(frame_widget, unhandled_input=exit_on_enter)
+
+def received_output(data):
+ output_widget.set_text(output_widget.text + data)
+
+write_fd = loop.watch_pipe(received_output)
+proc = subprocess.Popen(
+ ['python', '-u', 'subproc2.py', str(factor_me)],
+ stdout=write_fd,
+ close_fds=True)
+
+loop.run()
+proc.kill()
diff --git a/examples/subproc2.py b/examples/subproc2.py
new file mode 100644
index 0000000..79c73b2
--- /dev/null
+++ b/examples/subproc2.py
@@ -0,0 +1,8 @@
+# this is part of the subproc.py example
+
+import sys
+
+num = int(sys.argv[1])
+for c in xrange(1,10000000):
+ if num % c == 0:
+ print "factor:", c
diff --git a/examples/terminal.py b/examples/terminal.py
new file mode 100755
index 0000000..18edba1
--- /dev/null
+++ b/examples/terminal.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+#
+# Urwid terminal emulation widget example app
+# Copyright (C) 2010 aszlig
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+import urwid
+
+def main():
+ term = urwid.Terminal(None)
+
+ mainframe = urwid.LineBox(
+ urwid.Pile([
+ ('weight', 70, term),
+ ('fixed', 1, urwid.Filler(urwid.Edit('focus test edit: '))),
+ ]),
+ )
+
+ def set_title(widget, title):
+ mainframe.set_title(title)
+
+ def quit(*args, **kwargs):
+ raise urwid.ExitMainLoop()
+
+ def handle_key(key):
+ if key in ('q', 'Q'):
+ quit()
+
+ urwid.connect_signal(term, 'title', set_title)
+ urwid.connect_signal(term, 'closed', quit)
+
+ loop = urwid.MainLoop(
+ mainframe,
+ handle_mouse=False,
+ unhandled_input=handle_key)
+
+ term.main_loop = loop
+ loop.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/examples/tour.py b/examples/tour.py
new file mode 100755
index 0000000..55211b3
--- /dev/null
+++ b/examples/tour.py
@@ -0,0 +1,333 @@
+#!/usr/bin/python
+#
+# Urwid tour. It slices, it dices..
+# Copyright (C) 2004-2011 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid tour. Shows many of the standard widget types and features.
+"""
+
+import urwid
+import urwid.raw_display
+import urwid.web_display
+
+def main():
+ text_header = (u"Welcome to the urwid tour! "
+ u"UP / DOWN / PAGE UP / PAGE DOWN scroll. F8 exits.")
+ text_intro = [('important', u"Text"),
+ u" widgets are the most common in "
+ u"any urwid program. This Text widget was created "
+ u"without setting the wrap or align mode, so it "
+ u"defaults to left alignment with wrapping on space "
+ u"characters. ",
+ ('important', u"Change the window width"),
+ u" to see how the widgets on this page react. "
+ u"This Text widget is wrapped with a ",
+ ('important', u"Padding"),
+ u" widget to keep it indented on the left and right."]
+ text_right = (u"This Text widget is right aligned. Wrapped "
+ u"words stay to the right as well. ")
+ text_center = u"This one is center aligned."
+ text_clip = (u"Text widgets may be clipped instead of wrapped.\n"
+ u"Extra text is discarded instead of wrapped to the next line. "
+ u"65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n"
+ u"Newlines embedded in the string are still respected.")
+ text_right_clip = (u"This is a right aligned and clipped Text widget.\n"
+ u"<100 <-95 <-90 <-85 <-80 <-75 <-70 <-65 "
+ u"Text will be cut off at the left of this widget.")
+ text_center_clip = (u"Center aligned and clipped widgets will have "
+ u"text cut off both sides.")
+ text_any = (u"The 'any' wrap mode will wrap on any character. This "
+ u"mode will not collapse space characters at the end of the "
+ u"line but it still honors embedded newline characters.\n"
+ u"Like this one.")
+ text_padding = (u"Padding widgets have many options. This "
+ u"is a standard Text widget wrapped with a Padding widget "
+ u"with the alignment set to relative 20% and with its width "
+ u"fixed at 40.")
+ text_divider = [u"The ", ('important', u"Divider"),
+ u" widget repeats the same character across the whole line. "
+ u"It can also add blank lines above and below."]
+ text_edit = [u"The ", ('important', u"Edit"),
+ u" widget is a simple text editing widget. It supports cursor "
+ u"movement and tries to maintain the current column when focus "
+ u"moves to another edit widget. It wraps and aligns the same "
+ u"way as Text widgets." ]
+ text_edit_cap1 = ('editcp', u"This is a caption. Edit here: ")
+ text_edit_text1 = u"editable stuff"
+ text_edit_cap2 = ('editcp', u"This one supports newlines: ")
+ text_edit_text2 = (u"line one starts them all\n"
+ u"== line 2 == with some more text to edit.. words.. whee..\n"
+ u"LINE III, the line to end lines one and two, unless you "
+ u"change something.")
+ text_edit_cap3 = ('editcp', u"This one is clipped, try "
+ u"editing past the edge: ")
+ text_edit_text3 = u"add some text here -> -> -> ...."
+ text_edit_alignments = u"Different Alignments:"
+ text_edit_left = u"left aligned (default)"
+ text_edit_center = u"center aligned"
+ text_edit_right = u"right aligned"
+ text_intedit = ('editcp', [('important', u"IntEdit"),
+ u" allows only numbers: "])
+ text_edit_padding = ('editcp', u"Edit widget within a Padding widget ")
+ text_columns1 = [('important', u"Columns"),
+ u" are used to share horizontal screen space. "
+ u"This one splits the space into two parts with "
+ u"three characters between each column. The "
+ u"contents of each column is a single widget."]
+ text_columns2 = [u"When you need to put more than one "
+ u"widget into a column you can use a ",('important',
+ u"Pile"), u" to combine two or more widgets."]
+ text_col_columns = u"Columns may be placed inside other columns."
+ text_col_21 = u"Col 2.1"
+ text_col_22 = u"Col 2.2"
+ text_col_23 = u"Col 2.3"
+ text_column_widths = (u"Columns may also have uneven relative "
+ u"weights or fixed widths. Use a minimum width so that "
+ u"columns don't become too small.")
+ text_weight = u"Weight %d"
+ text_fixed_9 = u"<Fixed 9>" # should be 9 columns wide
+ text_fixed_14 = u"<--Fixed 14-->" # should be 14 columns wide
+ text_edit_col_cap1 = ('editcp', u"Edit widget within Columns")
+ text_edit_col_text1 = u"here's\nsome\ninfo"
+ text_edit_col_cap2 = ('editcp', u"and within Pile ")
+ text_edit_col_text2 = u"more"
+ text_edit_col_cap3 = ('editcp', u"another ")
+ text_edit_col_text3 = u"still more"
+ text_gridflow = [u"A ",('important', u"GridFlow"), u" widget "
+ u"may be used to display a list of flow widgets with equal "
+ u"widths. Widgets that don't fit on the first line will "
+ u"flow to the next. This is useful for small widgets that "
+ u"you want to keep together such as ", ('important', u"Button"),
+ u", ",('important', u"CheckBox"), u" and ",
+ ('important', u"RadioButton"), u" widgets." ]
+ text_button_list = [u"Yes", u"No", u"Perhaps", u"Certainly", u"Partially",
+ u"Tuesdays Only", u"Help"]
+ text_cb_list = [u"Wax", u"Wash", u"Buff", u"Clear Coat", u"Dry",
+ u"Racing Stripe"]
+ text_rb_list = [u"Morning", u"Afternoon", u"Evening", u"Weekend"]
+ text_listbox = [u"All these widgets have been diplayed "
+ u"with the help of a ", ('important', u"ListBox"), u" widget. "
+ u"ListBox widgets handle scrolling and changing focus. A ",
+ ('important', u"Frame"), u" widget is used to keep the "
+ u"instructions at the top of the screen."]
+
+
+ def button_press(button):
+ frame.footer = urwid.AttrWrap(urwid.Text(
+ [u"Pressed: ", button.get_label()]), 'header')
+
+ radio_button_group = []
+
+ blank = urwid.Divider()
+ listbox_content = [
+ blank,
+ urwid.Padding(urwid.Text(text_intro), ('fixed left',2),
+ ('fixed right',2), 20),
+ blank,
+ urwid.Text(text_right, align='right'),
+ blank,
+ urwid.Text(text_center, align='center'),
+ blank,
+ urwid.Text(text_clip, wrap='clip'),
+ blank,
+ urwid.Text(text_right_clip, align='right', wrap='clip'),
+ blank,
+ urwid.Text(text_center_clip, align='center', wrap='clip'),
+ blank,
+ urwid.Text(text_any, wrap='any'),
+ blank,
+ urwid.Padding(urwid.Text(text_padding), ('relative', 20), 40),
+ blank,
+ urwid.AttrWrap(urwid.Divider("=", 1), 'bright'),
+ urwid.Padding(urwid.Text(text_divider), ('fixed left',2),
+ ('fixed right',2), 20),
+ urwid.AttrWrap(urwid.Divider("-", 0, 1), 'bright'),
+ blank,
+ urwid.Padding(urwid.Text(text_edit), ('fixed left',2),
+ ('fixed right',2), 20),
+ blank,
+ urwid.AttrWrap(urwid.Edit(text_edit_cap1, text_edit_text1),
+ 'editbx', 'editfc'),
+ blank,
+ urwid.AttrWrap(urwid.Edit(text_edit_cap2, text_edit_text2,
+ multiline=True ), 'editbx', 'editfc'),
+ blank,
+ urwid.AttrWrap(urwid.Edit(text_edit_cap3, text_edit_text3,
+ wrap='clip' ), 'editbx', 'editfc'),
+ blank,
+ urwid.Text(text_edit_alignments),
+ urwid.AttrWrap(urwid.Edit("", text_edit_left, align='left'),
+ 'editbx', 'editfc' ),
+ urwid.AttrWrap(urwid.Edit("", text_edit_center,
+ align='center'), 'editbx', 'editfc' ),
+ urwid.AttrWrap(urwid.Edit("", text_edit_right, align='right'),
+ 'editbx', 'editfc' ),
+ blank,
+ urwid.AttrWrap(urwid.IntEdit(text_intedit, 123),
+ 'editbx', 'editfc' ),
+ blank,
+ urwid.Padding(urwid.AttrWrap(urwid.Edit(text_edit_padding, ""),
+ 'editbx','editfc' ), ('fixed left',10), 50 ),
+ blank,
+ blank,
+ urwid.AttrWrap(urwid.Columns([
+ urwid.Divider("."),
+ urwid.Divider(","),
+ urwid.Divider("."),
+ ]), 'bright'),
+ blank,
+ urwid.Columns([
+ urwid.Padding(urwid.Text(text_columns1),
+ ('fixed left',2), ('fixed right',0), 20),
+ urwid.Pile([
+ urwid.Divider("~"),
+ urwid.Text(text_columns2),
+ urwid.Divider("_")])
+ ], 3),
+ blank,
+ blank,
+ urwid.Columns([
+ urwid.Text(text_col_columns),
+ urwid.Columns([
+ urwid.Text(text_col_21),
+ urwid.Text(text_col_22),
+ urwid.Text(text_col_23),
+ ], 1),
+ ], 2),
+ blank,
+ urwid.Padding(urwid.Text(text_column_widths),
+ ('fixed left',2), ('fixed right',2), 20),
+ blank,
+ urwid.Columns( [
+ urwid.AttrWrap(urwid.Text(text_weight % 1),'reverse'),
+ ('weight', 2, urwid.Text(text_weight % 2)),
+ ('weight', 3, urwid.AttrWrap(urwid.Text(
+ text_weight % 3), 'reverse')),
+ ('weight', 4, urwid.Text(text_weight % 4)),
+ ('weight', 5, urwid.AttrWrap(urwid.Text(
+ text_weight % 5), 'reverse')),
+ ('weight', 6, urwid.Text(text_weight%2)),
+ ], 0, min_width=8),
+ blank,
+ urwid.Columns([
+ ('weight', 2, urwid.AttrWrap(urwid.Text(
+ text_weight % 2), 'reverse')),
+ ('fixed', 9, urwid.Text(text_fixed_9)),
+ ('weight', 3, urwid.AttrWrap(urwid.Text(
+ text_weight % 2), 'reverse')),
+ ('fixed', 14, urwid.Text(text_fixed_14)),
+ ], 0, min_width=8),
+ blank,
+ urwid.Columns([
+ urwid.AttrWrap(urwid.Edit(text_edit_col_cap1,
+ text_edit_col_text1, multiline=True),
+ 'editbx','editfc'),
+ urwid.Pile([
+ urwid.AttrWrap(urwid.Edit(
+ text_edit_col_cap2,
+ text_edit_col_text2),
+ 'editbx','editfc'),
+ blank,
+ urwid.AttrWrap(urwid.Edit(
+ text_edit_col_cap3,
+ text_edit_col_text3),
+ 'editbx','editfc'),
+ ]),
+ ], 1),
+ blank,
+ urwid.AttrWrap(urwid.Columns([
+ urwid.Divider("'"),
+ urwid.Divider('"'),
+ urwid.Divider("~"),
+ urwid.Divider('"'),
+ urwid.Divider("'"),
+ ]), 'bright'),
+ blank,
+ blank,
+ urwid.Padding(urwid.Text(text_gridflow), ('fixed left',2),
+ ('fixed right',2), 20),
+ blank,
+ urwid.Padding(urwid.GridFlow(
+ [urwid.AttrWrap(urwid.Button(txt, button_press),
+ 'buttn','buttnf') for txt in text_button_list],
+ 13, 3, 1, 'left'),
+ ('fixed left',4), ('fixed right',3)),
+ blank,
+ urwid.Padding(urwid.GridFlow(
+ [urwid.AttrWrap(urwid.CheckBox(txt),'buttn','buttnf')
+ for txt in text_cb_list],
+ 10, 3, 1, 'left') ,
+ ('fixed left',4), ('fixed right',3)),
+ blank,
+ urwid.Padding(urwid.GridFlow(
+ [urwid.AttrWrap(urwid.RadioButton(radio_button_group,
+ txt), 'buttn','buttnf')
+ for txt in text_rb_list],
+ 13, 3, 1, 'left') ,
+ ('fixed left',4), ('fixed right',3)),
+ blank,
+ blank,
+ urwid.Padding(urwid.Text(text_listbox),
+ ('fixed left',2),('fixed right',2),20),
+ blank,
+ blank,
+ ]
+
+ header = urwid.AttrWrap(urwid.Text(text_header), 'header')
+ listbox = urwid.ListBox(urwid.SimpleListWalker(listbox_content))
+ frame = urwid.Frame(urwid.AttrWrap(listbox, 'body'), header=header)
+
+ palette = [
+ ('body','black','light gray', 'standout'),
+ ('reverse','light gray','black'),
+ ('header','white','dark red', 'bold'),
+ ('important','dark blue','light gray',('standout','underline')),
+ ('editfc','white', 'dark blue', 'bold'),
+ ('editbx','light gray', 'dark blue'),
+ ('editcp','black','light gray', 'standout'),
+ ('bright','dark gray','light gray', ('bold','standout')),
+ ('buttn','black','dark cyan'),
+ ('buttnf','white','dark blue','bold'),
+ ]
+
+
+ # use appropriate Screen class
+ if urwid.web_display.is_web_request():
+ screen = urwid.web_display.Screen()
+ else:
+ screen = urwid.raw_display.Screen()
+
+ def unhandled(key):
+ if key == 'f8':
+ raise urwid.ExitMainLoop()
+
+ urwid.MainLoop(frame, palette, screen,
+ unhandled_input=unhandled).run()
+
+def setup():
+ urwid.web_display.set_preferences("Urwid Tour")
+ # try to handle short web requests quickly
+ if urwid.web_display.handle_short_request():
+ return
+
+ main()
+
+if '__main__'==__name__ or urwid.web_display.is_web_request():
+ setup()
diff --git a/examples/treesample.py b/examples/treesample.py
new file mode 100755
index 0000000..5d492c4
--- /dev/null
+++ b/examples/treesample.py
@@ -0,0 +1,138 @@
+#!/usr/bin/python
+#
+# Trivial data browser
+# This version:
+# Copyright (C) 2010 Rob Lanphier
+# Derived from browse.py in urwid distribution
+# Copyright (C) 2004-2007 Ian Ward
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+"""
+Urwid example lazy directory browser / tree view
+
+Features:
+- custom selectable widgets for files and directories
+- custom message widgets to identify access errors and empty directories
+- custom list walker for displaying widgets in a tree fashion
+"""
+
+import urwid
+import os
+
+
+class ExampleTreeWidget(urwid.TreeWidget):
+ """ Display widget for leaf nodes """
+ def get_display_text(self):
+ return self.get_node().get_value()['name']
+
+
+class ExampleNode(urwid.TreeNode):
+ """ Data storage object for leaf nodes """
+ def load_widget(self):
+ return ExampleTreeWidget(self)
+
+
+class ExampleParentNode(urwid.ParentNode):
+ """ Data storage object for interior/parent nodes """
+ def load_widget(self):
+ return ExampleTreeWidget(self)
+
+ def load_child_keys(self):
+ data = self.get_value()
+ return range(len(data['children']))
+
+ def load_child_node(self, key):
+ """Return either an ExampleNode or ExampleParentNode"""
+ childdata = self.get_value()['children'][key]
+ childdepth = self.get_depth() + 1
+ if 'children' in childdata:
+ childclass = ExampleParentNode
+ else:
+ childclass = ExampleNode
+ return childclass(childdata, parent=self, key=key, depth=childdepth)
+
+
+class ExampleTreeBrowser:
+ palette = [
+ ('body', 'black', 'light gray'),
+ ('focus', 'light gray', 'dark blue', 'standout'),
+ ('head', 'yellow', 'black', 'standout'),
+ ('foot', 'light gray', 'black'),
+ ('key', 'light cyan', 'black','underline'),
+ ('title', 'white', 'black', 'bold'),
+ ('flag', 'dark gray', 'light gray'),
+ ('error', 'dark red', 'light gray'),
+ ]
+
+ footer_text = [
+ ('title', "Example Data Browser"), " ",
+ ('key', "UP"), ",", ('key', "DOWN"), ",",
+ ('key', "PAGE UP"), ",", ('key', "PAGE DOWN"),
+ " ",
+ ('key', "+"), ",",
+ ('key', "-"), " ",
+ ('key', "LEFT"), " ",
+ ('key', "HOME"), " ",
+ ('key', "END"), " ",
+ ('key', "Q"),
+ ]
+
+ def __init__(self, data=None):
+ self.topnode = ExampleParentNode(data)
+ self.listbox = urwid.TreeListBox(urwid.TreeWalker(self.topnode))
+ self.listbox.offset_rows = 1
+ self.header = urwid.Text( "" )
+ self.footer = urwid.AttrWrap( urwid.Text( self.footer_text ),
+ 'foot')
+ self.view = urwid.Frame(
+ urwid.AttrWrap( self.listbox, 'body' ),
+ header=urwid.AttrWrap(self.header, 'head' ),
+ footer=self.footer )
+
+ def main(self):
+ """Run the program."""
+
+ self.loop = urwid.MainLoop(self.view, self.palette,
+ unhandled_input=self.unhandled_input)
+ self.loop.run()
+
+ def unhandled_input(self, k):
+ if k in ('q','Q'):
+ raise urwid.ExitMainLoop()
+
+
+def get_example_tree():
+ """ generate a quick 100 leaf tree for demo purposes """
+ retval = {"name":"parent","children":[]}
+ for i in range(10):
+ retval['children'].append({"name":"child " + str(i)})
+ retval['children'][i]['children']=[]
+ for j in range(10):
+ retval['children'][i]['children'].append({"name":"grandchild " +
+ str(i) + "." + str(j)})
+ return retval
+
+
+def main():
+ sample = get_example_tree()
+ ExampleTreeBrowser(sample).main()
+
+
+if __name__=="__main__":
+ main()
+
diff --git a/examples/twisted_serve_ssh.py b/examples/twisted_serve_ssh.py
new file mode 100644
index 0000000..9562bc6
--- /dev/null
+++ b/examples/twisted_serve_ssh.py
@@ -0,0 +1,456 @@
+# encoding: utf-8
+"""
+Twisted integration for Urwid.
+
+This module allows you to serve Urwid applications remotely over ssh.
+
+The idea is that the server listens as an SSH server, and each connection is
+routed by Twisted to urwid, and the urwid UI is routed back to the console.
+The concept was a bit of a head-bender for me, but really we are just sending
+escape codes and the what-not back to the console over the shell that ssh has
+created. This is the same service as provided by the UI components in
+twisted.conch.insults.window, except urwid has more features, and seems more
+mature.
+
+This module is not highly configurable, and the API is not great, so
+don't worry about just using it as an example and copy-pasting.
+
+Process
+-------
+
+
+TODO:
+
+- better gpm tracking: there is no place for os.Popen in a Twisted app I
+ think.
+
+Copyright: 2010, Ali Afshar <aafshar@gmail.com>
+License: MIT <http://www.opensource.org/licenses/mit-license.php>
+
+Portions Copyright: 2010, Ian Ward <ian@excess.org>
+Licence: LGPL <http://opensource.org/licenses/lgpl-2.1.php>
+"""
+
+import os
+
+import urwid
+
+from zope.interface import Interface, Attribute, implements
+from twisted.application.service import Application
+from twisted.application.internet import TCPServer
+from twisted.cred.portal import Portal
+from twisted.conch.interfaces import IConchUser, ISession
+from twisted.conch.insults.insults import TerminalProtocol, ServerProtocol
+from twisted.conch.manhole_ssh import (ConchFactory, TerminalRealm,
+ TerminalUser, TerminalSession, TerminalSessionTransport)
+
+from twisted.python.components import Componentized, Adapter
+
+
+
+class IUrwidUi(Interface):
+
+ """Toplevel urwid widget
+ """
+ toplevel = Attribute('Urwid Toplevel Widget')
+ palette = Attribute('Urwid Palette')
+ screen = Attribute('Urwid Screen')
+ loop = Attribute('Urwid Main Loop')
+
+ def create_urwid_toplevel():
+ """Create a toplevel widget.
+ """
+
+ def create_urwid_mainloop():
+ """Create the urwid main loop.
+ """
+
+
+class IUrwidMind(Interface):
+ ui = Attribute('')
+ terminalProtocol = Attribute('')
+ terminal = Attribute('')
+ checkers = Attribute('')
+ avatar = Attribute('The avatar')
+
+ def push(data):
+ """Push data"""
+
+ def draw():
+ """Refresh the UI"""
+
+
+
+
+class UrwidUi(object):
+
+ def __init__(self, urwid_mind):
+ self.mind = urwid_mind
+ self.toplevel = self.create_urwid_toplevel()
+ self.palette = self.create_urwid_palette()
+ self.screen = TwistedScreen(self.mind.terminalProtocol)
+ self.loop = self.create_urwid_mainloop()
+
+ def create_urwid_toplevel(self):
+ raise NotImplementedError
+
+ def create_urwid_palette(self):
+ return
+
+ def create_urwid_mainloop(self):
+ evl = urwid.TwistedEventLoop(manage_reactor=False)
+ loop = urwid.MainLoop(self.toplevel, screen=self.screen,
+ event_loop=evl,
+ unhandled_input=self.mind.unhandled_key,
+ palette=self.palette)
+ self.screen.loop = loop
+ loop.run()
+ return loop
+
+
+
+class UnhandledKeyHandler(object):
+
+ def __init__(self, mind):
+ self.mind = mind
+
+ def push(self, key):
+ if isinstance(key, tuple):
+ pass
+ else:
+ f = getattr(self, 'key_%s' % key.replace(' ', '_'), None)
+ if f is None:
+ return
+ else:
+ return f(key)
+
+ def key_ctrl_c(self, key):
+ self.mind.terminal.loseConnection()
+
+
+class UrwidMind(Adapter):
+
+ implements(IUrwidMind)
+
+ cred_checkers = []
+ ui = None
+
+ ui_factory = None
+ unhandled_key_factory = UnhandledKeyHandler
+
+ @property
+ def avatar(self):
+ return IConchUser(self.original)
+
+ def set_terminalProtocol(self, terminalProtocol):
+ self.terminalProtocol = terminalProtocol
+ self.terminal = terminalProtocol.terminal
+ self.unhandled_key_handler = self.unhandled_key_factory(self)
+ self.unhandled_key = self.unhandled_key_handler.push
+ self.ui = self.ui_factory(self)
+
+ def push(self, data):
+ self.ui.screen.push(data)
+
+ def draw(self):
+ self.ui.loop.draw_screen()
+
+
+
+
+
+class TwistedScreen(urwid.BaseScreen):
+ """A Urwid screen which knows about the Twisted terminal protocol that is
+ driving it.
+
+ A Urwid screen is responsible for:
+
+ 1. Input
+ 2. Output
+
+ Input is achieved in normal urwid by passing a lsit of available readable
+ file descriptors to the event loop for polling/selecting etc. In the
+ Twisted situation, this is not necessary because Twisted polls the input
+ descriptors itself. Urwid allows this by being driven using the main loop
+ instance's `process_input` method which is triggered on Twisted protocol's
+ standard `dataReceived` method.
+ """
+
+ def __init__(self, terminalProtocol):
+ # We will need these later
+ self.terminalProtocol = terminalProtocol
+ self.terminal = terminalProtocol.terminal
+ urwid.BaseScreen.__init__(self)
+ self.colors = 16
+ self._pal_escape = {}
+ self.bright_is_bold = True
+ self.register_palette_entry(None, 'black', 'white')
+ urwid.signals.connect_signal(self, urwid.UPDATE_PALETTE_ENTRY,
+ self._on_update_palette_entry)
+ # Don't need to wait for anything to start
+ self._started = True
+
+ # Urwid Screen API
+
+ def get_cols_rows(self):
+ """Get the size of the terminal as (cols, rows)
+ """
+ return self.terminalProtocol.width, self.terminalProtocol.height
+
+ def draw_screen(self, (maxcol, maxrow), r ):
+ """Render a canvas to the terminal.
+
+ The canvas contains all the information required to render the Urwid
+ UI. The content method returns a list of rows as (attr, cs, text)
+ tuples. This very simple implementation iterates each row and simply
+ writes it out.
+ """
+ #self.terminal.eraseDisplay()
+ lasta = None
+ for i, row in enumerate(r.content()):
+ self.terminal.cursorPosition(0, i)
+ for (attr, cs, text) in row:
+ if attr != lasta:
+ text = '%s%s' % (self._attr_to_escape(attr), text)
+ lasta = attr
+ #if cs or attr:
+ # print cs, attr
+ self.write(text)
+ cursor = r.get_cursor()
+ if cursor is not None:
+ self.terminal.cursorPosition(*cursor)
+
+ # XXX from base screen
+ def set_mouse_tracking(self):
+ """
+ Enable mouse tracking.
+
+ After calling this function get_input will include mouse
+ click events along with keystrokes.
+ """
+ self.write(urwid.escape.MOUSE_TRACKING_ON)
+
+ # twisted handles polling, so we don't need the loop to do it, we just
+ # push what we get to the loop from dataReceived.
+ def get_input_descriptors(self):
+ return []
+
+ # Do nothing here either. Not entirely sure when it gets called.
+ def get_input(self, raw_keys=False):
+ return
+
+ # Twisted driven
+ def push(self, data):
+ """Receive data from Twisted and push it into the urwid main loop.
+
+ We must here:
+
+ 1. filter the input data against urwid's input filter.
+ 2. Calculate escapes and other clever things using urwid's
+ `escape.process_keyqueue`.
+ 3. Pass the calculated keys as a list to the Urwid main loop.
+ 4. Redraw the screen
+ """
+ keys = self.loop.input_filter(data, [])
+ keys, remainder = urwid.escape.process_keyqueue(map(ord, keys), True)
+ self.loop.process_input(keys)
+ self.loop.draw_screen()
+
+ # Convenience
+ def write(self, data):
+ self.terminal.write(data)
+
+ # Private
+ def _on_update_palette_entry(self, name, *attrspecs):
+ # copy the attribute to a dictionary containing the escape seqences
+ self._pal_escape[name] = self._attrspec_to_escape(
+ attrspecs[{16:0,1:1,88:2,256:3}[self.colors]])
+
+ def _attr_to_escape(self, a):
+ if a in self._pal_escape:
+ return self._pal_escape[a]
+ elif isinstance(a, urwid.AttrSpec):
+ return self._attrspec_to_escape(a)
+ # undefined attributes use default/default
+ # TODO: track and report these
+ return self._attrspec_to_escape(
+ urwid.AttrSpec('default','default'))
+
+ def _attrspec_to_escape(self, a):
+ """
+ Convert AttrSpec instance a to an escape sequence for the terminal
+
+ >>> s = Screen()
+ >>> s.set_terminal_properties(colors=256)
+ >>> a2e = s._attrspec_to_escape
+ >>> a2e(s.AttrSpec('brown', 'dark green'))
+ '\\x1b[0;33;42m'
+ >>> a2e(s.AttrSpec('#fea,underline', '#d0d'))
+ '\\x1b[0;38;5;229;4;48;5;164m'
+ """
+ if a.foreground_high:
+ fg = "38;5;%d" % a.foreground_number
+ elif a.foreground_basic:
+ if a.foreground_number > 7:
+ if self.bright_is_bold:
+ fg = "1;%d" % (a.foreground_number - 8 + 30)
+ else:
+ fg = "%d" % (a.foreground_number - 8 + 90)
+ else:
+ fg = "%d" % (a.foreground_number + 30)
+ else:
+ fg = "39"
+ st = "1;" * a.bold + "4;" * a.underline + "7;" * a.standout
+ if a.background_high:
+ bg = "48;5;%d" % a.background_number
+ elif a.background_basic:
+ if a.background_number > 7:
+ # this doesn't work on most terminals
+ bg = "%d" % (a.background_number - 8 + 100)
+ else:
+ bg = "%d" % (a.background_number + 40)
+ else:
+ bg = "49"
+ return urwid.escape.ESC + "[0;%s;%s%sm" % (fg, st, bg)
+
+
+class UrwidTerminalProtocol(TerminalProtocol):
+ """A terminal protocol that knows to proxy input and receive output from
+ Urwid.
+
+ This integrates with the TwistedScreen in a 1:1.
+ """
+
+ def __init__(self, urwid_mind):
+ self.urwid_mind = urwid_mind
+ self.width = 80
+ self.height = 24
+
+ def connectionMade(self):
+ self.urwid_mind.set_terminalProtocol(self)
+ self.terminalSize(self.height, self.width)
+
+ def terminalSize(self, height, width):
+ """Resize the terminal.
+ """
+ self.width = width
+ self.height = height
+ self.urwid_mind.ui.loop.screen_size = None
+ self.terminal.eraseDisplay()
+ self.urwid_mind.draw()
+
+ def dataReceived(self, data):
+ """Received data from the connection.
+
+ This overrides the default implementation which parses and passes to
+ the keyReceived method. We don't do that here, and must not do that so
+ that Urwid can get the right juice (which includes things like mouse
+ tracking).
+
+ Instead we just pass the data to the screen instance's dataReceived,
+ which handles the proxying to Urwid.
+ """
+ self.urwid_mind.push(data)
+
+ def _unhandled_input(self, input):
+ # evil
+ proceed = True
+ if hasattr(self.urwid_toplevel, 'app'):
+ proceed = self.urwid_toplevel.app.unhandled_input(self, input)
+ if not proceed:
+ return
+ if input == 'ctrl c':
+ self.terminal.loseConnection()
+
+
+class UrwidServerProtocol(ServerProtocol):
+ def dataReceived(self, data):
+ self.terminalProtocol.dataReceived(data)
+
+
+class UrwidUser(TerminalUser):
+ """A terminal user that remembers its avatarId
+
+ The default implementation doesn't
+ """
+ def __init__(self, original, avatarId):
+ TerminalUser.__init__(self, original, avatarId)
+ self.avatarId = avatarId
+
+
+class UrwidTerminalSession(TerminalSession):
+ """A terminal session that remembers the avatar and chained protocol for
+ later use. And implements a missing method for changed Window size.
+
+ Note: This implementation assumes that each SSH connection will only
+ request a single shell, which is not an entirely safe assumption, but is
+ by far the most common case.
+ """
+
+ def openShell(self, proto):
+ """Open a shell.
+ """
+ self.chained_protocol = UrwidServerProtocol(
+ UrwidTerminalProtocol, IUrwidMind(self.original))
+ TerminalSessionTransport(
+ proto, self.chained_protocol,
+ IConchUser(self.original),
+ self.height, self.width)
+
+ def windowChanged(self, (h, w, x, y)):
+ """Called when the window size has changed.
+ """
+ self.chained_protocol.terminalProtocol.terminalSize(h, w)
+
+
+class UrwidRealm(TerminalRealm):
+ """Custom terminal realm class-configured to use our custom Terminal User
+ Terminal Session.
+ """
+ def __init__(self, mind_factory):
+ self.mind_factory = mind_factory
+
+ def _getAvatar(self, avatarId):
+ comp = Componentized()
+ user = UrwidUser(comp, avatarId)
+ comp.setComponent(IConchUser, user)
+ sess = UrwidTerminalSession(comp)
+ comp.setComponent(ISession, sess)
+ mind = self.mind_factory(comp)
+ comp.setComponent(IUrwidMind, mind)
+ return user
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ for i in interfaces:
+ if i is IConchUser:
+ return (IConchUser,
+ self._getAvatar(avatarId),
+ lambda: None)
+ raise NotImplementedError()
+
+
+def create_server_factory(urwid_mind_factory):
+ """Convenience to create a server factory with a portal that uses a realm
+ serving a given urwid widget against checkers provided.
+ """
+ rlm = UrwidRealm(urwid_mind_factory)
+ ptl = Portal(rlm, urwid_mind_factory.cred_checkers)
+ return ConchFactory(ptl)
+
+
+def create_service(urwid_mind_factory, port, *args, **kw):
+ """Convenience to create a service for use in tac-ish situations.
+ """
+ f = create_server_factory(urwid_mind_factory)
+ return TCPServer(port, f, *args, **kw)
+
+
+def create_application(application_name, urwid_mind_factory,
+ port, *args, **kw):
+ """Convenience to create an application suitable for tac file
+ """
+ application = Application(application_name)
+ svc = create_service(urwid_mind_factory, 6022)
+ svc.setServiceParent(application)
+ return application
+
diff --git a/examples/twisted_serve_ssh.tac b/examples/twisted_serve_ssh.tac
new file mode 100644
index 0000000..80b8bcb
--- /dev/null
+++ b/examples/twisted_serve_ssh.tac
@@ -0,0 +1,41 @@
+# encoding: utf-8
+
+"""
+Example application for integrating serving a Urwid application remotely.
+
+Run this application with::
+
+ twistd -ny twisted_serve_ssh.tac
+
+Then in another terminal run::
+
+ ssh -p 6022 user@localhost
+
+(The password is 'pw' without the quotes.)
+
+Note: To use this in real life, you must use some real checker.
+"""
+
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+
+import urwid
+from twisted_serve_ssh import UrwidMind, UrwidUi, create_application
+
+
+class HelloUi(UrwidUi):
+ def create_urwid_toplevel(self):
+ txt = urwid.Edit('Hello World?\n ')
+ txt2 = urwid.Edit('Hello World?\n ')
+ fill = urwid.Filler(urwid.Pile([txt, txt2]), 'top')
+ return fill
+
+
+class HelloMind(UrwidMind):
+ ui_factory = HelloUi
+ cred_checkers = [InMemoryUsernamePasswordDatabaseDontUse(user='pw')]
+
+
+application = create_application('TXUrwid Demo', HelloMind, 6022)
+
+# vim: ft=python
+