- #!/usr/bin/env python
- # twander - Wander around the file system
- # Copyright (c) 2002 TundraWare Inc. All Rights Reserved.
- # For Updates See: http://www.tundraware.com/Software/twander
-
- PROGNAME = "twander"
- RCSID = "$Id: twander.py,v 1.93 2002/12/09 20:08:16 tundra Exp $"
- VERSION = RCSID.split()[2]
-
-
- #----------------------------------------------------------#
- # Imports #
- #----------------------------------------------------------#
-
- import getopt
- import mutex
- import os
- import re
- from socket import getfqdn
- from stat import *
- import sys
- import thread
- import time
-
- from Tkinter import *
- from tkMessageBox import showerror, showwarning
- from tkSimpleDialog import askstring
-
- # Imports conditional on OS
-
- # Set OS type - this allows us to trigger OS-specific code
- # where needed.
-
- OSNAME = os.name
-
-
- if OSNAME == 'posix':
- import grp
- import pwd
-
-
-
- #----------------------------------------------------------#
- # Variables User Might Change #
- #----------------------------------------------------------#
-
- #####
- # Defaults
- #####
-
- #####
- # Key Assignments
- #####
-
- # General Program Commands
-
- KEYPRESS = '<KeyPress>' # Any keypress (for commands)
- QUITPROG = '<Control-q>' # Quit the program
- READCONF = '<Control-r>' # Re-read the configuration file
- REFRESH = '<Control-l>' # Refresh screen
- RUNCMD = '<Control-z>' # Run arbitrary user command
- TOGDETAIL = '<Control-t>' # Toggle detail view
-
- # Directory Navigation
-
- CHANGEDIR = '<Control-x>' # Enter a new path
- DIRHOME = '<Control-h>' # Goto $HOME
- DIRBACK = '<Control-b>' # Goto previous directory
- DIRSTART = '<Control-s>' # Goto starting directory
- DIRUP = '<Control-u>' # Go up one directory level
- MSEBACK = '<Control-Double-ButtonRelease-1>' # Go back one directory with mouse
- MSEUP = '<Control-Double-ButtonRelease-3>' # Go up one directory with mouse
-
- # Selection Keys
-
- SELALL = '<Control-comma>' # Select all items
- SELNEXT = '<Control-n>' # Select next item
- SELNONE = '<Control-period>' # Unselect all items
- SELPREV = '<Control-p>' # Select previous item
- SELEND = '<Control-e>' # Select bottom item
- SELTOP = '<Control-a>' # Select top item
- SELKEY = '<Control-space>' # Select item w/keyboard
- SELMOUSE = '<Double-ButtonRelease-1>' # Select item w/mouse
-
- # Intra-Display Movement
-
- PGDN = '<Control-v>' # Move page down
- PGUP = '<Control-c>' # Move page up
-
-
- #####
- # System-Related Defaults
- #####
-
- # Default startup directory
- STARTDIR = "." + os.sep
-
- # Home director
- HOME = os.getenv("HOME") or STARTDIR
-
-
- #####
- # Program Constants
- #####
-
- HEIGHT = 25
- WIDTH = 90
-
- #####
- # Colors
- #####
-
- BCOLOR = "black"
- FCOLOR = "green"
-
-
- #####
- # Fonts
- #####
-
- FNAME = "Courier"
- FSZ = 12
- FWT = "bold"
-
-
- #------------------- Nothing Below Here Should Need Changing ------------------#
-
-
- #----------------------------------------------------------#
- # Constants & Literals #
- #----------------------------------------------------------#
-
-
-
- #####
- # Booleans
- #####
-
- # Don't need to define TRUE & FALSE - they are defined in the Tkinter module
-
-
- #####
- # Defaults
- #####
-
-
- AUTOREFRESH = TRUE # Automatically refresh the directory display
- DEBUG = FALSE # Debugging on
- WARN = TRUE # Warnings on
-
- #####
- # Constants
- #####
-
- # General constants
-
- KB = 1024 # 1 KB constant
- MB = KB * KB # 1 MB constant
- GB = MB * KB # 1 GB constant
- HOSTNAME = getfqdn() # Full name of this host
- POLLINT = 20 # Interval (ms) the poll routine should run
- REFRESHINT = 3000 # Interval (ms) for automatic refresh
-
-
- # Stat-related
-
- # Permissions
-
- ST_PERMIT = ["---", "--x", "-w-", "-wx",
- "r--", "r-x", "rw-", "rwx"]
-
- # Special file type lookup
-
- ST_SPECIALS = {"01":"p", "02":"c", "04":"d", "06":"b",
- "10":"-", "12":"l", "14":"s"}
-
- # Size of each status display field including trailing space
-
- ST_SZMODE = 11
- ST_SZNLINK = 5
- ST_SZUNAME = 12
- ST_SZGNAME = 12
- ST_SZLEN = 12
- ST_SZMTIME = 18
-
- ST_SZTOTAL = ST_SZMODE + ST_SZNLINK + ST_SZUNAME + ST_SZGNAME + \
- ST_SZLEN + ST_SZMTIME
-
-
- # String used to separate symlink entry from its real path
-
- SYMPTR = " -> "
-
-
- #####
- # General Literals
- #####
-
- 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
- reVAR = r"\[.*?\]" # Regex describing variable notation
-
-
- # Builtins
-
- DIR = r'[DIR]'
- DSELECTION = r'[DSELECTION]'
- DSELECTIONS = r'[DSELECTIONS]'
- PROMPT = r'[PROMPT]'
- SELECTION = r'[SELECTION]'
- SELECTIONS = r'[SELECTIONS]'
-
-
- #----------------------------------------------------------#
- # Prompts, & Application Strings #
- #----------------------------------------------------------#
-
-
- #####
- # Error, Information, & Warning Messages
- #####
-
- # Errors
-
- eBADCFGLINE = "Bogus Configuration Entry In Line %s: %s"
- eBADENVVBL = "Environment Variable %s In Line %s Not Set: %s"
- eBADROOT = " %s Is Not A Directory"
- eDIRRD = "Cannot Open Directory : %s --- Check Permissions."
- eDUPKEY = "Found Duplicate Command Key '%s' In Line %s: %s"
- eERROR = "ERROR"
- eINITDIRBAD = "Cannot Open Starting Directory : %s - Check Permissions - ABORTING!."
- eOPEN = "Cannot Open File: %s"
- eREDEFVAR = "Variable %s Redefined In Line %s: %s"
- eTOOMANY = "You Can Only Specify One Starting Directory."
- eUNDEFVBL = "Undefined Variable %s Referenced In Line %s: %s"
- eVBLTOODEEP = "Variable Definition Nested Too Deeply At Line %s: %s"
-
- # Information
-
- iNOSTAT = "Details Unavailable For This File ->"
-
- # Prompts
-
- pCHPATH = "Change Path"
- pENPATH = "Enter New Path Desired:"
- pRUNCMD = "Run Command"
- pENCMD = "Enter Command To Run:"
-
-
- # Warnings
-
- wCMDKEY = "Configuration File Entry For: \'%s\' Has No Command Key Defined."
- wCONFOPEN = "Cannot Open Configuration File:\n%s\n\n%s"
- wNOCMDS = "Running With No Commands Defined!"
- wSYMBACK = " Symbolic Link %s Points Back To Own Directory"
- wWARN = "WARNING"
-
-
- #####
- # Usage Information
- #####
-
- uTable = [PROGNAME + " " + VERSION + " - Copyright 2002, TundraWare Inc., All Rights Reserved\n",
- "usage: " + PROGNAME + " [-bcdfhnqrsvwxy] [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",
- " -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)",
- " -v print detailed version information",
- " -w wght weight/style of font to use (default: bold)",
- " -x width window width (default: 60)",
- " -y height window height (default: 25)",
- ]
-
-
- #---------------------------Code Begins Here----------------------------------#
-
-
- #----------------------------------------------------------#
- # General Support Functions #
- #----------------------------------------------------------#
-
-
- #####
- # Print An Error Message
- #####
-
- def ErrMsg(emsg):
-
- showerror(PROGNAME + " " + VERSION + " " + eERROR, emsg)
-
- # End of 'ErrMsg()'
-
-
- #####
- # Convert A File Size Into Equivalent String With Scaling
- # Files under 1 MB show actual length
- # Files < 1 MB < 1 GB shown in KB
- # Files 1 GB or greater, shown in MB
- #####
-
- def FileLength(flen):
-
- if flen >= GB:
- flen = str(flen/MB) + "m"
- elif flen >= MB:
- flen = str(flen/KB) + "k"
- else:
- flen = str(flen)
-
- return flen
-
- # End of 'FileLength()'
-
-
- #####
- # Parse & Process The Configuraton File
- # This is called once at program start time
- # and again any time someone hits the READCONF key
- # while the program is running.
- #####
-
- def ParseConfFile(event):
- global CONF, UI
-
- # If user specified a config file, try that
- # Otherwise use HOME == either $HOME or ./
-
- if not CONF:
- CONF = os.path.join(HOME, "." + PROGNAME)
- try:
- cf = open(CONF)
- except:
- WrnMsg(wCONFOPEN % (CONF, wNOCMDS))
- UI.CmdTable = {}
- UI.Symtable = {}
- return
-
- # Successful open of config file - Begin processing it
-
- # Cleanout any old
- UI.CmdTable = {}
- UI.SymTable = {}
- linenum = 0
-
- # Process and massage the configuration file
- for line in cf.read().splitlines():
- linenum += 1
-
- # Lex for comment token and discard until EOL.
-
- idx = line.find(COMMENT)
- if idx > -1:
- line = line[:idx]
-
- # Parse whatever is left on non-blank lines
- if line:
- ParseLine(line, linenum)
-
- cf.close()
-
- # Dump tables if we're debugging
- if DEBUG:
- print "SYMBOL TABLE:\n"
- for sym in UI.SymTable.keys():
- print sym + " " * (16-len(sym)) + UI.SymTable[sym]
-
- print"\nCOMMAND TABLE:\n"
- for key in UI.CmdTable.keys():
- name = UI.CmdTable[key][0]
- cmd = UI.CmdTable[key][1]
- print key + " " + name + " " * (16-len(name)) + cmd
-
-
- # End of 'ParseConfFile()'
-
-
- #####
- # Parse A Line From A Configuration File
- # Routine Assumes That Comments Previously Removed
- #####
-
-
- def ParseLine(line, num):
- global UI
- revar = re.compile(reVAR)
-
- # Get rid of trailing newline, if any
- if line[-1] == '\n':
- line = line[:-1]
-
- fields = line.split()
-
- #####
- # Blank Lines - Ignore
- #####
-
- if len(fields) == 0:
- pass
-
- #####
- # Variable Definitions
- #####
-
- elif fields[0].find(ASSIGN) > 0:
- assign = fields[0].find(ASSIGN)
- name = line[:assign]
- val=line[assign+1:]
-
- # Warn on variable redefinitions
- if UI.SymTable.has_key(name):
- ErrMsg(eREDEFVAR % (name, num, line))
- sys.exit(1)
-
- UI.SymTable[name] = val
-
- #####
- # Command Definitions
- #####
-
- elif len(fields[0]) == 1:
-
- # Must have at least 3 fields for a valid command definition
- if len(fields) < 3:
- ErrMsg(eBADCFGLINE % (num, line))
- sys.exit(1)
- else:
- cmdkey = fields[0]
- cmdname = fields[1]
- cmd = " ".join(fields[2:])
-
- # Evaluate the command line, replacing
- # variables as needed
-
- doeval = TRUE
- depth = 0
-
- while doeval:
- # Get a list of variable references
- vbls = revar.findall(cmd)
-
- # Throw away references to builtins - these are
- # processed at runtime and should be left alone here.
-
- # Note that we iterate over a *copy* of the variables
- # list, because we may be changing that list contents
- # as we go. i.e., It is bogus to iterate over a list
- # which we are changing during the iteration.
-
- for x in vbls[:]:
- vbl = x[1:-1]
- if UI.BuiltIns.has_key(vbl):
- 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]
-
- 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 the visual elements
-
- self.hSB = Scrollbar(root, orient=HORIZONTAL)
- self.vSB = Scrollbar(root, orient=VERTICAL)
- self.DirList = Listbox(root,
- foreground = FCOLOR,
- background = BCOLOR,
- font=(FNAME, FSZ, FWT),
- selectmode=EXTENDED,
- exportselection=0,
- xscrollcommand=self.hSB.set,
- yscrollcommand=self.vSB.set,
- height = HEIGHT,
- width = WIDTH,
- )
-
- # Make them visible by packing
-
- self.hSB.config(command=self.DirList.xview)
- self.hSB.pack(side=BOTTOM, fill=X)
- self.vSB.config(command=self.DirList.yview)
- self.vSB.pack(side=RIGHT, fill=Y)
- self.DirList.pack(side=LEFT, fill=BOTH, expand=1)
-
- #####
- # Bind the relevant event handlers
- #####
-
- # General Program Commands
-
- # Bind handler for individual keystrokes
- self.DirList.bind(KEYPRESS, KeystrokeHandler)
-
- # Bind handler for "Quit Program"
- self.DirList.bind(QUITPROG, KeyQuitProg)
-
- # Bind handler for "Read Config File"
- self.DirList.bind(READCONF, ParseConfFile)
-
- # Bind handler for "Refresh Screen"
- self.DirList.bind(REFRESH, RefreshDirList)
-
- # Bind handler for "Run Command"
- self.DirList.bind(RUNCMD, KeyRunCommand)
-
- # Bind handler for "Toggle Detail"
- self.DirList.bind(TOGDETAIL, KeyToggleDetail)
-
-
- # Directory Navigation
-
- # Bind handler for "Change Directory"
- self.DirList.bind(CHANGEDIR, ChangeDir)
-
- # Bind handler for "Home Dir"
- self.DirList.bind(DIRHOME, KeyHomeDir)
-
- # Bind handler for "Previous Dir"
- self.DirList.bind(DIRBACK, KeyBackDir)
-
- # Bind handler for "Starting Dir"
- self.DirList.bind(DIRSTART, KeyStartDir)
-
- # Bind handler for "Up Dir"
- self.DirList.bind(DIRUP, KeyUpDir)
-
- # Bind handler for "Mouse Back Dir"
- self.DirList.bind(MSEBACK, KeyBackDir)
-
- # Bind handler for "Mouse Up Dir"
- self.DirList.bind(MSEUP, KeyUpDir)
-
-
- # Selection Keys
-
- # Bind handler for "Select All"
- self.DirList.bind(SELALL, KeySelAll)
-
- # Bind handler for "Next Item"
- self.DirList.bind(SELNEXT, KeySelNext)
-
- # Bind handler for "Select No Items"
- self.DirList.bind(SELNONE, KeySelNone)
-
- # Bind handler for "Previous Item"
- self.DirList.bind(SELPREV, KeySelPrev)
-
- # Bind handler for "First Item"
- self.DirList.bind(SELTOP, KeySelTop)
-
- # Bind handler for "Last Item"
- self.DirList.bind(SELEND, KeySelEnd)
-
- # Bind handler for "Item Select"
- self.DirList.bind(SELKEY, DirListHandler)
-
- # Bind handler for "Mouse Select"
- self.DirList.bind(SELMOUSE, DirListHandler)
-
-
- # Intra-display movement
-
- # Bind Handler for "Move Page Down
- self.DirList.bind(PGDN, KeyPageDown)
-
- # Bind Handler for "Move Page Up"
- self.DirList.bind(PGUP, KeyPageUp)
-
-
- # Give the listbox focus so it gets keystrokes
- self.DirList.focus()
-
- # End if method 'twanderUI.__init__()'
-
-
- #####
- # Return tuple of all selected items
- #####
-
- def AllSelection(self):
- sellist = []
-
- for entry in self.DirList.curselection():
- sellist.append(self.DirList.get(entry)[UI.NameFirst:].split(SYMPTR)[0])
-
- return sellist
-
- # End of method 'twanderUI.AllSelection()'
-
-
- #####
- # Return name of currently selected item
- #####
-
- def LastInSelection(self):
-
- index = self.DirList.curselection()
- if index:
- return self.DirList.get(index[-1])[UI.NameFirst:].split(SYMPTR)[0]
- else:
- return ""
-
- # End of method 'twanderUI.LastInSelection()'
-
-
- #####
- # Support periodic polling to make sure widget stays
- # in sync with reality of current directory.
- #####
-
- def poll(self):
-
- # If new dir entered via mouse, force correct activation
- if self.MouseNewDir:
- self.DirList.activate(0)
- self.MouseNewDir = FALSE
-
- # See if its time to do a refresh
-
- if AUTOREFRESH:
- self.ElapsedTime += POLLINT
- if self.ElapsedTime >= REFRESHINT:
- RefreshDirList()
- self.ElapsedTime = 0
-
- # Setup next polling event
- self.DirList.after(POLLINT, self.poll)
-
- # End of method 'twanderUI.poll()'
-
-
- #####
- # Set Detailed View -> FALSE == No Details, TRUE == Details
- #####
-
- def SetDetailedView(self, details):
-
- self.DetailsOn = details
-
- # Tell system where actual file name begins
- # For both choices below, we have to set the UI.NameFirst
- # value. This tells other handlers where in a given
- # selection the actual name of the file can be found.
- # This is necessary because we may be selecting a from
- # a detailed view and need to know where in that view
- # the file name lives. It is not good enough to just
- # split() the selected string and use the [-1] entry because
- # we may be dealing with a file which has spaces in its
- # name.
-
-
- if details:
- self.NameFirst = ST_SZTOTAL
- else:
- self.NameFirst = 0
-
- # End of method 'twanderUI.SetDetailedView()'
-
-
- #####
- # Update title bar with most current information
- #####
-
- def UpdateTitle(self, mainwin):
-
- mainwin.title(PROGNAME + " " + VERSION +
- " " + HOSTNAME + ": "+
- UI.CurrentDir + " Total Files: " +
- str(UI.DirList.size()) +
- " Total Size: " + FileLength(UI.TotalSize))
-
- # End of method 'twanderUI.UpdateTitle()'
-
- # End of class definition, 'twanderUI'
-
-
- #----------------------------------------------------------#
- # Handler Functions #
- #----------------------------------------------------------#
-
-
- #--------------- General Program Commands -----------------#
-
-
- #####
- # Event Handler: Individual Keystrokes
- #####
-
- def KeystrokeHandler(event):
-
- # If the key pressed is a command key (i.e., it is in the table of
- # defined commands), get its associated string and execute the command.
-
- cmd = UI.CmdTable.get(event.char, ["", "", ""])[1]
-
-
- # cmd == null means no matching command key - do nothing
- # Otherwise, replace config tokens with actual file/dir names
- if cmd:
- # Replace runtime-determined tokens
-
- selection = UI.LastInSelection()
-
- selections = ""
- for selected in UI.AllSelection():
- selections += selected + " "
-
- dselections = ""
- for sel in selections.split():
- dselections += UI.CurrentDir + sel + " "
-
- cmd = cmd.replace(DIR, UI.CurrentDir)
- cmd = cmd.replace(DSELECTION, UI.CurrentDir + selection)
- cmd = cmd.replace(DSELECTIONS, dselections)
- cmd = cmd.replace(SELECTION, selection)
- cmd = cmd.replace(SELECTIONS, selections)
-
- # Just dump command if we're debugging
-
- if DEBUG:
- print cmd
-
- # Otherwise,actually execute the command
- else:
- if OSNAME == 'win32':
- thread.start_new_thread(os.system, (cmd,))
- else:
- os.system(cmd)
-
- # end of 'KeystrokeHandler()'
-
-
- #####
- # Event Handler: Program Quit
- #####
-
- def KeyQuitProg(event):
- sys.exit()
-
- # End of 'KeyQuitProg()'
-
-
- #####
- # Event Handler: Run Command
- ####
-
- def KeyRunCommand(event):
-
- cmd = askstring(pRUNCMD, pENCMD)
- if cmd:
- thread.start_new_thread(os.system, (cmd,))
- UI.DirList.focus()
-
- # End of 'ChangeDir()'
-
-
- #####
- # Event Handler: Toggle Detail View
- #####
-
- def KeyToggleDetail(event):
-
- UI.SetDetailedView(not UI.DetailsOn)
- RefreshDirList(event)
-
- # End of 'KeyToggleDetail()'
-
-
- #------------------- Directory Navigation -----------------#
-
-
- #####
- # Event Handler: Change Directory/Path
- ####
-
- def ChangeDir(event):
-
- newpath = askstring(pCHPATH, pENPATH)
- if newpath:
- LoadDirList(newpath)
- KeySelTop(event)
- UI.DirList.focus()
-
- # End of 'ChangeDir()'
-
-
- #####
- # Event Handler: Goto $HOME
- #####
-
- def KeyHomeDir(event):
-
- if HOME:
- LoadDirList(HOME)
- else:
- LoadDirList(STARTDIR)
-
- # End of 'KeyHomeDir()'
-
-
- #####
- # Event Handler: Move To Previous Directory
- #####
-
- def KeyBackDir(event):
-
- # Move to last directory visited, if any - inhibit this from
- # being placed on the directory traversal stack
- if UI.LastDir:
- LoadDirList(UI.LastDir.pop(), save=FALSE)
-
- # No previous directory
- else:
- pass
-
- # End of 'KeyBackDir()'
-
-
- #####
- # Event Handler: Go Back to Initial Directory
- #####
-
- def KeyStartDir(event):
- global STARTDIR
-
- LoadDirList(STARTDIR)
-
- # End of 'KeyStartDir()'
-
-
- #####
- # Event Handler: Move up one directory
- #####
-
- def KeyUpDir(event):
-
- # Move up one directory level unless we're already at the root
- if UI.CurrentDir != os.path.abspath(PSEP):
- LoadDirList(UI.CurrentDir + "..")
-
- # End of 'KeyUpDir()'
-
-
- #---------------------- Selection Keys ---------------------#
-
-
- #####
- # Event Handler: Select All Items
- #####
-
- def KeySelAll(event):
- UI.DirList.selection_set(0, END)
-
- # End of 'KeySelAll()'
-
-
- #####
- # Event Handler: Select Next Item
- #####
-
- def KeySelNext(event):
- next = UI.DirList.index(ACTIVE) + 1
- SetSelection((str(next),), next)
-
- # End of 'KeySelNext()'
-
-
- #####
- # Event Handler: Select No Items
- #####
-
- def KeySelNone(event):
- UI.DirList.selection_clear(0, END)
-
- # End of 'KeySelNone()'
-
-
- #####
- # Event Handler: Select Previous Item
- #####
-
- def KeySelPrev(event):
- prev = UI.DirList.index(ACTIVE) - 1
- SetSelection((str(prev),), prev)
-
- # End of 'KeySelPrev()'
-
-
- #####
- # Event Handler: Select Last Item
- #####
-
- def KeySelEnd(event):
-
- # Get current number of items in listbox
- sz = UI.DirList.size()
-
- # And setup to last item accordingly
- SetSelection((str(sz),), sz)
-
- # End of 'KeySelEnd()'
-
-
- #####
- # Event Handler: Select First Item
- #####
-
- def KeySelTop(event):
- SetSelection(('0',),0)
-
- # End of 'KeySelTop()'
-
-
- #####
- # Process Current Selection
- #####
-
- def DirListHandler(event):
- global UI
-
- SAVE = TRUE
-
- # Get current selection. If none, just return, otherwise process
- selected = UI.LastInSelection()
- if not selected:
- return
-
- # If selection is a directory, move there and list contents.
-
- if os.path.isdir(os.path.join(UI.CurrentDir, selected)):
-
- # If we're on Unix, don't follow symlinks pointing back to themselves
-
- if OSNAME == 'posix' and os.path.samefile(UI.CurrentDir, UI.CurrentDir + selected):
- WrnMsg(wSYMBACK % (UI.CurrentDir + selected))
- return
-
- # We don't push this selection on the stack if
- # we are at root directory and user presses '..'
-
- if (selected == '..') and (UI.CurrentDir == os.path.abspath(PSEP)):
- SAVE = FALSE
-
- # Build full path name
- selected = os.path.join(os.path.abspath(UI.CurrentDir), selected)
-
- # Convert ending ".." into canonical path
- if selected.endswith(".."):
- selected = PSEP.join(selected.split(PSEP)[:-2])
-
- # Need to end the directory string with a path
- # separator character so that subsequent navigation
- # will work when we hit the root directory of the file
- # system. In the case of Unix, this means that
- # if we ended up at the root directory, we'll just
- # get "/". In the case of Win32, we will get
- # "DRIVE:/".
-
- selected += PSEP
-
- # Load UI with new directory
- LoadDirList(selected, save=SAVE)
-
- # Indicate that we entered a new directory this way.
- # This is a workaround for Tk madness. When this
- # routine is invoked via the mouse, Tk sets the
- # activation *when this routine returns*. That means
- # that the activation set at the end of LoadDirList
- # gets overidden. We set this flag here so that
- # we can subsequently do the right thing in our
- # background polling loop. Although this routine
- # can also be invoked via a keyboard selection,
- # we run things this way regardless since no harm
- # will be done in the latter case.
-
- UI.MouseNewDir = TRUE
-
-
- # No, a *file* was selected with a double-click
- # We know what to do on Win32 and Unix. We ignore
- # the selection elsewhere.
-
- elif OSNAME == 'nt':
- os.startfile(os.path.join(os.path.abspath(UI.CurrentDir), selected))
-
- elif OSNAME == 'posix':
- thread.start_new_thread(os.system, (os.path.join(os.path.abspath(UI.CurrentDir), selected),))
-
-
- # Have to update the window title because selection changed
- UI.UpdateTitle(UIroot)
-
- # End of 'DirListHandler()'
-
-
- #####
- # Load UI With Selected Directory
- #####
-
- def LoadDirList(newdir, save=TRUE):
-
- # Get path into canonical form
- newdir = os.path.abspath(newdir)
-
- # Make sure newdir properly terminated
- if newdir[-1] != PSEP:
- newdir += PSEP
-
- # Check right now to see if we can read
- # the directory. If not, at least we
- # haven't screwed up the widget's current
- # contents or program state.
-
- try:
- contents = BuildDirList(newdir)
- except:
- # If CurrentDir set, we're still there: error w/ recovery
- if UI.CurrentDir:
- ErrMsg(eDIRRD % newdir)
- return
-
- # If not, we failed on the initial directory: error & abort
- else:
- ErrMsg(eINITDIRBAD % newdir)
- sys.exit(1)
-
- # Push last directory visited onto the visited stack
-
- # Do not do this if we've been told not to OR if
- # what we're about to save is the same as the top
- # of the stack OR if the current directory is ""
-
- # If there is anything on the stack, see if last element
- # matches what we're about to put there.
-
- if UI.LastDir and UI.LastDir[-1] == UI.CurrentDir:
- save = FALSE
-
- if save and UI.CurrentDir:
- UI.LastDir.append(UI.CurrentDir)
-
- # And select new directory to visit
- UI.CurrentDir = newdir
-
- # Wait until we have exclusive access to the widget
-
- while not UI.DirListMutex.testandset():
- pass
-
- # Clear out the old contents
- UI.DirList.delete(0,END)
-
- # Load new directory contents into UI
- for x in contents:
- UI.DirList.insert(END, x)
-
- # 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()'
-
-
- #----------------------------------------------------------#
- # Program Entry Point #
- #----------------------------------------------------------#
-
- # Command line processing
-
- try:
- opts, args = getopt.getopt(sys.argv[1:], '-b:c:df:hn:qrs:vw:x:y:')
- except getopt.GetoptError:
- Usage()
- sys.exit(1)
-
- # Parse command line
-
- for opt, val in opts:
- if opt == "-b":
- BCOLOR = val
- if opt == "-c":
- CONF = val
- if opt == "-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 == "-v":
- print RCSID
- sys.exit(0)
- if opt == "-w":
- FWT = val
- if opt == "-x":
- WIDTH = val
- if opt == "-y":
- HEIGHT = val
-
-
- # Create an instance of the UI
- UIroot = Tk()
- UI = twanderUI(UIroot)
-
- # Make the Tk window the topmost in the Z stack.
- # 'Gotta do this or Win32 will not return input
- # focus to our program after a startup warning
- # display.
-
- UIroot.tkraise()
-
- #####
- # Setup global UI variables
- #####
-
- # Figure out where to start
- # Program can only have 0 or 1 arguments
- # Make sure any startdir argument is legit
-
- if len(args) > 1:
- ErrMsg(eTOOMANY)
- sys.exit(1)
-
- if len(args) == 1:
- STARTDIR = args[0]
- if not os.path.isdir(STARTDIR):
- ErrMsg(eBADROOT % STARTDIR)
- sys.exit(1)
-
- # Get starting directory into canonical form
- STARTDIR = os.path.abspath(STARTDIR)
-
- # Setup builtin variables
- UI.BuiltIns = {"DIR":"", "SELECTION":"", "SELECTIONS":"", "DSELECTION":"",
- "DSELECTIONS":"", "PROMPT:":""}
-
- # Parse the and store configuration file, if any
- ParseConfFile(None)
-
- # Initialize directory stack
- UI.LastDir = []
-
- # 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()
-