Newer
Older
twander / twander.py
#!/usr/bin/env python
# twander - Wander around the file system
# Copyright (c) 2002 TundraWare Inc.  All Rights Reserved.
# For Updates See:  http://www.tundraware.com/Software/twander

PROGNAME = "twander"
RCSID = "$Id: twander.py,v 2.57 2003/01/14 22:05:09 tundra Exp $"
VERSION = RCSID.split()[2]


#----------------------------------------------------------#
#                     Imports                              #
#----------------------------------------------------------#

import getopt
import mutex
import os
import re
from socket import getfqdn
from stat import *
import sys
import thread
import time

from Tkinter import *
from tkMessageBox import showerror, showwarning
from tkSimpleDialog import askstring

#####
# Imports conditional on OS
#####

# Set OS type - this allows us to trigger OS-specific code
# where needed.

OSNAME = os.name

if OSNAME == 'nt':
    try:
        from win32api import GetLogicalDriveStrings as GetDrives
    except:
        def GetDrives():
            return ""

if OSNAME == 'posix':
    import grp
    import pwd


#----------------------------------------------------------#
#          Variables User Might Change                     #
#----------------------------------------------------------#

#####
# Defaults
#####

#####
# Key Assignments
#####

# General Program Commands

CLRHIST   = '<Control-w>'                # Clear Command History
MOUSECTX  = '<ButtonRelease-3>'          # Pop-up Context Menu
MOUSEDIR  = '<Shift-ButtonRelease-3>'    # Pop-up Directory Menu
KEYPRESS  = '<KeyPress>'                 # Any keypress (for commands)
QUITPROG  = '<Control-q>'                # Quit the program
READCONF  = '<Control-r>'                # Re-read the configuration file
REFRESH   = '<Control-l>'                # Refresh screen
TOGDETAIL = '<Control-t>'                # Toggle detail view

# Directory Navigation

CHANGEDIR = '<Control-x>'                # Enter a new path
DIRHOME   = '<Control-h>'                # Goto $HOME
DIRBACK   = '<Control-b>'                # Goto previous directory
DIRROOT   = '<Control-j>'                # Goto root directory
DIRSTART  = '<Control-s>'                # Goto starting directory
DIRUP     = '<Control-u>'                # Go up one directory level
DRIVELIST = '<Control-k>'                # On Win32, display Drive List View if possible
MOUSEBACK = '<Control-Double-ButtonRelease-1>'  # Go back one directory with mouse
MOUSEUP   = '<Control-Double-ButtonRelease-3>'  # Go up one directory with mouse

# Selection Keys

SELALL    = '<Control-comma>'            # Select all items
SELINV    = '<Control-i>'                # Invert the current selection
SELNONE   = '<Control-period>'           # Unselect all items
SELNEXT   = '<Control-n>'                # Select next item
SELPREV   = '<Control-p>'                # Select previous item
SELEND    = '<Control-e>'                # Select bottom item
SELTOP    = '<Control-a>'                # Select top item

# Scrolling Commands

PGDN      = '<Control-v>'                # Move page down
PGUP      = '<Control-c>'                # Move page up
PGRT      = '<Control-g>'                # Move page right
PGLFT     = '<Control-f>'                # Move page left


# Execute Commands

RUNCMD    = '<Control-z>'                # Run arbitrary user command
SELKEY    = '<Return>'                   # Select item w/keyboard
MOUSESEL  = '<Double-ButtonRelease-1>'   # Select item w/mouse


# Name The Key/Mouse Assignments Which We Do Not Allow To Be Rebound In The Config File

NOREBIND =  ["MOUSECTX", "MOUSEDIR", "MOUSEBACK", "MOUSEUP", "MOUSESEL"]


#####
# System-Related Defaults
#####

# Default startup directory
STARTDIR = os.path.abspath("." + os.sep)

# Home director
HOME = os.getenv("HOME") or STARTDIR


#####
# Program Constants
#####

HEIGHT   = 25
WIDTH    = 90

#####
# Colors
#####

BCOLOR  = "black"
FCOLOR  = "green"


#####
# Fonts
#####

FNAME = "Courier"
FSZ   = 12
FWT   = "bold"


#------------------- Nothing Below Here Should Need Changing ------------------#


#----------------------------------------------------------#
#              Constants & Literals                        #
#----------------------------------------------------------#



#####
# Booleans
#####

# Don't need to define TRUE & FALSE - they are defined in the Tkinter module


#####
# Defaults
#####


AUTOREFRESH  = TRUE             # Automatically refresh the directory display
DEBUGLEVEL   = 0                # No debug output
WARN         = TRUE             # Warnings on

#####
# Constants
#####

# General Constants

KB            = 1024      # 1 KB constant
MB            = KB * KB   # 1 MB constant
GB            = MB * KB   # 1 GB constant
HOSTNAME      = getfqdn() # Full name of this host
POLLINT       = 20        # Interval (ms) the poll routine should run
REFRESHINT    = 3000      # Interval (ms) for automatic refresh

# Key & Button Event Masks

ShiftMask     = (1<<0)
LockMask      = (1<<1)
ControlMask   = (1<<2)
Mod1Mask      = (1<<3)
Mod2Mask      = (1<<4)
Mod3Mask      = (1<<5)
Mod4Mask      = (1<<6)
Mod5Mask      = (1<<7)
Button1Mask   = (1<<8)
Button2Mask   = (1<<9)
Button3Mask   = (1<<10)
Button4Mask   = (1<<11)
Button5Mask   = (1<<12)

# Alt Key is OS-dependent

if OSNAME == 'nt':
    AltMask = (1<<17)
else:
    AltMask = Mod1Mask


# Stat-Related Constants

# Permissions

ST_PERMIT     = ["---", "--x", "-w-", "-wx",
                 "r--", "r-x", "rw-", "rwx"]

# Special file type lookup

ST_SPECIALS   = {"01":"p", "02":"c", "04":"d", "06":"b",
                 "10":"-", "12":"l", "14":"s"}

# Size of each status display field including trailing space

ST_SZMODE     = 11
ST_SZNLINK    = 5
ST_SZUNAME    = 12
ST_SZGNAME    = 12
ST_SZLEN      = 12
ST_SZMTIME    = 18

ST_SZTOTAL    = ST_SZMODE + ST_SZNLINK + ST_SZUNAME + ST_SZGNAME + \
                ST_SZLEN + ST_SZMTIME


# String used to separate symlink entry from its real path

SYMPTR        = " -> "


#####
# General Literals
#####

COMMANDMENU   = 'Commands'      # Title for Command Menu button
DIRMENU       = 'Directories'   # Title for Directory Menu button
HISTMENU      = 'History'       # Title for History Menu button
PSEP          = os.sep          # Character separating path components
SHOWDRIVES    = '\\\\'          # Logical directory name for Win32 Drive Lists

#####
# GUI-Related Contsants
####

ROOTBORDER   =  1
MENUBORDER   =  2
MENUPADX     =  2
MENUOFFSET   = ROOTBORDER + MENUBORDER + MENUPADX

#####
# Configuration File Related Constants
#####

ASSIGN     = "="               # Assignment for variable definitions
CONF       = ""                # Config file user selected with -c option
COMMENT    = r"#"              # Comment introducer string
ENVVBL     = r'$'              # Symbol denoting an environment variable
MAXDIR     = 32                # Maximum number of directories to track
MAXHIST    = 32                # Maximum length of Command History
MAXNESTING = 32                # Maximum depth of nested variable definitions
QUOTECHAR  = '\"'              # Character to use when quoting Built-In Variables

# Variable Name Pattern Matching Stuff

DIRSC      = "DIRSC"                     # Directory Shortcut naming
reDIRSC    = r'^' + DIRSC + r'\d{1,2}$'  # Regex describing Directory Shortcut names
reVAR      = r"\[.+?\]"                  # Regex describing variable notation

# Create actual regex matching engines

REDIRSC = re.compile(reDIRSC)
REVAR   = re.compile(reVAR)

# Built-In Variables

DIR         = r'[DIR]'
DSELECTION  = r'[DSELECTION]'
DSELECTIONS = r'[DSELECTIONS]'
HASH        = r'[HASH]'
PROMPT      = r'[PROMPT:'
SELECTION   = r'[SELECTION]'
SELECTIONS  = r'[SELECTIONS]'


#----------------------------------------------------------#
#            Prompts, & Application Strings                #
#----------------------------------------------------------#


#####
# Error, Information, & Warning  Messages
#####

# Errors

eBADCFGLINE = "Bogus Configuration Entry In Line %s:\n\n%s"
eBADENVVBL  = "Environment Variable %s In Line %s Not Set:\n\n%s"
eBADROOT    = " %s Is Not A Directory"
eDIRRD      = "Cannot Open Directory : %s  ---  Check Permissions."
eDUPKEY     = "Found Duplicate Command Key '%s' In Line %s:\n\n%s"
eERROR      = "ERROR"
eINITDIRBAD = "Cannot Open Starting Directory : %s - Check Permissions - ABORTING!."
eOPEN       = "Cannot Open File: %s"
eREDEFVAR   = "Variable %s Redefined In Line %s:\n\n%s"
eTOOMANY    = "You Can Only Specify One Starting Directory."
eUNDEFVBL   = "Undefined Variable %s Referenced In Line %s:\n\n%s"
eVBLTOODEEP = "Variable Definition Nested Too Deeply At Line %s:\n\n%s"

# Information

iNOSTAT     = "Details Unavailable For This File ->"

# Prompts

pCHPATH     = "Change Path"
pENPATH     = "Enter New Path Desired:"
pRUNCMD     = "Run Command"
pENCMD      = "Enter Command To Run:"


# Warnings

wBADEXE     = "Could Not Execute Command:\n\n%s"
wBADVAL     = "Option Assignment Has Bad Righthand Side.\nIgnoring Line %s:\n\n%s"
wCMDKEY     = "Configuration File Entry For: \'%s\' Has No Command Key Defined."
wCONFOPEN   = "Cannot Open Configuration File:\n%s\n\n%s"
wDIRSCREDEF = "Directory Shortcut Defined More Than Once.\nIgnoring Line %s:\n\n%s"
wLINKBACK   = "%s Points Back To Own Directory"
wNOCMDS     = "Running With No Commands Defined!"
wNOREBIND   = "Cannot Rebind This Keyboard Or Mouse Button Combination.\nIgnoring Line %s:\n\n%s"
wWARN       = "WARNING"


#####
# Debug-Related Stuff
#####

# Debug Levels

DEBUGQUIT     = (1<<0)   # Dump debug info and quit program
DEBUGVARS     = (1<<1)   # Dump internal variables
DEBUGSYMS     = (1<<2)   # Dump symbol table
DEBUGCTBL     = (1<<3)   # Dump command table
DEBUGCMDS     = (1<<4)   # Dump command execution string
DEBUGKEYS     = (1<<5)   # Dump key bindings
DEBUGDIRS     = (1<<6)   # Dump directory stack contents as it changes
DEBUGHIST     = (1<<7)   # Dump contents of Command History stack after command execution

# Debug Strings

dCMD        = "<COMMAND>"
dCMDTBL     = "<COMMAND TABLE>"
dDIRSTK     = "<DIRECTORY STACK>"
dFUNCKEYS   = "<FUNCTION KEYS / DIRECTORY SHORTCUTS>"
dHEADER     = "twander Debug Dump Run On: %s\n"
dHIST       = "<COMMAND HISTORY STACK>"
dINTVAR     = "<INTERNAL VARIABLES>"
dKEYBINDS   = "<KEY BINDINGS>"
dNULL       = "None"
dSYMTBL     = "<SYMBOL TABLE>"

# List of internal program variables to dump during debug sessions

DebugVars = ["RCSID", "OSNAME", "HOSTNAME", "OPTIONS", "STARTDIR", "HOME", "CONF",
             "HEIGHT", "WIDTH", "BCOLOR", "FCOLOR", "FNAME", "FSZ", "FWT",
             "MAXDIR", "MAXHIST", "MAXNESTING", "AUTOREFRESH", "DEBUGLEVEL", "WARN", "PSEP",
             "QUOTECHAR", "POLLINT", "REFRESHINT"]


#####
# Usage Information
#####

uTable = [PROGNAME + " " + VERSION + " - Copyright 2002, TundraWare Inc., All Rights Reserved\n",
          "usage:  " + PROGNAME + " [-bcdfhnqrstvwxy] [startdir] where,\n",
          "          startdir  name of directory in which to begin (default: current dir)",
          "          -b color  background color (default: black)",
          "          -c file   name of configuration file (default: $HOME/." + PROGNAME +
                     " or PROGDIR/." + PROGNAME + ")",
          "          -d level  set debugging level (default: 0, debugging off)",
          "          -f color  foreground color (default: green)",
          "          -h        print this help information",
          "          -n name   name of font to use (default: courier)",
          "          -q        quiet mode - no warnings (default: warnings on)",
          "          -r        turn off automatic content refreshing (default: refresh on)",
          "          -s size   size of font to use (default: 12)",
          "          -t        no quoting when substituting Built-In Variables (default: quoting on)",
          "          -v        print detailed version information",
          "          -w wght   weight/style of font to use (default: bold)",
          "          -x width  window width (default: 90)",
          "          -y height window height (default: 25)",
          ]


#---------------------------Code Begins Here----------------------------------#


#----------------------------------------------------------#
#             General Support Functions                    #
#----------------------------------------------------------#


#####
# Print An Error Message
#####

def ErrMsg(emsg):

    showerror(PROGNAME + " " + VERSION + "    " + eERROR, emsg)

# End of 'ErrMsg()'


#####
# Build List Of Win32 Drives
#####

def GetWin32Drives():

    # Get Win32 drive string, split on nulls, and get
    # rid of any resulting null entries.

    return filter(lambda x : x, GetDrives().split('\x00'))

# End of 'GetWin32Drives()'


#####
# Convert A File Size Into Equivalent String With Scaling
# Files under 1 MB show actual length
# Files < 1 MB < 1 GB shown in KB
# Files 1 GB or greater, shown in MB
#####

def FileLength(flen):

    if flen >= GB:
        flen = str(flen/MB) + "m"
    elif flen >= MB:
        flen = str(flen/KB) + "k"
    else:
        flen = str(flen)

    return flen

# End of 'FileLength()'


#####
# Parse & Process The Configuraton File
# This is called once at program start time
# and again any time someone hits the READCONF key
# while the program is running.
#####

def ParseConfFile(event):
    global CONF, UI

    # Cleanout any old configuration data
    UI.CmdTable = {}
    UI.FuncKeys = ["", "", "", "", "", "",
                   "", "", "", "", "", ""]
    UI.SymTable = {}
    linenum = 0

    # Unbind all existing key bindings
    for x in UI.KeyBindings.keys():
        UI.DirList.unbind(UI.KeyBindings[x])

    # Initialize keyboard binding variables to their defaults
    # These may be overriden in the configuration file
    # parsing process.

    UI.KeyBindings = {"CLRHIST":CLRHIST,
                      "MOUSECTX":MOUSECTX,
                      "MOUSEDIR":MOUSEDIR,
                      "KEYPRESS":KEYPRESS,
                      "QUITPROG":QUITPROG,
                      "READCONF":READCONF,
                      "REFRESH":REFRESH,
                      "TOGDETAIL":TOGDETAIL,
                      "CHANGEDIR":CHANGEDIR,
                      "DIRHOME":DIRHOME,
                      "DIRBACK":DIRBACK,
                      "DIRROOT":DIRROOT,
                      "DIRSTART":DIRSTART,
                      "DIRUP":DIRUP,
                      "DRIVELIST":DRIVELIST,
                      "MOUSEBACK":MOUSEBACK,
                      "MOUSEUP":MOUSEUP,
                      "SELALL":SELALL,
                      "SELINV":SELINV,
                      "SELNONE":SELNONE,
                      "SELNEXT":SELNEXT,
                      "SELPREV":SELPREV,
                      "SELEND":SELEND,
                      "SELTOP":SELTOP,
                      "PGDN":PGDN,
                      "PGUP":PGUP,
                      "PGRT":PGRT,
                      "PGLFT":PGLFT,
                      "RUNCMD":RUNCMD,
                      "SELKEY":SELKEY,
                      "MOUSESEL":MOUSESEL
                      }

    # Initialize the command menu
    UI.CmdBtn.menu.delete(0,END)

    # And disable it
    UI.CmdBtn.config(state=DISABLED)
    
    # If user specified a config file, try that
    # Otherwise use HOME == either $HOME or ./

    if not CONF:
        CONF = os.path.join(HOME, "." + PROGNAME)
    try:
        cf = open(CONF)
    except:
        WrnMsg(wCONFOPEN % (CONF, wNOCMDS))
        return

    # Successful open of config file - Begin processing it
    
    # Process and massage the configuration file
    for line in cf.read().splitlines():
        linenum += 1

        # Parse this line
        if line:
            ParseLine(line, linenum)


    # Rebind all the handlers
    UI.BindAllHandlers()

    # Close the config file
    cf.close()

    # Set the Command Menu Contents, if any,
    # and enable the menu if it has entries.
    # If no commands are defined, warn the user.
    
    if UI.CmdBtn.menu.index(END):
        UI.CmdBtn['menu'] = UI.CmdBtn.menu
        UI.CmdBtn.configure(state=NORMAL)
    else:
        WrnMsg(wNOCMDS)

    # Any user-set options have now been read, set the GUI

    for i in (UI.CmdBtn, UI.DirBtn, UI.HistBtn):
        i.config(font=(FNAME, FSZ, "bold"))
        i.menu.config(font=("courier", FSZ, "bold"))

    UI.DirList.config(font=(FNAME, FSZ, FWT),
                      foreground=FCOLOR, background=BCOLOR,
                      height=HEIGHT, width=WIDTH)

    # Dump requested debug information

    if int(DEBUGLEVEL) & DEBUGKEYS:

        debuginfo = []

        # Command keys
        for key in UI.KeyBindings.keys():
            debuginfo.append(key + " " * (10-len(key)) + UI.KeyBindings[key])
        debuginfo.sort()
        PrintDebug(dKEYBINDS, debuginfo)

        debuginfo = []

        # Function Keys (Directory Shortcuts)
        for x in range(len(UI.FuncKeys)):
            key = "F" + str(x+1)
            debuginfo.append(key + " " * (10-len(key)) + UI.FuncKeys[x])            

        PrintDebug(dFUNCKEYS, debuginfo)

    if int(DEBUGLEVEL) & DEBUGSYMS:

        debuginfo = []
        for sym in UI.SymTable.keys():
            debuginfo.append(sym + " " * (16-len(sym)) + UI.SymTable[sym])

        debuginfo.sort()
        PrintDebug(dSYMTBL, debuginfo)

    if int(DEBUGLEVEL) & DEBUGCTBL:

        debuginfo = []
        for key in UI.CmdTable.keys():
            name = UI.CmdTable[key][0]
            cmd  = UI.CmdTable[key][1]
            debuginfo.append(key + " " + name + " " * (16-len(name)) + cmd)

        PrintDebug(dCMDTBL, debuginfo)        

    # Dump program variable during debug sessions
    if int(DEBUGLEVEL) & DEBUGVARS:

        debuginfo = []
        for v in DebugVars:
            debuginfo.append(v + " " * (12-len(v)) + (str(eval(v)) or dNULL))

        PrintDebug(dINTVAR, debuginfo)

# End of 'ParseConfFile()'


#####
# Parse A Line From A Configuration File
#####


def ParseLine(line, num):
    global UI
    # Get rid of trailing newline, if any
    if line[-1] == '\n':
        line = line[:-1]

    # Strip comments out
    
    idx = line.find(COMMENT)
    if idx > -1:
        line = line[:idx]

    # Split what's left into separate fields again
    fields = line.split()

    # Make a copy of the fields which is guaranteed to have at 
    # least two fields for use in the variable declaration tests.
    
    dummy = fields[:]
    dummy.append("")


    #####
    # Blank Lines - Ignore
    #####

    if len(fields) == 0:
        pass

    #####
    # Variable Definitions
    #####

    # A valid variable definition can
    # be 1, 2, or 3 fields:
    #
    #  x=y    - 1 field
    #  x= y   - 2 fields
    #  x =y
    #  x = y  - 3 fields
    #
    # But this is not legit:
    #
    #  =.......
    #
    # However, the assignment character
    # must always been in the 1st or second
    # field.  If it is a 3rd field, it is not
    # a variable definition, but a command definition.
    #
    # If the LHS is one of the Program Function Names
    # used in key binding, the statement is understood
    # to be a key rebinding, not a user variable definition.
    #
    # If the LHS is one of the Directory Shortcut variables,
    # the RHS is added to the Directory Menu and assigned
    # to the associated Function Key (1-12).
    #
    # Finally, the LHS cannot be one of the program
    # Built-In Variables - it is an error, for example,
    # to have something like:
    #
    #        DIR = string
    #
    # because "DIR" is a Built-In Variable name.
    #

    elif ((dummy[0].count(ASSIGN) + dummy[1].count(ASSIGN)) > 0) and (fields[0][0] != ASSIGN):
        
        assign = line.find(ASSIGN)
        name = line[:assign].strip()
        val=line[assign+1:].strip()

        # Error out on variable redefinitions of either
        # existing user-defined variables or built-ins
        
        if UI.SymTable.has_key(name) or \
           UI.BuiltIns.has_key('[' + name + ']') or \
           UI.BuiltIns.has_key('[' + name):
            
            ErrMsg(eREDEFVAR % (name, num, line))
            sys.exit(1)
            
        # Handle Directory Shortcut entries.
        
        if REDIRSC.match(name):

            # Get shortcut number
            sc = int(name.split(DIRSC)[1])

            # Process if 1-12
            
            if 0 < sc < 13:

                # Ignore attempts to redefine a shortcut within the config file

                if UI.FuncKeys[sc-1]:
                    WrnMsg(wDIRSCREDEF % (num, line))

                # Everything OK - process the entry
                else:
                    # Associate the directory with the correct function key
                    UI.FuncKeys[sc-1] = val

                    # Add to Directory Menu making sure it is PSEP-terminated

                    if val and (val[-1] != PSEP):
                        val += PSEP

                    if val:
                        UpdateDirMenu(val)

        # Process any option variables

        elif name in UI.OptionsBoolean:
            val = val.upper()
            if val == 'TRUE' or val == 'FALSE':
                globals()[name] = eval(val)               # !!! Cheater's way to get to global variables.
            else:
                WrnMsg(wBADVAL % (num, line))

        elif name in UI.OptionsNumeric:
            try:
                globals()[name] = int(val)
            except:
                WrnMsg(wBADVAL % (num, line))

        elif name in UI.OptionsString:
            try:
                globals()[name] = val
            except:
                WrnMsg(wBADVAL % (num, line))

        # Process other variable types.
        # Distinguish between internal program variables and
        # user-defined variables and act accordingly.  We inhibit
        # the rebinding of certain, special assignments, however.

        elif name in UI.KeyBindings.keys():
            if name in NOREBIND:
                WrnMsg(wNOREBIND % (num, line))
            else:
                UI.KeyBindings[name] = val
        else:
            UI.SymTable[name] = val

    #####
    # Command Definitions
    #####

    elif len(fields[0]) == 1:

        # Must have at least 3 fields for a valid command definition
        if len(fields) < 3:
            ErrMsg(eBADCFGLINE % (num, line))
            sys.exit(1)
        else:
            cmdkey = fields[0]
            cmdname = fields[1]
            cmd = " ".join(fields[2:])

            # Evaluate the command line, replacing
            # variables as needed

            doeval = TRUE
            depth  = 0

            while doeval:

                # Bound the number of times we can nest a definition
                # to prevent self-references which give infinite nesting depth

                depth += 1
                if (depth > MAXNESTING):
                    doeval = FALSE

                    # See if there are still unresolved variable references.
                    # If so, let the user know
                    
                    if REVAR.findall(cmd):
                        ErrMsg(eVBLTOODEEP % (num, cmd))
                        sys.exit(1)

                # Get a list of variable references
                vbls = REVAR.findall(cmd)

                # Throw away references to Built-In Variables - these are
                # processed at runtime and should be left alone here.

                # Note that we iterate over a *copy* of the variables
                # list, because we may be changing that list contents
                # as we go.  i.e., It is bogus to iterate over a list
                # which we are changing during the iteration.

                for x in vbls[:]:

                    # Ignore references to Built-In Variables here - They are
                    # processed at runtime.
                    
                    if UI.BuiltIns.has_key(x):
                        vbls.remove(x)

                    elif x.startswith(PROMPT):
                        vbls.remove(x)
                
                if vbls:
                    for x in vbls:
                        vbl = x[1:-1]

                        # Process ordinary variables
                        if UI.SymTable.has_key(vbl):
                            cmd = cmd.replace(x, UI.SymTable[vbl])

                        # Process environment variables.
                        # If an environment variable is referenced,
                        # but not defined, this is a fatal error
                        
                        elif vbl[0] == ENVVBL:
                            envvbl = os.getenv(vbl[1:])
                            if envvbl:
                                cmd = cmd.replace(x, envvbl)
                            else:
                                ErrMsg(eBADENVVBL % (x, num, line))
                                sys.exit(1)

                        # Process references to undefined variables
                        else:
                            ErrMsg(eUNDEFVBL % (x, num, line))
                            sys.exit(1)

                # No substitutions left to do
                else:
                    doeval = FALSE
                        
            # Add the command entry to the command table.
            # Prevent duplicate keys from being entered.
            
            if UI.CmdTable.has_key(cmdkey):
                ErrMsg(eDUPKEY % (cmdkey, num, line))
                sys.exit(1)
            else:
                UI.CmdTable[cmdkey] = [cmdname, cmd]
                UI.CmdBtn.menu.add_command(label=cmdname + " " * (15-len(cmdname)) + "(" + cmdkey + ")",
                                           command=lambda cmd=cmdkey: CommandMenuSelection(cmd))

    else:
        ErrMsg(eBADCFGLINE % (num, line))
        sys.exit(1)

# End of 'ParseLine()'


#####
# Print Debug Information On stdout
#####

def PrintDebug(title, content):

    print title + '\n'
    if content:
        for i in content:
            print i
    else:
        print dNULL
    print

# End of 'PrintDebug()'


#####
# Strip Trailing Path Separator
#####

def StripPSEP(s):

    if s and s[-1] == PSEP:
        return s[:-1]
    else:
        return s

# End of 'StripPSEP()'


#####
# Print Usage Information
#####

def Usage():

    for x in uTable:
        print x

# End of 'Usage()'


#####
# Print A Warning Message
#####

def WrnMsg(wmsg):
    if WARN:
        showwarning(PROGNAME + " " + VERSION + "    " + wWARN, wmsg)

# End of 'WrnMsg()'


#----------------------------------------------------------#
#                    GUI Definition                        #
#----------------------------------------------------------#



#####
# Enacapsulate the UI in a class
#####


class twanderUI:

    def __init__(self, root):

        # Setup Menubar frame
        
        self.mBar = Frame(root, relief=RAISED, borderwidth=MENUBORDER)
        self.mBar.pack(fill=X)
        
        # Setup the Command Menu

        self.CmdBtn = Menubutton(self.mBar, text=COMMANDMENU, underline=0, state=DISABLED)
        self.CmdBtn.menu = Menu(self.CmdBtn)
        self.CmdBtn.pack(side=LEFT, padx=MENUPADX)

        # Setup the Directory Menu

        self.DirBtn = Menubutton(self.mBar, text=DIRMENU, underline=0, state=DISABLED)
        self.DirBtn.menu = Menu(self.DirBtn)
        self.DirBtn.pack(side=LEFT, padx=MENUPADX)

        # Setup the History Menu

        self.HistBtn = Menubutton(self.mBar, text=HISTMENU, underline=0, state=DISABLED)
        self.HistBtn.menu = Menu(self.HistBtn)
        self.HistBtn.pack(side=LEFT, padx=MENUPADX)

        # Setup the Directory Listing and Scrollbars

        self.hSB = Scrollbar(root, orient=HORIZONTAL)
        self.vSB = Scrollbar(root, orient=VERTICAL)
        self.DirList = Listbox(root, selectmode=EXTENDED, exportselection=0,
                               xscrollcommand=self.hSB.set, yscrollcommand=self.vSB.set)

        # Make them visible by packing
        
        self.hSB.config(command=self.DirList.xview)
        self.hSB.pack(side=BOTTOM, fill=X)
        self.vSB.config(command=self.DirList.yview)
        self.vSB.pack(side=RIGHT, fill=Y)
        self.DirList.pack(side=LEFT, fill=BOTH, expand=1)

        # End of method 'twanderUI.__init__()'


    #####
    # Bind the relevant event handlers
    #####
        
    def BindAllHandlers(self):

        ###
        # General Program Commands
        ###

        # Bind handler to invoke Clear Command History
        self.DirList.bind(self.KeyBindings["CLRHIST"], ClearHistory)

        # Bind handler to invoke Context Menu
        self.DirList.bind(self.KeyBindings["MOUSECTX"], MouseClick)

        # Bind handler to invoke Directory Menu
        self.DirList.bind(self.KeyBindings["MOUSEDIR"], MouseClick)

        # Bind handler for individual keystrokes
        self.DirList.bind(self.KeyBindings["KEYPRESS"], KeystrokeHandler)

        # Bind handler for "Quit Program"
        self.DirList.bind(self.KeyBindings["QUITPROG"], KeyQuitProg)

        # Bind handler for "Read Config File"
        self.DirList.bind(self.KeyBindings["READCONF"], ParseConfFile)

        # Bind handler for "Refresh Screen" 
        self.DirList.bind(self.KeyBindings["REFRESH"], RefreshDirList)

        # Bind handler for "Toggle Detail" 
        self.DirList.bind(self.KeyBindings["TOGDETAIL"], KeyToggleDetail)

        ###
        # Directory Navigation
        ###

        # Bind handler for "Change Directory"
        self.DirList.bind(self.KeyBindings["CHANGEDIR"], KeyChangeDir)

        # Bind handler for "Home Dir"
        self.DirList.bind(self.KeyBindings["DIRHOME"], KeyHomeDir)

        # Bind handler for "Previous Dir"
        self.DirList.bind(self.KeyBindings["DIRBACK"], KeyBackDir)

        # Bind handler for "Root Dir"
        self.DirList.bind(self.KeyBindings["DIRROOT"], KeyRootDir)

        # Bind handler for "Starting Dir"
        self.DirList.bind(self.KeyBindings["DIRSTART"], KeyStartDir)

        # Bind handler for "Up Dir"
        self.DirList.bind(self.KeyBindings["DIRUP"], KeyUpDir)

        # Bind handler for "Display Drive View"
        self.DirList.bind(self.KeyBindings["DRIVELIST"], KeyDriveList)

        # Bind handler for "Mouse Back Dir"
        self.DirList.bind(self.KeyBindings["MOUSEBACK"], MouseDblClick)

        # Bind handler for "Mouse Up Dir"
        self.DirList.bind(self.KeyBindings["MOUSEUP"], MouseDblClick)

        ###
        # Selection Keys
        ###

        # Bind handler for "Select All"
        self.DirList.bind(self.KeyBindings["SELALL"], KeySelAll)

        # Bind handler for "Invert Current Selection"
        self.DirList.bind(self.KeyBindings["SELINV"], KeySelInv)

        # Bind handler for "Select No Items"
        self.DirList.bind(self.KeyBindings["SELNONE"], KeySelNone)

        # Bind handler for "Next Item"
        self.DirList.bind(self.KeyBindings["SELNEXT"], KeySelNext)

        # Bind handler for "Previous Item"
        self.DirList.bind(self.KeyBindings["SELPREV"], KeySelPrev)

        # Bind handler for "First Item"
        self.DirList.bind(self.KeyBindings["SELTOP"], KeySelTop)

        # Bind handler for "Last Item"
        self.DirList.bind(self.KeyBindings["SELEND"], KeySelEnd)

        ###
        # Scrolling Keys
        ###

        # Bind Handler for "Move Page Down
        self.DirList.bind(self.KeyBindings["PGDN"], KeyPageDown)

        # Bind Handler for "Move Page Up"
        self.DirList.bind(self.KeyBindings["PGUP"], KeyPageUp)

        # Bind Handler for "Move Page Right"
        self.DirList.bind(self.KeyBindings["PGRT"], KeyPageRight)

        # Bind Handler for "Move Page Up"
        self.DirList.bind(self.KeyBindings["PGLFT"], KeyPageLeft)

        ###
        # Execute commands
        ###

        # Bind handler for "Run Command"
        self.DirList.bind(self.KeyBindings["RUNCMD"], KeyRunCommand)

        # Bind handler for "Item Select"
        self.DirList.bind(self.KeyBindings["SELKEY"], DirListHandler)

        # Bind handler for "Mouse Select"
        self.DirList.bind(self.KeyBindings["MOUSESEL"], MouseDblClick)


        #####
        # Function Keys
        #####

        # Bind function keys to a common handler
        for x in range(len(self.FuncKeys)):
            self.DirList.bind('<F%d>' % (x+1), lambda event, index=x :FuncKeypress(index))
        

        # Give the listbox focus so it gets keystrokes
        self.DirList.focus()

    # End Of method 'twanderUI.BindAllHandlers()

        


    #####
    # Return tuple of all selected items
    #####

    def AllSelection(self):
        sellist = []

        # On Win32 Systems a Drive List View is always forced
        # to display with no details.  So we have to consider this
        # when figuring out where the selection name begins.

        if self.CurrentDir == SHOWDRIVES:
            nameindex = 0
        else:
            nameindex = self.NameFirst

        for entry in self.DirList.curselection():
            sellist.append(self.DirList.get(entry)[nameindex:].split(SYMPTR)[0])

        return sellist

    # End of method 'twanderUI.AllSelection()'
        

    #####
    # Return name of currently selected item
    #####

    def LastInSelection(self):

        # As above in AllSelection()
        
        if self.CurrentDir == SHOWDRIVES:
            nameindex = 0
        else:
            nameindex = self.NameFirst

        index = self.DirList.curselection()
        if index:
            return self.DirList.get(index[-1])[nameindex:].split(SYMPTR)[0]
        else:
            return ""

    # End of method 'twanderUI.LastInSelection()'


    #####
    # Support periodic polling to make sure widget stays
    # in sync with reality of current directory.
    #####

    def poll(self):

        # If new dir entered via mouse, force correct activation
        if self.MouseNewDir:
            self.DirList.activate(0)
            self.MouseNewDir = FALSE

        # See if its time to do a refresh

        if AUTOREFRESH:
            self.ElapsedTime += POLLINT
            if self.ElapsedTime >= REFRESHINT:
                RefreshDirList()
                self.ElapsedTime = 0

        # Setup next polling event
        self.DirList.after(POLLINT, self.poll)

    # End of method 'twanderUI.poll()'


    #####
    # Set Detailed View -> FALSE == No Details, TRUE == Details
    #####

    def SetDetailedView(self, details):

        self.DetailsOn = details
        
        # Tell system where actual file name begins
        # For both choices below, we have to set the UI.NameFirst
        # value.  This tells other handlers where in a given
        # selection the actual name of the file can be found.
        # This is necessary because we may be selecting a from
        # a detailed view and need to know where in that view
        # the file name lives.  It is not good enough to just
        # split() the selected string and use the [-1] entry because
        # we may be dealing with a file which has spaces in its
        # name.


        if self.DetailsOn:
            self.NameFirst = ST_SZTOTAL
        else:
            self.NameFirst = 0

    # End of method 'twanderUI.SetDetailedView()'


    #####
    # Set a particular selection, w/bounds checking
    # Note that 'selection' is passed as a string
    # but 'active' is passed as a number.
    #####

    def SetSelection(self, selection, active):

        # Clear all current selection(s)
        self.DirList.selection_clear(0, END)

        # Get current maximum index
        maxindex =  self.DirList.size() - 1

        # And bounds check/adjust

        if active > maxindex:
            active = maxindex

        # Set desired selected items, if any

        if selection:
            for entry in selection:
                self.DirList.select_set(entry)
            self.DirList.see(selection[-1])

        # Now set the active entry
        self.DirList.activate(active)

    # End of method 'twanderUI.SetSelection()'


    #####
    # Update title bar with most current information
    #####

    def UpdateTitle(self, mainwin):

        mainwin.title(PROGNAME + " " + VERSION + "       " + HOSTNAME + \
                      ":    "+ UI.CurrentDir + "        Total Files: " + \
                      str(self.DirList.size()) + \
                      "        Total Size: " + FileLength(self.TotalSize))

    # End of method 'twanderUI.UpdateTitle()'

# End of class definition, 'twanderUI'


#----------------------------------------------------------#
#                   Handler Functions                      #
#----------------------------------------------------------#

#---------------- Mouse Click Dispatchers -----------------#

# We intercept all mouse clicks (of interest) here so it
# is easy to uniquely handle the Control, Shift, Alt,
# variations of button presses.  We use Tkinter itself
# keep track of single- vs. double-clicks and hand-off
# the event to the corresponding Mouse Click Dispatcher.

#####
# Event Handler: Single Mouse Clicks
#####

def MouseClick(event):

    # Some Win32 systems always set Mod1 as well as Alt
    # Get rid of this bit, so it doesn't pester us.

    if OSNAME == 'nt':
        event.state = event.state & ~Mod1Mask

    if event.state == Button3Mask:                   # Button-3 / No Modifier
        x, y = UI.DirList.winfo_pointerxy()          # Position near mouse
        PopupMenu(UI.CmdBtn.menu, x, y)              # Display Command Menu

    elif event.state == (Button3Mask | ShiftMask):   # ShiftButton-3
        x, y = UI.DirList.winfo_pointerxy()          # Position near mouse
        PopupMenu(UI.DirBtn.menu, x, y)              # Display Directory Menu

    
# End Of 'MouseClick()


#####
# Event Handler: Mouse Double-Clicks
#####

def MouseDblClick(event):

    # Some Win32 systems always set Mod1 as well as Alt
    # Get rid of this bit, so it doesn't pester us.

    if OSNAME == 'nt':
        event.state = event.state & ~Mod1Mask

    if event.state == Button1Mask:                    # Double-Button-2 / No Modifier
        DirListHandler(event)                         # Execute selected item

    elif event.state == (Button1Mask | ControlMask):  # Control-DoubleButton-1
        KeyBackDir(event)                             # Move back one directory

    elif event.state == (Button3Mask | ControlMask):  # Control-DoubleButton-3
        KeyUpDir(event)                               # Move up one directory        

# End Of 'MouseDblClick()


#--------------- General Program Commands -----------------#

#####
# Event Handler: Clear Various Program Histories
#####

def ClearHistory(event):
    global UI
    
    UI.AllDirs = []
    UI.CmdHist = []
    UI.LastCmd = ""
    UI.LastDir = []
    UI.LastPathEntered = ""

    for x in [UI.HistBtn, UI.DirBtn]:
        x.menu.delete(0,END)
        x['menu'] = x.menu
        x.config(state=DISABLED)
    
# End of 'ClearHistory()'


#####
# Event Handler: Individual Keystrokes
#####

def KeystrokeHandler(event):

    # Some Win32 systems always set Mod1 as well as Alt
    # Get rid of this bit, so it doesn't pester us.

    if OSNAME == 'nt':
        event.state = event.state & ~Mod1Mask

    # Check for, and handle accelerator keys
    if event.state == AltMask:
        
        # Set menu button associated with accelerator

        # Command Menu
        if event.char == 'c':
            button = UI.CmdBtn

        # Directory Menu
        elif event.char == 'd':
            button = UI.DirBtn

        # History Menu
        elif event.char == 'h':
            button = UI.HistBtn

        # Unrecognized - Ignore
        else:
            return

        parts = button.winfo_geometry().split('+')     # Geometry returned as "WidthxHeight+X+Y"
        dims  = parts[0].split('x')

        x, y = int(parts[1]), int(parts[2])
        w, h = int(dims[0]), int(dims[1])

        x += UIroot.winfo_rootx()                      # This is relative to root window position
        y += UIroot.winfo_rooty()                      # So adjust accordingly

        # Display the requested menu
        PopupMenu(button.menu, x+MENUOFFSET, y+h)

        # Inhibit event from getting picked up by local accelerator key handlers
        return "break"

    #####
    # Otherwise, process single character command invocations.
    #####

    # We *only* want to handle simple single-character
    # keystrokes.  This means that there must be a character
    # present and that the only state modifier permitted
    # is the Shift key
    
    if not event.char or (event.state and event.state != 1):
        return

    # If the key pressed is a command key (i.e., it is in the table of
    # defined commands), get its associated string and execute the command.

    cmd  = UI.CmdTable.get(event.char, ["", "", ""])[1]
    name = UI.CmdTable.get(event.char, ["", "", ""])[0]


    # cmd == null means no matching command key -  do nothing
    # Otherwise, replace config tokens with actual file/dir names

    if cmd:
        # Replace runtime-determined tokens

        # First do any prompting required

        for x in range(cmd.count(PROMPT)):
            b = cmd.find(PROMPT)
            e = cmd.find("]", b)
            prompt = cmd[b + len(PROMPT):e]
            val = askstring(name, prompt)

            # Make sure our program gets focus back
            UI.DirList.focus()
 
            if val:
                cmd = cmd.replace(cmd[b:e+1], QUOTECHAR + val + QUOTECHAR)

            # Null input means the command is being aborted
            else:
                return
            
        # Now do files & directories
        # Strip trailing path separators in each case to
        # give the command author the maximum flexibility possible

        selection = StripPSEP(UI.LastInSelection())

        selections = ""
        dselections = ""
        for selected in UI.AllSelection():
            selected = StripPSEP(selected)
            dselections += QUOTECHAR + UI.CurrentDir + selected + QUOTECHAR + " "
            selections  += QUOTECHAR + selected + QUOTECHAR + " "

        cmd = cmd.replace(DIR, QUOTECHAR + StripPSEP(UI.CurrentDir) + QUOTECHAR)
        cmd = cmd.replace(DSELECTION, QUOTECHAR + UI.CurrentDir + selection + QUOTECHAR)
        cmd = cmd.replace(DSELECTIONS, dselections)
        cmd = cmd.replace(HASH, COMMENT)
        cmd = cmd.replace(SELECTION, QUOTECHAR + selection + QUOTECHAR)
        cmd = cmd.replace(SELECTIONS, selections)

        # Run the command
        ExecuteCommand(cmd)

    
# end of 'KeystrokeHandler()'
    

#####
# Event Handler: Program Quit
#####

def KeyQuitProg(event):
    sys.exit()

# End of 'KeyQuitProg()'


#####
# Event Handler: Toggle Detail View
#####

def KeyToggleDetail(event):

    UI.SetDetailedView(not UI.DetailsOn)
    RefreshDirList(event)

# End of 'KeyToggleDetail()'


#------------------- Directory Navigation -----------------#


#####
# Event Handler: Change Directory/Path
####

def KeyChangeDir(event):

    newpath = askstring(pCHPATH, pENPATH, initialvalue=UI.LastPathEntered)

    if newpath:
        if MAXDIR > 0:
            UI.LastPathEntered = newpath
        LoadDirList(newpath)
        KeySelTop(event)
    UI.DirList.focus()

# End of 'KeyChangeDir()'


#####
# Event Handler: Goto $HOME
#####

def KeyHomeDir(event):

    if HOME:
        LoadDirList(HOME)

# End of 'KeyHomeDir()'


#####
# Event Handler: Move To Previous Directory
#####

def KeyBackDir(event):

    # Move to last directory visited, if any - inhibit this from
    # being placed on the directory traversal stack
    if UI.LastDir:
        LoadDirList(UI.LastDir.pop(), save=FALSE)

    # No previous directory
    else:
        pass

# End of 'KeyBackDir()'


#####
# Event Handler: Go To Root Directory
#####

def KeyRootDir(event):
    global STARTDIR
    
    LoadDirList(PSEP)

# End of 'KeyRootDir()'


#####
# Event Handler: Go Back To Initial Directory
#####

def KeyStartDir(event):
    global STARTDIR
    
    LoadDirList(STARTDIR)

# End of 'KeyStartDir()'


#####
# Event Handler: Move Up One Directory
#####

def KeyUpDir(event):

    # Move up one directory level unless we're already at the root
    if UI.CurrentDir != os.path.abspath(PSEP):
        LoadDirList(UI.CurrentDir + "..")

    # Unless we're running on Win32 and we are able to do
    # a Drive List View

    elif OSNAME == 'nt' and GetWin32Drives():
        LoadDirList(SHOWDRIVES)

# End of 'KeyUpDir()'

#####
# Event Handler: Display Drive List View On Win32, If Possible
#####

def KeyDriveList(event):

    # This is only possible on Win32 and if there is a list of
    # drives available - i.e, If Win32All is installed

    if OSNAME == 'nt' and GetWin32Drives():
        LoadDirList(SHOWDRIVES)

# End of 'KeyDriveList()


#---------------------- Selection Keys ---------------------#


#####
# Event Handler: Select All Items
#####

def KeySelAll(event):

    # Unselect first item in case it was
    UI.DirList.selection_clear(0)
    
    # We never want to select the first item which is ".."
    UI.DirList.selection_set(1, END)

# End of 'KeySelAll()'


#####
# Event Handler: Invert Current Selection
#####

def KeySelInv(event):

    # List of current selections
    cs= UI.DirList.curselection()

    # Select everything
    UI.DirList.selection_set(0, END)

    # And unselect what was selected
    for v in cs:
        UI.DirList.selection_clear(v)

    # And we never select ".." this way
    UI.DirList.selection_clear(0)

# End of 'KeySelInv()'


#####
# Event Handler: Select Next Item
#####

def KeySelNext(event):

    next = UI.DirList.index(ACTIVE)

    # Don't increment if at end of list
    if (next < UI.DirList.size() - 1):
        next += 1

    UI.SetSelection((str(next),), next)

# End of 'KeySelNext()'


#####
# Event Handler: Select No Items
#####

def KeySelNone(event):
    UI.DirList.selection_clear(0, END)

# End of 'KeySelNone()'


#####
# Event Handler: Select Previous Item
#####

def KeySelPrev(event):
    prev = UI.DirList.index(ACTIVE)

    # Only decrement if > 0
    if prev:
        prev -= 1

    UI.SetSelection((str(prev),), prev)

# End of 'KeySelPrev()'


#####
# Event Handler: Select Last Item
#####

def KeySelEnd(event):

    # Get index of last item in listbox
    sz = UI.DirList.size() - 1

    # And setup to last item accordingly
    UI.SetSelection((str(sz),), sz)

# End of 'KeySelEnd()'


#####
# Event Handler: Select First Item
#####

def KeySelTop(event):
    UI.SetSelection(('0',),0)

# End of 'KeySelTop()'


#---------------------- Scrolling Keys ---------------------#


#####
# Event Handler: Move Down A Page
#####

def KeyPageDown(event):
    UI.DirList.yview_scroll(1, "pages")
    UI.DirList.activate("@0,0")

# End of 'KeyPageDown()'


#####
# Event Handler: Move Up A Page
#####

def KeyPageUp(event):
    UI.DirList.yview_scroll(-1, "pages")
    UI.DirList.activate("@0,0")

# End of 'KeyPageUp()'


#####
# Event Handler: Move Page Right
#####

def KeyPageRight(event):
    UI.DirList.xview_scroll(1, "pages")

# End of 'KeyPageRight()'


#####
# Event Handler: Move Page Left
#####

def KeyPageLeft(event):
    UI.DirList.xview_scroll(-1, "pages")

# End of 'KeyPageLeft()'


#---------------------- Execute Commands -------------------#



#####
# Event Handler: Run Command
####

def KeyRunCommand(event, initial=""):
    global UI

    # Prompt with passed initial edit string
    if initial:
        cmd = askstring(pRUNCMD, pENCMD, initialvalue=initial)

    # Prompt with last manually entered command
    elif UI.LastCmd:
        cmd = askstring(pRUNCMD, pENCMD, initialvalue=UI.LastCmd )

    # Prompt with no initial string
    else:
        cmd = askstring(pRUNCMD, pENCMD)

    # Execute command (if any) - Blank entry means do nothing/return
    if cmd:
        ExecuteCommand(cmd)

        # Save the command only if Command History is enable (MAXHIST > 0)
        # AND one of two conditions exist:
        #
        #    1) No initial string was provided (The user entered a command manually).
        #    2) An initial string was provided, but the user edited it.

        if (MAXHIST > 0) and ((not initial) or (cmd != initial)):
            UI.LastCmd = cmd

    UI.DirList.focus()

# End of 'KeyRunCommand()'


#####
# Event Handler: Process Current Selection
#####

def DirListHandler(event):
    global UI
    
    # Get current selection.  If none, just return, otherwise process
    selected =  UI.LastInSelection()
    if not selected:
        return
    
    # If we're on Win32 and we just selected ".." from the root of
    # a drive, request a display of the Drive List.  LoadDirList()
    # will check to see if there is anything in the Drive List and
    # do nothing if it is empty (which happens if the user has not
    # installed the Win32All package).

    if OSNAME=='nt' and \
               os.path.abspath(UI.CurrentDir) == os.path.abspath(UI.CurrentDir + selected):

        LoadDirList(SHOWDRIVES, save=TRUE)
        UI.MouseNewDir = TRUE

    # If selection is a directory, move there and list contents.

    elif os.path.isdir(os.path.join(UI.CurrentDir, selected)):

        # On Unix, don't follow links pointing back to themselves

        if OSNAME == 'posix' and os.path.samefile(UI.CurrentDir, UI.CurrentDir + selected):
            WrnMsg(wLINKBACK % (UI.CurrentDir + selected[:-1]))
            return

        # Build full path name
        selected = os.path.join(os.path.abspath(UI.CurrentDir), selected)

        # Convert ending ".." into canonical path
        if selected.endswith(".."):
            selected = PSEP.join(selected.split(PSEP)[:-2])
        
        # Need to end the directory string with a path
        # separator character so that subsequent navigation
        # will work when we hit the root directory of the file
        # system.  In the case of Unix, this means that
        # if we ended up at the root directory, we'll just
        # get "/".  In the case of Win32, we will get
        # "DRIVE:/".

        if selected[-1] != PSEP:
            selected += PSEP

        # Load UI with new directory
        LoadDirList(selected, save=TRUE)

        # Indicate that we entered a new directory this way.
        # This is a workaround for Tk madness.  When this
        # routine is invoked via the mouse, Tk sets the
        # activation *when this routine returns*.  That means
        # that the activation set at the end of LoadDirList
        # gets overidden.  We set this flag here so that
        # we can subsequently do the right thing in our
        # background polling loop.  Although this routine
        # can also be invoked via a keyboard selection,
        # we run things this way regardless since no harm
        # will be done in the latter case.

        UI.MouseNewDir = TRUE


    # No, a *file* was selected with a double-click
    # We know what to do on Win32 and Unix.  We ignore
    # the selection elsewhere.

    elif OSNAME == 'nt':
        ExecuteCommand(os.path.join(os.path.abspath(UI.CurrentDir), selected), UseStartDir=TRUE)

    elif OSNAME == 'posix':
        ExecuteCommand(os.path.join(os.path.abspath(UI.CurrentDir), selected))

# End of 'DirListHandler()'


#####
# Event Handler: Handler Function Keys
#####

def FuncKeypress(index):

    dir = UI.FuncKeys[index]
    if dir:
        LoadDirList(dir)

    # Inhibit other processing of this keystroke
    return "break"

# End of 'FuncKeypress()'


#-------------- Handler Utility Functions -----------------#

#####
# Event Handler: Popup Menus
#####

def PopupMenu(menu, x, y):

    # Popup requested menu at specified coordinates
    # but only if the menu has at least one entry.

    if menu.index(END):
        menu.tk_popup(x, y)

# End of 'PopupMenu()'


#####
# Execute A Command
#####

def ExecuteCommand(cmd, UseStartDir=FALSE):
    global UI

    # Just dump command if we're debugging

    if int(DEBUGLEVEL) & DEBUGCMDS:
        PrintDebug(dCMD, [cmd,])
    
    # Otherwise,actually execute the command
    else:
        
        # Run the command on Win32 using filename associations
        if UseStartDir:
            try:
                os.startfile(cmd)
            except:
                WrnMsg(wBADEXE % cmd)

        # Normal command execution for both Unix and Win32
        else:
            try:
                thread.start_new_thread(os.system, (cmd,))
            except:
                WrnMsg(wBADEXE % cmd)


    # If the user has Command History enabled (MAXHIST > 0),
    # save command history, maintaining max stack depth

    if (MAXHIST > 0):
        if len(UI.CmdHist) ==  MAXHIST:
            UI.CmdHist = UI.CmdHist[1:]
        UI.CmdHist.append(cmd)

        # Now update the History Menu accordingly

        UI.HistBtn.menu.delete(0,END)
        for entry in UI.CmdHist:
            UI.HistBtn.menu.add_command(label=entry, command=lambda cmd=entry: KeyRunCommand(None, initial=cmd))
        UI.HistBtn['menu'] = UI.HistBtn.menu

        # Enable the History Menu button if there is now content there
        if UI.HistBtn.menu.size():
            UI.HistBtn.config(state=NORMAL)
                
    # Dump Command History stack if requested
    
    if int(DEBUGLEVEL) & DEBUGHIST:
        PrintDebug(dHIST, UI.CmdHist)
        
# End of 'ExecuteCommand()


#####
# Load UI With Selected Directory
#####

def LoadDirList(newdir, save=TRUE):

    # Transform double forward-slashes into a single
    # forward-slash.  This keeps the Directory Stack
    # and Visited lists sane under Unix and prevents
    # Win32 from attempting to enter a Drive List View
    # when the user enters this string but Win32All has
    # not been loaded.

    if newdir == '//':
        newdir = '/'

    # Get path into canonical form unless we're trying
    # to display a Win32 Drive List
    
    if newdir != SHOWDRIVES:
        newdir = os.path.abspath(newdir)

        # Make sure newdir properly terminated
        if newdir[-1] != PSEP:
            newdir += PSEP

    # User has requested a Drive List View.  Make sure we're
    # running on Win32 and see the feature is available.  If
    # not available (which means Win32All is not installed),
    # just ignore and return, doing nothing.

    elif OSNAME != 'nt' or  not GetWin32Drives():
        return

    # Check right now to see if we can read
    # the directory.  If not, at least we
    # haven't screwed up the widget's current
    # contents or program state.

    try:
        contents = BuildDirList(newdir)
    except:
        # If CurrentDir set, we're still there: error w/ recovery
        if UI.CurrentDir:
            ErrMsg(eDIRRD % newdir)
            return

        # If not, we failed on the initial directory: error & abort
        else:
            ErrMsg(eINITDIRBAD % newdir)
            sys.exit(1)

    # Push last directory visited onto the visited stack

    # We do NOT save this to the stack if:
    #
    #    1) We've been told not to. - Passed when we're called (save=FALSE).
    #    2) If we're trying to move into the current directory again.
    #       This can happen either when the user does a manual directory
    #       change or if they press ".." while in root.  We don't
    #       actually want to save the directory until we *leave* it,
    #       otherwise we'll end up with a stack top and current
    #       directory which are the same, and we'll have to go
    #       back *twice* to move to the previous directory.
    
    # Are we trying to move back into same directory?
    if os.path.abspath(UI.CurrentDir) == os.path.abspath(newdir):
        save = FALSE

    # Now save if we're supposed to.
    if save and UI.CurrentDir:
        UI.LastDir.append(UI.CurrentDir)

    # Dump directory stack if debug requested it
    if int(DEBUGLEVEL) & DEBUGDIRS:
        PrintDebug(dDIRSTK, UI.LastDir)

    # And select new directory to visit
    UI.CurrentDir = newdir

    # Wait until we have exclusive access to the widget

    while not UI.DirListMutex.testandset():
        pass

    # Clear out the old contents
    UI.DirList.delete(0,END)

    # Load new directory contents into UI
    for x in contents:
        UI.DirList.insert(END, x)

    # Also move the program context to the new directory
    # for everything except a Drive List View.  In that case
    # the program context remains in the directory from
    # which the Drive List View was selected

    if newdir != SHOWDRIVES:
        os.chdir(newdir)

    # Keep list of all unique directories visited in the Directory Menu
    UpdateDirMenu(newdir)

    # And always force selection of first item there.
    # This guarantees a selection in the new
    # directory context, so subsequent commands
    # won't try to operate on an item selected in a
    # previous directory

    KeySelTop(None)

    # Update titlebar to reflect any changes
    UI.UpdateTitle(UIroot)

    #Release the lock
    UI.DirListMutex.unlock()

# End of 'LoadDirList():


#####
# Return Ordered List Of Directories & Files For Current Root
#####

def BuildDirList(currentdir):
    global UI
    
    dList, fList = [], []
    
    # Two possible cases have to be handled:
    # A normal directory read and a Drive List View
    # under Win32.

    # Normal directory reads
    if currentdir != SHOWDRIVES:

        # Walk the directory separate subdirs and files
        for file in os.listdir(currentdir):
            if os.path.isdir(os.path.join(currentdir, file)):
                dList.append(file + PSEP)
            else:
                fList.append(file)

        # On Win32, adjust file-name case to make sorting meaningful

        if OSNAME == 'nt':
            l = fList
            for x in range(len(l)):
                l[x] = l[x].lower()

        # Put each in sorted order

        dList.sort()
        fList.sort()

        # Entry to move up one directory is always first,
        # no matter what the sort.  This is necessary because
        # OSs like Win32 like to use '$' in file names  which
        # sorts before "."

        dList.insert(0, ".." + PSEP)

    # The user requested Drive List View. This is always displayed
    # without details, so we can return directly from here.
    
    else:
        UI.TotalSize = 0
        return GetWin32Drives()

    # Get details on directory contents

    all     = dList + fList
    detlist = []
    UI.TotalSize = 0
    for index in range(len(all)):

        # Make room for the new detailed entry
        detlist.append("")

        # Get file details from OS
        try:
            fn = os.path.join(currentdir, all[index])
            if fn[-1] == PSEP:
                fn =fn[:-1]
            stinfo =  os.lstat(fn)

        # 'lstat' failed - provide entry with some indication of this

        except:
            pad  = (UI.NameFirst - len(iNOSTAT) - 1) * " "
            detlist[index] = pad + iNOSTAT + " " + all[index]

            # Done with this file, but keep going
            continue
        
        # Mode - 1st get into octal string

        mode = stinfo[ST_MODE]
        modestr =  str("%06o" % mode)

        # Set the permission bits

        mode = ""
        for x in [-3, -2, -1]:
            mode +=  ST_PERMIT[int(modestr[x])]

        # Deal with the special permissions

        sp = int(modestr[-4])

        # Sticky Bit

        if 1 & sp:
            if mode[-1] == "x":
                mode = mode[:-1] + "t"
            else:
                mode = mode[:-1] + "T"

        # Setgid Bit

        if 2 & sp:
            if mode[-4] == "x":
                mode = mode[:-4] + "g" + mode[-3:]
            else:
                mode = mode[:-4] + "G" + mode[-3:]

        # Setuid Bit

        if 4 & sp:
            if mode[-7] == "x":
                mode = mode[:-7] + "g" + mode[-6:]
            else:
                mode = mode[:-7] + "G" + mode[-6:]

        # Pickup the special file types
        mode = ST_SPECIALS.get(modestr[0:2], "?") + mode

        detlist[index] += mode + (ST_SZMODE - len(mode)) * " "

        # Number of links to entry
        detlist[index] += str(stinfo[ST_NLINK]) + \
                          ( ST_SZNLINK - len(str(stinfo[ST_NLINK]))) * " "

        # Get first ST_SZxNAME chars of owner and group names on unix

        if OSNAME == 'posix':

            # Convert UID to name, if possible
            try:
                owner = pwd.getpwuid(stinfo[ST_UID])[0][:ST_SZUNAME-1]

            # No valid name associated with UID, so use number instead
            except:
                owner = str(stinfo[ST_UID])

            # Convert GID to name, if possible
            try:
                group = grp.getgrgid(stinfo[ST_GID])[0][:ST_SZGNAME-1]

            # No valid name associated with GID, so use number instead
            except:
                group = str(stinfo[ST_GID])


        # Handle Win32 systems
        elif OSNAME == 'nt':
            owner = 'win32user'
            group = 'win32group'

        # Default names for all other OSs
        else:
            owner = OSNAME + 'user'
            group = OSNAME + 'group'

        # Add them to the detail

        detlist[index] += owner + (ST_SZUNAME - len(owner)) * " "
        detlist[index] += group + (ST_SZUNAME - len(group)) * " "

        # Length

        flen = FileLength(stinfo[ST_SIZE])
        UI.TotalSize += stinfo[ST_SIZE]
        detlist[index] += flen + (ST_SZLEN - len(flen)) * " "

        # mtime

        # Get the whole time value
        ftime = time.ctime(stinfo[ST_MTIME]).split()[1:]

        # Pad single-digit dates with leading space

        if len(ftime[1]) == 1:
            ftime[1] = " " + ftime[1]

        # Drop the seconds
        ftime[-2] = ":".join(ftime[-2].split(":")[:-1])

        # Turn into a single string
        ftime = " ".join(ftime)

        detlist[index] += ftime + (ST_SZMTIME - len(ftime)) * " "

        # File name
        detlist[index] += all[index]

        #  Include symlink details as necessary
        if detlist[index][0] == 'l':

            # If the symlink points to a file
            # in the same directory, just show
            # the filename and not the whole path

            f = os.path.realpath(currentdir + all[index])
            r = os.path.split(f)
            if r[0] == currentdir[:-1]:
                f = r[1]

            detlist[index] += SYMPTR + f

    if UI.DetailsOn:
        return detlist
    else:
        return dList + fList

# End of  'BuildDirList()'


#####
# Refresh contents of directory listing to stay in sync with reality
#####

def RefreshDirList(*args):

    # Wait until we have exclusive access to the widget

    while not UI.DirListMutex.testandset():
        pass

    # Get current selection and active

    sellist  = UI.DirList.curselection()
    active = UI.DirList.index(ACTIVE)

    # Save current scroll positions

    xs = UI.hSB.get()
    ys = UI.vSB.get()

    # Clean out old listbox contents
    UI.DirList.delete(0,END)

    # Save the new directory information
    UI.DirList.insert(0, *BuildDirList(UI.CurrentDir))

    # Restore selection(s)
    UI.SetSelection(sellist, active)

    # Restore scroll positions

    UI.DirList.xview(MOVETO, xs[0])
    UI.DirList.yview(MOVETO, ys[0])

    # Update titlebar to reflect any changes
    UI.UpdateTitle(UIroot)

    # Release the mutex
    UI.DirListMutex.unlock()

# End of 'RefreshDirList()


#---------------- Menu Support Functions ------------------#

#####
# Handle Command Menu Selections
#####

def CommandMenuSelection(cmdkey):

    class event:
        pass

    event.char = cmdkey
    event.state = 0
    KeystrokeHandler(event)    

# End Of 'CommandMenuSelection()'


#####
# Add An Entry To The List Of All Directories Visited
# Do this only if it is not already on the list and
# observe the MAXDIR variable to keep list length bounded.
#####

def UpdateDirMenu(newdir):
    global UI
    
    # Win32 collapses case so that 'x' and 'X' refer to the same
    # directory.  We want to preserve this in the user's display
    # but we have to collapse case for the purposes of doing our
    # checks below otherwise the same directory with different
    # capitalization (if the user manually enteres it that way)
    # can appear twice in the Directory Menu

    addentry = FALSE
    
    
    if OSNAME == 'nt':

        # First make a case-collapsed copy of the existing list

        dlc = []
        for d in UI.AllDirs:
            dlc.append(d.lower())

        # Now see if our new entry is already there

        if newdir.lower() not in dlc:
            addentry = TRUE

    elif newdir not in UI.AllDirs:
        addentry = TRUE

    # Now add the entry if we decided it was necessary. observing MAXDIR value.

    if addentry:
        UI.AllDirs.append(newdir)
        if len(UI.AllDirs) > MAXDIR:
            UI.AllDirs = UI.AllDirs[1:]

    # If there's anything to display and we've actually changed something, update the menu

    if len(UI.AllDirs) and addentry:
        UI.AllDirs.sort()
        UI.DirBtn.menu.delete(0,END)
        for dir in UI.AllDirs:
            UI.DirBtn.menu.add_command(label=dir, command=lambda dir=dir: LoadDirList(dir))
        UI.DirBtn['menu'] = UI.DirBtn.menu

        # Directory Menu now has content, enable it
        UI.DirBtn.config(state=NORMAL)


# End of 'UpdateDirMenu()'


#----------------------------------------------------------#
#                  Program Entry Point                     #
#----------------------------------------------------------#

#####
# Create an instance of the UI
#####

UIroot = Tk()
UI = twanderUI(UIroot)

# Make the Tk window the topmost in the Z stack.
# 'Gotta do this or Win32 will not return input
# focus to our program after a startup warning
# display.

UIroot.tkraise()

#####
# Setup global UI variables
#####

# Setup Built-In Variables
UI.BuiltIns = {DIR:"", DSELECTION:"", DSELECTIONS:"", HASH:"",
               PROMPT:"", SELECTION:"", SELECTIONS:""}

# Options which can be set in the configuration file

UI.OptionsBoolean = ["AUTOREFRESH", "WARN"]
UI.OptionsNumeric = ["DEBUGLEVEL", "FSZ", "HEIGHT", "MAXDIR", "MAXHIST", "MAXNESTING", "REFRESHINT", "WIDTH"]
UI.OptionsString  = ["BCOLOR", "FCOLOR", "FNAME", "FWT", "QUOTECHAR", "STARTDIR"]

# Prepare storage for key bindings
UI.KeyBindings = {}

# Initialize list of all directories visited
UI.AllDirs    = []

# Initialize directory stack
UI.LastDir    = []

# Initialize storage for last manually entered directory path
UI.LastPathEntered = ""

# And current location
UI.CurrentDir = ""

# Initialize Command History data structures
ClearHistory(None)


#####
# Command line processing - Options are set with the
# following priority scheme (lowest to highest):
#
#    1) Defaults coded into the program
#    2) Options set in the configuration file
#    3) Options set in the environment variable
#    4) Options set on the command line
#
#####

# Concatenate any environment variable with the
# command line so the command line takes precedence.

OPTIONS = sys.argv[1:]
envopt = os.getenv(PROGNAME.upper())
if envopt:
    OPTIONS = envopt.split() + OPTIONS

try:
    opts, args = getopt.getopt(OPTIONS, '-b:c:d:f:hn:qrs:tvw:x:y:')
except getopt.GetoptError:
    Usage()
    sys.exit(1)

# If the user wants help or version information, do this first
# so we don't bother with other options processing

for opt, val in opts:
    if opt == "-h":
        Usage()
        sys.exit(0)
    if opt == "-v":
        print RCSID
        sys.exit(0)

# Read the debug level before anything else so that any
# subsequent operations will observe this.

for opt, val in opts:
    if opt == "-d":
        DEBUGLEVEL = val
        if int(DEBUGLEVEL):
            print dHEADER % time.asctime()

# Read configuration file before any other arguments.  This allows the
# environment variable and then the command line to override any
# settings in the configuration file.

for opt, val in opts:
    if opt == "-c":
        CONF = os.path.abspath(val)

ParseConfFile(None)

# Process the rest of the options, if any

for opt, val in opts:
    if opt == "-b":
        BCOLOR = val
    if opt == "-f":
        FCOLOR = val
    if opt == "-n":
        FNAME = val
    if opt == "-q":
        WARN = FALSE
    if opt == "-r":
        AUTOREFRESH = FALSE
    if opt == "-s":
        FSZ = val
    if opt == "-t":
        QUOTECHAR = ""
    if opt == "-w":
        FWT = val
    if opt == "-x":
        WIDTH = val
    if opt == "-y":
        HEIGHT = val

# Figure out where to start
# Program can only have 0 or 1 arguments
# Make sure any startdir argument is legit

if len(args) > 1:
    ErrMsg(eTOOMANY)
    sys.exit(1)

if len(args) == 1:
    STARTDIR = args[0]

    # Windows is sloppy about accepting both '//' and '\\'
    # so we have to condition the command line input for consistency.

    if OSNAME == 'nt' and STARTDIR == '//':
        STARTDIR = SHOWDRIVES

    # Make sure any user request to start in a Drive List View
    # is possible.  If not, just start in the root directory.
    
    if STARTDIR == SHOWDRIVES and (OSNAME != 'nt' or not GetWin32Drives()):
        STARTDIR = PSEP
        
    if not os.path.isdir(STARTDIR):
        ErrMsg(eBADROOT % STARTDIR)
        sys.exit(1)

# Get starting directory into canonical form
STARTDIR = os.path.abspath(STARTDIR)

# Setup event handlers - We have to do this here just
# in case we could not read any config file, which
# is when we normally bind the handlers.

UI.BindAllHandlers()

# Need mutex to serialize on widget updates
UI.DirListMutex = mutex.mutex()

# Intialize the "new dir via mouse" flag
UI.MouseNewDir = FALSE

# Initialize the polling counter
UI.ElapsedTime = 0

# Start in detailed mode
UI.SetDetailedView(TRUE)

# Initialize the UI directory listing
LoadDirList(STARTDIR)
KeySelTop(None)

# If we just wanted debug output, quit now
if int(DEBUGLEVEL) & DEBUGQUIT:
    sys.exit()

# And start the periodic polling of the widget
UI.poll()

# Run the program interface
UIroot.mainloop()