diff options
author | Ian Ward <ian@excess.org> | 2011-11-29 00:20:13 -0500 |
---|---|---|
committer | Ian Ward <ian@excess.org> | 2011-11-29 00:20:13 -0500 |
commit | 001d8940531111f34297943d7e75b34ffa02c8d2 (patch) | |
tree | da7076988d9816e675de6731192cc57a2dda74fb /examples | |
parent | b16c4b3269339f657a97be383770c54730626d12 (diff) | |
download | urwid-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-x | examples/bigtext.py | 161 | ||||
-rwxr-xr-x | examples/browse.py | 388 | ||||
-rwxr-xr-x | examples/calc.py | 819 | ||||
-rwxr-xr-x | examples/dialog.py | 343 | ||||
-rwxr-xr-x | examples/edit.py | 255 | ||||
-rwxr-xr-x | examples/fib.py | 112 | ||||
-rwxr-xr-x | examples/graph.py | 356 | ||||
-rwxr-xr-x | examples/input_test.py | 102 | ||||
-rwxr-xr-x | examples/lcd_cf635.py | 291 | ||||
-rwxr-xr-x | examples/palette_test.py | 252 | ||||
-rwxr-xr-x | examples/pop_up.py | 41 | ||||
-rwxr-xr-x | examples/subproc.py | 30 | ||||
-rw-r--r-- | examples/subproc2.py | 8 | ||||
-rwxr-xr-x | examples/terminal.py | 56 | ||||
-rwxr-xr-x | examples/tour.py | 333 | ||||
-rwxr-xr-x | examples/treesample.py | 138 | ||||
-rw-r--r-- | examples/twisted_serve_ssh.py | 456 | ||||
-rw-r--r-- | examples/twisted_serve_ssh.tac | 41 |
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 + |