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

# Program Information

PROGNAME = "twander"
RCSID = "$Id: twander.py,v 2.88 2003/01/31 19:01:05 tundra Exp $"
VERSION = RCSID.split()[2]

# Copyright Information

DATE       = "2002-2003"
CPRT         = chr(169)
OWNER        = "TundraWare Inc."
RIGHTS       = "All Rights Reserved."
COPYRIGHT    = "Copyright %s %s %s  %s" % (CPRT, DATE, OWNER, RIGHTS)


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

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

from Tkinter import *
from tkMessageBox import askyesno, showerror, showinfo, 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 we're on Win32, try to load win32all stuff if possible

WIN32ALL = FALSE
if OSNAME == 'nt':
    try:
        from win32api import GetLogicalDriveStrings as GetDrives
        from win32api import GetUserName, GetFileAttributes, GetComputerName, GetDiskFreeSpace, GetVolumeInformation
        from win32file import GetDriveType
        from win32wnet import WNetGetUniversalName
        import win32con
        from win32security import *
        WIN32HOST = GetComputerName()
        WIN32ALL = TRUE
    except:
        WIN32ALL = FALSE


# Get unix password and group features

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


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

#####
# Default Key Assignments
#####

# General Program Commands

CLRHIST     = '<Control-y>'              # Clear Command History
FONTDECR    = '<Control-bracketleft>'    # Decrease font size
FONTINCR    = '<Control-bracketright>'   # Increase font size
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
TOGWIN32ALL = '<Control-w>'              # Toggle win32all features, if available

# 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


#####
# GUI Defaults
#####

#####
# Initial Size And Postition In Pixels
#####

HEIGHT   = 600
WIDTH    = 800
STARTX   = 0
STARTY   = 0

#####
# Default Colors
#####

# Main Display Colors

BCOLOR  = "black"
FCOLOR  = "green"

# Menu Colors

MBARCOL = "beige"
MBCOLOR = "beige"
MFCOLOR = "black"

# Help Screen Colors

HBCOLOR = "lightgreen"
HFCOLOR = "black"

#####
# Default Display Fonts
#####

# Main Display Font

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

# Menu Font

MFNAME = "Courier"
MFSZ   = 12
MFWT   = "bold"

# Help Screen Font

HFNAME = "Courier"
HFSZ   = 10
HFWT   = "italic"


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


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



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

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


#####
# Symbolic Constants Needed Below
#####

NOCMDSHELL    = '""'

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

AUTOREFRESH   = TRUE            # Automatically refresh the directory display
CMDSHELL      = NOCMDSHELL      # Shell definition to use with manual command invocations
DEBUGLEVEL    = 0               # No debug output
MAXDIR        = 32              # Maximum number of directories to track
MAXDIRBUF     = 250             # Maximum size of UI.AllDirs
MAXHIST       = 32              # Maximum length of Command History
MAXHISTBUF    = 250             # Maximum size of UI.CmdHist
MAXNESTING    = 32              # Maximum depth of nested variable definitions
NODETAILS     = FALSE           # TRUE means details can never be displayed
NONAVIGATE    = FALSE           # TRUE means that all directory navigation is prevented
REFRESHINT    = 3000            # Interval (ms) for automatic refresh
QUOTECHAR     = '\"'            # Character to use when quoting Built-In Variables
USETHREADS    = TRUE            # Use threads on Unix
USEWIN32ALL   = TRUE            # Use win32all features if available
WARN          = TRUE            # Warnings on
WIN32ALLON    = TRUE            # Flag for toggling win32all features while running


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

# General Constants

KB            = 1024            # 1 KB constant
MB            = KB * KB         # 1 MB constant
GB            = MB * KB         # 1 GB constant
POLLINT       = 20              # Interval (ms) the poll routine should run
PSEP          = os.sep          # Character separating path components
SHOWDRIVES    = '\\\\'          # Logical directory name for Win32 Drive Lists


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

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

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

# Get hostname

HOSTNAME      = getfqdn()       # Full name of this host

# Get the user name

if OSNAME == 'nt':
    if WIN32ALL and USEWIN32ALL and WIN32ALLON:
        USERNAME = GetUserName()
    else:
        USERNAME = os.getenv("LOGNAME")
    

elif OSNAME == 'posix':
    USERNAME = os.getenv("USER")

else:
    USERNAME = ""

# Concatenate them if we got a user name

if USERNAME:
    FULLNAME = "%s@%s" % (USERNAME, HOSTNAME)
else:
    FULLNAME = HOSTNAME



#####
# GUI-Related Stuff
#####

# Constants

CMDMENU_WIDTH = 16


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

# 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)

# There are some event bits we don't care about -
# We'll use the following constant to mask them out
# later in the keyboard and mouse handlers.

DontCareMask  = LockMask | Mod2Mask | Mod3Mask | Mod4Mask | Mod5Mask

# Some things are OS-dependent

if OSNAME == 'nt':
    AltMask = (1<<17)
    DontCareMask |= Mod1Mask   # Some versions of Win32 set this when Alt is pressed
else:
    AltMask = Mod1Mask


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

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


#####
# Stat-Related Stuff
#####

if WIN32ALL:
    
    # Used with win32all-based permissions logic.
    # Format for each entry is: (mask to test, symbol if true).
    # Position in tuple determines position in display.

    win32all_mode = ((win32con.FILE_ATTRIBUTE_DIRECTORY,  'd'),
                     (win32con.FILE_ATTRIBUTE_ARCHIVE,    'A'),
                     (win32con.FILE_ATTRIBUTE_COMPRESSED, 'C'),
                     (win32con.FILE_ATTRIBUTE_HIDDEN,     'H'),
                     (win32con.FILE_ATTRIBUTE_NORMAL,     'N'),
                     (win32con.FILE_ATTRIBUTE_READONLY,   'R'),
                     (win32con.FILE_ATTRIBUTE_SYSTEM,     'S'))

    win32all_type = ((win32con.DRIVE_CDROM,     'CD/DVD'),
                     (win32con.DRIVE_FIXED,     'Fixed'),
                     (win32con.DRIVE_RAMDISK,   'Ramdisk'),
                     (win32con.DRIVE_REMOTE,    'Remote'),
                     (win32con.DRIVE_REMOVABLE, 'Removable'))

    # Size of each of the drive detail fields, including room for trailing space.
    
    SZ_DRIVE_LABEL = 14         # Must be wide enough to hold NODRIVE + 1 or NOLABEL + 1
    SZ_DRIVE_FREE  = 13         # In this case there is no trailing space but a trailing '/'
    SZ_DRIVE_TTL   = 13         # so we can have a 'free/total' presentation
    SZ_DRIVE_TYPE  = 20
    SZ_DRIVE_HOST  = 19         # Leave room for leading '\\' - no trailing space, trailing '\'

    SZ_DRIVE_TOTAL = SZ_DRIVE_LABEL + SZ_DRIVE_FREE + SZ_DRIVE_TTL + SZ_DRIVE_TYPE + SZ_DRIVE_HOST

# Constants used with the more usual Unix-style details
# Used both by Unix and Win32 when win32all is not present or disabled.

# 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
# These are sized for the worst-case: Win32 with win32all features

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

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


# Special Bit Masks

STICKY_MASK   = 1
SETGID_MASK   = 2
SETUID_MASK   = 4

# Misc. Stat-Related Strings

FILEGROUP     = "group"
FILEOWNER     = "owner"
NODRIVE       = "<Drive Empty>"
NOLABEL       = "<No Label>"
SYMPTR        = " -> "
UNAVAILABLE   = "Unavailable"
WIN32GROUP    = "win32" + FILEGROUP
WIN32OWNER    = "win32" + FILEOWNER


#####
# 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


# 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]'
YESNO       = r'[YESNO:'


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


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

# Title-Bar Strings

TTLFILES    = "Total Files:"
TTLSIZE     = "Total Size:"

# Menu Button Titles

COMMANDMENU = 'Commands'      # Title for Command Menu button
DIRMENU     = 'Directories'   # Title for Directory Menu button
HISTMENU    = 'History'       # Title for History Menu button
HELPMENU    = 'Help'          # Title for Help Menu button

# Help Menu-Related

ABOUT       = "%s %s\n\nCopyright %s %s\n%s\n\n%s"  % (PROGNAME, VERSION, CPRT, DATE, OWNER, RIGHTS)
hABOUT      = 'About'
hCOMMANDS   = 'Command Definitions'
hDIRSC      = 'Directory Shortcuts'
hINTVBLS    = 'Internal Program Variables'
hKEYS       = 'Keyboard Assignments'
hNONE       = 'No %s Found.'
hOPTVBLS    = 'User-Settable Options'
hUSERVBLS   = 'User-Defined Variables'


# Errors

eBADROOT    = " %s Is Not A Directory"
eDIRRD      = "Cannot Open Directory : %s  ---  Check Permissions."
eERROR      = "ERROR"
eINITDIRBAD = "Cannot Open Starting Directory : %s - Check Permissions - ABORTING!."
eOPEN       = "Cannot Open File: %s"
eTOOMANY    = "You Can Only Specify One Starting Directory."

# Information

iNOSTAT     = "Details Unavailable For This File ->"

# Prompts

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

# Warnings

wBADCFGLINE = "Ignoring Line %s.\nBogus Configuration Entry:\n\n%s"
wBADDEBUG   = "Ignoring Bogus Debug Level! - %s Is Not In Integer Or Hex Format."
wBADENVVBL  = "Ignoring Line %s.\nEnvironment Variable %s Not Set:\n\n%s"
wBADEXE     = "Could Not Execute Command:\n\n%s"
wBADRHS     = "Ignoring Line %s.\nOption Assignment Has Bad Righthand Side:\n\n%s"
wBADSCNUM   = "Ignoring Line %s.\nShortcut Number Must Be From 1-12:\n\n%s"
wCONFOPEN   = "Cannot Open Configuration File:\n%s"
wDIRSCREDEF = "Ignoring Line %s.\nDirectory Shortcut Defined More Than Once:\n\n%s"
wDUPKEY     = "Ignoring Line %s.\nFound Duplicate Command Key '%s':\n\n%s"
wLINKBACK   = "%s Points Back To Own Directory"
wNOCMDS     = "Running With No Commands Defined!"
wNOREBIND   = "Ignoring Line %s.\nCannot Rebind This Keyboard Or Mouse Button Combination:\n\n%s"
wREDEFVAR   = "Ignoring Line %s.\nVariable %s Redefined:\n\n%s"
wUNDEFVBL   = "Ignoring Line %s.\nUndefined Variable %s Referenced:\n\n%s"
wVBLTOODEEP = "Ignoring Line %s.\nVariable Definition Nested Too Deeply:\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     = hCOMMANDS
dDIRSTK     = "DIRECTORY STACK"
dFUNCKEYS   = hDIRSC
dHEADER     = "twander Debug Dump Run On: %s\n"
dHIST       = "COMMAND HISTORY STACK"
dINTVAR     = hINTVBLS
dKEYBINDS   = hKEYS
dNULL       = "None"
dOPTVAR     = hOPTVBLS
dSYMTBL     = hUSERVBLS

# Debug Formatting

dCMDWIDTH    = 20
dINTVARWIDTH = 12
dKEYWIDTH    = 16
dOPTIONWIDTH = 16
dSCWIDTH     = 6
dUSRVBLWIDTH = 16

# List of internal program variables to dump during debug sessions

DebugVars = ["RCSID", "OSNAME", "HOSTNAME", "USERNAME", "OPTIONS", "CONF", "HOME", "PSEP", "POLLINT"]


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

uTable = [PROGNAME + " " + VERSION + " - %s\n" % COPYRIGHT,
          "usage:  " + PROGNAME + " [-cdhqrstvwxy] [startdir] where,\n",
          "          startdir  name of directory in which to begin (default: current dir)",
          "          -c file   name of configuration file (default: $HOME/." + PROGNAME +
                     " or PROGDIR/." + PROGNAME + ")",
          "          -d level  set debugging level (default: 0, debugging off)",
          "                Bit Assignments:",
          "                         0 - Dump Requested Debug Information And Exit Immediately",
          "                         1 - Dump Internal Program Variables And User-Settable Options",
          "                         2 - Dump User-Defined Variables",
          "                         3 - Dump Command Definitions",
          "                         4 - Display, Do Not Execute, Commands When Invoked",
          "                         5 - Dump Key Bindings And Directory Shortcut Assignments",
          "                         6 - Dump Directory Stack As It Changes",
          "                         7 - Dump Command History Stack After Command Executes",
          "          -h        print this help information",
          "          -q        quiet mode - no warnings (default: warnings on)",
          "          -r        turn off automatic content refreshing (default: refresh on)",
          "          -t        no quoting when substituting Built-In Variables (default: quoting on)",
          "          -v        print detailed version information",
          ]


#---------------------------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.

    if WIN32ALL and USEWIN32ALL and WIN32ALLON:
        return filter(lambda x : x, GetDrives().split('\x00'))
    else:
        return ""

# 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()'


#####
# Pad A String With Trailing Spaces To Specified Width
# Return either that padded string or, if the passed
# string is too large, truncate it and return a string
# of exactly the specified with whose last character is
# a space.
#####

def PadString(string, width):

    string  = string[:(width-1)]
    string += " " * (width - len(string))
    return string

# End of 'PadString()'


#####
# 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, DoOptionsProcessing=TRUE):
    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,
                      "FONTDECR":FONTDECR,
                      "FONTINCR":FONTINCR,
                      "MOUSECTX":MOUSECTX,
                      "MOUSEDIR":MOUSEDIR,
                      "KEYPRESS":KEYPRESS,
                      "QUITPROG":QUITPROG,
                      "READCONF":READCONF,
                      "REFRESH":REFRESH,
                      "TOGDETAIL":TOGDETAIL,
                      "TOGWIN32ALL":TOGWIN32ALL,
                      "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
                      }

    # Set all the program options to their default values
    # This means that a configuration file reload can
    # override the options set previously in the environment
    # variable or on the command line.

    for x in (UI.OptionsBoolean, UI.OptionsNumeric, UI.OptionsString):
        for o in x.keys():
            globals()[o] = x[o]

    # 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)
        # 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)

        # Close the config file
        cf.close()

    except:
        WrnMsg(wCONFOPEN % CONF)

    # Make sure any options we've changed are implemented
    if DoOptionsProcessing:
        ProcessOptions()

# 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):
            
            WrnMsg(wREDEFVAR % (num, name, line))
            return
            
        # 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))
                    return

                # 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)

            # User specified an invalid shortcut number
            else:
                WrnMsg(wBADSCNUM % (num, line))
                return

        # Process any option variables

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

        elif name in UI.OptionsNumeric.keys():
            try:
                val = StringToNum(val)
                if val >= 0:
                    globals()[name] = val
                else:
                    WrnMsg(wBADRHS % (num, line))                    
            except:
                WrnMsg(wBADRHS % (num, line))
                return

        elif name in UI.OptionsString.keys():
            # RHS cannot be null
            if not val:
                WrnMsg(wBADRHS % (num, line))
                return
            else:
                globals()[name] = val


        # 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))
                return
            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:
            WrnMsg(wBADCFGLINE % (num, line))
            return
        else:
            cmdkey = fields[0]
            cmdname = fields[1]
            cmd = " ".join(fields[2:])

            # Process any User-Defined variables
            cmd = ProcessVariables(cmd, num, line)

            # A null return means there was a problem - abort
            if not cmd:
                return

            # Add the command entry to the command table.
            # Prevent duplicate keys from being entered.
            
            if UI.CmdTable.has_key(cmdkey):
                WrnMsg(wDUPKEY % (num, cmdkey, line))
                return
            else:
                UI.CmdTable[cmdkey] = [cmdname, cmd]
                UI.CmdBtn.menu.add_command(label=PadString(cmdname, CMDMENU_WIDTH) + "(" + cmdkey + ")",
                                           command=lambda cmd=cmdkey: CommandMenuSelection(cmd))

    else:
        WrnMsg(wBADCFGLINE % (num, line))

# End of 'ParseLine()'


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

def PrintDebug(title, content):

    print '<%s>\n' % title.upper()
    if content:
        for i in content:
            print i
    else:
        print dNULL
    print

# End of 'PrintDebug()'


#####
# Setup The GUI Visual Parameters, Menus, & Help Information 
#####

def SetupGUI():

    # Start in detailed mode unless details are inhibited
    UI.SetDetailedView(~NODETAILS)

    # Rebind all the handlers
    UI.BindAllHandlers()

    # 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, UI.HelpBtn):
        i.config(foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))
        i.menu.config(foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))

    # Set Menu Bar background to match buttons
    UI.mBar.config(background=MBARCOL)

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


    # Make sure menus conform to max lengths (which may have changed).

    UpdateMenu(UI.DirBtn, UI.AllDirs, MAXDIR, MAXDIRBUF, LoadDirList, sort=TRUE)    
    UpdateMenu(UI.HistBtn, UI.CmdHist, MAXHIST, MAXHISTBUF, KeyRunCommand, fakeevent=TRUE)
    
    #####
    # Initialize Help Menu with latest information
    #####

    # Clear out existing content
    
    UI.HelpBtn.config(state=DISABLED)
    UI.HelpBtn.menu.delete(0,END)


    # Update the cascading submenus
    # We iterate across tuples of (Menu Name, Menu Variable, List Of Items)

    for mname, mvbl, mlist in ((hUSERVBLS, UI.UserVbls, GetUserVbls()),
                               (hCOMMANDS, UI.CmdDefs,  GetCommandTable()),
                               (hINTVBLS,  UI.IntVbls,  GetIntVars()),
                               (hOPTVBLS,  UI.OptVbls,  GetOptions()),
                               (hKEYS,     UI.Keys,     GetKeyBindings()),
                               (hDIRSC,    UI.DirSCs,   GetDirShortcuts())):

        mvbl.delete(0,END)                       
        
        # Indicated if there is nothing to display for this class of help
        if not mlist:
            mvbl.add_command(label=hNONE % mname, command=None, foreground=HFCOLOR, background=HBCOLOR,
                             font=(HFNAME, HFSZ, HFWT))

        # Load the help class with relevant information
        else:
            for l in mlist:
                mvbl.add_command(label=l, command=None, foreground=HFCOLOR, background=HBCOLOR, font=(HFNAME, HFSZ, HFWT))

        UI.HelpBtn.menu.add_cascade(label=mname, menu=mvbl)

    # Setup the About item

    UI.HelpBtn.menu.add_command(label=hABOUT, command=lambda title=hABOUT, text=ABOUT : showinfo(title, text))

    # Enable the menu content
    
    UI.HelpBtn['menu'] = UI.HelpBtn.menu
    UI.HelpBtn.config(state=NORMAL)

    # Size and position the display
    UIroot.geometry("%sx%s+%s+%s" % (WIDTH,  HEIGHT, STARTX, STARTY))

# End of 'SetupGUI()'


#####
# Convert A String In Integer Or Hex Format To An Equivalent Numeric
# We assume that the string is either in correct format or that
# the calling routine will catch any error.
#####

def StringToNum(string):

    if string.lower().startswith('0x'):
        value = atoi(string, 16)
    else:
        value = atoi(string)

    return value

# End of 'StringToNum()


#####
# 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()'


#####
# Check Debug Level To See If It Is A Properly Formed Integer Or Hex Value
# If So, Convert To Numeric, If Not, Warn User, And Set To 0
#####

def ValidateDebugLevel():
    global DEBUGLEVEL

    d = DEBUGLEVEL  # Save, in case of error
    try:
        DEBUGLEVEL = StringToNum(DEBUGLEVEL)
    except:
        DEBUGLEVEL = 0
        WrnMsg(wBADDEBUG % d)

# End of 'ValidateDebugLevel()'


#####
# 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 Help Menu

        self.HelpBtn = Menubutton(self.mBar, text=HELPMENU, underline=2, state=DISABLED)
        self.HelpBtn.menu = Menu(self.HelpBtn)
        self.HelpBtn.pack(side=LEFT, padx=MENUPADX)
        
        # Setup the cascading submenus
        
        self.UserVbls = Menu(self.HelpBtn.menu, foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))
        self.CmdDefs  = Menu(self.HelpBtn.menu, foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))
        self.IntVbls  = Menu(self.HelpBtn.menu, foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))
        self.OptVbls  = Menu(self.HelpBtn.menu, foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))
        self.Keys     = Menu(self.HelpBtn.menu, foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))
        self.DirSCs   = Menu(self.HelpBtn.menu, foreground=MFCOLOR, background=MBCOLOR, font=(MFNAME, MFSZ, MFWT))

        # 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 Decrement Font Size
        self.DirList.bind(self.KeyBindings["FONTDECR"], FontDecr)

        # Bind handler to invoke Increment Font Size
        self.DirList.bind(self.KeyBindings["FONTINCR"], FontIncr)

        # 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)

        # Bind handler for "Toggle win32all Features" 
        self.DirList.bind(self.KeyBindings["TOGWIN32ALL"], KeyToggleWin32All)

        ###
        # 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 = []
        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):

        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:
                # Don't autorefresh on drive list views
                if UI.CurrentDir != SHOWDRIVES:
                    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):

        # See if we're forcing details to always be off
        if NODETAILS:
            self.DetailsOn = FALSE
        else:
            self.DetailsOn = details
        
    # 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("%s %s        %s:        %s        %s  %s        %s  %s" % 
                      (PROGNAME, VERSION, FULLNAME, UI.CurrentDir,
                       TTLFILES, str(self.DirList.size()), TTLSIZE, 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):

    event.state &= ~DontCareMask                     # Kill the bits we don't care about
    
    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 | ControlMask):     # Shift-Control-Button-3
        x, y = UI.DirList.winfo_pointerxy()          # Position near mouse
        PopupMenu(UI.HistBtn.menu, x, y)             # Display Directory Menu

    elif event.state == (Button3Mask | ShiftMask):   # Shift-Button-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):

    event.state &= ~DontCareMask                      # Kill the bits we don't care about
    
    if event.state == Button1Mask:                    # Double-Button-2 / No Modifier
        DirListHandler(event)                         # Execute selected item

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

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

    return "break"

# 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()'


#####
# Decrement Font Size
#####

def FontDecr(event):
    global FSZ, MFSZ, HFSZ
    
    FSZ  -= 1
    MFSZ -= 1
    HFSZ -= 1
    
    SetupGUI()

# End of 'FontDecr()'


#####
# Increment Font Size
#####

def FontIncr(event):
    global FSZ, MFSZ, HFSZ

    FSZ  += 1
    MFSZ += 1
    HFSZ += 1

    SetupGUI()

# End of 'FontIncr()'


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

def KeystrokeHandler(event):

    event.state &= ~DontCareMask                      # Kill the bits we don't care about

    # 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

        # Help Menu
        elif event.char == 'l':
            button = UI.HelpBtn

        # 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:
        ExecuteCommand(cmd, name)
   
# 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()'

#####
# Event Handler: Toggle win32all Features, If Available
#####

def KeyToggleWin32All(event):
    global WIN32ALLON
    
    if USEWIN32ALL and (UI.CurrentDir != SHOWDRIVES):
        WIN32ALLON = not WIN32ALLON
        RefreshDirList(event)

# End of 'KeyToggleWin32All()'


#------------------- 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):
    LoadDirList(PSEP)

# End of 'KeyRootDir()'


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

def KeyStartDir(event):
    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):

    # In the case of a Drive List View, we want to literally
    # select everything.  In all other cases, we do not
    # want this feature to select the first item which is ".."

    if UI.CurrentDir == SHOWDRIVES:
        UI.DirList.selection_set(0, END)

    else:
        # 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)

    # If we're not in a Drive List View, we never
    # select the first entry (".." this way)

    if UI.CurrentDir != SHOWDRIVES:
        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 Manually Entered Command
####

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

    # Prompt with passed initial edit string
    if initial:

        # This feature is invoked with an initial string when we
        # are playing back something from the command history.
        # If the CMDSHELL feature is enabled, we need to insure
        # that we do not prepend that string multiple times as
        # the command is reinvoked. We do that by removing that
        # prepended string from the passed initial string.
        # We also want to remove the previously added single quotes.

        if (CMDSHELL != NOCMDSHELL) and initial.startswith(CMDSHELL):
            initial = initial[len(CMDSHELL)+2:-1]
                
        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:

        # Prepend the 
        mycmd = cmd

        if CMDSHELL != NOCMDSHELL:                       # See if feature is enabled
            if not mycmd.startswith('\\'):               # See if user is trying to escape the feature
                mycmd = "%s '%s'" % (CMDSHELL, cmd)

            else:                                        # User is escaping the feature
                mycmd = mycmd[1:]
        
        ExecuteCommand(mycmd, pMANUALCMD, ResolveVars=TRUE, SaveUnresolved=TRUE)

        # Save the command only if Command History is enabled (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):

            # Protect with try/except because Tk loses track of things
            # if you keep hitting this selection very rapidly - i.e. Select
            # the entry and lean on the Enter key.  The try/except
            # prevents the error message (which is benign) from ever
            # appearing on stdout.
            
            try:
                WrnMsg(wLINKBACK % (UI.CurrentDir + selected[:-1]))
            except:
                pass
            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), '', ResolveBuiltIns=FALSE, 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 further processing of key - some Function Keys
    # have default behavior in Tk which we want to suppress.
    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, name, UseStartDir=FALSE, ResolveVars=FALSE, ResolveBuiltIns=TRUE, SaveUnresolved=FALSE):
    global UI

    # Work with a copy of the passed command
    newcmd = cmd
    
    # Process references to any Built-In variables
    if ResolveBuiltIns:
        newcmd = ProcessBuiltIns(cmd, name)

    # Replace references to any Environment or User-Defined variables
    # but only when asked to. - i.e., The command was manually
    # entered by the user and may contain unresolved variables.
    # (Commands the user defined in the configuration file
    # already have their variables dereferenced when that file is
    # read and parsed.)
    
    if ResolveVars:
        newcmd = ProcessVariables(newcmd, 0 , name)

    # A null return value means there was a problem - abort
    if not newcmd:
        return

    # Just dump command if we're debugging

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

        # Normal command execution for both Unix and Win32
        else:
            try:
                if (OSNAME == 'posix') and not USETHREADS:
                    os.system(newcmd + " &")
                else:
                    thread.start_new_thread(os.system, (newcmd,))
            except:
                WrnMsg(wBADEXE % newcmd)


    # Update the Command History observing MAXHIST
    # In most cases, we want to save the command with all the
    # variables (Built-In, Environment, User-Defined) resolved (dereferenced).
    # However, sometimes (e.g. manual command entry via KeyRunCommand()) we
    # want to save the *unresolved* version.

    if SaveUnresolved:
        savecmd = cmd
    else:
        savecmd = newcmd

    UpdateMenu(UI.HistBtn, UI.CmdHist, MAXHIST, MAXHISTBUF, KeyRunCommand, newentry=savecmd, fakeevent=TRUE)
                
    # Dump Command History stack if requested
    
    if DEBUGLEVEL & DEBUGHIST:
        PrintDebug(dHIST, UI.CmdHist)
        
# End of 'ExecuteCommand()


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

def LoadDirList(newdir, save=TRUE):

    # Make sure we're permitted to navigate - we have to allow initial entry into STARTDIR
    if NONAVIGATE and (newdir != STARTDIR):
        return

    # 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 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 = [], []
    
    # Indicate where in each display string the actual file name
    # can be found.  This is used both in the code in this routine
    # and other places in the program where just the file name is
    # needed - for example, when we want to execute it or replace it
    # in one of the built-ins.  This value depends on whether or
    # not we are viewing details or not.
    
    if UI.DetailsOn:
        if currentdir == SHOWDRIVES:
            UI.NameFirst = SZ_DRIVE_TOTAL
        else:
            UI.NameFirst = ST_SZTOTAL
    else:
        UI.NameFirst = 0

    # 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, do case-insensitive sorting since case
        # is irrelevant in file names on these systems
        # On Win32 we also have to promote the file name to unicode.
        # This is because, if the file name contains non-ASCII
        # characters, Python will throw an error if we attempt
        # to concatentate it to a unicode string later.  Since
        # the file name gets used in a variety of places in
        # the code, we do this promotion here to insure correct
        # behavior.
        

        if OSNAME == 'nt':

            for l in (dList, fList):
                lowerlist = [(x.lower(), x) for x in l]
                lowerlist.sort()
                l[:] = []
                [l.append(x[1]) for x in lowerlist]

        else:
            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)

        # Save the complete list for details extraction below
        
        all     = dList + fList

    # The user requested Drive List View.
    
    else:
        UI.TotalSize = 0
        drivelist = GetWin32Drives()

        # If details are off, just return the list of drives
        if not UI.DetailsOn:
            return drivelist

        # We're returning detailed information about each drive
        else:
            
            details = []

            # Create a detailed entry for each drive on the system
            # Store it as a string in the details list which will
            # then be returned to the caller

            for drive in drivelist:

                # Drive Label - Drive Might Not Be Available
                try:
                    label = GetVolumeInformation(drive)[0]
                except:
                    label = NODRIVE

                if not label:
                    label = NOLABEL
                    
                # Type Of Drive - We need this now to get hostname
                drivetype = GetDriveType(drive)
                typestring = ''

                for type, stype in win32all_type:
                    if drivetype == type:
                        typestring = stype

                # Machine Hosting The Drive
                if drivetype == win32con.DRIVE_REMOTE:
                    name = WNetGetUniversalName(drive, 1)
                else:
                    name = label
                    
                entry = PadString(name, SZ_DRIVE_HOST + SZ_DRIVE_LABEL)

                # Add the drive type
                entry += PadString(typestring, SZ_DRIVE_TYPE)
                
                # Get Drive Space Information - Drive Might Not Be Available
                try:
                    clustersz, sectorsz, freeclusters, totalclusters = GetDiskFreeSpace(drive)

                    # Free Space
                    freespace = FileLength(clustersz * sectorsz * freeclusters)

                    # Total Space
                    totalspace = FileLength(clustersz * sectorsz * totalclusters)
                except:
                    freespace, totalspace = ('0', '0')

                sizes = "%s/%s" % (freespace, totalspace)
                entry += PadString(sizes, SZ_DRIVE_FREE + SZ_DRIVE_TTL)

                # Finally, tack on the drive name
                entry += drive

                # Save final result in detailed list
                details.append(entry)

            return details
    
    # Get details on directory contents

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

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

        # Condition the name
        
        fn = os.path.join(currentdir, all[index])
        if fn[-1] == PSEP:
            fn =fn[:-1]

        # Get file details from OS
        try:
            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
        
        # Do Win32-specific mode if win32all is loaded
        if WIN32ALL and USEWIN32ALL and WIN32ALLON:
            modlen = len(win32all_mode)

            try:
                win32stat = GetFileAttributes(fn)
                mode = ""
            except:
                mode = UNAVAILABLE

            if not mode:

                # Test each attribute and set symbol in respective
                # position, if attribute is true.

                for i in range(modlen):
                    mask, sym = win32all_mode[i]
                    if win32stat & mask:
                        mode += sym
                    else:
                        mode += '-'
            
        # We're either on Unix or Win32 w/o win32all available
        else:
            # 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 sp & STICKY_MASK:
                if mode[-1] == "x":
                    mode = mode[:-1] + "t"
                else:
                    mode = mode[:-1] + "T"

            # Setgid Bit

            if sp & SETGID_MASK:
                if mode[-4] == "x":
                    mode = mode[:-4] + "s" + mode[-3:]
                else:
                    mode = mode[:-4] + "S" + mode[-3:]

            # Setuid Bit

            if sp & SETUID_MASK:
                if mode[-7] == "x":
                    mode = mode[:-7] + "s" + mode[-6:]
                else:
                    mode = mode[:-7] + "S" + mode[-6:]

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


        # Pad the result for column alignment
        detlist[index] += PadString(mode, ST_SZMODE)

        # Number of links to entry
        detlist[index] += PadString(str(stinfo[ST_NLINK]), ST_SZNLINK)

        # 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':

            # Defaults
            owner = WIN32OWNER
            group = WIN32GROUP

            if WIN32ALL and USEWIN32ALL and WIN32ALLON:
                try:
                    # Get the internal Win32 security information for this file.
                    ho     = GetFileSecurity(fn, OWNER_SECURITY_INFORMATION)
                    hg     = GetFileSecurity(fn, GROUP_SECURITY_INFORMATION)
                    sido   = ho.GetSecurityDescriptorOwner()
                    sidg   = hg.GetSecurityDescriptorGroup()

                    # We have to know who is hosting the filesytem for this file

                    drive = fn[0:3]
                    if GetDriveType(drive) == win32con.DRIVE_REMOTE:
                        fnhost = WNetGetUniversalName(drive, 1).split('\\')[2]
                    else:
                        fnhost = WIN32HOST

                    # Now we can translate the sids into names

                    owner  = LookupAccountSid(fnhost, sido)[0]
                    group  = LookupAccountSid(fnhost, sidg)[0]

                # We assume the values are unavailable on any error
                except:
                    owner = UNAVAILABLE
                    group = UNAVAILABLE

         # Default names for all other OSs
        else:
            owner = OSNAME + FILEOWNER
            group = OSNAME + FILEGROUP

        # Add them to the detail

        detlist[index] += PadString(owner, ST_SZUNAME)
        detlist[index] += PadString(group, ST_SZGNAME)

        # Length

        flen = FileLength(stinfo[ST_SIZE])
        UI.TotalSize += stinfo[ST_SIZE]
        detlist[index] += PadString(flen, ST_SZLEN)

        # 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] += PadString(ftime, ST_SZMTIME)

        # Add the 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()'


#####
# Process A Command Line Containing Built-In Variables
#####

def ProcessBuiltIns(cmd, name):

    # First do any prompting required

    for promptvar, handler, replace in ((YESNO, askyesno, FALSE), (PROMPT, askstring, TRUE)):

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

            # Make sure our program gets focus back
            UI.DirList.focus()

            if val:
                if replace:
                    cmd = cmd.replace(cmd[b:e+1], QUOTECHAR + val + QUOTECHAR)
                else:
                    cmd = cmd.replace(cmd[b:e+1], '')

            # 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

    # We have to treat built-ins specially if we're in a Drive List View
    # Most noteably, there is no meaning to [DIR] in this context

    if UI.CurrentDir == SHOWDRIVES:
        currentdir = ""
    else:
        currentdir = UI.CurrentDir

    selection = StripPSEP(UI.LastInSelection())
    selections = ""
    dselections = ""
    for selected in UI.AllSelection():

        # Fill the various built-ins
        selected = StripPSEP(selected)
        dselections += QUOTECHAR + currentdir + selected + QUOTECHAR + " "
        selections  += QUOTECHAR + selected + QUOTECHAR + " "
        
    cmd = cmd.replace(DIR, QUOTECHAR + StripPSEP(currentdir) + QUOTECHAR)
    cmd = cmd.replace(DSELECTION, QUOTECHAR + 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)

    return cmd

# End of 'ProcessBuiltIns()'    


#####
# Process/Replace References To User-Defined & Environment Variables
#####

def ProcessVariables(cmd, num, line):

    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):
                WrnMsg(wVBLTOODEEP % (num, cmd))
                return ""

        # 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)

            elif x.startswith(YESNO):
                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:
                        WrnMsg(wBADENVVBL % (num, x, line))
                        return ""

                # Process references to undefined variables
                else:
                    WrnMsg(wUNDEFVBL % (num, x, line))
                    return ""

        # No substitutions left to do
        else:
            doeval = FALSE

    return cmd
                        
# End of 'ProcessVariables()'


#####
# 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.state = 0
    event.char  = cmdkey
    KeystrokeHandler(event)    

# End Of 'CommandMenuSelection()'


#####
# Process Options
#####

def ProcessOptions():

    #####
    # Setup The GUI Visual Parameters, Menus, & Help Information
    #####

    SetupGUI()

    #####
    # Reflect Our Changes In The Interface
    #####

    RefreshDirList()

    #####
    # Dump requested debug information
    #####
    
    # Keyboard Assignments
    if DEBUGLEVEL & DEBUGKEYS:

        # Keyboard Bindings
        PrintDebug(dKEYBINDS, GetKeyBindings())
        
        # Function Keys (Directory Shortcuts)
        PrintDebug(dFUNCKEYS, GetDirShortcuts())

    # User-Defined Variables
    if DEBUGLEVEL & DEBUGSYMS:
        PrintDebug(dSYMTBL, GetUserVbls())

    # Command Definitions
    if DEBUGLEVEL & DEBUGCTBL:
        PrintDebug(dCMDTBL, GetCommandTable())        

    # Internal Program Variables AndOptions
    if DEBUGLEVEL & DEBUGVARS:

        # Internal variabled
        PrintDebug(dINTVAR, GetIntVars())

        # User-Settable options
        PrintDebug(dOPTVAR, GetOptions())

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

# End of 'ProcessOptions()'


#####
# 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 = []
        [dlc.append(d.lower()) for d in UI.AllDirs]

        # 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:
        UpdateMenu(UI.DirBtn, UI.AllDirs, MAXDIR, MAXDIRBUF, LoadDirList, sort=TRUE, newentry=newdir)

# End of 'UpdateDirMenu()'


#####
# Generic Menu Update Routine
#####

def UpdateMenu(menubtn, datastore, max, maxbuf, func, sort=FALSE, newentry="", fakeevent=FALSE):

    # First add the new data, if any, to the specified data storage stucture.

    if newentry:
        datastore.append(newentry)
    
    # Now trim it to requested maximum length. 'max' sets how
    # many entries we see in the menu, and 'maxbuf' sets how large
    # the actual storage buffer is allowed to get.

    datastore[:] = datastore[-maxbuf:]

    # We do not sort the data storage itself if sorting has been requested.
    # We sort the *copy*.  That way we get the 'last _max_ items in
    # sorted order.' (If we sorted the master copy, we would lose
    # track of the order in which things were placed there.)

    data = datastore[:]
    if not max:
        data = []
    elif len(data) > max:
        data = datastore[-max:]

    # Initialize the menu to empty

    menubtn.menu.delete(0,END)
    menubtn.config(state=DISABLED)
    
    if len(data):
        if sort:
            data.sort()
        for entry in data:
            if fakeevent:
                menubtn.menu.add_command(label=entry, command=lambda item=entry: func(None, item))
            else:
                menubtn.menu.add_command(label=entry, command=lambda item=entry: func(item))
        menubtn['menu'] = menubtn.menu

        # Menu now has content, enable it
        menubtn.config(state=NORMAL)

# End of 'UpdateMenu()'


#---------------- Debug Support Functions -----------------#


#####
# Return List Of Command Table Entries
#####

def GetCommandTable():

    debuginfo = []
    for key in UI.CmdTable.keys():
        name = UI.CmdTable[key][0]
        cmd  = UI.CmdTable[key][1]
        debuginfo.append(PadString(key + "   " + name, dCMDWIDTH) + cmd)

    return debuginfo

# End of 'GetCommandTable()'


#####
# Return List Of Current Directory Shortcuts
#####

def GetDirShortcuts():

    debuginfo = []
    for x in range(len(UI.FuncKeys)):
        key = "F" + str(x+1)
        path = UI.FuncKeys[x]
        if path:
            debuginfo.append(PadString(key, dSCWIDTH) + path)

    return debuginfo

# End of 'GetDirShortcuts()'


#####
# Return List Of Internal Variables
#####

def GetIntVars():

    debuginfo = []
    for v in DebugVars:
            debuginfo.append(PadString(v, dINTVARWIDTH) + (str(eval(v)) or dNULL))

    debuginfo.sort()
    return debuginfo

# End of 'GetIntVars()'


#####
# Return List Of Current Key Bindings
#####

def GetKeyBindings():

    debuginfo = []
    for key in UI.KeyBindings.keys():
        debuginfo.append(PadString(key, dKEYWIDTH) + UI.KeyBindings[key])

    debuginfo.sort()
    return debuginfo

# End of 'GetKeyBindings()'


#####
# Return List Of User-Settable Options
#####

def GetOptions():

    debuginfo = []
    for l in (UI.OptionsBoolean, UI.OptionsNumeric, UI.OptionsString):
        for v in l:
            debuginfo.append(PadString(v, dOPTIONWIDTH) + (str(eval(v)) or dNULL))

    debuginfo.sort()
    return debuginfo

# End of 'GetOptions()'


#####
# Return List Of User-Defined Variables
#####

def GetUserVbls():

    debuginfo = []
    for sym in UI.SymTable.keys():
        debuginfo.append(PadString(sym, dUSRVBLWIDTH) + UI.SymTable[sym])

    debuginfo.sort()

    return debuginfo

# End of 'GetUserVbls()'


#----------------------------------------------------------#
#                  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:"", YESNO:""}

# Options (and their default values) which can be set in the configuration file

UI.OptionsBoolean = {"AUTOREFRESH":AUTOREFRESH, "NODETAILS":NODETAILS, "NONAVIGATE":NONAVIGATE,
                     "USETHREADS":USETHREADS, "USEWIN32ALL":USEWIN32ALL, "WARN":WARN}

UI.OptionsNumeric = {"DEBUGLEVEL":DEBUGLEVEL, "FSZ":FSZ, "MFSZ":MFSZ, "HFSZ":HFSZ, "HEIGHT":HEIGHT,
                     "MAXDIR":MAXDIR, "MAXDIRBUF":MAXDIRBUF, "MAXHIST":MAXHIST, "MAXHISTBUF":MAXHISTBUF,
                     "MAXNESTING":MAXNESTING, "REFRESHINT":REFRESHINT, "STARTX":STARTX, "STARTY":STARTY,
                     "WIDTH":WIDTH}

UI.OptionsString  = {"BCOLOR":BCOLOR,   "FCOLOR":FCOLOR,   "FNAME":FNAME,   "FWT":FWT,   # Main Font/Colors
                     "MBCOLOR":MBCOLOR, "MFCOLOR":MFCOLOR, "MFNAME":MFNAME, "MFWT":MFWT, # Menu Font/Colors
                     "HBCOLOR":HBCOLOR, "HFCOLOR":HFCOLOR, "HFNAME":HFNAME, "HFWT":HFWT, # Help Font/Colors
                     "MBARCOL":MBARCOL, "QUOTECHAR":QUOTECHAR, "STARTDIR":STARTDIR, "CMDSHELL":CMDSHELL}    # Other

# 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)

# 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)


#####
# 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, '-c:d:hqrtv')
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 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)

# Parse the configuration file, but suppress options
# processing - on startup this is done just before
# we enter the main message loop to make sure we
# pickup any options changes from the environment
# variable or command line.

ParseConfFile(None, DoOptionsProcessing=FALSE)

# Process the rest of the options, if any

for opt, val in opts:
    if opt == "-d":
        DEBUGLEVEL = val
        ValidateDebugLevel()

        # If we're going to be dumping debug info, print header
        if DEBUGLEVEL:
            print dHEADER % time.asctime()

    if opt == "-q":
        WARN = FALSE
    if opt == "-r":
        AUTOREFRESH = FALSE
    if opt == "-t":
        QUOTECHAR = ""
    if opt == "-x":
        WIDTH = val
    if opt == "-y":
        HEIGHT = val

# Figure out where to start - Environment/Command Line overrides config
# file STARTDIR option.  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)

    # Save this value as the default for STARTDIR
    UI.OptionsString["STARTDIR"] = STARTDIR

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

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

# Process options to catch any changes detected in
# environment variable or command line

ProcessOptions()

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

# Run the program interface
UIroot.mainloop()