diff --git a/hb.py b/hb.py
new file mode 100644
index 0000000..5a5c1bc
--- /dev/null
+++ b/hb.py
@@ -0,0 +1,1254 @@
+#!/usr/local/bin/python
+# hb.py - A Home Budget Management Program
+# Copyright (c) 2001, 2002 TundraWare Inc.  All Rights Reserved.
+
+
+# If you don't already, learn how to use RCS for version control.
+# It can save your bacon if you fumble your keyboard during editing.
+# This is a nice way to embedd version information in your
+# program.  In the BANNER definition  below, you'll see  VERSION
+# getting split, so I can pick up just the version number.
+
+VERSION = "$Id: hb.py,v 1.89 2002/09/08 23:31:43 tundra Exp $"
+
+
+
+############################################################
+###          Variables User Might Change                 ###
+############################################################
+
+
+####################
+# Default budget file in "Name Burn Bal" format.  At a minimum,
+# this file must have a CASH account.  The budget must also have
+# a BALACCT account or the Balance feature will do nothing.
+####################
+
+from os import getenv
+
+BASEDIR  = getenv("HOME") + "/"
+BUDFIL   = "hb.txt"  
+
+BALACCT = "Savings"            # Account used to Balance Budgets
+CASH = "Cash"                  # Name of cash account
+
+####################
+# Default printer
+####################
+
+LPTW = "lpt1:"                    # Default print device for Win32
+LPTX = BASEDIR + "HB-Report.txt"  # Default print file for non-Win32
+
+####################
+# Screen Dimensions
+####################
+
+CWIDTH   = 12                 # Column width
+OHEIGHT  = 25                 # Output height
+OWIDTH   = 79                 # Output width
+
+
+#------------------- Nothing Below Here Should Need Changing ------------------#
+
+############################################################
+###                     Imports                          ###
+############################################################
+
+# If you use 'import libname', then every feature of that
+# library has to be referenced in its namespace as in
+# 'os.path.exists("file")'.  OTOH, if you import a single
+# part of a library like 'from string import atof', that
+# feature is in your namespace and you can refer to it
+# directly as in 'atof(x)' - not 'string.atof(x)'.
+
+import getopt
+import os
+import re
+from string import atof
+from time import ctime
+import sys
+
+
+############################################################
+###               Aliases & Redefinitions                ###
+############################################################
+
+# I don't like verbs like Python's 'print' that stick
+# spaces into my output without my permission.  There is
+# a way to suppress that with 'print', but I wanted to
+# demonstrate how object name assignment can help you alias
+# things for clarity and simplicity's sake.  This is also
+# a way to be able to reference something in your namespace
+# that is actually in another (its library's) namespace.
+
+tprint = sys.stdout.write
+
+
+############################################################
+###              Constants & Literals                    ###
+############################################################
+
+####################
+# Constants
+####################
+
+FALSE = 0 == 1                # Booleans
+TRUE = not FALSE
+
+APL = OWIDTH/(CWIDTH * 2)     # Accounts per line to display
+
+
+####################
+# Literals
+####################
+
+BKUEXT  = "~"                 # Extension string for backup file    
+CLSUNIX = "clear"             # Clear screen on unix
+CLSWIN  = "cls"               # Clear screen on windows
+
+OSWIN32 = 'nt'                # Operating system name - Win32
+OSUNIX  = 'posix'             # Operating system name - Unix 
+
+SEP = '-' * 79                # Separator bar
+
+# Regular Expression used to validate legitimate currency formats.
+# Will not allow negative values or leading zeros.  If a decimal
+# point is present, requires two digits following it.
+
+ISCURRENCY = r"^(([1-9]\d*(\.\d{2})?)|(\.\d{2}))$"
+
+
+############################################################
+###            Prompts, & Application Strings            ###
+###                                                      ###
+### These are separated out both for ease of maintenance ###
+### as well as making it easy to later change if we want ###
+###              to Internationalize the code.           ###
+###                                                      ###
+###   i.e., By "destringing" the code, there are no      ###
+###   literals embedded in the logic.  Any changes to    ###
+###    prompts (for content, language...) can be done    ###
+###     here without touching the code proper itself.    ###
+############################################################
+
+
+####################
+# Display Strings
+####################
+
+BANNER = ("HomeBudget - Version " + VERSION.split()[2] + " - " + \
+          ctime()).center(OWIDTH) + "\n" + \
+          "Copyright (c) 2001, 2002 TundraWare Inc.  All Rights Reserved.".center(OWIDTH) +\
+          "\n"
+
+ACASH =      "Available Cash: "
+TCASH =      "Total Cash: "
+TDEBIT =     "Total Debits: "
+
+
+##############################
+# Common Prompt & Delimiter Strings
+##############################
+
+DELIMITL = "<"                             # Delimiters for emphasized text
+DELIMITR = ">"
+PROMPT   = "? "                            # Command prompt
+TRAILER  = "...\n"                         # Ending for informational messages
+
+
+####################
+# Error Messages
+####################
+
+eCONTINUE  = "Press Enter To Continue."
+eNOCASH    = "Cannot Continue! - No Cash Account Defined."
+eNOLOAD    = "Cannot Load Budget: %s%s!"
+eNOUNDO    = "Nothing To Undo!"
+eUNKNOWN   = "Unknown Error Occurred."
+
+
+####################
+# Informational Messages
+####################
+
+iBALANCING = "Balancing" + TRAILER
+iBUDGET    = "Budget: "
+iCREDIT    = "Credit"
+iDEBIT     = "Debit"
+iEXITING   = "Exiting" + TRAILER 
+iLOADING   = "Loading Budget From %s%s" + TRAILER
+iMONTHLY   = "Doing Monthly Account Update" + TRAILER
+iPAY       = "Pay"
+iPRINTING  = "Printing To %s" + TRAILER
+iSAVING    = "Saving Budget" + TRAILER
+iTRANSFER  = "Transfer"
+iUNDO      = "UnDoing Last Transaction" + TRAILER
+iUSAGE     = "hb " + VERSION.split()[2] + \
+             "  Copyright (c), 2001, 2002 TundraWare Inc." + \
+             "  All Rights Reserved.\n" + \
+             "  usage:\n     hb [-d budget-directory] [-f budget-file]\n"
+iWHOLEAMT  = "(Enter For Entire Account Balance)"
+
+
+####################
+# Interactive User Prompts
+####################
+
+pACCOUNT   = "Account to %s" + PROMPT
+pAMOUNT    = "Amount to %s" + PROMPT
+pBALANCING = "Balancing Budget" + TRAILER
+pCOMMAND   = "Command" + PROMPT
+pLOADFROM  = "Load New Budget From " + DELIMITL + "%s" + DELIMITR 
+pPRINTTO   = "Print To " + DELIMITL + "%s" + DELIMITR
+pTXFROM    = iTRANSFER + " From"
+pTXTO      = iTRANSFER + " To"
+
+
+############################################################
+###                Command Options                       ###
+###                                                      ###
+### This is a tuple of tuples containing all the options ###
+### to be presented to the user.  At program startup,    ###
+### this is read and a Jump Table is created for the     ###
+### Command Interpreter to use.  Each option is          ###
+### described with a tuple in the following format:      ###
+###                                                      ###
+###  (  ("&ItemName", "Name of Main Handler Function"),  ###
+###     (("Func to get 1st argument" ", 1st prompt"),    ###
+###      ...                                             ###
+###      ("Func to get last argument", " last prompt")   ###
+###     )                                                ###
+###  )                                                   ###
+###                                                      ###
+###  The letter following the "&" is understood to be    ###
+###  the hotkey by which the user selects that feature,  ###
+###  so make sure they are all unique.                   ###
+###                                                      ###
+###   At runtime, the Command Interpreter will first     ###
+###   look for a command name (hotkey) it recognizes.    ###
+###   Then, it will serially execute each argument-      ###
+###   getting function, passing the corresponding        ###
+###   prompt when doing so.  When all the arguments      ###
+###   have been retrieved, the Command Interpreter       ###
+###   ships them off to the Main Handler Function        ###
+###   in the form of a list.                             ###
+###                                                      ###
+###   Each time the a Main Handler Function is called,   ###
+###   a record of that call and it's parameters is       ###
+###   stored on the Undo stack so that the user has      ###
+###   unlimited levels of undo.  It is the job           ###
+###   of each Main Handler Function to implement its     ###
+###   own correct Undo logic which is indicated by       ###
+###   a flag setting in the invocation.                  ###
+###   At the very least, a Main Handler should be        ###
+###   able to ignore an Undo, otherwise the function     ###
+###   will be executed when it is called AND when        ###
+###   it is Undone - usually not what you want.          ###
+###                                                      ###
+###  This approach makes is very easy to add new         ###
+###  features or change the prompt string (into, say,    ###
+###  another language) or assign a different hot key     ###
+###  for a given feature.                                ###
+###                                                      ###
+###  If you change this table, do a quick check and      ###
+###  see if any of the constant(s) in the next section   ###
+###  also need updating.                                 ###
+###                                                      ###
+############################################################
+
+
+Options  = (
+            (("&Balance", "Balance"),
+             (("BalAmount", ""),)       
+            ),                          
+
+            # Explanation of an entry:
+            # In the entry above, the feature is named "Balance".
+            # It's user hotkey will be 'B'.
+            # The Main Handler Function that supports it is
+            # Balance() -  At runtime, the Jump Table loader
+            # does an eval("Balance") and thereby loads the
+            # Jump Table with the correct object corresponding
+            # to Balance().
+            # Balance() only needs one argument when it is called
+            # and this is computed by BalAmount() which needs no
+            # prompt string.  Notice that there is a trailing
+            # comma in this tuple.  Without it, we would have
+            #              (("BalAmount", ""))
+            # and Python would not be able to tell that this is
+            # supposed to be a tuple of tuples.  The trailing
+            # comma tells Python to arrange the data structure
+            # as such.  In the entries below with multiple
+            # argument handlers, this is not, strictly speaking,
+            # required, but I put them in for consistency of style.
+
+                                        
+            (("&Credit", "Credit"),
+             (("GetAccount", iCREDIT), ("GetAmount", iCREDIT),)
+            ),
+                               
+            (("&Debit", "Debit"),
+             (("GetAccount", iDEBIT), ("GetDebitAmount", iDEBIT),)
+            ),
+
+            (("&Monthly", "Monthly"), ()),
+
+            (("P&rint", "Print"),
+             (("GetPrintDevice", pPRINTTO),)
+             ),
+
+            (("&Load", "Load"),
+             (("GetBudgetFile", pLOADFROM),)
+             ),
+
+            (("&Save", "Save"), ()),
+
+            (("&Transfer", "Transfer"),
+             (("GetAccount", pTXFROM), ("GetAccount", pTXTO,),
+              ("GetAmount", iTRANSFER ))
+            ),
+
+            (("Save+&Quit", "SaveExit"), ()),
+
+            (("&Pay", "Pay"),
+             (("GetAccount", iPAY), ("GetPayAmount", iPAY),)
+            ),
+
+            (("&Undo", "Undo"), ()),
+
+            (("&!Abandon", "Exit"), ())
+           )
+
+
+####################
+# Features That Have Dependencies Or Special Behavior
+#
+# The constant(s) below, are the hotkeys for program features
+# that have special dependencies in order to be enabled.
+# They *must* be identical to the hotkey selected for the
+# option in the table above (the character preceded by '&').
+# If the table above is changed, make sure to reflect any
+# relevant changes here as well.  While this is a little
+# clumsy, it is nessary in order to keep from having to embed
+# literal strings in the code itself.
+#
+# BuiltJT() - the routine that builds the Jump Table for the
+# Command Interpreter - needs this information to determine whether
+# these features should be enabled or disabled for a given
+# budget.  The information is kept here in this manner to keep
+# code below destringed.
+####################
+
+sBALANCE = "B"
+
+############################################################
+###        Global Variables & Data Structures            ###
+############################################################
+
+# Global Variables
+
+ALLDONE    = FALSE            # Controls exit from main execution loop
+DEBIT      = 0                # Running debit total
+INPUTABORT = FALSE            # Input aborted by user
+OSNAME     = ""               # Name of the operating system we're using
+
+# Global Data Structures
+
+Budget        = {}            # Dictionary of all account objects
+LoadStack     = []            # Place we preserve old budgets when loading new
+AccountNames  = []            # Account names discovered at load time
+JumpTable     = {}            # Command Interpreter dispatch
+UndoStack     = []            # The Undo stack
+
+
+#---------------------------Code Begins Here----------------------------------#
+
+############################################################
+#                     General Note                         #
+#                                                          #
+#   Python has no special block delimiters like {} in 'C'  #
+#   or DO-END in other languages.  It uses *INDENTATION*   #
+#   to indicate block scope.  Personally, I like this a    #
+#   lot - it's much harder to write ugly code in Python    #
+#   and you don't waste all those keystrokes ;)  Just      #
+#   pay plenty of attention to indentation or you will     #
+#   get strange results.                                   #
+############################################################
+
+
+############################################################
+###           Object Base Class Definitions              ###
+############################################################
+
+####################
+# Account Base Class           
+####################
+
+# Stuff declared in  __init__() is local to a given instance
+# of a class.  Stuff declared elsewhere in the class
+# is global to all instances of that class.  This can bite
+# you if you have variables declared outside of __init__()
+# because one instance of a class will be changing a
+# variable and all other instances will see the change
+# as well.  This can be very painful to debug if you
+# do this unintentionally. (DAMHIKT ;)
+
+class Account:
+    def __init__(self):
+        self.Burn  =  0           # Periodic burn rate
+        self.Bal   =  0           # Current balance
+
+    def Credit(self, amt):        # Credit the account
+        self.Bal += amt
+
+    def Debit (self, amt):        # Debit the account
+        self.Bal -= amt
+
+    def Monthly(self, Undo):      # Monthly update of account
+        if Undo:                  # Support Undoing
+            self.Bal -= self.Burn
+        else:
+            self.Bal += self.Burn
+
+####################
+# Jump Table Entry Base Class
+####################
+
+class JumpEntry:
+    def __init__(self):
+        self.Handler = None   # Function handling this action
+        self.Name    = ""     # Name of action
+        self.Args    = []     # List of argument function-prompt pairs
+    
+
+############################################################
+###               Main Handler Functions                 ###
+############################################################
+
+# There is one Main Handler Function for each feature
+# the program presents to the user.
+
+############################################################
+
+# Balance debits against savings account (if present)
+#
+# Excess available cash is put into BALACCT.  If there
+# is a cash deficit, BALACCT is reduced accordingly to
+# eliminate it.
+#
+# Notice the two forms of the arguments in this function
+# protype.  Args has no assigned value so it is *required*
+# to be passed at call time.  Undo is not required in the call
+# and defaults to FALSE if not passed by the caller.
+
+def Balance(Args, Undo=FALSE):
+    if Undo:
+        Budget[BALACCT].Debit(Args[0])
+    else:
+        # Balance the budget
+        tprint(pBALANCING)
+        Budget[BALACCT].Credit(Args[0])
+
+############################################################
+
+# Add to an account
+
+def Credit(Args, Undo=FALSE):
+    if Undo:
+        Budget[Args[0]].Debit(Args[1])
+    else:
+        Budget[Args[0]].Credit(Args[1])
+                
+############################################################
+
+# Decrement an account.  A 0 amount passed into this function
+# is understood to mean that the entire account balance should
+# be debited.
+
+def Debit(Args, Undo=FALSE):
+
+    if Undo:
+        Budget[Args[0]].Credit(Args[1])
+    else:
+        # Find out the amount being requested
+
+        amt = Args[1]
+
+        # If a 0 amount was passed it means we want to
+        # pay off the whole account.
+        # If amt is 0, it is logically FALSE
+
+        if not amt:        
+            amt = Budget[Args[0]].Bal     # What's the account balance?
+            ir = UndoStack.pop()          # Fix the Invocation Record
+            ir[1][1] = amt
+            UndoStack.append(ir)
+        Budget[Args[0]].Debit(amt)        # And do the math
+        
+
+############################################################
+
+# Exit the program
+#
+# As a matter of clean coding style, I don't like to
+# just exit from a called subroutine.  Instead, I change
+# state in the variable ALLDONE and let the main control
+# loop exit when control is passed back to it.
+#
+# Variables such as ALLDONE are globally *readable*
+# when they are instantiated in a scope higher than
+# the one in which it is referenced.  However, if you are
+# going to *write* them, you have explicitly tell Python that
+# the variable is global or it will create a new variable with
+# local scope when you first write the value.  That's what
+# 'global ALLDONE' does - it tells Python you want to
+# modify the global copy of ALLDONE when it is set to
+# TRUE at the end of the function, not create a new, local
+# variable called ALLDONE.
+
+def Exit(Args, Undo=FALSE):
+    tprint(iEXITING)
+    global ALLDONE
+    ALLDONE = TRUE
+
+############################################################
+
+# Load budget from disk and initialize account objects   
+
+def Load(Args, Undo=FALSE):
+    global BASEDIR
+    global AccountNames
+    global Budget
+    global JumpTable
+
+    # A little Armor - Make sure BASEDIR ends with slash
+    if BASEDIR[-1] != "/":
+        BASEDIR = BASEDIR + "/"
+
+    if Undo:
+        # Reestablish the program state as it was before the last Load()
+        AccountNames = LoadStack.pop()
+        JumpTable    = LoadStack.pop()
+        Budget       = LoadStack.pop()
+    else:
+        # If there is currently a budget, save for later undo
+        # We also save the current command Jump Table because some
+        # budgets having features others do not - the presence of
+        # a BALACCT account, for instance, enables the Balance feature.
+        # Finally, we have to save the AccountNames data structure,
+        # because it is a globally used *sorted* list of available accounts.
+        if Budget:
+            LoadStack.append(Budget)
+            LoadStack.append(JumpTable)
+            LoadStack.append(AccountNames)
+
+        # Initialize the global data structures for this budget
+        Budget = {}
+        AccountNames = {}
+            
+        # Open the new budget
+        # The 'iLOADING % Args[0]' business is Python's version
+        # of output formatting in the spirit of (and very similar to)
+        # printf() and sprintf() you find in 'C'.
+        
+        try:
+            tprint(iLOADING % (BASEDIR, Args[0]))
+            f = open(BASEDIR + Args[0])
+            accounts = f.read().splitlines()
+            f.close()
+        except IOError:
+           Error(eNOLOAD % (BASEDIR, Args[0]), Fatal=TRUE)
+
+        # Intitialize all account objects
+
+        for BudgetItem in accounts:
+            # Split the text line we read from disk
+            # into a list with three strings: [Name, Burn, Bal]
+            x = BudgetItem.split()
+
+            # Make sure dictionary index is always capitalized
+            index = x[0].capitalize()
+
+            # Instantiate a new Account object in the budget dictionary
+            Budget[index] = Account()
+            Budget[index].Burn = float(x[1])   # Convert the numeric strings
+            Budget[index].Bal  = float(x[2])   # to float and store
+
+        # Build and sort list of available account names
+        # So they'll display alphabetically
+        AccountNames = Budget.keys()
+        AccountNames.sort()
+
+        # Every budget must have a cash account
+        if not Budget.has_key(CASH):
+            Error(eNOCASH, Fatal=TRUE)  # We cannot recover from this error.
+
+        # Build the Command Interpreter Jump Table for this budget
+        BuildJT()
+
+############################################################
+
+# Update each account by its monthly increment
+#
+# The second entry on each line of our budget file is the
+# so-called monthly "Burn Rate" - It is the amount you have to
+# budget every month for that account.
+
+def Monthly(Args, Undo=FALSE):
+    tprint(iMONTHLY)
+    for x in AccountNames:
+        Budget[x].Monthly(Undo)
+
+############################################################
+
+# Pay off some or all of an account.  This means the account
+# is decremented by the desired amount and so it the CASH
+# account.
+#
+# If Pay() is passed 0 as the amount, it understands
+# this to mean paying off the entire amount in that account.
+# In this special case, the Undo logic of Pay() has to make sure
+# the amount paid off is properly preserved on the Undo stack for
+# potential Undoing later. This is necessary, because GetPayAmount()
+# sends a value of 0 if the user enters a blank line.
+#
+# When Pay() is actually dispatched out of the Jump Table, it's
+# Invocation Record (which is pushed on the UnDo stack), will have
+# 0 passed in the argument list. So, an Undo later, would do nothing 
+# more than "UnPay" 0 Dollars (or Euros, or Yen, or Pesos...) into
+# the appropriate account and CASH - this is not all that useful ;)
+#
+# So, in the event we get a 0 amount passed,  Pay() simply pops
+# its own Invocation Record off the Undo stack, loads that
+# record with the actual amount about to be paid, and pushes
+# the record back onto the Undo stack.  This assures that a
+# subsequent Undo can  "remember" how much to put back into the
+# account and into the CASH accounts.
+#
+# To see another way to handle this kind of situation, see how
+# the Balance() function is implemented without having to fiddle
+# with the UnDo stack.
+
+def Pay(Args, Undo=FALSE):
+
+    # Find out the amount being requested
+    amt = Args[1]
+
+    if Undo:
+        Budget[Args[0]].Credit(amt)
+        if Args[0] != CASH:
+            Budget[CASH].Credit(amt)
+    else:
+        # If a 0 amount was passed it means we want to
+        # pay off the whole account.
+
+        # If amt is 0, it is logically FALSE
+        if not amt:        
+            amt = Budget[Args[0]].Bal     # What's the account balance?
+            ir = UndoStack.pop()          # Fix the Invocation Record
+            ir[1][1] = amt
+            UndoStack.append(ir)
+        Budget[Args[0]].Debit(amt)        # And do the math
+        if Args[0] != CASH:               # If we are "paying" from CASH,
+            Budget[CASH].Debit(amt)       # don't debit the account twice
+
+############################################################
+
+# Print the budget to the file or device passed in Args
+
+def Print(Args, Undo=FALSE):
+    if not Undo:                    # Without this we'd print on do *and* undo
+        tprint(iPRINTING % Args[0])
+        f = open(Args[0], "w")
+        for x in ReCalc():
+            f.write(x)
+        f.write("\f")
+        f.close()
+
+############################################################
+
+# Save budget to disk
+
+def Save(Args, Undo=FALSE):
+    if not Undo:
+        tprint(iSAVING)
+        # All budget files are presumed to be relative to BASEDIR
+        BUDGET = BASEDIR + BUDFIL
+        BKU = BUDGET + BKUEXT
+
+        if os.path.exists(BKU):
+            os.remove(BKU)
+        if os.path.exists(BUDGET):
+            os.rename(BUDGET, BKU)
+        f = open(BUDGET, "w")
+        for x in AccountNames:
+            Burn = str(Budget[x].Burn)
+            Bal  = str(Budget[x].Bal)
+            f.write(x + (CWIDTH - len(x)) * " " +
+                    Burn + (CWIDTH - len(Burn)) * " " +
+                    Bal  + (CWIDTH - len(Bal))  * " " +
+                    "\n")
+        f.close()
+
+############################################################
+
+# Save budget and exit program
+#
+# We just call the other Main Handler Function that do
+# this for us directly (as opposed to having the Command
+# Interpreter do it).  But, we have to send an Args argument
+# along because that's how it's done when these functions
+# are called via the Jump Table.
+
+def SaveExit(Args, Undo=FALSE):
+    Save(Args=[])
+    Exit(Args=[])
+
+############################################################
+
+# Transfer funds between accounts
+
+def Transfer(Args, Undo=FALSE):
+    if Undo:
+        Budget[Args[0]].Credit(Args[2])
+        Budget[Args[1]].Debit(Args[2])
+    else:
+        Budget[Args[0]].Debit(Args[2])
+        Budget[Args[1]].Credit(Args[2])
+
+
+############################################################
+
+# Undo the last transaction
+#
+# This simply involves popping the last Invocation
+# Record (if there is one)  off the UnDo stack
+# and manually executing the Main Handler Function
+# found in that record with the Undo flag passed as TRUE.
+# (Once again, we're just doing manually what the Command
+# Interpreter did in the first place when the user first
+# did that function.)  It's up to each Main Handler Function
+# to do the right thing with UnDo.  If it does not care,
+# it can just ignore the UnDo calls and return quietly.
+
+def Undo(Args, Undo=FALSE):
+    # Whoops, the Undo Stack is empty - there's nothing to undo
+    # We'll tell the user, but it's a recoverable error
+    if not len(UndoStack):
+               Error(eNOUNDO, Fatal=FALSE)
+
+    # There's UnDoing to be done    
+    else:
+        tprint(iUNDO)
+        action = UndoStack.pop()
+        action[0](Args=action[1], Undo=TRUE)
+
+
+############################################################
+###                 Support Functions                    ###
+############################################################
+
+# These are one of two kinds of functions.  They are either
+# argument producing functions named in the Options table
+# above (so the Command Interpreter can get the required
+# arguments before calling a given Main Handler Function).
+# Or, they are internal routines the program needs to
+# do its job.
+
+############################################################
+
+# Figure out how out-of-balance the budget is
+#
+# We calculate this here as an argument producing function
+# invoked by the Command Interpreter, not in the Balance()
+# Main Handler Function (even though it could be done there).
+# By doing it here, the balance amount is pushed onto the
+# UnDo stack as part of invoking Balance() so we can later
+# 'un-Balance' if we want to.  If we just had the Command
+# Interpreter call Balance() directly, and computed the amount
+# there, we'd have to fiddle with the UnDo stack to keep
+# track of the amount.  This is exactly the scenario in Pay()
+# so you can see it done both ways.
+
+def BalAmount(str):
+    return Budget[CASH].Bal - DEBIT
+
+############################################################
+
+# Build the runtime Jump Table from the options list
+#
+# The Jump Table is a dictionary of JumpEntry objects -
+# one for each main feature in the program .  Each feature prompt has
+# a "hotkey" which is used to invoke it.  The relevant hotkey is
+# delimited for visual emphasis.
+#
+# The contents of the Jump Table is described with *strings* in the
+# Options tuple above (to separate string literals from the body
+# of the code itelf).  Here, we read in those strings and turn
+# them into an actual Jump Table.
+
+
+def BuildJT():
+    global JumpTable
+    # Initialize a new Jump Table
+    JumpTable = {}
+    
+    # The next line of code uses so-called 'tuple unpacking' - very handy
+    # shorthand for tearing data structures apart.
+    
+    for ((name, handler), argpairs) in Options:
+
+        # Instantiate a new JumpEntry
+        je = JumpEntry()
+
+        # Find the hotkey
+        y = name.split("&")
+        index = y[1][0].upper()
+
+        # Form the prompt
+        je.Name = y[0].lower() + DELIMITL + index + DELIMITR + y[1][1:].lower()
+
+        # Find the Main Handler Function
+        # The name of the Main Handler Function is stored in Options
+        # as a *string*.  This line evaluates the string and stores
+        # a reference to the actual handler object so we can
+        # execute it when we want to.  Kinda nifty, no?
+        #
+        # This indirectly illustrates an important central idea in Python:
+        # **** PYTHON VARIABLES ARE JUST NAMES WHICH REFER TO OBJECTS!**
+        # These variables themselves have no type - it is the objects
+        # that carry the type. Understanding this idea well is kinda
+        # at the heart of "getting" Python-   Most everything is
+        # an object *reference* not a copy of the object.
+
+        je.Handler = eval(handler)
+
+        # Now build the list of ArgFunction-prompt pairs
+        for z in argpairs:
+            p = []
+            p.append(eval(z[0]))
+            p.append(z[1])
+            je.Args.append(p)
+
+        # Now add this entry to the Jump Table
+
+        # Only install the Balance feature if the corresponding
+        # BALACCT is present in the current budget.
+
+        if index == sBALANCE.upper():
+
+            # list.count(val) returns how many instances of val
+            # exist in list.  An account should only appear 0 or 1
+            # times in AccountNames so we use the call as a logic
+            # test. (If you look and see how AccountNames gets built
+            # in the first place, you'll see that each entry has
+            # to be unique - its an index into a dictionary and these
+            # can never be duplicates.)
+
+            if AccountNames.count(BALACCT):
+                JumpTable[index] = je
+        else:
+            JumpTable[index] = je
+
+
+############################################################
+
+# Error display handler
+
+def Error(msg=eUNKNOWN, Fatal=TRUE):
+    tprint(msg + "\n")
+    if Fatal:
+        sys.exit()
+    else:
+        UserInput(eCONTINUE + TRAILER)
+
+
+############################################################
+
+# Clear screen
+#
+# A cheater's way of clearing the screen without resorting
+# to (non-portable) cursor addressing libaries like 'curses'.
+
+def cls():
+    if OSNAME == OSUNIX:
+        os.system(CLSUNIX)
+    elif OSNAME == OSWIN32:
+        os.system(CLSWIN)
+    else:
+        # OK, I have no idea how to do it on a Mac
+        # so we'll just shove newlines at it until
+        # nothing is left.  Anyone who recognizes this
+        # technique also knows what an ASR-33 is ;))
+        
+        tprint("\n" * OHEIGHT)
+    
+############################################################
+
+# Display current budget on screen
+#
+# All we do is call ReCalc which creates a list of lines
+# to display (or print - the Print() Main Handler Function
+# calls ReCalc() too) and then display them.
+
+def Display():
+
+    # Clear the screen
+    cls()
+    # Calculate and display the current budget
+    for x in ReCalc():
+        tprint(x)
+    # Display the command options toolbar
+    ToolBar()
+
+############################################################
+
+# Ask user for an account name
+#
+# The prompt to use is supplied as an input parameter.
+# The user  can respond with either the exact name or a
+# unique starting substring which identifies the account.
+# Return the name of the selected account.
+
+def GetAccount(Action):
+    DONE = FALSE
+    retval = ""
+
+    while not DONE:
+        # Ask user for account name
+        x = UserInput(pACCOUNT % Action, "").lower()
+
+        if  INPUTABORT:         # Did the user abort the action?
+            DONE = TRUE
+        else:
+            account = []
+            for y in AccountNames:             # See if it matches
+                if y.lower() == x:             # Exact match
+                    retval = y
+                    DONE = TRUE
+                    break
+                else:
+                    if y.lower().startswith(x): # beginning of any
+                        account.append(y)       # known account name
+
+            # If we got here it means there was no exact
+            # match.  We have to make sure we have a *unique*
+            # starting substring or we won't know which account
+            # the user is talking about.
+
+            if len(account) == 1:  
+                retval = account[0]
+                DONE = TRUE
+    return retval
+
+############################################################
+
+# Ask user for a currency amount
+#
+# The prompt to use is supplied as an input parameter.
+# Returns floating point value of amount specified.
+#
+# By default, AllowBlank is FALSE which means if the user
+# enters a blank line, they want to abort input.
+#
+# However, if AllowBlank is set TRUE, a blank line is allowed and
+# will force the return value of 0. This can be interpreted
+# by the calling function as it likes.  The Pay() function,
+# for example, takes this to mean paying off the whole amount
+# in the selected account.
+
+def GetAmount(Action, AllowBlank = FALSE):
+    global INPUTABORT
+    DONE = FALSE
+    while not DONE:
+
+        # If we are allowing blank lines,  don't set abort flag,
+        # but return a value of 0.
+        #
+        # The 'AllowBlank=AllowBlank' needs some explaining.
+        # The left side refers to the formal parameter that
+        # UserInput() is expecting from us.  The right side
+        # refers to the the parameter that was passed to *us*
+        # in this routine.  This is *really* crappy coding
+        # style but I kind of backed myself into it as I
+        # hacked away.  I was going to fix it, but decided
+        # to leave it in to illustrate this very point -
+        # That's my story, and I'm stickin' to it.
+        
+        y = UserInput(pAMOUNT % Action, AllowBlank=AllowBlank)
+
+        # If y is a blank line, then 'not y' will be TRUE
+        if AllowBlank and not y:  # We're allowing blank lines
+            y = "0"
+            DONE = TRUE
+        elif not INPUTABORT:      # They did not type a blank line
+            DONE = IsCurrency.match(y)
+        else:                     # User typed a blank - they want to abort input
+            y = "0"
+            DONE = TRUE
+
+    return atof(y)
+
+############################################################
+
+# Find out what budget file the user wants to load
+#
+# A blank line from the user here means that they want
+# to accept the current default value for a file name.
+
+def GetBudgetFile(Action):
+    global BUDFIL
+    while 1:
+        BUDFIL = UserInput(Action % BUDFIL + PROMPT, BUDFIL, AllowBlank=TRUE)
+        if INPUTABORT:
+            return ""
+        else:
+            if os.path.exists(BASEDIR + BUDFIL):
+                return BUDFIL
+            else:
+                Error(eNOLOAD % (BASEDIR, BUDFIL), Fatal=FALSE)
+
+############################################################
+
+# Get the amount to debit
+#
+# If the user just hits Enter, then debit the entire amount in that
+# account.  This function is nothing more than a  wrapper to
+# GetAmount() with the proper flags set.
+
+def GetDebitAmount(Action):
+    return GetAmount(Action + "\n" + iWHOLEAMT, AllowBlank = TRUE)
+
+############################################################
+
+# Get the amount to pay
+#
+# If the user just hits Enter, then pay the entire amount in that
+# account.  This function is nothing more than a  wrapper to
+# GetAmount() with the proper flags set.
+
+def GetPayAmount(Action):
+    return GetAmount(Action + "\n" + iWHOLEAMT, AllowBlank = TRUE)
+
+############################################################
+
+# Find out what printer or file the user want to print to
+#
+# A blank line from the user here means they want to
+# accept the current default printer or file name.
+# If the user enters something other than a device name,
+# we will 'print' to a file.
+#
+# Under WinDoze, the intial default is the first printer device,
+# LPT1:  Under anything else, the default is a report file.
+
+def GetPrintDevice(Action):
+    global LPTW
+    while 1:
+        LPTW = UserInput(Action % LPTW + PROMPT, LPTW, AllowBlank=TRUE)
+        if INPUTABORT:
+            return ""
+        else:
+            return LPTW
+
+############################################################
+
+# Calculate the current budget and return as a list of strings
+#
+# We can then display, print, or save these strings as desired.
+#
+# I'm fond of the Python idiom: 'string' * number' which returns
+# a new string of 'number' copies of 'string' concatenated together.
+#
+# Notice that Python strings are *objects* which have methods.
+# This is very handy.  You can do things like "mystring".center(80)
+# or "mystring".upper() or you can do the same to string variables
+# like 'stringvar.lower()'.  Mighty useful...
+
+def ReCalc():
+    global DEBIT
+    retval =[]
+    retval.append(BANNER)
+    retval.append((iBUDGET + BASEDIR + BUDFIL).center(OWIDTH))
+    retval.append("\n" * 5)
+    DEBIT = 0
+    apl = 0
+    for cat in AccountNames:
+        if cat.capitalize() != CASH:
+            if apl == APL:
+                retval.append("\n")
+                apl = 0
+            bal = Budget[cat].Bal
+            DEBIT += bal
+            bal = str(bal)
+            retval.append(cat + (CWIDTH-len(cat))*" " +
+                              bal + (CWIDTH-len(bal))*" ")
+            apl += 1
+    retval.append("\n" + SEP + "\n")
+    income = Budget[CASH].Bal
+    retval.append(TCASH + str(income) + 4 * " " +
+                  TDEBIT + str(DEBIT) + 5 * " " +
+                  ACASH + str(income-DEBIT) + 3 * "\n")
+    return retval
+
+############################################################
+
+# Display command toolbar
+
+def ToolBar():
+    # Display the command in alphabetic order
+    y = JumpTable.keys()
+    y.sort()
+
+    apl = 0
+    for x in y:
+        tprint(JumpTable[x].Name + " " * (CWIDTH - len(JumpTable[x].Name)))
+        apl += 1
+        if apl == APL * 2:
+            tprint("\n")
+            apl = 0
+    if apl != 0:
+            tprint("\n")
+
+
+############################################################
+
+# Primitive used by all routines asking for user input.
+#
+# By default, a blank line from the user means they
+# are aborting the input underway.  However, if AllowBlank
+# is passed as TRUE, this tells the routine that a blank line
+# is legitimate input and to pass it back as such.
+
+def UserInput(Prompt="", Default="", AllowBlank=FALSE):
+    global INPUTABORT
+    INPUTABORT = FALSE
+    x = ""
+
+    # Go talk to the user
+    # Watch for keyboard interrupts and EOFs.
+    try:
+        x = raw_input(Prompt)
+    except (EOFError, KeyboardInterrupt):
+        Exit(Args=[])
+
+    # We got some input
+    if x:
+        retval = x
+    # We got a blank line
+    else:
+        if not AllowBlank:
+            INPUTABORT = TRUE  # User can abort w/blank line
+        retval = Default
+    return retval
+
+
+############################################################
+###               hp.py program entry point              ###
+############################################################
+
+# Do some setup to prepare for program runtime.
+
+# First, we compile our regular expression string for
+# validating numeric input into an re object.  We can then
+# use it to see if a given string matches our requirements.
+# in the GetAmount() function, you'll see this done via:
+#
+#             IsCurrency.match(StringToCheck)
+
+IsCurrency = re.compile(ISCURRENCY)
+
+# Find out what operating system we're running on.  Some
+# functions like cls() and Print() care.
+
+OSNAME = os.name
+
+# Setup the default print device/file
+
+if OSNAME != OSWIN32:         # We're not printing on WinDoze
+    LPTW = LPTX               # so default to writing a file.
+
+
+# Command line argument processing
+
+try:
+    opts, args = getopt.getopt(sys.argv[1:], '-d:f:')
+
+except getopt.GetoptError:
+    tprint(iUSAGE)
+    sys.exit(2)
+    
+for opt, val in opts:
+    if opt == "-d":
+        BASEDIR = val
+    if opt == "-f":
+        BUDFIL = val
+
+
+# Load budget from disk.  Must give it an
+# Args parameter to satisfy the loading function.
+# Load() is setup to be table driven as a Main
+# Handler Function.  These are always invoked
+# with an 'Args' argument by the command interpeter.
+# We're just calling it here manually to get things rolling.
+#
+# Load also initializes the Command Interpreter Jump Table.
+# This is done at load time because some program
+# features are enabled/disabled on a per-budget
+# basis.  For example, the Balance function only works
+# if the account named in BALACCT is present.  If such an
+# account is not present, then Load() sees to it that the
+# Balance feature is not presented to the user.
+
+Load(Args=[BUDFIL])
+
+
+#############################
+# OK , now we're ready to actually start the program.
+# Run The Command Interpreter
+#############################
+
+# We'll do this until something decides we're done
+while not ALLDONE:
+
+    # Show the user the current state of the budget
+    Display()
+    
+    # Wait for them to tell us what they want
+    x = UserInput("\n" + pCOMMAND, "").upper()
+
+    # If it's something we recognize, we'll do it
+    if JumpTable.has_key(x):
+
+        # First, get the function to execute
+        func = JumpTable[x].Handler
+
+        # Then, go get the arguments.
+        # This is done by iterating through all
+        # arg function-prompt pairs until every
+        # function has been executed and its results
+        # appended to the args[] list.  Then we'll
+        # ship the args off the the Main Handler Function
+        # for this feature to finish the work.
+        
+        args = []
+        for y in JumpTable[x].Args:
+            if not INPUTABORT:           # Somwhere during input, the user
+                args.append(y[0](y[1]))  # might have decided to bag it
+
+        # Unless the user aborted input (thereby indicating their
+        # desire to get out of the selected function), go do it.
+
+        if not INPUTABORT:
+            # Save the function and its arguments for potential Undo later.
+            # Don't save the Undos themselves, however.
+            if JumpTable[x].Handler != Undo:
+                UndoStack.append([func, args])
+
+            # Actually execute the request
+            func(Args=args)
+
+
+# So, that's it - if you have comments or questions, ship 'em
+# off to: tundra@tundraware.com
+# Thank you for playing...