#!/usr/bin/env python # twander - Wander around the file system # Copyright (c) 2002 TundraWare Inc. All Rights Reserved. # For Updates See: http://www.tundraware.com/Software/twander PROGNAME = "twander" RCSID = "$Id: twander.py,v 2.15 2002/12/15 22:29:56 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 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 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 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 # Intra-Display Movement PGDN = '<Control-v>' # Move page down PGUP = '<Control-c>' # Move page up # Execute Commands RUNCMD = '<Control-z>' # Run arbitrary user command SELKEY = '<Control-space>' # Select item w/keyboard SELMOUSE = '<Double-ButtonRelease-1>' # Select item w/mouse ##### # System-Related Defaults ##### # Default startup directory STARTDIR = os.path.abspath("." + os.sep) # Home director HOME = os.getenv("HOME") or STARTDIR ##### # Program Constants ##### HEIGHT = 25 WIDTH = 90 ##### # Colors ##### BCOLOR = "black" FCOLOR = "green" ##### # Fonts ##### FNAME = "Courier" FSZ = 12 FWT = "bold" #------------------- Nothing Below Here Should Need Changing ------------------# #----------------------------------------------------------# # Constants & Literals # #----------------------------------------------------------# ##### # Booleans ##### # Don't need to define TRUE & FALSE - they are defined in the Tkinter module ##### # Defaults ##### AUTOREFRESH = TRUE # Automatically refresh the directory display 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 # #----------------------------------------------------------# ##### # Debug, Error, Information, & Warning Messages ##### # Debug Strings dCMD = "COMMAND: " dCMDTBL = "<COMMAND TABLE>" dINTVAR = "<INTERNAL VARIABLES>" dKEYBINDS = "<KEY BINDINGS>" dNULL = "None" dSYMTBL = "<SYMBOL TABLE>" # 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 # Cleanout any old configuration data UI.CmdTable = {} 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 = {"KEYPRESS":KEYPRESS, "QUITPROG":QUITPROG, "READCONF":READCONF, "REFRESH":REFRESH, "TOGDETAIL":TOGDETAIL, "CHANGEDIR":CHANGEDIR, "DIRHOME":DIRHOME, "DIRBACK":DIRBACK, "DIRSTART":DIRSTART, "DIRUP":DIRUP, "MOUSEBACK":MOUSEBACK, "MOUSEUP":MOUSEUP, "SELALL":SELALL, "SELNEXT":SELNEXT, "SELNONE":SELNONE, "SELPREV":SELPREV, "SELEND":SELEND, "SELTOP":SELTOP, "PGDN":PGDN, "PGUP":PGUP, "RUNCMD":RUNCMD, "SELKEY":SELKEY, "SELMOUSE":SELMOUSE } # Initialize the command menu UI.CmdBtn.menu.delete(0,END) # If user specified a config file, try that # Otherwise use HOME == either $HOME or ./ if not CONF: CONF = os.path.join(HOME, "." + PROGNAME) try: cf = open(CONF) except: WrnMsg(wCONFOPEN % (CONF, wNOCMDS)) return # Successful open of config file - Begin processing it # Process and massage the configuration file for line in cf.read().splitlines(): linenum += 1 # 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) # Rebind all the handlers UI.BindAllHandlers() # Close the config file cf.close() # Set the Command Menu Contents UI.CmdBtn['menu'] = UI.CmdBtn.menu # Dump tables if we're debugging if DEBUG: print dSYMTBL + "\n" for sym in UI.SymTable.keys(): print sym + " " * (16-len(sym)) + UI.SymTable[sym] print print dCMDTBL + "\n" for key in UI.CmdTable.keys(): name = UI.CmdTable[key][0] cmd = UI.CmdTable[key][1] print key + " " + name + " " * (16-len(name)) + cmd print print dKEYBINDS + "\n" for key in UI.KeyBindings.keys(): print key + " " * (10-len(key)) + UI.KeyBindings[key] print # End of 'ParseConfFile()' ##### # Parse A Line From A Configuration File # Routine Assumes That Comments Previously Removed ##### def ParseLine(line, num): global UI, PGUP 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) # Distinguish between internal program variables and # user-defined variables and act accordingly if name in UI.KeyBindings.keys(): UI.KeyBindings[name] = val else: UI.SymTable[name] = val ##### # Command Definitions ##### elif len(fields[0]) == 1: # Must have at least 3 fields for a valid command definition if len(fields) < 3: ErrMsg(eBADCFGLINE % (num, line)) sys.exit(1) else: cmdkey = fields[0] cmdname = fields[1] cmd = " ".join(fields[2:]) # Evaluate the command line, replacing # variables as needed doeval = TRUE depth = 0 while doeval: # 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) # End if method 'twanderUI.__init__()' ##### # Bind the relevant event handlers ##### def BindAllHandlers(self): # General Program Commands # Bind handler for individual keystrokes self.DirList.bind(self.KeyBindings["KEYPRESS"], KeystrokeHandler) # Bind handler for "Quit Program" self.DirList.bind(self.KeyBindings["QUITPROG"], KeyQuitProg) # Bind handler for "Read Config File" self.DirList.bind(self.KeyBindings["READCONF"], ParseConfFile) # Bind handler for "Refresh Screen" self.DirList.bind(self.KeyBindings["REFRESH"], RefreshDirList) # Bind handler for "Toggle Detail" self.DirList.bind(self.KeyBindings["TOGDETAIL"], KeyToggleDetail) # Directory Navigation # Bind handler for "Change Directory" self.DirList.bind(self.KeyBindings["CHANGEDIR"], ChangeDir) # 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 "Starting Dir" self.DirList.bind(self.KeyBindings["DIRSTART"], KeyStartDir) # Bind handler for "Up Dir" self.DirList.bind(self.KeyBindings["DIRUP"], KeyUpDir) # Bind handler for "Mouse Back Dir" self.DirList.bind(self.KeyBindings["MOUSEBACK"], KeyBackDir) # Bind handler for "Mouse Up Dir" self.DirList.bind(self.KeyBindings["MOUSEUP"], KeyUpDir) # Selection Keys # Bind handler for "Select All" self.DirList.bind(self.KeyBindings["SELALL"], KeySelAll) # Bind handler for "Next Item" self.DirList.bind(self.KeyBindings["SELNEXT"], KeySelNext) # Bind handler for "Select No Items" self.DirList.bind(self.KeyBindings["SELNONE"], KeySelNone) # 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) # Intra-display movement # 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) # 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["SELMOUSE"], DirListHandler) # 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 = [] 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 dCMD, cmd # Otherwise,actually execute the command else: thread.start_new_thread(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) # 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 = os.path.abspath(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 # List of internal program variables to dump during debug sessions DebugVars = ["OSNAME", "STARTDIR", "HOME", "CONF", "HEIGHT", "WIDTH", "BCOLOR", "FCOLOR", "FNAME", "FSZ", "FWT", "AUTOREFRESH", "DEBUG", "WARN", "QUOTECHAR"] # Dump program variable during debug sessions if DEBUG: print dINTVAR + "\n" for v in DebugVars: print v + " " * (12-len(v)) + (str(eval(v)) or dNULL) print # 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:""} # Prepare storage for key bindings UI.KeyBindings = {} # Parse the and store configuration file, if any ParseConfFile(None) # Setup event handlers UI.BindAllHandlers() # 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()