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

PROGNAME = "twander"
RCSID = "$Id: twander.py,v 1.100 2002/12/12 17:52:55 tundra Exp $"
VERSION = RCSID.split()[2]


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

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

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

# Imports conditional on OS

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

OSNAME = os.name


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


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

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

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

# General Program Commands

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
RUNCMD    = '<Control-z>'                # Run arbitrary user command
TOGDETAIL = '<Control-t>'                # Toggle detail view

# Directory Navigation

CHANGEDIR = '<Control-x>'                # Enter a new path
DIRHOME   = '<Control-h>'                # Goto $HOME
DIRBACK   = '<Control-b>'                # Goto previous directory
DIRSTART  = '<Control-s>'                # Goto starting directory
DIRUP     = '<Control-u>'                # Go up one directory level
MSEBACK   = '<Control-Double-ButtonRelease-1>'  # Go back one directory with mouse
MSEUP     = '<Control-Double-ButtonRelease-3>'  # Go up one directory with mouse

# Selection Keys

SELALL    = '<Control-comma>'            # Select all items
SELNEXT   = '<Control-n>'                # Select next item
SELNONE   = '<Control-period>'           # Unselect all items
SELPREV   = '<Control-p>'                # Select previous item
SELEND    = '<Control-e>'                # Select bottom item
SELTOP    = '<Control-a>'                # Select top item
SELKEY    = '<Control-space>'            # Select item w/keyboard
SELMOUSE  = '<Double-ButtonRelease-1>'   # Select item w/mouse

# Intra-Display Movement

PGDN      = '<Control-v>'                # Move page down
PGUP      = '<Control-c>'                # Move page up


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

# Default startup directory
STARTDIR = "." + os.sep

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


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

HEIGHT   = 25
WIDTH    = 90

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

BCOLOR  = "black"
FCOLOR  = "green"


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

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


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


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



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

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


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


AUTOREFRESH  = TRUE             # Automatically refresh the directory display
DEBUG        = FALSE            # Debugging on
WARN         = TRUE             # Warnings on

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

# General constants

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


# Stat-related

# Permissions

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

# Special file type lookup

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

# Size of each status display field including trailing space

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

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


# String used to separate symlink entry from its real path

SYMPTR        = " -> "


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

COMMANDMENU = 'Commands'      # Title for Command Menu button
DIRMENU     = 'Directories'   # Title for Directory Menu button
PSEP        = os.sep          # Character separating path components


#####
# Configuration File Related Literals
#####


ASSIGN     = "="               # Assignment for variable definitions
CONF       = ""                # Config file user selected with -c option
COMMENT    = r"#"              # Comment character
ENVVBL     = r'$'              # Symbol denoting an environment variable
MAXNESTING = 32                # Maximum depth of nested variable definitions
QUOTECHAR  = '\"'              # Character to use when quoting builtin substitutions
reVAR      = r"\[.+?\]"        # Regex describing variable notation


# Builtins

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


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


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

# Errors

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

# Information

iNOSTAT     = "Details Unavailable For This File ->"

# Prompts

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


# Warnings

wCMDKEY     = "Configuration File Entry For: \'%s\' Has No Command Key Defined."
wCONFOPEN   = "Cannot Open Configuration File:\n%s\n\n%s"
wNOCMDS     = "Running With No Commands Defined!"
wSYMBACK    = " Symbolic Link %s Points Back To Own Directory"
wWARN       = "WARNING"


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

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


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


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


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

def ErrMsg(emsg):

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

# End of 'ErrMsg()'


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

def FileLength(flen):

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

    return flen

# End of 'FileLength()'


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

def ParseConfFile(event):
    global CONF, UI

    # If user specified a config file, try that
    # Otherwise use HOME == either $HOME or ./

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

    # Successful open of config file - Begin processing it
    
    # Cleanout any old configuration data
    UI.CmdTable = {}
    UI.SymTable = {}
    linenum = 0

    # Initialize the command menu
    UI.CmdBtn.menu.delete(0,END)
    
    # Process and massage the configuration file
    for line in cf.read().splitlines():
        linenum += 1

        # Lex for comment token and discard until EOL.

        idx = line.find(COMMENT)
        if idx > -1:
            line = line[:idx]

        # Parse whatever is left on non-blank lines
        if line:
            ParseLine(line, linenum)

    cf.close()

    # Set the Command Menu Contents
    UI.CmdBtn['menu'] = UI.CmdBtn.menu

    # Dump tables if we're debugging
    if DEBUG:
        print "SYMBOL TABLE:\n"
        for sym in UI.SymTable.keys():
            print sym + " " * (16-len(sym)) + UI.SymTable[sym]

        print"\nCOMMAND TABLE:\n"
        for key in UI.CmdTable.keys():
            name = UI.CmdTable[key][0]
            cmd  = UI.CmdTable[key][1]
            print key + " " + name + " " * (16-len(name)) + cmd
        

# End of 'ParseConfFile()'


#####
# Parse A Line From A Configuration File
# Routine Assumes That Comments Previously Removed
#####


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

    fields = line.split()

    # Make a copy of the fields 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.
    #

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

        # Warn on variable redefinitions
        if UI.SymTable.has_key(name):
            ErrMsg(eREDEFVAR % (name, num, line))
            sys.exit(1)
            
        UI.SymTable[name] = val

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

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

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

            # Evaluate the command line, replacing
            # variables as needed

            doeval = TRUE
            depth  = 0

            while doeval:
                # Get a list of variable references
                vbls = revar.findall(cmd)

                # Throw away references to builtins - 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 builtins here - They are
                    # processed at runtime.
                    
                    if UI.BuiltIns.has_key(x):
                        vbls.remove(x)

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

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

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

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

                # No substitutions left to do
                else:
                    doeval = FALSE
                        
                # Bound the number of times we can nest a definition
                # to prevent self-references which give infinite nesting depth

                depth += 1
                if depth == MAXNESTING:
                    doeval = FALSE

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

            # Add the command entry to the command table.
            # Prevent duplicate keys from being entered.
            
            if UI.CmdTable.has_key(cmdkey):
                ErrMsg(eDUPKEY % (cmdkey, num, line))
                sys.exit(1)
            else:
                UI.CmdTable[cmdkey] = [cmdname, cmd]
                UI.CmdBtn.menu.add_command(label=cmdname, command=lambda cmd=cmdkey: CommandMenuSelection(cmd))

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

# End of 'ParseLine()'


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

def Usage():
    ustring =""
    
    for x in uTable:
        print x
# End of 'Usage()'


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

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

# End of 'WrnMsg()'


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



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


class twanderUI:

    def __init__(self, root):

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

        self.CmdBtn = Menubutton(self.mBar, text=COMMANDMENU, underline=0)
        self.CmdBtn.menu = Menu(self.CmdBtn)
        self.CmdBtn.pack(side=LEFT, padx='2m')

        # Setup the Directory Menu

        self.DirBtn = Menubutton(self.mBar, text=DIRMENU, underline=0)
        self.DirBtn.menu = Menu(self.DirBtn)
        self.DirBtn.pack(side=LEFT, padx='2m')

        # Setup the visual elements

        self.hSB = Scrollbar(root, orient=HORIZONTAL)
        self.vSB = Scrollbar(root, orient=VERTICAL)
        self.DirList = Listbox(root,
                               foreground = FCOLOR,
                               background  = BCOLOR,
                               font=(FNAME, FSZ, FWT),
                               selectmode=EXTENDED,
                               exportselection=0,
                               xscrollcommand=self.hSB.set,
                               yscrollcommand=self.vSB.set,
                               height = HEIGHT,
                               width = WIDTH,
                               )

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

        #####
        # Bind the relevant event handlers
        #####
        
        # General Program Commands

        # Bind handler for individual keystrokes
        self.DirList.bind(KEYPRESS, KeystrokeHandler)

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

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

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

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

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


        # Directory Navigation

        # Bind handler for "Change Directory"
        self.DirList.bind(CHANGEDIR, ChangeDir)

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

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

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

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

        # Bind handler for "Mouse Back Dir"
        self.DirList.bind(MSEBACK, KeyBackDir)

        # Bind handler for "Mouse Up Dir"
        self.DirList.bind(MSEUP, KeyUpDir)


        # Selection Keys

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

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

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

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

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

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

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

        # Bind handler for "Mouse Select"
        self.DirList.bind(SELMOUSE, DirListHandler)


        # Intra-display movement

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

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


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

        # End if method 'twanderUI.__init__()'


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

    def AllSelection(self):
        sellist = []

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

        return sellist

    # End of method 'twanderUI.AllSelection()'
        

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

    def LastInSelection(self):

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

    # End of method 'twanderUI.LastInSelection()'


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

    def poll(self):

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

        # See if its time to do a refresh

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

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

    # End of method 'twanderUI.poll()'


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

    def SetDetailedView(self, details):

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


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

    # End of method 'twanderUI.SetDetailedView()'


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

    def UpdateTitle(self, mainwin):

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

    # End of method 'twanderUI.UpdateTitle()'

# End of class definition, 'twanderUI'


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


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


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

def KeystrokeHandler(event):

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

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


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

    if cmd:
        # Replace runtime-determined tokens

        # First do any prompting required

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

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

            # Null input means the command is being aborted
            else:
                return
            
        # Now do files & directories

        selection = UI.LastInSelection()

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

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

        # Just dump command if we're debugging

        if DEBUG:
            print cmd
    
        # Otherwise,actually execute the command
        else:
            if OSNAME == 'nt':
                thread.start_new_thread(os.system, (cmd,))
            else:
                os.system(cmd)
    
# end of 'KeystrokeHandler()'
    

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

def KeyQuitProg(event):
    sys.exit()

# End of 'KeyQuitProg()'


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

def KeyRunCommand(event):

    cmd = askstring(pRUNCMD, pENCMD)
    if cmd:
        thread.start_new_thread(os.system, (cmd,))
    UI.DirList.focus()

# End of 'KeyRunCommand()'


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

def KeyToggleDetail(event):

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

# End of 'KeyToggleDetail()'


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


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

def ChangeDir(event):

    newpath = askstring(pCHPATH, pENPATH)
    if newpath:
        LoadDirList(newpath)
        KeySelTop(event)
    UI.DirList.focus()

# End of 'ChangeDir()'


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

def KeyHomeDir(event):

    if HOME:
        LoadDirList(HOME)
    else:
        LoadDirList(STARTDIR)

# 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 Back to Initial Directory
#####

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

# End of 'KeyStartDir()'


#####
# Event Handler: Move up one directory
#####

def KeyUpDir(event):

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

# End of 'KeyUpDir()'


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


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

def KeySelAll(event):
    UI.DirList.selection_set(0, END)

# End of 'KeySelAll()'


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

def KeySelNext(event):
    next = UI.DirList.index(ACTIVE) + 1
    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) - 1
    SetSelection((str(prev),), prev)

# End of 'KeySelPrev()'


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

def KeySelEnd(event):

    # Get current number of items in listbox
    sz = UI.DirList.size()

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

# End of 'KeySelEnd()'


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

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

# End of 'KeySelTop()'


#####
# Process Current Selection
#####

def DirListHandler(event):
    global UI
    
    SAVE = TRUE

    # Get current selection.  If none, just return, otherwise process
    selected =  UI.LastInSelection()
    if not selected:
        return
    
    # If selection is a directory, move there and list contents.

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

        # If we're on Unix, don't follow symlinks pointing back to themselves

        if OSNAME == 'posix' and os.path.samefile(UI.CurrentDir, UI.CurrentDir + selected):
            WrnMsg(wSYMBACK % (UI.CurrentDir + selected))
            return

        # We don't push this selection on the stack if
        # we are at root directory and user presses '..'

        if (selected == '..') and (UI.CurrentDir == os.path.abspath(PSEP)):
            SAVE = FALSE

        # 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:/".

        selected += PSEP

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

        # 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':
        os.startfile(os.path.join(os.path.abspath(UI.CurrentDir), selected))

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


    # Have to update the window title because selection changed
    UI.UpdateTitle(UIroot)

# End of 'DirListHandler()'


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

def LoadDirList(newdir, save=TRUE):

    # Get path into canonical form
    newdir = os.path.abspath(newdir)

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

    # 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

    # Do not do this if we've been told not to OR if
    # what we're about to save is the same as the top
    # of the stack OR if the current directory is ""
    
    # If there is anything on the stack, see if last element
    # matches what we're about to put there.
    
    if UI.LastDir and UI.LastDir[-1] == UI.CurrentDir:
        save = FALSE

    if save and UI.CurrentDir:
        UI.LastDir.append(UI.CurrentDir)

    # 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
    os.chdir(newdir)

    # Keep list of all unique directories visited in the Directory Menu
    
    if newdir not in UI.AllDirs:
        UI.AllDirs.append(newdir)
        UI.AllDirs.sort()
        UI.DirBtn.menu.delete(0,END)
        for dir in UI.AllDirs:
            UI.DirBtn.menu.add_command(label=dir, command=lambda dir=dir: LoadDirList(dir))
        UI.DirBtn['menu'] = UI.DirBtn.menu

    # And update the title to reflect changes
    UI.UpdateTitle(UIroot)

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

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

    # Put each in sorted order
    
    dList.sort()
    fList.sort()


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

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

    # If user has not requested detailed display, we're done
    if not UI.DetailsOn:
        return dList + fList

    # Detailed display requested, do the work

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

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

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

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

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

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

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

        # Set the permission bits

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

        # Deal with the special permissions

        sp = int(modestr[-4])

        # Sticky Bit

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

        # Setgid Bit

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

        # Setuid Bit

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

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

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

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

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

        if OSNAME == 'posix':
            owner = pwd.getpwuid(stinfo[ST_UID])[0][:ST_SZUNAME-1]
            group = grp.getgrgid(stinfo[ST_GID])[0][:ST_SZGNAME-1]

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

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

        # Add them to the detail

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

        # Length

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

        # mtime

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

        # Pad single-digit dates with leading space

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

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

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

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

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

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

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

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

            detlist[index] += SYMPTR + f

    return detlist

# End of  'BuildDirList()'


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

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

# End of 'KeyPageDown()'


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

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


# End of 'KeyPageUp()'


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


#####
# 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)
    SetSelection(sellist, active)

    # Restore scroll positions

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

    # Release the mutex
    UI.DirListMutex.unlock()

# End of 'RefreshDirList()


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

def SetSelection(selection, active):

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

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

    # And bounds check/adjust

    if active > maxindex:
        active = maxindex

    # Set desired selected items, if any

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

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

# End of 'SetSelection()'


#---------------------- Menu Handlers ---------------------#

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

def CommandMenuSelection(cmdkey):

    class event:
        pass

    event.char = cmdkey
    KeystrokeHandler(event)    

# End Of 'CommandMenuSelection()'


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

# Command line processing

try:
    opts, args = getopt.getopt(sys.argv[1:], '-b:c:df:hn:qrs:tvw:x:y:')
except getopt.GetoptError:
    Usage()
    sys.exit(1)

# Parse command line

for opt, val in opts:
    if opt == "-b":
        BCOLOR = val
    if opt == "-c":
        CONF = val
    if opt == "-d":
        DEBUG = TRUE
    if opt == "-f":
        FCOLOR = val
    if opt == "-h":
        Usage()
        sys.exit(0)
    if opt == "-n":
        FNAME = val
    if opt == "-q":
        WARN = FALSE
    if opt == "-r":
        AUTOREFRESH = FALSE
    if opt == "-s":
        FSZ = val
    if opt == "-t":
        QUOTECHAR = ""
    if opt == "-v":
        print RCSID
        sys.exit(0)
    if opt == "-w":
        FWT = val
    if opt == "-x":
        WIDTH = val
    if opt == "-y":
        HEIGHT = val


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

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

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

if len(args) == 1:
    STARTDIR = args[0]
    if not os.path.isdir(STARTDIR):
        ErrMsg(eBADROOT % STARTDIR)
        sys.exit(1)

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

# Setup builtin variables
UI.BuiltIns = {DIR:"", SELECTION:"", SELECTIONS:"", DSELECTION:"",
               DSELECTIONS:"", PROMPT:""}

# Parse the and store configuration file, if any
ParseConfFile(None)

# Initialize directory stack
UI.LastDir    = []

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

# And current location
UI.CurrentDir = ""

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

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

# Initialize the polling counter
UI.ElapsedTime = 0

# Start in detailed mode
UI.SetDetailedView(TRUE)

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

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

# Run the program interface
UIroot.mainloop()