Add experimental urwid-based GUI
authorMichael Spang <mspang@uwaterloo.ca>
Mon, 24 Sep 2007 02:32:56 +0000 (22:32 -0400)
committerMichael Spang <mspang@uwaterloo.ca>
Tue, 25 Sep 2007 08:24:49 +0000 (04:24 -0400)
12 files changed:
bin/ceo
bin/ceo-old [new file with mode: 0755]
debian/rules
docs/TODO
pylib/csc/apps/urwid/__init__.py [new file with mode: 0644]
pylib/csc/apps/urwid/info.py [new file with mode: 0644]
pylib/csc/apps/urwid/main.py [new file with mode: 0644]
pylib/csc/apps/urwid/newmember.py [new file with mode: 0644]
pylib/csc/apps/urwid/renew.py [new file with mode: 0644]
pylib/csc/apps/urwid/search.py [new file with mode: 0644]
pylib/csc/apps/urwid/widgets.py [new file with mode: 0644]
pylib/csc/apps/urwid/window.py [new file with mode: 0644]

diff --git a/bin/ceo b/bin/ceo
index 8550f79..1034117 100755 (executable)
--- a/bin/ceo
+++ b/bin/ceo
@@ -15,7 +15,7 @@ os.environ['LESSSECURE'] = '1'
 os.environ['PATH'] = '/usr/sbin:/usr/bin:/sbin:/bin'
 
 for pathent in sys.path[:]:
-    if not pathent.find('/usr') == 0:
+    if not pathent.find('/usr') == 0 and not pathent.find('/var') == 0:
         sys.path.remove(pathent)
 
 euid = os.geteuid()
@@ -27,5 +27,5 @@ except OSError, e:
     print str(e)
     sys.exit(1)
 
-import csc.apps.legacy.main
-csc.apps.legacy.main.run()
+import csc.apps.urwid.main
+csc.apps.urwid.main.start()
diff --git a/bin/ceo-old b/bin/ceo-old
new file mode 100755 (executable)
index 0000000..8550f79
--- /dev/null
@@ -0,0 +1,31 @@
+#!/usr/bin/python2.4 --
+"""CEO SUID Python Wrapper Script"""
+import os, sys
+
+safe_environment = ['LOGNAME', 'USERNAME', 'USER', 'HOME', 'TERM', 'LANG'
+    'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY',
+    'LC_NUMERIC', 'LC_TIME', 'UID', 'GID', 'SSH_CONNECTION', 'SSH_AUTH_SOCK',
+    'SSH_CLIENT']
+
+for key in os.environ.keys():
+    if key not in safe_environment:
+        del os.environ[key]
+
+os.environ['LESSSECURE'] = '1'
+os.environ['PATH'] = '/usr/sbin:/usr/bin:/sbin:/bin'
+
+for pathent in sys.path[:]:
+    if not pathent.find('/usr') == 0:
+        sys.path.remove(pathent)
+
+euid = os.geteuid()
+egid = os.getegid()
+try:
+    os.setreuid(euid, euid)
+    os.setregid(egid, egid)
+except OSError, e:
+    print str(e)
+    sys.exit(1)
+
+import csc.apps.legacy.main
+csc.apps.legacy.main.run()
index 332259d..f4af3e4 100755 (executable)
@@ -7,6 +7,7 @@ build: build-stamp
 build-stamp:
        mkdir build
        $(CC) -DFULL_PATH='"/usr/lib/csc/ceo"' -o build/ceo misc/setuid-prog.c
+       $(CC) -DFULL_PATH='"/usr/lib/csc/ceo-old"' -o build/ceo-old misc/setuid-prog.c
        $(CC) -DFULL_PATH='"/usr/lib/csc/addhomedir"' -o build/addhomedir misc/setuid-prog.c
        $(CC) -DFULL_PATH='"/usr/lib/csc/ceoquery"' -o build/ceoquery misc/setuid-prog.c
        $(CC) -DFULL_PATH='"/usr/lib/csc/csc-chfn"' -o build/csc-chfn misc/setuid-prog.c
@@ -31,8 +32,8 @@ install: build
        dh_install etc/* etc/csc/
        dh_install sql/* usr/share/csc/
 
-       dh_install bin/ceo bin/addhomedir bin/ceoquery bin/csc-chsh bin/csc-chfn usr/lib/csc/
-       dh_install build/ceo build/addhomedir build/ceoquery build/csc-chsh build/csc-chfn usr/bin/
+       dh_install bin/ceo bin/ceo-old bin/addhomedir bin/ceoquery bin/csc-chsh bin/csc-chfn usr/lib/csc/
+       dh_install build/ceo build/ceo-old build/addhomedir build/ceoquery build/csc-chsh build/csc-chfn usr/bin/
        dh_install misc/csc.schema etc/ldap/schema/
        
 binary-arch: build install
index f634051..e146ba5 100644 (file)
--- a/docs/TODO
+++ b/docs/TODO
@@ -2,7 +2,6 @@ TODO:
 
 * Python bindings for libkadm5
 * Python bindings for quota?
-* New UI: urwid-based?
 * Logging via syslog
 * Try to recover and roll-back on error during account creation
 * Write manpages
diff --git a/pylib/csc/apps/urwid/__init__.py b/pylib/csc/apps/urwid/__init__.py
new file mode 100644 (file)
index 0000000..67bb77a
--- /dev/null
@@ -0,0 +1,3 @@
+"""
+Urwid User Interface
+"""
diff --git a/pylib/csc/apps/urwid/info.py b/pylib/csc/apps/urwid/info.py
new file mode 100644 (file)
index 0000000..8e0bd76
--- /dev/null
@@ -0,0 +1,39 @@
+import urwid
+
+from csc.apps.urwid.widgets import *
+from csc.apps.urwid.window import *
+
+from csc.adm import accounts, members
+from csc.common.excep import InvalidArgument
+
+class InfoPage(WizardPanel):
+    def init_widgets(self):
+        self.userid = urwid.Text("")
+        self.name = urwid.Text("")
+        self.terms = urwid.Text("")
+        self.program = urwid.Text("")
+
+        self.widgets = [
+            urwid.Text( "Member Details" ),
+            urwid.Divider(),
+            self.name,
+            self.userid,
+            self.program,
+            urwid.Divider(),
+            self.terms,
+        ]
+    def focusable(self):
+        return False
+    def activate(self):
+        member  = self.state.get('member', {})
+        name    = member.get('cn', [''])[0]
+        userid  = self.state['userid']
+        program = member.get('program', [''])[0]
+        terms   = member.get('term', [])
+
+        self.name.set_text("Name: %s" % name)
+        self.userid.set_text("User: %s" % userid)
+        self.program.set_text("Program: %s" % program)
+        self.terms.set_text("Terms: %s" % ", ".join(terms))
+    def check(self):
+        pop_window()
diff --git a/pylib/csc/apps/urwid/main.py b/pylib/csc/apps/urwid/main.py
new file mode 100644 (file)
index 0000000..a05f9f6
--- /dev/null
@@ -0,0 +1,122 @@
+import random, time
+import urwid, urwid.curses_display
+
+from csc.apps.urwid.widgets import *
+from csc.apps.urwid.window import *
+import csc.apps.urwid.newmember as newmember
+import csc.apps.urwid.renew as renew
+import csc.apps.urwid.info as info
+import csc.apps.urwid.search as search
+
+from csc.adm import accounts, members, terms
+from csc.common.excep import InvalidArgument
+
+ui = urwid.curses_display.Screen()
+
+ui.register_palette([
+    # name, foreground, background, mono
+    ('banner', 'light gray', 'default', None),
+    ('menu', 'light gray', 'default', 'bold'),
+    ('selected', 'black', 'light gray', 'bold'),
+])
+
+
+def program_name():
+    cwords = [ "CSC" ] * 20 + [ "Club" ] * 10 + [ "Campus" ] * 5 + \
+        [ "Communist", "Canadian", "Celestial", "Cryptographic", "Calum's",
+          "Canonical", "Capitalist", "Catastrophic", "Ceremonial", "Chaotic", "Civic",
+          "City", "County", "Caffeinated" ]
+    ewords = [ "Embellished", "Ergonomic", "Electric", "Eccentric", "European", "Economic",
+        "Evil", "Egotistical", "Elliptic", "Emasculating", "Embalming",
+        "Embryonic", "Emigrant", "Emissary's", "Emoting", "Employment", "Emulated",
+        "Enabling", "Enamoring", "Encapsulated", "Enchanted", "Encoded", "Encrypted",
+        "Encumbered", "Endemic", "Enhanced", "Enigmatic", "Enlightened", "Enormous",
+        "Enrollment", "Enshrouded", "Ephermal", "Epidemic", "Episodic", "Epsilon",
+        "Equitable", "Equestrian", "Equilateral", "Erroneous", "Erratic",
+        "Espresso", "Essential", "Estate", "Esteemed", "Eternal", "Ethical", "Eucalyptus",
+        "Euphemistic", "Envangelist", "Evasive", "Everyday", "Evidence", "Eviction", "Evildoer's",
+        "Evolution", "Exacerbation", "Exalted", "Examiner's", "Excise", "Exciting", "Exclusion",
+        "Exec", "Executioner's", "Exile", "Existential", "Expedient", "Expert", "Expletive",
+        "Exploiter's", "Explosive", "Exponential", "Exposing", "Extortion", "Extraction",
+        "Extraneous", "Extravaganza", "Extreme", "Extraterrestrial", "Extremist", "Eerie" ]
+    owords = [ "Office" ] * 50 + [ "Outhouse", "Outpost" ]
+
+    cword = random.choice(cwords)
+    eword = random.choice(ewords)
+    oword = random.choice(owords)
+
+    return "%s %s %s" % (cword, eword, oword)
+
+def menu_items(items):
+    return [ urwid.AttrWrap( ButtonText( cb, txt ), 'menu', 'selected') for (txt, cb) in items ]
+
+def main_menu():
+    menu = [
+        ("New Member", new_member),
+        ("Renew Membership", renew_member),
+        ("Display Member", display_member),
+        ("Search", search_members),
+        ("Exit", raise_abort),
+    ]
+
+    listbox = urwid.ListBox( menu_items( menu ) )
+    return listbox
+
+def push_wizard(name, pages, dimensions=(50, 10)):
+    state = {}
+    wiz = Wizard()
+    for page in pages:
+        wiz.add_panel( page(state) )
+    push_window( urwid.Filler( urwid.Padding(
+        urwid.LineBox(wiz), 'center', dimensions[0]),
+        'middle', dimensions[1] ), name )
+    
+def new_member(*args, **kwargs):
+    push_wizard("New Member", [
+        newmember.IntroPage,
+        newmember.InfoPage,
+        newmember.SignPage,
+        newmember.PassPage,
+        newmember.EndPage,
+    ])
+
+def renew_member(*args, **kwargs):
+    push_wizard("Renew Membership", [
+        renew.IntroPage,
+        renew.UserPage,
+        renew.TermPage,
+        renew.PayPage,
+        renew.EndPage,
+    ])
+
+def display_member(a):
+    push_wizard("Display Member", [
+        renew.UserPage,
+        info.InfoPage,
+    ], (60, 15))
+
+def search_members(a):
+    menu = [
+        ("Members by term", search_term),
+        ("Members by name", search_name),
+        ("Back", raise_back),
+    ]
+
+    listbox = urwid.ListBox( menu_items( menu ) )
+    push_window(listbox, "Search")
+
+def search_name(a):
+    push_wizard("By Name", [ search.NamePage ])
+
+def search_term(a):
+    push_wizard("By Term", [ search.TermPage ])
+
+def run():
+    push_window( main_menu(), program_name() )
+    event_loop( ui )
+
+def start():
+    ui.run_wrapper( run )
+
+if __name__ == '__main__':
+    start()
diff --git a/pylib/csc/apps/urwid/newmember.py b/pylib/csc/apps/urwid/newmember.py
new file mode 100644 (file)
index 0000000..994de80
--- /dev/null
@@ -0,0 +1,134 @@
+import urwid
+from csc.apps.urwid.widgets import *
+from csc.apps.urwid.window import *
+
+from csc.adm import accounts, members
+from csc.common.excep import InvalidArgument
+
+class IntroPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Joining the Computer Science Club" ),
+            urwid.Divider(),
+            urwid.Text( "CSC membership is $2.00 for one term. Please ensure "
+                        "the fee is deposited into the safe before continuing." ),
+        ]
+    def focusable(self):
+        return False
+
+class InfoPage(WizardPanel):
+    def init_widgets(self):
+        self.userid = WordEdit("UWdir ID: ")
+        self.name = SingleEdit("Full name: ")
+        self.program = SingleEdit("Program of Study: ")
+        self.widgets = [
+            urwid.Text( "Member Information - Please Check ID" ),
+            urwid.Divider(),
+            self.userid,
+            self.name,
+            self.program,
+        ]
+    def check(self):
+        self.state['userid'] = self.userid.get_edit_text()
+        self.state['name'] = self.name.get_edit_text()
+        self.state['program'] = self.program.get_edit_text()
+
+        if len( self.state['userid'] ) < 4:
+            self.focus_widget( self.userid )
+            set_status("Username is too short")
+            return True
+        elif len( self.state['name'] ) < 4:
+            self.focus_widget( self.name )
+            set_status("Name is too short")
+            return True
+        clear_status()
+
+class SignPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Machine Usage Policy" ),
+            urwid.Divider(),
+            urwid.Text( "Ensure the new member has signed the "
+                        "Machine Usage Policy. Accounts of users who have not "
+                        "signed will be suspended if discovered." ),
+        ]
+    def focusable(self):
+        return False
+
+class PassPage(WizardPanel):
+    def init_widgets(self):
+        self.password = PassEdit("Password: ")
+        self.pwcheck = PassEdit("Re-enter: ")
+        self.widgets = [
+            urwid.Text( "Member Password" ),
+            urwid.Divider(),
+            self.password,
+            self.pwcheck,
+        ]
+    def focus_widget(self, widget):
+        self.box.set_focus( self.widgets.index( widget ) )
+    def clear_password(self):
+        self.focus_widget( self.password )
+        self.password.set_edit_text("")
+        self.pwcheck.set_edit_text("")
+    def check(self):
+        self.state['password'] = self.password.get_edit_text()
+        pwcheck = self.pwcheck.get_edit_text()
+
+        if self.state['password'] != pwcheck:
+            self.clear_password()
+            set_status("Passwords do not match")
+            return True
+        elif len(self.state['password']) < 5:
+            self.clear_password()
+            set_status("Password is too short")
+            return True
+        clear_status()
+
+class EndPage(WizardPanel):
+    def init_widgets(self):
+        self.headtext = urwid.Text("")
+        self.midtext = urwid.Text("")
+        self.widgets = [
+            self.headtext,
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def check(self):
+        pop_window()
+    def activate(self):
+        try:
+            if not members.connected(): members.connect()
+            members.new( self.state['userid'], self.state['name'], self.state['program'] )
+            problem = None
+        except members.InvalidRealName:
+            problem = "Invalid real name"
+        except InvalidArgument, e:
+            if e.argname == 'uid' and e.explanation == 'duplicate uid':
+                problem = 'Duplicate userid'
+            else:
+                raise
+        if not problem:
+            try:
+                if not accounts.connected(): accounts.connect()
+                accounts.create_member( self.state['userid'], self.state['password'], self.state['name'] )
+            except accounts.NameConflict, e:
+                problem = str(e)
+            except accounts.NoAvailableIDs, e:
+                problem = str(e)
+            except accounts.InvalidArgument, e:
+                problem = str(e)
+            except accounts.LDAPException, e:
+                problem = str(e)
+            except accounts.KrbException, e:
+                problem = str(e)
+        if problem:
+            self.headtext.set_text("Failed to add member")
+            self.midtext.set_text("The error was: '%s'" % problem)
+        else:
+            self.headtext.set_text("Member Added")
+            self.midtext.set_text("Congratulations, %s has been added "
+                "successfully. Please run 'addhomedir %s'."
+                % (self.state['userid'], self.state['userid']))
diff --git a/pylib/csc/apps/urwid/renew.py b/pylib/csc/apps/urwid/renew.py
new file mode 100644 (file)
index 0000000..b0932ba
--- /dev/null
@@ -0,0 +1,118 @@
+import urwid
+
+from csc.apps.urwid.widgets import *
+from csc.apps.urwid.window import *
+
+from csc.adm import members, terms
+
+class IntroPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Renewing Membership" ),
+            urwid.Divider(),
+            urwid.Text( "CSC membership is $2.00 per term. You may pre-register "
+                        "for future terms if desired." )
+        ]
+    def focusable(self):
+        return False
+
+class UserPage(WizardPanel):
+    def init_widgets(self):
+        self.userid = WordEdit("Username: ")
+
+        self.widgets = [
+            urwid.Text( "Member Information" ),
+            urwid.Divider(),
+            self.userid,
+        ]
+    def check(self):
+        self.state['userid'] = self.userid.get_edit_text()
+        self.state['member'] = None
+        if self.state['userid']:
+            if not members.connected(): members.connect()
+            self.state['member'] = members.get(self.userid.get_edit_text())
+        if not self.state['member']:
+            set_status("Member not found")
+            self.focus_widget(self.userid)
+            return True
+
+class TermPage(WizardPanel):
+    def init_widgets(self):
+        self.start = SingleEdit("Start: ")
+        self.count = SingleIntEdit("Count: ")
+
+        self.widgets = [
+            urwid.Text( "Terms to Register" ),
+            urwid.Divider(),
+            self.start,
+            self.count,
+        ]
+    def activate(self):
+        if not self.start.get_edit_text():
+            old_terms = []
+            if 'term' in self.state['member']:
+                old_terms = self.state['member']['term']
+            self.start.set_edit_text( terms.next_unregistered( old_terms ) )
+            self.count.set_edit_text( "1" )
+    def check(self):
+        try:
+            self.state['terms'] = terms.interval( self.start.get_edit_text(), self.count.value() )
+        except e:
+            self.focus_widget( self.start )
+            set_status( "Invalid start term" )
+            return True
+        for term in self.state['terms']:
+            if members.registered( self.state['userid'], term):
+                self.focus_widget( self.start )
+                set_status( "Already registered for " + term )
+                return True
+        if len(self.state['terms']) == 0:
+            self.focus_widget(self.count)
+            set_status( "Registering for zero terms?" )
+            return True
+
+class PayPage(WizardPanel):
+    def init_widgets(self):
+        self.midtext = urwid.Text("")
+
+        self.widgets = [
+            urwid.Text("Membership Fee"),
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def activate(self):
+        regterms = self.state['terms']
+        plural = "term"
+        if len(self.state['terms']) > 1:
+            plural = "terms"
+        self.midtext.set_text("You are registering for %d %s, and owe the "
+                       "Computer Science Club $%d.00 in membership fees. "
+                       "Please deposit the money in the safe before "
+                       "continuing. " % ( len(regterms), plural, len(regterms * 2)))
+
+class EndPage(WizardPanel):
+    def init_widgets(self):
+        self.headtext = urwid.Text("")
+        self.midtext = urwid.Text("")
+
+        self.widgets = [
+            self.headtext,
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def activate(self):
+        try:
+            members.register( self.state['userid'], self.state['terms'] )
+            self.headtext.set_text("Registration Succeeded")
+            self.midtext.set_text("The member has been registered for the following "
+                             "terms: " + ", ".join(self.state['terms']) + ".")
+        except Exception, e:
+            self.headtext.set_text("Failed to Register")
+            self.midtext.set_text("You may refund any fees paid or retry."
+                             "The error was: '%s'" % e)
+    def check(self):
+        pop_window()
diff --git a/pylib/csc/apps/urwid/search.py b/pylib/csc/apps/urwid/search.py
new file mode 100644 (file)
index 0000000..c6eb8f6
--- /dev/null
@@ -0,0 +1,69 @@
+import urwid
+
+from csc.apps.urwid.widgets import *
+from csc.apps.urwid.window import *
+
+from csc.adm import accounts, members, terms
+from csc.common.excep import InvalidArgument
+
+class TermPage(WizardPanel):
+    def init_widgets(self):
+        self.term = SingleEdit("Term: ")
+
+        self.widgets = [
+            urwid.Text( "Terms Members" ),
+            urwid.Divider(),
+            self.term,
+        ]
+    def check(self):
+        if not members.connected(): members.connect()
+        try:
+            self.state['term'] = self.term.get_edit_text()
+            terms.parse( self.state['term'] )
+        except:
+            self.focus_widget( self.term )
+            set_status( "Invalid term" )
+            return True
+        mlist = members.list_term( self.state['term'] ).values()
+        pop_window()
+        member_list( mlist )
+
+class NamePage(WizardPanel):
+    def init_widgets(self):
+        self.name = SingleEdit("Name: ")
+
+        self.widgets = [
+            urwid.Text( "Members by Name" ),
+            urwid.Divider(),
+            self.name,
+        ]
+    def check(self):
+        if not members.connected(): members.connect()
+        self.state['name'] = self.name.get_edit_text()
+        if not self.state['name']:
+            self.focus_widget( self.term )
+            set_status( "Invalid name" )
+            return True
+        mlist = members.list_name( self.state['name'] ).values()
+        pop_window()
+        member_list( mlist )
+
+def member_list(mlist):
+    mlist = list(mlist)
+    mlist.sort( lambda x, y: cmp(x['uid'], y['uid']) )
+    buf = ''
+    for member in mlist:
+        if 'uid' in member:
+            uid = member['uid'][0]
+        else:
+            uid = None
+        if 'program' in member:
+            program = member['program'][0]
+        else:
+            program = None
+        attrs = ( uid, member['cn'][0], program )
+        buf += "%10s %30s\n%41s\n\n" % attrs
+    set_status("Press escape to return to the menu")
+    push_window(urwid.ListBox([urwid.Text(buf)]))
+
+
diff --git a/pylib/csc/apps/urwid/widgets.py b/pylib/csc/apps/urwid/widgets.py
new file mode 100644 (file)
index 0000000..2216240
--- /dev/null
@@ -0,0 +1,92 @@
+import urwid
+
+class ButtonText(urwid.Text):
+    def __init__(self, callback, *args, **kwargs):
+        self.callback = callback
+        urwid.Text.__init__(self, *args, **kwargs)
+    def selectable(self):
+        return True
+    def keypress(self, size, key):
+        if key == 'enter':
+            self.callback(self.get_text())
+        else:
+            return key
+
+class SingleEdit(urwid.Edit):
+    def keypress(self, size, key):
+        if key == 'enter':
+            return urwid.Edit.keypress(self, size, 'down')
+        else:
+            return urwid.Edit.keypress(self, size, key)
+
+class SingleIntEdit(urwid.IntEdit):
+    def keypress(self, size, key):
+        if key == 'enter':
+            return urwid.Edit.keypress(self, size, 'down')
+        else:
+            return urwid.Edit.keypress(self, size, key)
+
+class WordEdit(SingleEdit):
+    def valid_char(self, ch):
+        return urwid.Edit.valid_char(self, ch) and ch != ' '
+
+class PassEdit(SingleEdit):
+    def get_text(self):
+        text = urwid.Edit.get_text(self)
+        return (self.caption + " " * len(self.get_edit_text()), text[1])
+
+class Wizard(urwid.WidgetWrap):
+    def __init__(self):
+        self.selected = None
+        self.panels = []
+
+        self.panelwrap = urwid.WidgetWrap( urwid.SolidFill() )
+        self.back = urwid.Button("Back", self.back)
+        self.next = urwid.Button("Next", self.next)
+        self.buttons = urwid.Columns( [ self.back, self.next ], dividechars=3, focus_column=1 )
+        pad = urwid.Padding( self.buttons, ('fixed right', 2), 19 )
+        self.pile = urwid.Pile( [self.panelwrap, ('flow', pad)], 0 )
+        urwid.WidgetWrap.__init__(self, self.pile)
+
+    def add_panel(self, panel):
+        self.panels.append( panel )
+        if len(self.panels) == 1:
+            self.select(0)
+    
+    def select(self, panelno, set_focus=True):
+        if 0 <= panelno < len(self.panels):
+            self.selected = panelno
+            self.panelwrap.set_w( self.panels[panelno] )
+            self.panels[panelno].activate()
+
+            if set_focus:
+                if self.panels[panelno].focusable():
+                    self.pile.set_focus( 0 )
+                else:
+                    self.pile.set_focus( 1 )
+    
+    def next(self, *args, **kwargs):
+        if self.panels[self.selected].check():
+            self.select( self.selected )
+            return
+        self.select(self.selected + 1)
+    
+    def back(self, *args, **kwargs):
+        self.select(self.selected - 1, False)
+
+class WizardPanel(urwid.WidgetWrap):
+    def __init__(self, state):
+        self.state = state
+        self.init_widgets()
+        self.box = urwid.ListBox( urwid.SimpleListWalker( self.widgets ) )
+        urwid.WidgetWrap.__init__( self, self.box )
+    def init_widgets(self):
+        self.widgets = []
+    def focus_widget(self, widget):
+        self.box.set_focus( self.widgets.index( widget ) )
+    def focusable(self):
+        return True
+    def check(self):
+        return
+    def activate(self):
+        return
diff --git a/pylib/csc/apps/urwid/window.py b/pylib/csc/apps/urwid/window.py
new file mode 100644 (file)
index 0000000..370b6c1
--- /dev/null
@@ -0,0 +1,65 @@
+import urwid
+
+window_stack = []
+window_names = []
+
+header = urwid.Text( "" )
+footer = urwid.Text( "" )
+top = urwid.Frame( urwid.SolidFill(), header, footer )
+
+def push_window( frame, name=None ):
+    window_stack.append( frame )
+    window_names.append( name )
+    update_top()
+
+def pop_window():
+    if len(window_stack) == 1:
+       return False
+    window_stack.pop()
+    window_names.pop()
+    update_top()
+    return True
+
+def update_top():
+    names = [ n for n in window_names if n ]
+    header.set_text(" - ".join( names ) + "\n")
+    top.set_body( window_stack[-1] )
+
+def set_status(message):
+    footer.set_text(message)
+
+def clear_status():
+    footer.set_text("")
+
+class Abort(Exception):
+    pass
+
+class Back(Exception):
+    pass
+
+def raise_abort(*args, **kwargs):
+    raise Abort()
+
+def raise_back(*args, **kwarg):
+    raise Back()
+
+def event_loop(ui):
+    while True:
+        try:
+           cols, rows = ui.get_cols_rows()
+           canvas = top.render( (cols, rows), focus=True )
+           ui.draw_screen( (cols, rows), canvas )
+
+           keys = ui.get_input()
+           for k in keys:
+              if k == "esc":
+                 if not pop_window():
+                     break   
+              elif k == "window resize":
+                 (cols, rows) = ui.get_cols_rows()
+              else:
+                 top.keypress( (cols, rows), k )
+        except Back:
+            pop_window()
+        except (Abort, KeyboardInterrupt):
+            return