Newer
Older
twander / twander.py
#!/usr/bin/env python
# twander - Wander around the file system
# Copyright (c) 2002 TundraWare Inc.  All Rights Reserved.


PROGNAME = "twander"
RCSID = "$Id: twander.py,v 1.53 2002/11/14 21:48:24 tundra Exp $"
VERSION = RCSID.split()[2]


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

from Tkinter import *
import getopt
import os
import sys

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

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

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

GOHOME       = '<Control-h>'
GOPREV       = '<Control-p>' 
GOSTART      = '<Control-s>'
GOUP         = '<BackSpace>'
KEYPRESS     = '<KeyPress>'
QUITPROG     = '<Escape>'
READCONF     = '<Control-r>'
SELECTKEY    = '<space>'
SELECTMOUSE  = '<ButtonRelease-1>'


#####
# Default Directories
#####

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

# Default directory for config file
# Use $HOME if available, otherwise use starting directory

HOME = os.getenv("HOME") or STARTDIR

# Configuration file
CONF   = os.path.join(HOME + "." + PROGNAME)


#####
# Initial Dimensions
#####

HEIGHT = 25
WIDTH  = 60


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

BCOLOR  = "black"
FCOLOR  = "green"


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

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


#####
# Enable Warnings
#####

WARN = TRUE


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


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



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

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

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


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

DIR_LDELIM = '['              # Directory left dsply. delimiter
DIR_RDELIM = ']'              # Directory left dsply. delimiter
PSEP       = os.sep           # Character separating path components


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

CMDKEY     = r'&'             # Command key delimiter
COMMENT    = r"#"             # Comment character
DIRNAME    = r'[DIRECTORY]'   # Substitution field in config files
FILENAME   = r'[FILE]'        # Substitution field in config files
ENVIRO     = r'$'             # Introduces environment variables


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


#####
# Error & Warning  Messages
#####

eBADROOT = " %s Is Not A Directory"
eDIRRD   = "Cannot Open Directory : %s  ---  Check Permissions."
eDUPKEY  = "Duplicate Key In Configuration File Found In Entry: \'%s\'"
eERROR   = "ERROR"
eNOCONF  = "Cannot Find Configuration File: %s"
eNOENV   = "Configuration File References Undefined Environment Variable: %s"
eOPEN    = "Cannot Open File: %s"
eTOOMANY = "You Can Only Specify One Starting Directory."

wCMDKEY  = "Configuration File Entry For: \'%s\' Has No Command Key Defined."
wWARN    = "WARNING"


#####
# Usage Prompts
#####

uTable = [PROGNAME + " " + VERSION + " - Copyright 2002, TundraWare Inc., All Rights Reserved\n",
          "usage:  " + PROGNAME + " [-bcfhnsvwxy] [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: " + CONF + ")",
          "          -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)",
          "          -s size   size of font to use (default: 12)",
          "          -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):
    print PROGNAME + " " + VERSION + " " + eERROR + ": " + emsg

# End of 'ErrMsg()'


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

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

# End of 'WrnMsg()'


#####
# 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(*args):
    global UI
    
    try:
        cf = open(CONF)
    except:
        ErrMsg(eOPEN % CONF)
        sys.exit(1)

    # Cleanout any old 
    UI.rcfile = {}

    # Process and massage the configuration file
    for line in cf.read().splitlines():

        # Lex for comment token and discard until EOL
        # A line beginning with the comment token is thus
        # turned into a blank line, which is discarded.

        idx = line.find(COMMENT)
        if idx > -1:             # found a comment character
            line = line[:idx]

        # Anything which gets through the next conditional
        # must be a non-blank line - i.e., Configuration information
        # we care about, so process it and save for future use.

        if line != "":
            fields = line.split()
            for x in range(1,len(fields)):

                # Process environment variables
                if fields[x][0] == ENVIRO:
                    envval = os.getenv(fields[x][1:])
                    if not envval:       # Environment variable not defined
                        ErrMsg(eNOENV % fields[x])
                        sys.exit(1)
                    else:
                        fields[x] = envval

            # Get command key value and store in dictionary

            keypos = fields[0].find(CMDKEY) # Look for key delimiter

            # No delimiter or delimiter at end of string
            if (keypos < 0) or (CMDKEY == fields[0][-1]):
                WrnMsg(wCMDKEY % fields[0])
                key = fields[0]             # Use whole word as index

            # Found legit delimiter, so use it as
            # dictionary index - mapped to lower case so
            # command keys are case insensitive.
            else:
                key = fields[0][keypos+1].lower()

            if key in UI.rcfile:               # This is a Python 2.2 or later idiom
                ErrMsg(eDUPKEY % fields[0]) # Duplicate key found
                cf.close()
                sys.exit(1)
                
            # Save command name and command using key as index
            UI.rcfile[key] = ["".join(fields[0].split(CMDKEY)),
                           " ".join(fields[1:])
                          ]

    cf.close()

# End of 'ParseConfFile()'


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

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


#----------------------------------------------------------#
#        GUI Classes, Handlers, & Support Functions        #
#----------------------------------------------------------#



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


class twanderUI:

    def __init__(self, root):

        # 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=BROWSE,
                               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 widget event handlers
        #####
        
        # Bind the relevant root window handlers

        # Set up keystroke handler for application
        # These will be checked against the command
        # key definitions in the configuration file
        root.bind(KEYPRESS, KeystrokeHandler)

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

        # Bind handler for "Home Dir"
        root.bind(GOHOME, KeyHomeDir)

        # Bind handler for "Previous Dir"
        root.bind(GOPREV, KeyPrevDir)

        # Bind handler for "Starting Dir"
        root.bind(GOSTART, KeyStartDir)

        # Bind handler for "Up Dir"
        root.bind(GOUP, KeyUpDir)

        # Bind handler of "Read Config File"
        root.bind(READCONF, ParseConfFile)

        # Bind handler for "Item Select"
        root.bind(SELECTKEY, DirListHandler)

        # We'll accept Single-Clicks as a selection
        self.DirList.bind(SELECTMOUSE, DirListHandler)

        # Give the listbox focus so arrow keys work
        self.DirList.focus()

        # End if method 'twanderUI.__init__()'


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

    def poll(self):

        RefreshDirList()
        self.DirList.after(500, self.poll)

    # End of method 'twanderUI.poll()'


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

    def CurrentSelection(self):
        index = self.DirList.curselection()
        if index:
            return self.DirList.get(index)
        else:
            return ""

    # End of method 'twanderUI.CurrentSelection()'


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

    def UpdateTitle(self, mainwin):

        # Get current selection
        selected = self.CurrentSelection()

        # If there was a file selection append to title
        if selected \
                 and selected[0] == DIR_LDELIM \
                 and selected[-1] == DIR_RDELIM:

            selected = ""

        # Update the titlebar
        mainwin.title(PROGNAME + " " + VERSION + "       " + UI.rootdir + selected)

    # End of method 'twanderUI.UpdateTitle()'

# End of class definition, 'twanderUI'


#####
# Event Hander: Process Current Selection
#####

def DirListHandler(event):

    # Get current selection.  If none, just return, otherwise process
    selected =  UI.CurrentSelection()
    if not selected:
        return
    
    # If selection is a directory, move there and list contents.
    # We examine this by checking the string for the directory
    # delimiter characters previously inserted in BuildDirList()
    
    if selected[0] == DIR_LDELIM and selected[-1] == DIR_RDELIM:

        # Strip off delimiters to get real name
        selected = selected[1:-1]

        # Build full path name
        selected = os.path.join(os.path.abspath(UI.rootdir), 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)

        # 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
        
        UI.DirList.selection_set(('0',))

    # File selected
    else:
        pass

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

# End of 'DirListHandler()'


#####
# Get Directory Of Current UI.rootdir And Load Into UI
#####

def LoadDirList(newdir, save=TRUE):
    
    # Canonicalize the current directory name
    newdir = os.path.abspath(newdir)

    # 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
    
    # If there is anything on the stack, see if last element
    # matches what we're about to put there.
    
    if UI.lastdir:
        if UI.lastdir[-1] == UI.rootdir:
            save = FALSE
    if save:
        UI.lastdir.append(UI.rootdir)

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

    # And make sure it ends with a path separator character
    if UI.rootdir[-1] != PSEP:
        UI.rootdir = UI.rootdir + PSEP

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

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

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

# End of 'LoadDirList():


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

def BuildDirList(rootdir):

    dList, fList = [], []
    
    # Walk the directory separate subdirs and files
    try:
        for file in os.listdir(rootdir):
            if os.path.isdir(os.path.join(rootdir,file)):
                dList.append(DIR_LDELIM + file + DIR_RDELIM)
            else:
                fList.append(file)
    except:
        ErrMsg(eDIRRD % rootdir)

    dList.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 "."

    if rootdir != os.path.abspath(PSEP):
        dList.insert(0, DIR_LDELIM + ".." + DIR_RDELIM)

    fList.sort()
    return dList + fList

# End of  'BuildDirList()'


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

def RefreshDirList(*args):


    # Save current selection and active entry
    number = 0
    active=UI.DirList.index(ACTIVE)
    
    index = UI.DirList.curselection()
    if index:
        number = UI.DirList.index(index)

    LoadDirList(UI.rootdir, save=FALSE)

    
    # Restore current selection and active entry
    # Make sure they are still in range singe we
    # may have fewer items after the refresh.
    # If out of range, just set to last item in list
    
    maxindex =  UI.DirList.size() - 1

    if number > maxindex:
        number = maxindex
    if active >maxindex:
       active = maxindex
    
    UI.DirList.select_set(number)
    UI.DirList.see(number)

    UI.DirList.activate(active)

    # We have to update the title because the selection
    # may have changed.

    UI.UpdateTitle(UIroot)

# End of 'RefreshDirList()


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

def KeyHomeDir(event):

    LoadDirList(HOME)

# End of 'KeyHomeDir()'


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

def KeyUpDir(event):

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

# End of 'KeyUpDir()'


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

def KeyQuitProg(event):
    sys.exit()

# End of 'KeyQuitProg()'


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

def KeyPrevDir(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 'KeyPrevDir()'


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

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

# End of 'KeyStartDir()'


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

def KeystrokeHandler(event):

    # If the key pressed is a command key,
    # get its associated string and
    # execute the command.

    cmd = UI.rcfile.get(event.char.lower(), ["",""])[1]

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

        # Replace runtime-determined tokens
        cmd = cmd.replace(FILENAME, UI.CurrentSelection())
        cmd = cmd.replace(DIRNAME, UI.rootdir)

        # Actually execute the command
        os.system(cmd)
    
# end of 'KeystrokeHandler()'
    

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

# Newline to make sure cursor of invoking window is
# at LHS in case we get errors or warnings.

print ""

# Command line processing

try:
    opts, args = getopt.getopt(sys.argv[1:], '-b:c:f:hn:qs:vw: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 == "-f":
        FCOLOR = val
    if opt == "-h":
        Usage()
        sys.exit(0)
    if opt == "-n":
        FNAME = val
    if opt == "-q":
        WARN = FALSE
    if opt == "-s":
        FSZ = val
    if opt == "-v":
        print RCSID
        sys.exit(0)
    if opt == "-w":
        FWT = val
    if opt == "-x":
        WIDTH = val
    if opt == "-y":
        HEIGHT = val

# This program requires a config file

if not os.path.exists(CONF):
    ErrMsg(eNOCONF % CONF)
    sys.exit(1)

# Create an instance of the UI
UIroot = Tk()
UI = twanderUI(UIroot)

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

UI.rootdir = STARTDIR

# Initialize directory stack
UI.lastdir      = []

# Parse contents into dictionary

ParseConfFile()

# Canonicalize UI.rootdir
UI.rootdir = os.path.abspath(UI.rootdir) + PSEP

# Initialize the UI directory listing
LoadDirList(UI.rootdir)

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

# Run the program interface
UIroot.mainloop()