#!/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.52 2002/11/14 21:29:53 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=SINGLE, 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()