#!/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.66 2003/01/17 20:58:49 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 askyesno, 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 == 'nt': try: from win32api import GetLogicalDriveStrings as GetDrives except: def GetDrives(): return "" if OSNAME == 'posix': import grp import pwd #----------------------------------------------------------# # Variables User Might Change # #----------------------------------------------------------# ##### # Defaults ##### ##### # Key Assignments ##### # General Program Commands CLRHIST = '<Control-w>' # Clear Command History MOUSECTX = '<ButtonRelease-3>' # Pop-up Context Menu MOUSEDIR = '<Shift-ButtonRelease-3>' # Pop-up Directory Menu KEYPRESS = '<KeyPress>' # Any keypress (for commands) QUITPROG = '<Control-q>' # Quit the program READCONF = '<Control-r>' # Re-read the configuration file REFRESH = '<Control-l>' # Refresh screen TOGDETAIL = '<Control-t>' # Toggle detail view # Directory Navigation CHANGEDIR = '<Control-x>' # Enter a new path DIRHOME = '<Control-h>' # Goto $HOME DIRBACK = '<Control-b>' # Goto previous directory DIRROOT = '<Control-j>' # Goto root directory DIRSTART = '<Control-s>' # Goto starting directory DIRUP = '<Control-u>' # Go up one directory level DRIVELIST = '<Control-k>' # On Win32, display Drive List View if possible MOUSEBACK = '<Control-Double-ButtonRelease-1>' # Go back one directory with mouse MOUSEUP = '<Control-Double-ButtonRelease-3>' # Go up one directory with mouse # Selection Keys SELALL = '<Control-comma>' # Select all items SELINV = '<Control-i>' # Invert the current selection SELNONE = '<Control-period>' # Unselect all items SELNEXT = '<Control-n>' # Select next item SELPREV = '<Control-p>' # Select previous item SELEND = '<Control-e>' # Select bottom item SELTOP = '<Control-a>' # Select top item # Scrolling Commands PGDN = '<Control-v>' # Move page down PGUP = '<Control-c>' # Move page up PGRT = '<Control-g>' # Move page right PGLFT = '<Control-f>' # Move page left # Execute Commands RUNCMD = '<Control-z>' # Run arbitrary user command SELKEY = '<Return>' # Select item w/keyboard MOUSESEL = '<Double-ButtonRelease-1>' # Select item w/mouse # Name The Key/Mouse Assignments Which We Do Not Allow To Be Rebound In The Config File NOREBIND = ["MOUSECTX", "MOUSEDIR", "MOUSEBACK", "MOUSEUP", "MOUSESEL"] ##### # 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 DEBUGLEVEL = 0 # No debug output NODETAILS = FALSE # TRUE means details can never be displayed NONAVIGATE = FALSE # TRUE means that all directory navigation is prevented 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 # Key & Button Event Masks ShiftMask = (1<<0) LockMask = (1<<1) ControlMask = (1<<2) Mod1Mask = (1<<3) Mod2Mask = (1<<4) Mod3Mask = (1<<5) Mod4Mask = (1<<6) Mod5Mask = (1<<7) Button1Mask = (1<<8) Button2Mask = (1<<9) Button3Mask = (1<<10) Button4Mask = (1<<11) Button5Mask = (1<<12) # There are some event bits we don't care about - # We'll use the following constant to mask them out # later in the keyboard and mouse handlers. DontCareMask = LockMask | Mod2Mask | Mod3Mask | Mod4Mask | Mod5Mask # Some things are OS-dependent if OSNAME == 'nt': AltMask = (1<<17) DontCareMask |= Mod1Mask # Some versions of Win32 set this when Alt is pressed else: AltMask = Mod1Mask # Stat-Related Constants # 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 HISTMENU = 'History' # Title for History Menu button PSEP = os.sep # Character separating path components SHOWDRIVES = '\\\\' # Logical directory name for Win32 Drive Lists ##### # GUI-Related Contsants #### ROOTBORDER = 1 MENUBORDER = 2 MENUPADX = 2 MENUOFFSET = ROOTBORDER + MENUBORDER + MENUPADX ##### # Configuration File Related Constants ##### ASSIGN = "=" # Assignment for variable definitions CONF = "" # Config file user selected with -c option COMMENT = r"#" # Comment introducer string ENVVBL = r'$' # Symbol denoting an environment variable MAXDIR = 32 # Maximum number of directories to track MAXHIST = 32 # Maximum length of Command History MAXNESTING = 32 # Maximum depth of nested variable definitions QUOTECHAR = '\"' # Character to use when quoting Built-In Variables # Variable Name Pattern Matching Stuff DIRSC = "DIRSC" # Directory Shortcut naming reDIRSC = r'^' + DIRSC + r'\d{1,2}$' # Regex describing Directory Shortcut names reVAR = r"\[.+?\]" # Regex describing variable notation # Create actual regex matching engines REDIRSC = re.compile(reDIRSC) REVAR = re.compile(reVAR) # Built-In Variables DIR = r'[DIR]' DSELECTION = r'[DSELECTION]' DSELECTIONS = r'[DSELECTIONS]' HASH = r'[HASH]' PROMPT = r'[PROMPT:' SELECTION = r'[SELECTION]' SELECTIONS = r'[SELECTIONS]' YESNO = r'[YESNO:' #----------------------------------------------------------# # Prompts, & Application Strings # #----------------------------------------------------------# ##### # Error, Information, & Warning Messages ##### # Errors eBADROOT = " %s Is Not A Directory" eDIRRD = "Cannot Open Directory : %s --- Check Permissions." eERROR = "ERROR" eINITDIRBAD = "Cannot Open Starting Directory : %s - Check Permissions - ABORTING!." eOPEN = "Cannot Open File: %s" eTOOMANY = "You Can Only Specify One Starting Directory." # Information iNOSTAT = "Details Unavailable For This File ->" # Prompts pCHPATH = "Change Path" pENCMD = "Enter Command To Run:" pENPATH = "Enter New Path Desired:" pMANUALCMD = "Manual Command Entry" pRUNCMD = "Run Command" # Warnings wBADCFGLINE = "Ignoring Line %s.\nBogus Configuration Entry:\n\n%s" wBADENVVBL = "Ignoring Line %s.\nEnvironment Variable %s Not Set:\n\n%s" wBADEXE = "Could Not Execute Command:\n\n%s" wBADRHS = "Ignoring Line %s.\nOption Assignment Has Bad Righthand Side:\n\n%s" wBADSCNUM = "Ignoring Line %s.\nShortcut Number Must Be From 1-12:\n\n%s" wCONFOPEN = "Cannot Open Configuration File:\n%s" wDIRSCREDEF = "Ignoring Line %s.\nDirectory Shortcut Defined More Than Once:\n\n%s" wDUPKEY = "Ignoring Line %s.\nFound Duplicate Command Key '%s':\n\n%s" wLINKBACK = "%s Points Back To Own Directory" wNOCMDS = "Running With No Commands Defined!" wNOREBIND = "Ignoring Line %s.\nCannot Rebind This Keyboard Or Mouse Button Combination:\n\n%s" wREDEFVAR = "Ignoring Line %s.\nVariable %s Redefined:\n\n%s" wUNDEFVBL = "Ignoring Line %s.\nUndefined Variable %s Referenced:\n\n%s" wVBLTOODEEP = "Ignoring Line %s.\nVariable Definition Nested Too Deeply:\n\n%s" wWARN = "WARNING" ##### # Debug-Related Stuff ##### # Debug Levels DEBUGQUIT = (1<<0) # Dump debug info and quit program DEBUGVARS = (1<<1) # Dump internal variables DEBUGSYMS = (1<<2) # Dump symbol table DEBUGCTBL = (1<<3) # Dump command table DEBUGCMDS = (1<<4) # Dump command execution string DEBUGKEYS = (1<<5) # Dump key bindings DEBUGDIRS = (1<<6) # Dump directory stack contents as it changes DEBUGHIST = (1<<7) # Dump contents of Command History stack after command execution # Debug Strings dCMD = "<COMMAND>" dCMDTBL = "<COMMAND TABLE>" dDIRSTK = "<DIRECTORY STACK>" dFUNCKEYS = "<FUNCTION KEYS / DIRECTORY SHORTCUTS>" dHEADER = "twander Debug Dump Run On: %s\n" dHIST = "<COMMAND HISTORY STACK>" dINTVAR = "<INTERNAL VARIABLES>" dKEYBINDS = "<KEY BINDINGS>" dNULL = "None" dOPTVAR = "<USER-SETTABLE OPTIONS>" dSYMTBL = "<SYMBOL TABLE>" # List of internal program variables to dump during debug sessions DebugVars = ["RCSID", "OSNAME", "HOSTNAME", "OPTIONS", "CONF", "HOME", "PSEP", "POLLINT"] ##### # 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 level set debugging level (default: 0, 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 Built-In Variables (default: quoting on)", " -v print detailed version information", " -w wght weight/style of font to use (default: bold)", " -x width window width (default: 90)", " -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()' ##### # Build List Of Win32 Drives ##### def GetWin32Drives(): # Get Win32 drive string, split on nulls, and get # rid of any resulting null entries. return filter(lambda x : x, GetDrives().split('\x00')) # End of 'GetWin32Drives()' ##### # Convert A File Size Into Equivalent String With Scaling # Files under 1 MB show actual length # Files < 1 MB < 1 GB shown in KB # Files 1 GB or greater, shown in MB ##### def FileLength(flen): if flen >= GB: flen = str(flen/MB) + "m" elif flen >= MB: flen = str(flen/KB) + "k" else: flen = str(flen) return flen # End of 'FileLength()' ##### # Parse & Process The Configuraton File # This is called once at program start time # and again any time someone hits the READCONF key # while the program is running. ##### def ParseConfFile(event, DoOptionsProcessing=TRUE): global CONF, UI # Cleanout any old configuration data UI.CmdTable = {} UI.FuncKeys = ["", "", "", "", "", "", "", "", "", "", "", ""] UI.SymTable = {} linenum = 0 # Unbind all existing key bindings for x in UI.KeyBindings.keys(): UI.DirList.unbind(UI.KeyBindings[x]) # Initialize keyboard binding variables to their defaults # These may be overriden in the configuration file # parsing process. UI.KeyBindings = {"CLRHIST":CLRHIST, "MOUSECTX":MOUSECTX, "MOUSEDIR":MOUSEDIR, "KEYPRESS":KEYPRESS, "QUITPROG":QUITPROG, "READCONF":READCONF, "REFRESH":REFRESH, "TOGDETAIL":TOGDETAIL, "CHANGEDIR":CHANGEDIR, "DIRHOME":DIRHOME, "DIRBACK":DIRBACK, "DIRROOT":DIRROOT, "DIRSTART":DIRSTART, "DIRUP":DIRUP, "DRIVELIST":DRIVELIST, "MOUSEBACK":MOUSEBACK, "MOUSEUP":MOUSEUP, "SELALL":SELALL, "SELINV":SELINV, "SELNONE":SELNONE, "SELNEXT":SELNEXT, "SELPREV":SELPREV, "SELEND":SELEND, "SELTOP":SELTOP, "PGDN":PGDN, "PGUP":PGUP, "PGRT":PGRT, "PGLFT":PGLFT, "RUNCMD":RUNCMD, "SELKEY":SELKEY, "MOUSESEL":MOUSESEL } # Set all the program options to their default values # This means that a configuration file reload can # override the options set previously in the environment # variable or on the command line. for x in (UI.OptionsBoolean, UI.OptionsNumeric, UI.OptionsString): for o in x.keys(): globals()[o] = x[o] # Initialize the command menu UI.CmdBtn.menu.delete(0,END) # And disable it UI.CmdBtn.config(state=DISABLED) # If user specified a config file, try that # Otherwise use HOME == either $HOME or ./ if not CONF: CONF = os.path.join(HOME, "." + PROGNAME) try: cf = open(CONF) # Successful open of config file - Begin processing it # Process and massage the configuration file for line in cf.read().splitlines(): linenum += 1 # Parse this line if line: ParseLine(line, linenum) # Close the config file cf.close() except: WrnMsg(wCONFOPEN % CONF) # Make sure any options we've changed are implemented if DoOptionsProcessing: ProcessOptions() # End of 'ParseConfFile()' ##### # Parse A Line From A Configuration File ##### def ParseLine(line, num): global UI # Get rid of trailing newline, if any if line[-1] == '\n': line = line[:-1] # Strip comments out idx = line.find(COMMENT) if idx > -1: line = line[:idx] # Split what's left into separate fields again fields = line.split() # Make a copy of the fields which is guaranteed to have at # least two fields for use in the variable declaration tests. dummy = fields[:] dummy.append("") ##### # Blank Lines - Ignore ##### if len(fields) == 0: pass ##### # Variable Definitions ##### # A valid variable definition can # be 1, 2, or 3 fields: # # x=y - 1 field # x= y - 2 fields # x =y # x = y - 3 fields # # But this is not legit: # # =....... # # However, the assignment character # must always been in the 1st or second # field. If it is a 3rd field, it is not # a variable definition, but a command definition. # # If the LHS is one of the Program Function Names # used in key binding, the statement is understood # to be a key rebinding, not a user variable definition. # # If the LHS is one of the Directory Shortcut variables, # the RHS is added to the Directory Menu and assigned # to the associated Function Key (1-12). # # Finally, the LHS cannot be one of the program # Built-In Variables - it is an error, for example, # to have something like: # # DIR = string # # because "DIR" is a Built-In Variable name. # elif ((dummy[0].count(ASSIGN) + dummy[1].count(ASSIGN)) > 0) and (fields[0][0] != ASSIGN): assign = line.find(ASSIGN) name = line[:assign].strip() val=line[assign+1:].strip() # Error out on variable redefinitions of either # existing user-defined variables or built-ins if UI.SymTable.has_key(name) or \ UI.BuiltIns.has_key('[' + name + ']') or \ UI.BuiltIns.has_key('[' + name): WrnMsg(wREDEFVAR % (num, name, line)) return # Handle Directory Shortcut entries. if REDIRSC.match(name): # Get shortcut number sc = int(name.split(DIRSC)[1]) # Process if 1-12 if 0 < sc < 13: # Ignore attempts to redefine a shortcut within the config file if UI.FuncKeys[sc-1]: WrnMsg(wDIRSCREDEF % (num, line)) return # Everything OK - process the entry else: # Associate the directory with the correct function key UI.FuncKeys[sc-1] = val # Add to Directory Menu making sure it is PSEP-terminated if val and (val[-1] != PSEP): val += PSEP if val: UpdateDirMenu(val) # User specified an invalid shortcut number else: WrnMsg(wBADSCNUM % (num, line)) return # Process any option variables elif name in UI.OptionsBoolean.keys(): val = val.upper() if val == 'TRUE' or val == 'FALSE': globals()[name] = eval(val) # !!! Cheater's way to get to global variables. else: WrnMsg(wBADRHS % (num, line)) return elif name in UI.OptionsNumeric.keys(): try: globals()[name] = int(val) except: WrnMsg(wBADRHS % (num, line)) return elif name in UI.OptionsString.keys(): # RHS cannot be null if not val: WrnMsg(wBADRHS % (num, line)) return else: globals()[name] = val # Process other variable types. # Distinguish between internal program variables and # user-defined variables and act accordingly. We inhibit # the rebinding of certain, special assignments, however. elif name in UI.KeyBindings.keys(): if name in NOREBIND: WrnMsg(wNOREBIND % (num, line)) return else: UI.KeyBindings[name] = val else: UI.SymTable[name] = val ##### # Command Definitions ##### elif len(fields[0]) == 1: # Must have at least 3 fields for a valid command definition if len(fields) < 3: WrnMsg(wBADCFGLINE % (num, line)) return else: cmdkey = fields[0] cmdname = fields[1] cmd = " ".join(fields[2:]) # Process any User-Defined variables cmd = ProcessVariables(cmd, num, line) # A null return means there was a problem - abort if not cmd: return # Add the command entry to the command table. # Prevent duplicate keys from being entered. if UI.CmdTable.has_key(cmdkey): WrnMsg(wDUPKEY % (num, cmdkey, line)) return else: UI.CmdTable[cmdkey] = [cmdname, cmd] UI.CmdBtn.menu.add_command(label=cmdname + " " * (15-len(cmdname)) + "(" + cmdkey + ")", command=lambda cmd=cmdkey: CommandMenuSelection(cmd)) else: WrnMsg(wBADCFGLINE % (num, line)) # End of 'ParseLine()' ##### # Print Debug Information On stdout ##### def PrintDebug(title, content): print title + '\n' if content: for i in content: print i else: print dNULL print # End of 'PrintDebug()' ##### # Strip Trailing Path Separator ##### def StripPSEP(s): if s and s[-1] == PSEP: return s[:-1] else: return s # End of 'StripPSEP()' ##### # Print Usage Information ##### def Usage(): for x in uTable: print x # End of 'Usage()' ##### # Print A Warning Message ##### def WrnMsg(wmsg): if WARN: showwarning(PROGNAME + " " + VERSION + " " + wWARN, wmsg) # End of 'WrnMsg()' #----------------------------------------------------------# # GUI Definition # #----------------------------------------------------------# ##### # Enacapsulate the UI in a class ##### class twanderUI: def __init__(self, root): # Setup Menubar frame self.mBar = Frame(root, relief=RAISED, borderwidth=MENUBORDER) self.mBar.pack(fill=X) # Setup the Command Menu self.CmdBtn = Menubutton(self.mBar, text=COMMANDMENU, underline=0, state=DISABLED) self.CmdBtn.menu = Menu(self.CmdBtn) self.CmdBtn.pack(side=LEFT, padx=MENUPADX) # Setup the Directory Menu self.DirBtn = Menubutton(self.mBar, text=DIRMENU, underline=0, state=DISABLED) self.DirBtn.menu = Menu(self.DirBtn) self.DirBtn.pack(side=LEFT, padx=MENUPADX) # Setup the History Menu self.HistBtn = Menubutton(self.mBar, text=HISTMENU, underline=0, state=DISABLED) self.HistBtn.menu = Menu(self.HistBtn) self.HistBtn.pack(side=LEFT, padx=MENUPADX) # Setup the Directory Listing and Scrollbars self.hSB = Scrollbar(root, orient=HORIZONTAL) self.vSB = Scrollbar(root, orient=VERTICAL) self.DirList = Listbox(root, selectmode=EXTENDED, exportselection=0, xscrollcommand=self.hSB.set, yscrollcommand=self.vSB.set) # Make them visible by packing self.hSB.config(command=self.DirList.xview) self.hSB.pack(side=BOTTOM, fill=X) self.vSB.config(command=self.DirList.yview) self.vSB.pack(side=RIGHT, fill=Y) self.DirList.pack(side=LEFT, fill=BOTH, expand=1) # End of method 'twanderUI.__init__()' ##### # Bind the relevant event handlers ##### def BindAllHandlers(self): ### # General Program Commands ### # Bind handler to invoke Clear Command History self.DirList.bind(self.KeyBindings["CLRHIST"], ClearHistory) # Bind handler to invoke Context Menu self.DirList.bind(self.KeyBindings["MOUSECTX"], MouseClick) # Bind handler to invoke Directory Menu self.DirList.bind(self.KeyBindings["MOUSEDIR"], MouseClick) # Bind handler for individual keystrokes self.DirList.bind(self.KeyBindings["KEYPRESS"], KeystrokeHandler) # Bind handler for "Quit Program" self.DirList.bind(self.KeyBindings["QUITPROG"], KeyQuitProg) # Bind handler for "Read Config File" self.DirList.bind(self.KeyBindings["READCONF"], ParseConfFile) # Bind handler for "Refresh Screen" self.DirList.bind(self.KeyBindings["REFRESH"], RefreshDirList) # Bind handler for "Toggle Detail" self.DirList.bind(self.KeyBindings["TOGDETAIL"], KeyToggleDetail) ### # Directory Navigation ### # Bind handler for "Change Directory" self.DirList.bind(self.KeyBindings["CHANGEDIR"], KeyChangeDir) # Bind handler for "Home Dir" self.DirList.bind(self.KeyBindings["DIRHOME"], KeyHomeDir) # Bind handler for "Previous Dir" self.DirList.bind(self.KeyBindings["DIRBACK"], KeyBackDir) # Bind handler for "Root Dir" self.DirList.bind(self.KeyBindings["DIRROOT"], KeyRootDir) # Bind handler for "Starting Dir" self.DirList.bind(self.KeyBindings["DIRSTART"], KeyStartDir) # Bind handler for "Up Dir" self.DirList.bind(self.KeyBindings["DIRUP"], KeyUpDir) # Bind handler for "Display Drive View" self.DirList.bind(self.KeyBindings["DRIVELIST"], KeyDriveList) # Bind handler for "Mouse Back Dir" self.DirList.bind(self.KeyBindings["MOUSEBACK"], MouseDblClick) # Bind handler for "Mouse Up Dir" self.DirList.bind(self.KeyBindings["MOUSEUP"], MouseDblClick) ### # Selection Keys ### # Bind handler for "Select All" self.DirList.bind(self.KeyBindings["SELALL"], KeySelAll) # Bind handler for "Invert Current Selection" self.DirList.bind(self.KeyBindings["SELINV"], KeySelInv) # Bind handler for "Select No Items" self.DirList.bind(self.KeyBindings["SELNONE"], KeySelNone) # Bind handler for "Next Item" self.DirList.bind(self.KeyBindings["SELNEXT"], KeySelNext) # Bind handler for "Previous Item" self.DirList.bind(self.KeyBindings["SELPREV"], KeySelPrev) # Bind handler for "First Item" self.DirList.bind(self.KeyBindings["SELTOP"], KeySelTop) # Bind handler for "Last Item" self.DirList.bind(self.KeyBindings["SELEND"], KeySelEnd) ### # Scrolling Keys ### # Bind Handler for "Move Page Down self.DirList.bind(self.KeyBindings["PGDN"], KeyPageDown) # Bind Handler for "Move Page Up" self.DirList.bind(self.KeyBindings["PGUP"], KeyPageUp) # Bind Handler for "Move Page Right" self.DirList.bind(self.KeyBindings["PGRT"], KeyPageRight) # Bind Handler for "Move Page Up" self.DirList.bind(self.KeyBindings["PGLFT"], KeyPageLeft) ### # Execute commands ### # Bind handler for "Run Command" self.DirList.bind(self.KeyBindings["RUNCMD"], KeyRunCommand) # Bind handler for "Item Select" self.DirList.bind(self.KeyBindings["SELKEY"], DirListHandler) # Bind handler for "Mouse Select" self.DirList.bind(self.KeyBindings["MOUSESEL"], MouseDblClick) ##### # Function Keys ##### # Bind function keys to a common handler for x in range(len(self.FuncKeys)): self.DirList.bind('<F%d>' % (x+1), lambda event, index=x :FuncKeypress(index)) # Give the listbox focus so it gets keystrokes self.DirList.focus() # End Of method 'twanderUI.BindAllHandlers() ##### # Return tuple of all selected items ##### def AllSelection(self): sellist = [] # On Win32 Systems a Drive List View is always forced # to display with no details. So we have to consider this # when figuring out where the selection name begins. if self.CurrentDir == SHOWDRIVES: nameindex = 0 else: nameindex = self.NameFirst for entry in self.DirList.curselection(): sellist.append(self.DirList.get(entry)[nameindex:].split(SYMPTR)[0]) return sellist # End of method 'twanderUI.AllSelection()' ##### # Return name of currently selected item ##### def LastInSelection(self): # As above in AllSelection() if self.CurrentDir == SHOWDRIVES: nameindex = 0 else: nameindex = self.NameFirst index = self.DirList.curselection() if index: return self.DirList.get(index[-1])[nameindex:].split(SYMPTR)[0] else: return "" # End of method 'twanderUI.LastInSelection()' ##### # Support periodic polling to make sure widget stays # in sync with reality of current directory. ##### def poll(self): # If new dir entered via mouse, force correct activation if self.MouseNewDir: self.DirList.activate(0) self.MouseNewDir = FALSE # See if its time to do a refresh if AUTOREFRESH: self.ElapsedTime += POLLINT if self.ElapsedTime >= REFRESHINT: RefreshDirList() self.ElapsedTime = 0 # Setup next polling event self.DirList.after(POLLINT, self.poll) # End of method 'twanderUI.poll()' ##### # Set Detailed View -> FALSE == No Details, TRUE == Details ##### def SetDetailedView(self, details): # See if we're forcing details to always be off if NODETAILS: self.DetailsOn = FALSE else: self.DetailsOn = details # 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 self.DetailsOn: self.NameFirst = ST_SZTOTAL else: self.NameFirst = 0 # End of method 'twanderUI.SetDetailedView()' ##### # Set a particular selection, w/bounds checking # Note that 'selection' is passed as a string # but 'active' is passed as a number. ##### def SetSelection(self, selection, active): # Clear all current selection(s) self.DirList.selection_clear(0, END) # Get current maximum index maxindex = self.DirList.size() - 1 # And bounds check/adjust if active > maxindex: active = maxindex # Set desired selected items, if any if selection: for entry in selection: self.DirList.select_set(entry) self.DirList.see(selection[-1]) # Now set the active entry self.DirList.activate(active) # End of method 'twanderUI.SetSelection()' ##### # Update title bar with most current information ##### def UpdateTitle(self, mainwin): mainwin.title(PROGNAME + " " + VERSION + " " + HOSTNAME + \ ": "+ UI.CurrentDir + " Total Files: " + \ str(self.DirList.size()) + \ " Total Size: " + FileLength(self.TotalSize)) # End of method 'twanderUI.UpdateTitle()' # End of class definition, 'twanderUI' #----------------------------------------------------------# # Handler Functions # #----------------------------------------------------------# #---------------- Mouse Click Dispatchers -----------------# # We intercept all mouse clicks (of interest) here so it # is easy to uniquely handle the Control, Shift, Alt, # variations of button presses. We use Tkinter itself # keep track of single- vs. double-clicks and hand-off # the event to the corresponding Mouse Click Dispatcher. ##### # Event Handler: Single Mouse Clicks ##### def MouseClick(event): event.state &= ~DontCareMask # Kill the bits we don't care about if event.state == Button3Mask: # Button-3 / No Modifier x, y = UI.DirList.winfo_pointerxy() # Position near mouse PopupMenu(UI.CmdBtn.menu, x, y) # Display Command Menu elif event.state == (Button3Mask | ShiftMask | ControlMask): # Shift-Control-Button-3 x, y = UI.DirList.winfo_pointerxy() # Position near mouse PopupMenu(UI.HistBtn.menu, x, y) # Display Directory Menu elif event.state == (Button3Mask | ShiftMask): # Shift-Button-3 x, y = UI.DirList.winfo_pointerxy() # Position near mouse PopupMenu(UI.DirBtn.menu, x, y) # Display Directory Menu # End Of 'MouseClick() ##### # Event Handler: Mouse Double-Clicks ##### def MouseDblClick(event): event.state &= ~DontCareMask # Kill the bits we don't care about if event.state == Button1Mask: # Double-Button-2 / No Modifier DirListHandler(event) # Execute selected item elif event.state == (Button1Mask | ControlMask): # Control-DblButton-1 KeyBackDir(event) # Move back one directory elif event.state == (Button3Mask | ControlMask): # Control-DblButton-3 KeyUpDir(event) # Move up one directory return "break" # End Of 'MouseDblClick() #--------------- General Program Commands -----------------# ##### # Event Handler: Clear Various Program Histories ##### def ClearHistory(event): global UI UI.AllDirs = [] UI.CmdHist = [] UI.LastCmd = "" UI.LastDir = [] UI.LastPathEntered = "" for x in [UI.HistBtn, UI.DirBtn]: x.menu.delete(0,END) x['menu'] = x.menu x.config(state=DISABLED) # End of 'ClearHistory()' ##### # Event Handler: Individual Keystrokes ##### def KeystrokeHandler(event): event.state &= ~DontCareMask # Kill the bits we don't care about # Check for, and handle accelerator keys if event.state == AltMask: # Set menu button associated with accelerator # Command Menu if event.char == 'c': button = UI.CmdBtn # Directory Menu elif event.char == 'd': button = UI.DirBtn # History Menu elif event.char == 'h': button = UI.HistBtn # Unrecognized - Ignore else: return "break" parts = button.winfo_geometry().split('+') # Geometry returned as "WidthxHeight+X+Y" dims = parts[0].split('x') x, y = int(parts[1]), int(parts[2]) w, h = int(dims[0]), int(dims[1]) x += UIroot.winfo_rootx() # This is relative to root window position y += UIroot.winfo_rooty() # So adjust accordingly # Display the requested menu PopupMenu(button.menu, x+MENUOFFSET, y+h) # Inhibit event from getting picked up by local accelerator key handlers return "break" ##### # Otherwise, process single character command invocations. ##### # We *only* want to handle simple single-character # keystrokes. This means that there must be a character # present and that the only state modifier permitted # is the Shift key if not event.char or (event.state and event.state != 1): return # If the key pressed is a command key (i.e., it is in the table of # defined commands), get its associated string and execute the command. cmd = UI.CmdTable.get(event.char, ["", "", ""])[1] name = UI.CmdTable.get(event.char, ["", "", ""])[0] # cmd == null means no matching command key - do nothing # Otherwise, replace config tokens with actual file/dir names if cmd: ExecuteCommand(cmd, name) # end of 'KeystrokeHandler()' ##### # Event Handler: Program Quit ##### def KeyQuitProg(event): sys.exit() # End of 'KeyQuitProg()' ##### # Event Handler: Toggle Detail View ##### def KeyToggleDetail(event): UI.SetDetailedView(not UI.DetailsOn) RefreshDirList(event) # End of 'KeyToggleDetail()' #------------------- Directory Navigation -----------------# ##### # Event Handler: Change Directory/Path #### def KeyChangeDir(event): newpath = askstring(pCHPATH, pENPATH, initialvalue=UI.LastPathEntered) if newpath: if MAXDIR > 0: UI.LastPathEntered = newpath LoadDirList(newpath) KeySelTop(event) UI.DirList.focus() # End of 'KeyChangeDir()' ##### # Event Handler: Goto $HOME ##### def KeyHomeDir(event): if HOME: LoadDirList(HOME) # End of 'KeyHomeDir()' ##### # Event Handler: Move To Previous Directory ##### def KeyBackDir(event): # Move to last directory visited, if any - inhibit this from # being placed on the directory traversal stack if UI.LastDir: LoadDirList(UI.LastDir.pop(), save=FALSE) # No previous directory else: pass # End of 'KeyBackDir()' ##### # Event Handler: Go To Root Directory ##### def KeyRootDir(event): LoadDirList(PSEP) # End of 'KeyRootDir()' ##### # Event Handler: Go Back To Initial Directory ##### def KeyStartDir(event): LoadDirList(STARTDIR) # End of 'KeyStartDir()' ##### # Event Handler: Move Up One Directory ##### def KeyUpDir(event): # Move up one directory level unless we're already at the root if UI.CurrentDir != os.path.abspath(PSEP): LoadDirList(UI.CurrentDir + "..") # Unless we're running on Win32 and we are able to do # a Drive List View elif OSNAME == 'nt' and GetWin32Drives(): LoadDirList(SHOWDRIVES) # End of 'KeyUpDir()' ##### # Event Handler: Display Drive List View On Win32, If Possible ##### def KeyDriveList(event): # This is only possible on Win32 and if there is a list of # drives available - i.e, If Win32All is installed if OSNAME == 'nt' and GetWin32Drives(): LoadDirList(SHOWDRIVES) # End of 'KeyDriveList() #---------------------- Selection Keys ---------------------# ##### # Event Handler: Select All Items ##### def KeySelAll(event): # Unselect first item in case it was UI.DirList.selection_clear(0) # We never want to select the first item which is ".." UI.DirList.selection_set(1, END) # End of 'KeySelAll()' ##### # Event Handler: Invert Current Selection ##### def KeySelInv(event): # List of current selections cs= UI.DirList.curselection() # Select everything UI.DirList.selection_set(0, END) # And unselect what was selected for v in cs: UI.DirList.selection_clear(v) # And we never select ".." this way UI.DirList.selection_clear(0) # End of 'KeySelInv()' ##### # Event Handler: Select Next Item ##### def KeySelNext(event): next = UI.DirList.index(ACTIVE) # Don't increment if at end of list if (next < UI.DirList.size() - 1): next += 1 UI.SetSelection((str(next),), next) # End of 'KeySelNext()' ##### # Event Handler: Select No Items ##### def KeySelNone(event): UI.DirList.selection_clear(0, END) # End of 'KeySelNone()' ##### # Event Handler: Select Previous Item ##### def KeySelPrev(event): prev = UI.DirList.index(ACTIVE) # Only decrement if > 0 if prev: prev -= 1 UI.SetSelection((str(prev),), prev) # End of 'KeySelPrev()' ##### # Event Handler: Select Last Item ##### def KeySelEnd(event): # Get index of last item in listbox sz = UI.DirList.size() - 1 # And setup to last item accordingly UI.SetSelection((str(sz),), sz) # End of 'KeySelEnd()' ##### # Event Handler: Select First Item ##### def KeySelTop(event): UI.SetSelection(('0',),0) # End of 'KeySelTop()' #---------------------- Scrolling Keys ---------------------# ##### # Event Handler: Move Down A Page ##### def KeyPageDown(event): UI.DirList.yview_scroll(1, "pages") UI.DirList.activate("@0,0") # End of 'KeyPageDown()' ##### # Event Handler: Move Up A Page ##### def KeyPageUp(event): UI.DirList.yview_scroll(-1, "pages") UI.DirList.activate("@0,0") # End of 'KeyPageUp()' ##### # Event Handler: Move Page Right ##### def KeyPageRight(event): UI.DirList.xview_scroll(1, "pages") # End of 'KeyPageRight()' ##### # Event Handler: Move Page Left ##### def KeyPageLeft(event): UI.DirList.xview_scroll(-1, "pages") # End of 'KeyPageLeft()' #---------------------- Execute Commands -------------------# ##### # Event Handler: Run Manually Entered Command #### def KeyRunCommand(event, initial=""): global UI # Prompt with passed initial edit string if initial: cmd = askstring(pRUNCMD, pENCMD, initialvalue=initial) # Prompt with last manually entered command elif UI.LastCmd: cmd = askstring(pRUNCMD, pENCMD, initialvalue=UI.LastCmd ) # Prompt with no initial string else: cmd = askstring(pRUNCMD, pENCMD) # Execute command (if any) - Blank entry means do nothing/return if cmd: ExecuteCommand(cmd, pMANUALCMD, ResolveVars=TRUE, SaveUnresolved=TRUE) # Save the command only if Command History is enabled (MAXHIST > 0) # AND one of two conditions exist: # # 1) No initial string was provided (The user entered a command manually). # 2) An initial string was provided, but the user edited it. if (MAXHIST > 0) and ((not initial) or (cmd != initial)): UI.LastCmd = cmd UI.DirList.focus() # End of 'KeyRunCommand()' ##### # Event Handler: Process Current Selection ##### def DirListHandler(event): global UI # Get current selection. If none, just return, otherwise process selected = UI.LastInSelection() if not selected: return # If we're on Win32 and we just selected ".." from the root of # a drive, request a display of the Drive List. LoadDirList() # will check to see if there is anything in the Drive List and # do nothing if it is empty (which happens if the user has not # installed the Win32All package). if OSNAME=='nt' and \ os.path.abspath(UI.CurrentDir) == os.path.abspath(UI.CurrentDir + selected): LoadDirList(SHOWDRIVES, save=TRUE) UI.MouseNewDir = TRUE # If selection is a directory, move there and list contents. elif os.path.isdir(os.path.join(UI.CurrentDir, selected)): # On Unix, don't follow links pointing back to themselves if OSNAME == 'posix' and os.path.samefile(UI.CurrentDir, UI.CurrentDir + selected): # Protect with try/except because Tk loses track of things # if you keep hitting this selection very rapidly - i.e. Select # the entry and lean on the Enter key. The try/except # prevents the error message (which is benign) from ever # appearing on stdout. try: WrnMsg(wLINKBACK % (UI.CurrentDir + selected[:-1])) except: pass return # Build full path name selected = os.path.join(os.path.abspath(UI.CurrentDir), selected) # Convert ending ".." into canonical path if selected.endswith(".."): selected = PSEP.join(selected.split(PSEP)[:-2]) # Need to end the directory string with a path # separator character so that subsequent navigation # will work when we hit the root directory of the file # system. In the case of Unix, this means that # if we ended up at the root directory, we'll just # get "/". In the case of Win32, we will get # "DRIVE:/". if selected[-1] != PSEP: selected += PSEP # Load UI with new directory LoadDirList(selected, save=TRUE) # Indicate that we entered a new directory this way. # This is a workaround for Tk madness. When this # routine is invoked via the mouse, Tk sets the # activation *when this routine returns*. That means # that the activation set at the end of LoadDirList # gets overidden. We set this flag here so that # we can subsequently do the right thing in our # background polling loop. Although this routine # can also be invoked via a keyboard selection, # we run things this way regardless since no harm # will be done in the latter case. UI.MouseNewDir = TRUE # No, a *file* was selected with a double-click # We know what to do on Win32 and Unix. We ignore # the selection elsewhere. elif OSNAME == 'nt': ExecuteCommand(os.path.join(os.path.abspath(UI.CurrentDir), selected), '', UseStartDir=TRUE) elif OSNAME == 'posix': ExecuteCommand(os.path.join(os.path.abspath(UI.CurrentDir), selected), '') # End of 'DirListHandler()' ##### # Event Handler: Handler Function Keys ##### def FuncKeypress(index): dir = UI.FuncKeys[index] if dir: LoadDirList(dir) return "break" # End of 'FuncKeypress()' #-------------- Handler Utility Functions -----------------# ##### # Event Handler: Popup Menus ##### def PopupMenu(menu, x, y): # Popup requested menu at specified coordinates # but only if the menu has at least one entry. if menu.index(END): menu.tk_popup(x, y) # End of 'PopupMenu()' ##### # Execute A Command ##### def ExecuteCommand(cmd, name, UseStartDir=FALSE, ResolveVars=FALSE, SaveUnresolved=FALSE): global UI # Process references to any Built-In variables newcmd = ProcessBuiltIns(cmd, name) # Replace references to any Environment or User-Defined variables # but only when asked to. - i.e., The command was manually # entered by the user and may contain unresolved variables. # (Commands the user defined in the configuration file # already have their variables dereferenced when that file is # read and parsed.) if ResolveVars: newcmd = ProcessVariables(newcmd, 0 , name) # A null return value means there was a problem - abort if not newcmd: return # Just dump command if we're debugging if int(DEBUGLEVEL) & DEBUGCMDS: PrintDebug(dCMD, [newcmd,]) # Otherwise,actually execute the command. elif newcmd: # Run the command on Win32 using filename associations if UseStartDir: try: os.startfile(newcmd) except: WrnMsg(wBADEXE % newcmd) # Normal command execution for both Unix and Win32 else: try: thread.start_new_thread(os.system, (newcmd,)) except: WrnMsg(wBADEXE % newcmd) # Update the Command History observing MAXHIST # In most cases, we want to save the command with all the # variables (Built-In, Environment, User-Defined) resolved (dereferenced). # However, sometimes (e.g. manual command entry via KeyRunCommand()) we # want to save the *unresolved* version. if SaveUnresolved: savecmd = cmd else: savecmd = newcmd UpdateMenu(UI.HistBtn, UI.CmdHist, MAXHIST, KeyRunCommand, newentry=savecmd, fakeevent=TRUE) # Dump Command History stack if requested if int(DEBUGLEVEL) & DEBUGHIST: PrintDebug(dHIST, UI.CmdHist) # End of 'ExecuteCommand() ##### # Load UI With Selected Directory ##### def LoadDirList(newdir, save=TRUE): # Make sure we're permitted to navigate - we have to allow initial entry into STARTDIR if NONAVIGATE and (newdir != STARTDIR): return # Transform double forward-slashes into a single # forward-slash. This keeps the Directory Stack # and Visited lists sane under Unix and prevents # Win32 from attempting to enter a Drive List View # when the user enters this string but Win32All has # not been loaded. if newdir == '//': newdir = '/' # Get path into canonical form unless we're trying # to display a Win32 Drive List if newdir != SHOWDRIVES: newdir = os.path.abspath(newdir) # Make sure newdir properly terminated if newdir[-1] != PSEP: newdir += PSEP # User has requested a Drive List View. Make sure we're # running on Win32 and see the feature is available. If # not available (which means Win32All is not installed), # just ignore and return, doing nothing. elif OSNAME != 'nt' or not GetWin32Drives(): return # Check right now to see if we can read # the directory. If not, at least we # haven't screwed up the widget's current # contents or program state. try: contents = BuildDirList(newdir) except: # If CurrentDir set, we're still there: error w/ recovery if UI.CurrentDir: ErrMsg(eDIRRD % newdir) return # If not, we failed on the initial directory: error & abort else: ErrMsg(eINITDIRBAD % newdir) sys.exit(1) # Push last directory visited onto the visited stack # We do NOT save this to the stack if: # # 1) We've been told not to. - Passed when we're called (save=FALSE). # 2) If we're trying to move into the current directory again. # This can happen either when the user does a manual directory # change or if they press ".." while in root. We don't # actually want to save the directory until we *leave* it, # otherwise we'll end up with a stack top and current # directory which are the same, and we'll have to go # back *twice* to move to the previous directory. # Are we trying to move back into same directory? if os.path.abspath(UI.CurrentDir) == os.path.abspath(newdir): save = FALSE # Now save if we're supposed to. if save and UI.CurrentDir: UI.LastDir.append(UI.CurrentDir) # Dump directory stack if debug requested it if int(DEBUGLEVEL) & DEBUGDIRS: PrintDebug(dDIRSTK, UI.LastDir) # And select new directory to visit UI.CurrentDir = newdir # Wait until we have exclusive access to the widget while not UI.DirListMutex.testandset(): pass # Clear out the old contents UI.DirList.delete(0,END) # Load new directory contents into UI for x in contents: UI.DirList.insert(END, x) # Also move the program context to the new directory # for everything except a Drive List View. In that case # the program context remains in the directory from # which the Drive List View was selected if newdir != SHOWDRIVES: os.chdir(newdir) # Keep list of all unique directories visited in the Directory Menu UpdateDirMenu(newdir) # And always force selection of first item there. # This guarantees a selection in the new # directory context, so subsequent commands # won't try to operate on an item selected in a # previous directory KeySelTop(None) # Update titlebar to reflect any changes UI.UpdateTitle(UIroot) #Release the lock UI.DirListMutex.unlock() # End of 'LoadDirList(): ##### # Return Ordered List Of Directories & Files For Current Root ##### def BuildDirList(currentdir): global UI dList, fList = [], [] # Two possible cases have to be handled: # A normal directory read and a Drive List View # under Win32. # Normal directory reads if currentdir != SHOWDRIVES: # Walk the directory separate subdirs and files for file in os.listdir(currentdir): if os.path.isdir(os.path.join(currentdir, file)): dList.append(file + PSEP) else: fList.append(file) # On Win32, do case-insensitive sorting since case # is irrelevant in file names on these systems if OSNAME == 'nt': for l in (dList, fList): lowerlist = [(x.lower(), x) for x in l] lowerlist.sort() l = [] [l.append(x[1]) for x in lowerlist] # Everywhere else, do absolute sorts else: dList.sort() fList.sort() # Entry to move up one directory is always first, # no matter what the sort. This is necessary because # OSs like Win32 like to use '$' in file names which # sorts before "." dList.insert(0, ".." + PSEP) # The user requested Drive List View. This is always displayed # without details, so we can return directly from here. else: UI.TotalSize = 0 return GetWin32Drives() # Get details on directory contents 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': # Convert UID to name, if possible try: owner = pwd.getpwuid(stinfo[ST_UID])[0][:ST_SZUNAME-1] # No valid name associated with UID, so use number instead except: owner = str(stinfo[ST_UID]) # Convert GID to name, if possible try: group = grp.getgrgid(stinfo[ST_GID])[0][:ST_SZGNAME-1] # No valid name associated with GID, so use number instead except: group = str(stinfo[ST_GID]) # Handle Win32 systems elif OSNAME == 'nt': 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 if UI.DetailsOn: return detlist else: return dList + fList # End of 'BuildDirList()' ##### # Process A Command Line Containing Unresolved Variables ##### def ProcessBuiltIns(cmd, name): # First do any prompting required for promptvar, handler, replace in ((YESNO, askyesno, FALSE), (PROMPT, askstring, TRUE)): for x in range(cmd.count(promptvar)): b = cmd.find(promptvar) e = cmd.find("]", b) prompt = cmd[b + len(promptvar):e] val = handler(name, prompt) # Make sure our program gets focus back UI.DirList.focus() if val: if replace: cmd = cmd.replace(cmd[b:e+1], QUOTECHAR + val + QUOTECHAR) else: cmd = cmd.replace(cmd[b:e+1], '') # Null input means the command is being aborted else: return # Now do files & directories # Strip trailing path separators in each case to # give the command author the maximum flexibility possible selection = StripPSEP(UI.LastInSelection()) selections = "" dselections = "" for selected in UI.AllSelection(): selected = StripPSEP(selected) dselections += QUOTECHAR + UI.CurrentDir + selected + QUOTECHAR + " " selections += QUOTECHAR + selected + QUOTECHAR + " " cmd = cmd.replace(DIR, QUOTECHAR + StripPSEP(UI.CurrentDir) + QUOTECHAR) cmd = cmd.replace(DSELECTION, QUOTECHAR + UI.CurrentDir + selection + QUOTECHAR) cmd = cmd.replace(DSELECTIONS, dselections) cmd = cmd.replace(HASH, COMMENT) cmd = cmd.replace(SELECTION, QUOTECHAR + selection + QUOTECHAR) cmd = cmd.replace(SELECTIONS, selections) return cmd # End of 'ProcessBuiltIns()' ##### # Process/Replace References To User-Defined & Environment Variables ##### def ProcessVariables(cmd, num, line): doeval = TRUE depth = 0 while doeval: # Bound the number of times we can nest a definition # to prevent self-references which give infinite nesting depth depth += 1 if (depth > MAXNESTING): doeval = FALSE # See if there are still unresolved variable references. # If so, let the user know if REVAR.findall(cmd): WrnMsg(wVBLTOODEEP % (num, cmd)) return "" # Get a list of variable references vbls = REVAR.findall(cmd) # Throw away references to Built-In Variables - these are # processed at runtime and should be left alone here. # Note that we iterate over a *copy* of the variables # list, because we may be changing that list contents # as we go. i.e., It is bogus to iterate over a list # which we are changing during the iteration. for x in vbls[:]: # Ignore references to Built-In Variables here - They are # processed at runtime. if UI.BuiltIns.has_key(x): vbls.remove(x) elif x.startswith(PROMPT): vbls.remove(x) elif x.startswith(YESNO): vbls.remove(x) if vbls: for x in vbls: vbl = x[1:-1] # Process ordinary variables if UI.SymTable.has_key(vbl): cmd = cmd.replace(x, UI.SymTable[vbl]) # Process environment variables. # If an environment variable is referenced, # but not defined, this is a fatal error elif vbl[0] == ENVVBL: envvbl = os.getenv(vbl[1:]) if envvbl: cmd = cmd.replace(x, envvbl) else: WrnMsg(wBADENVVBL % (num, x, line)) return "" # Process references to undefined variables else: WrnMsg(wUNDEFVBL % (num, x, line)) return "" # No substitutions left to do else: doeval = FALSE return cmd # End of 'ProcessVariables()' ##### # Refresh Contents Of Directory Listing To Stay In Sync With Reality ##### def RefreshDirList(*args): # Wait until we have exclusive access to the widget while not UI.DirListMutex.testandset(): pass # Get current selection and active sellist = UI.DirList.curselection() active = UI.DirList.index(ACTIVE) # Save current scroll positions xs = UI.hSB.get() ys = UI.vSB.get() # Clean out old listbox contents UI.DirList.delete(0,END) # Save the new directory information UI.DirList.insert(0, *BuildDirList(UI.CurrentDir)) # Restore selection(s) UI.SetSelection(sellist, active) # Restore scroll positions UI.DirList.xview(MOVETO, xs[0]) UI.DirList.yview(MOVETO, ys[0]) # Update titlebar to reflect any changes UI.UpdateTitle(UIroot) # Release the mutex UI.DirListMutex.unlock() # End of 'RefreshDirList() #---------------- Menu Support Functions ------------------# ##### # Handle Command Menu Selections ##### def CommandMenuSelection(cmdkey): class event: pass event.state = 0 event.char = cmdkey KeystrokeHandler(event) # End Of 'CommandMenuSelection()' ##### # Add An Entry To The List Of All Directories Visited # Do this only if it is not already on the list and # observe the MAXDIR variable to keep list length bounded. ##### def UpdateDirMenu(newdir): global UI # Win32 collapses case so that 'x' and 'X' refer to the same # directory. We want to preserve this in the user's display # but we have to collapse case for the purposes of doing our # checks below otherwise the same directory with different # capitalization (if the user manually enteres it that way) # can appear twice in the Directory Menu addentry = FALSE if OSNAME == 'nt': # First make a case-collapsed copy of the existing list dlc = [] [dlc.append(d.lower()) for d in UI.AllDirs] # Now see if our new entry is already there if newdir.lower() not in dlc: addentry = TRUE elif newdir not in UI.AllDirs: addentry = TRUE # Now add the entry if we decided it was necessary. observing MAXDIR value. if addentry: UpdateMenu(UI.DirBtn, UI.AllDirs, MAXDIR, LoadDirList, sort=TRUE, newentry=newdir) # End of 'UpdateDirMenu()' ##### # Generic Menu Update Routine ##### def UpdateMenu(menubtn, datastore, max, func, sort=FALSE, newentry="", fakeevent=FALSE): # First add the new data, if any, to the specified data storage stucture. if newentry: datastore.append(newentry) # Now trim it to requested maximum length - this only changes how many # entries are display on the menu - nothing is actually removed # from the data storage for this menu. We also do not sort the # actual data storage itself if sorting has been requested. # We sort the *copy*. That way we get the 'last _max_ items in # sorted order.' (If we sorted the master copy, we would lose # track of the order in which things were placed there.) data = datastore[:] if not max: data = [] elif len(data) > max: data = datastore[-max:] # Initialize the menu to empty menubtn.menu.delete(0,END) menubtn.config(state=DISABLED) if len(data): if sort: data.sort() for entry in data: if fakeevent: menubtn.menu.add_command(label=entry, command=lambda item=entry: func(None, item)) else: menubtn.menu.add_command(label=entry, command=lambda item=entry: func(item)) menubtn['menu'] = menubtn.menu # Menu now has content, enable it menubtn.config(state=NORMAL) # End of 'UpdateMenu()' ##### # Process Options ##### def ProcessOptions(): global UI # Start in detailed mode unless details are inhibited UI.SetDetailedView(~NODETAILS) # Rebind all the handlers UI.BindAllHandlers() # Set the Command Menu Contents, if any, # and enable the menu if it has entries. # If no commands are defined, warn the user. if UI.CmdBtn.menu.index(END): UI.CmdBtn['menu'] = UI.CmdBtn.menu UI.CmdBtn.configure(state=NORMAL) else: WrnMsg(wNOCMDS) # Any user-set options have now been read, set the GUI for i in (UI.CmdBtn, UI.DirBtn, UI.HistBtn): i.config(font=(FNAME, FSZ, "bold")) i.menu.config(font=("courier", FSZ, "bold")) UI.DirList.config(font=(FNAME, FSZ, FWT), foreground=FCOLOR, background=BCOLOR, height=HEIGHT, width=WIDTH) # Make sure menus conform to max lengths (which may have changed). UpdateMenu(UI.DirBtn, UI.AllDirs, MAXDIR, LoadDirList, sort=TRUE) UpdateMenu(UI.HistBtn, UI.CmdHist, MAXHIST, KeyRunCommand, fakeevent=TRUE) ##### # Dump requested debug information ##### if int(DEBUGLEVEL) & DEBUGKEYS: debuginfo = [] # Command keys for key in UI.KeyBindings.keys(): debuginfo.append(key + " " * (10-len(key)) + UI.KeyBindings[key]) debuginfo.sort() PrintDebug(dKEYBINDS, debuginfo) debuginfo = [] # Function Keys (Directory Shortcuts) for x in range(len(UI.FuncKeys)): key = "F" + str(x+1) debuginfo.append(key + " " * (10-len(key)) + UI.FuncKeys[x]) PrintDebug(dFUNCKEYS, debuginfo) if int(DEBUGLEVEL) & DEBUGSYMS: debuginfo = [] for sym in UI.SymTable.keys(): debuginfo.append(sym + " " * (16-len(sym)) + UI.SymTable[sym]) debuginfo.sort() PrintDebug(dSYMTBL, debuginfo) if int(DEBUGLEVEL) & DEBUGCTBL: debuginfo = [] for key in UI.CmdTable.keys(): name = UI.CmdTable[key][0] cmd = UI.CmdTable[key][1] debuginfo.append(key + " " + name + " " * (16-len(name)) + cmd) PrintDebug(dCMDTBL, debuginfo) # Dump program variable during debug sessions if int(DEBUGLEVEL) & DEBUGVARS: debuginfo = [] for v in DebugVars: debuginfo.append(v + " " * (12-len(v)) + (str(eval(v)) or dNULL)) PrintDebug(dINTVAR, debuginfo) debuginfo = [] for l in (UI.OptionsBoolean, UI.OptionsNumeric, UI.OptionsString): for v in l: debuginfo.append(v + " " * (12-len(v)) + (str(eval(v)) or dNULL)) debuginfo.sort() PrintDebug(dOPTVAR, debuginfo) # If we just wanted debug output, quit now if int(DEBUGLEVEL) & DEBUGQUIT: sys.exit() # End of 'ProcessOptions()' #----------------------------------------------------------# # Program Entry Point # #----------------------------------------------------------# ##### # Create an instance of the UI ##### UIroot = Tk() UI = twanderUI(UIroot) # Make the Tk window the topmost in the Z stack. # 'Gotta do this or Win32 will not return input # focus to our program after a startup warning # display. UIroot.tkraise() ##### # Setup global UI variables ##### # Setup Built-In Variables UI.BuiltIns = {DIR:"", DSELECTION:"", DSELECTIONS:"", HASH:"", PROMPT:"", SELECTION:"", SELECTIONS:"", YESNO:""} # Options (and their default values) which can be set in the configuration file UI.OptionsBoolean = {"AUTOREFRESH":AUTOREFRESH, "NODETAILS":NODETAILS, "NONAVIGATE":NONAVIGATE, "WARN":WARN} UI.OptionsNumeric = {"DEBUGLEVEL":DEBUGLEVEL, "FSZ":FSZ, "HEIGHT":HEIGHT, "MAXDIR":MAXDIR, "MAXHIST":MAXHIST, "MAXNESTING":MAXNESTING, "REFRESHINT":REFRESHINT, "WIDTH":WIDTH} UI.OptionsString = {"BCOLOR":BCOLOR, "FCOLOR":FCOLOR, "FNAME":FNAME, "FWT":FWT, "QUOTECHAR":QUOTECHAR, "STARTDIR":STARTDIR} # Prepare storage for key bindings UI.KeyBindings = {} # Initialize list of all directories visited UI.AllDirs = [] # Initialize directory stack UI.LastDir = [] # Initialize storage for last manually entered directory path UI.LastPathEntered = "" # And current location UI.CurrentDir = "" # Initialize Command History data structures ClearHistory(None) ##### # Command line processing - Options are set with the # following priority scheme (lowest to highest): # # 1) Defaults coded into the program # 2) Options set in the configuration file # 3) Options set in the environment variable # 4) Options set on the command line # ##### # Concatenate any environment variable with the # command line so the command line takes precedence. OPTIONS = sys.argv[1:] envopt = os.getenv(PROGNAME.upper()) if envopt: OPTIONS = envopt.split() + OPTIONS try: opts, args = getopt.getopt(OPTIONS, '-b:c:d:f:hn:qrs:tvw:x:y:') except getopt.GetoptError: Usage() sys.exit(1) # If the user wants help or version information, do this first # so we don't bother with other options processing for opt, val in opts: if opt == "-h": Usage() sys.exit(0) if opt == "-v": print RCSID sys.exit(0) # Read configuration file before any other arguments. This allows the # environment variable and then the command line to override any # settings in the configuration file. for opt, val in opts: if opt == "-c": CONF = os.path.abspath(val) # Parse the configuration file, but suppress options # processing - on startup this is done just before # we enter the main message loop to make sure we # pickup any options changes from the environment # variable or command line. ParseConfFile(None, DoOptionsProcessing=FALSE) # Process the rest of the options, if any for opt, val in opts: if opt == "-b": BCOLOR = val if opt == "-d": DEBUGLEVEL = val if int(DEBUGLEVEL): print dHEADER % time.asctime() if opt == "-f": FCOLOR = val 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 == "-w": FWT = val if opt == "-x": WIDTH = val if opt == "-y": HEIGHT = val # Figure out where to start - Environment/Command Line overrides config # file STARTDIR option. Program can only have 0 or 1 arguments. Make # sure any startdir argument is legit if len(args) > 1: ErrMsg(eTOOMANY) sys.exit(1) if len(args) == 1: STARTDIR = args[0] # Windows is sloppy about accepting both '//' and '\\' # so we have to condition the command line input for consistency. if OSNAME == 'nt' and STARTDIR == '//': STARTDIR = SHOWDRIVES # Make sure any user request to start in a Drive List View # is possible. If not, just start in the root directory. if STARTDIR == SHOWDRIVES and (OSNAME != 'nt' or not GetWin32Drives()): STARTDIR = PSEP if not os.path.isdir(STARTDIR): ErrMsg(eBADROOT % STARTDIR) sys.exit(1) # Save this value as the default for STARTDIR UI.OptionsString["STARTDIR"] = STARTDIR # Get starting directory into canonical form STARTDIR = os.path.abspath(STARTDIR) # Process options to catch any changes detected in # environment variable or command line ProcessOptions() # 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 # 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()