So hereâs version 2 of the âopen withâ applet/droplet.
The fancy new feature is that you can drop folders on the applet (or select them from the menu) and the folders will be scanned and all extensions of the files contained will be added to the âopen withâ appletâs info.plist file, and the applet reregistered with launch services. This takes the work out of getting it on the âopen withâ menu for all the file types you might want to use it for!
The scanning is pretty quick, a has table is used to maintain a list of extensions free of duplicates, and it getâs sorted before being reinserted into the info.plist file.
The script follows.
Thanks again for all the help guys.
Special thanks to DJ Bazzie Wazzie for the AS hash table implementation, Nigel Garvey for the merge sort, and kel1, McUsrII, and Shane Stanley for their help in this thread.
property SampleList : {{path:"Applications:gedit.app", name:"gedit", BundleID:"org.gnome.gedit", TypeExtList:{"log", "script"}}}
property DefaultAppListFile : "DefaultApps.b"
property SummaryFile : "File Association Summary.txt"
property HTableSize : 1000
property TheList : {}
on open theFiles
local FileExt, FileName, FoundAppList, OpenWithAppList, PickAppList, FileList, FolderList
set FileList to {}
set FolderList to {}
repeat with aFile in theFiles
if text item -1 of (aFile as text) is ":" then
set end of FolderList to aFile
else
set end of FileList to aFile
end if
end repeat
if (count of FolderList) > 0 then
GetExtsFromFolders(FolderList)
else
GetList()
repeat with aFile in FileList
tell application "System Events"
set FileExt to name extension of aFile
set FileName to displayed name of aFile
end tell
set FoundAppList to {}
set OpenWithAppList to {}
repeat with AnApp in TheList
if TypeExtList of AnApp contains FileExt then set end of FoundAppList to AnApp
end repeat
if (count of FoundAppList) = 1 then
set OpenWithAppList to FoundAppList
else if (count of FoundAppList) > 1 then
set PickAppList to {}
repeat with AnApp in FoundAppList
set end of PickAppList to name of AnApp
end repeat
set PickAppList to choose from list PickAppList with title "Open " & FileName & " using:" default items {beginning of PickAppList} with multiple selections allowed without empty selection allowed
if PickAppList is false then return 0
repeat with PickedAppName in PickAppList
repeat with AnApp in TheList
if PickedAppName as string is (name of AnApp) as string then
set end of OpenWithAppList to AnApp
exit repeat
end if
end repeat
end repeat
else
set end of OpenWithAppList to SelectDefaultApp(missing value, {FileExt})
if end of OpenWithAppList is false then return 0
end if
repeat with AnApp in OpenWithAppList
if BundleID of AnApp is not missing value then
tell application "Finder" to open aFile using application file id (BundleID of AnApp)
else
tell application (path of AnApp)
activate
open aFile
end tell
end if
end repeat
end repeat
end if
end open
on run
local ActionList, AddExt, RemExt, RemApp, OpenFiles, ViewSummary, UpdateInfoPlist
local TmpList, TmpStr, TmpButton
set AddExt to "Add File Type Association"
set RemExt to "Remove File Type Association"
set RemApp to "Remove Application Association"
set OpenFiles to "Open File(s)"
set ViewSummary to "View File Type Association Summary"
set UpdateInfoPlist to "Add this applet to more files' 'Open with...' menu"
repeat
GetList()
if (count of TheList) > 0 then
set ActionList to {AddExt, RemExt, RemApp, ViewSummary, UpdateInfoPlist, OpenFiles}
else
set ActionList to {AddExt, UpdateInfoPlist, OpenFiles}
end if
set TheAction to choose from list ActionList with prompt "What would you like to do?" with title "Open With..." default items {AddExt} without multiple selections allowed and empty selection allowed
if TheAction is false then return 0
set TheAction to beginning of TheAction
if TheAction is AddExt then
set {TmpStr, TmpButton} to {text returned, button returned} of (display dialog "Enter file extension(s) to add association for:" & return & return & "(separate multiple extensions with spaces)" default answer "" with icon note)
if TmpButton is "Cancel" then return 0
set TmpStr to parseLine(TmpStr, space)
set TmpList to GetTmpList(false)
set NewApp to "Select new application"
if (count of TmpList) > 0 then
set beginning of TmpList to NewApp
set TmpList to choose from list TmpList with prompt "Add file association(s) to which application(s)?" default items {beginning of TmpList} with multiple selections allowed and empty selection allowed
if TmpList is false then return 0
else
set TmpList to {NewApp}
end if
repeat with AnApp in TmpList
if AnApp as string is NewApp as string then
SelectDefaultApp(missing value, TmpStr)
else
SelectDefaultApp(AnApp, TmpStr)
end if
end repeat
else if TheAction is RemExt then
set TmpList to GetTmpList(true)
if (count of TmpList) > 1 then
set TmpStr to choose from list TmpList with prompt "Which file type extension would you like to remove?" default items {beginning of TmpList} without multiple selections allowed and empty selection allowed
if TmpStr is false then return 0
set TmpStr to beginning of TmpStr
else
set TmpStr to beginning of TmpList
end if
set TmpList to {}
repeat with AnApp in TheList
if TypeExtList of AnApp contains TmpStr then set end of TmpList to name of AnApp
end repeat
if (count of TmpList) > 1 then
sort(TmpList, 1, -1, {})
set TmpList to choose from list TmpList with prompt "Remove " & TmpStr & " association from which applications?" default items {beginning of TmpList} with multiple selections allowed without empty selection allowed
if TmpList is false then return 0
end if
repeat with ByeApp in TmpList
repeat with i from 1 to count of TheList
if name of (item i of TheList) as string is ByeApp as string then
if (count of TypeExtList of item i of TheList) > 1 then
set TypeExtList of item i of TheList to RemoveFromList(TypeExtList of item i of TheList, {TmpStr})
else
set TheList to RemoveFromList(TheList, {item i of TheList})
end if
exit repeat
end if
end repeat
end repeat
SaveList()
else if TheAction is RemApp then
set TmpList to GetTmpList(false)
if (count of TmpList) > 1 then
set TmpStr to choose from list TmpList with prompt "Which application would you like to remove association(s) for?" default items {beginning of TmpList} without multiple selections allowed and empty selection allowed
if TmpStr is false then return 0
set TmpStr to beginning of TmpStr
else
set TmpStr to beginning of TmpList
end if
repeat with i from 1 to count of TheList
if name of item i of TheList is TmpStr then
if (count of TypeExtList of item i of TheList) > 1 then
sort(TypeExtList of item i of TheList, 1, -1, {})
set TmpList to choose from list TypeExtList of item i of TheList with prompt "Remove which file type associations from " & name of item i of TheList & "?" default items {beginning of TmpList} with multiple selections allowed without empty selection allowed
if TmpList is false then return 0
if (count of TmpList) < (count of TypeExtList of item i of TheList) then
set TypeExtList of item i of TheList to RemoveFromList(TypeExtList of item i of TheList, TmpList)
else
set TheList to RemoveFromList(TheList, {item i of TheList})
end if
else
set TheList to RemoveFromList(TheList, {item i of TheList})
end if
exit repeat
end if
end repeat
SaveList()
else if TheAction is ViewSummary then
set TmpStr to (((path to me) as text) & "Contents:Resources:" & SummaryFile)
tell application "Finder"
activate
reveal TmpStr
end tell
return 0
else if TheAction is UpdateInfoPlist then
GetExtsFromFolders(choose folder with prompt "Select one or more folders that will be scanned, and this applet will be added to the 'Open with...' menu for all filetypes within" with multiple selections allowed)
return 0
else if TheAction is OpenFiles then
open (choose file with multiple selections allowed)
return 0
end if
end repeat
end run
on OpenInfoFile(InfoPlistFile)
if InfoPlistFile is not missing value then
set InfoRef to open for access (InfoPlistFile as text)
set InfoContents to read InfoRef
set InfoContents to paragraphs of InfoContents
set NewInfoContents to {}
set EndInfoContents to {}
set ExtList to {}
set SectionNumber to 1
repeat with aLine in InfoContents
if SectionNumber is 1 then
set end of NewInfoContents to aLine as string
if (aLine as string) contains "CFBundleTypeExtensions" then
set SectionNumber to SectionNumber + 1
end if
else if SectionNumber is 2 then
if (aLine as string) contains "<string>" then
set end of ExtList to beginning of parseLine(end of parseLine(aLine as text, "<string>"), "</string>")
else if (aLine as string) contains "</array>" then
set end of EndInfoContents to aLine as string
set SectionNumber to SectionNumber + 1
else
set end of NewInfoContents to aLine as string
end if
else
set end of EndInfoContents to aLine as string
end if
end repeat
close access InfoRef
return {NewInfoContents, EndInfoContents, ExtList}
end if
end OpenInfoFile
on SaveInfoFile(InfoPlistFile, NewInfoContents, EndInfoContents, ExtList)
if InfoPlistFile is not missing value then
set NewInfoFileName to (InfoPlistFile as text) & ".tmp" as text
set NewInfoRef to open for access NewInfoFileName with write permission
set eof of NewInfoRef to 0
repeat with i from 1 to count of NewInfoContents
if i > 1 then write return to NewInfoRef
write item i of NewInfoContents to NewInfoRef
end repeat
repeat with i in ExtList
write ((return & tab & tab & tab & tab & "<string>" & i as text) & "</string>") as text to NewInfoRef
end repeat
repeat with i in EndInfoContents
write return & i as text to NewInfoRef
end repeat
close access NewInfoRef
set NewInfoFile to NewInfoFileName as alias
set Scpt to "cp " & quoted form of POSIX path of InfoPlistFile & space & quoted form of (POSIX path of InfoPlistFile & ".bak")
try
do shell script Scpt --with administrator privileges
set Scpt to "mv " & quoted form of POSIX path of NewInfoFile & space & quoted form of POSIX path of InfoPlistFile
do shell script Scpt --with administrator privileges
set Scpt to "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -f " & quoted form of POSIX path of ((text items 1 through -2 of ((path to me) as text)) as text)
do shell script Scpt --with administrator privileges
on error
display alert "Failed to modify Info.plist file" as critical
return 0
end try
end if
end SaveInfoFile
on GetExtsFromFolders(FolderList)
if FolderList is not false then
set count1 to count of FolderList
SetProgress({message:"Adding file type information..."})
--tell application "ASObjC Runner"
-- activate
-- reset progress
-- set properties of progress window to {bar two visible:true, max value:count1, message:"Getting folder contents", button visible:true}
-- show progress
--end tell
set InfoPlistFile to missing value
try
set InfoPlistFileName to (((path to me) as text) & "Contents:Info.plist") as text
set InfoPlistFile to InfoPlistFileName as alias
on error
display alert "Couldn't get Info.plist file" as critical
return false
end try
set {TopInfoContents, BottomInfoContents, ExtList} to OpenInfoFile(InfoPlistFile)
set FullList to {}
repeat count of ExtList times
set end of FullList to true
end repeat
--set ExtList to {}
--set hTable to my hashTable's newInstance()'s init()
set hTable to my hashTable's newInstance()'s initWithKeysAndValues(ExtList, FullList)
set i to 1
repeat with aFolder in FolderList
--set FullList to paragraphs of (do shell script "cd " & quoted form of POSIX path of aFolder & ";ls -R")
set scr to "ls -R " & quoted form of POSIX path of aFolder & " | awk '
/:$/&&f{s=$0;f=0}
/:$/&&!f{sub(/:$/,\"\");s=$0;f=1;next}
NF&&f{ print s\"/\"$0 }'"
set FullList to paragraphs of (do shell script scr)
set count2 to count of FullList
SetProgress({max:count2})
--tell application "ASObjC Runner"
-- set properties of progress window to {max value two:count2, current value:i, detail:"folder " & i & " of " & count1}
--end tell
set j to 1
repeat with aLine in FullList
SetProgress({current:j, detail:"File " & j & " of " & count2 & " in folder " & i & " of " & count1})
--tell application "ASObjC Runner"
-- set properties of progress window to {current value two:j, detail two:"file " & j & " of " & count2}
-- if button was pressed of progress window then
-- hide progress
-- return 0
-- end if
--end tell
if aLine as string is not "" and (aLine as string) contains "." then
set TheExt to replaceString(replaceString(aLine, "//", "/"), "/", ":")
if text item 1 of TheExt is ":" then repeat until text item 1 of TheExt is not ":"
set TheExt to (text items 2 through -1 of TheExt) as text
end repeat
try
set TheExt to TheExt as alias
end try
if class of TheExt is alias then
tell application "System Events"
set TheExt to name extension of TheExt
end tell
if TheExt is not "" and TheExt is not missing value then
hTable's setValueforKey(TheExt, true)
end if
end if
--set TheExt to (end of parseLine(beginning of parseLine(beginning of parseLine(aLine, ":"), "/"), ".")) as string
--if ExtList does not contain TheExt then set end of ExtList to TheExt
end if
set j to j + 1
end repeat
set i to i + 1
end repeat
SetProgress({message:"Finishing...", current:-1})
--tell application "ASObjC Runner"
-- reset progress
-- set properties of progress window to {bar two visible:false, button visible:false, indeterminate:true, message:"Updating info.plist"}
--end tell
set ExtList to hTable's keys()
sort(ExtList, 1, -1, {})
SaveInfoFile(InfoPlistFile, TopInfoContents, BottomInfoContents, ExtList)
--set OutputFile to (DesktopPath & "output.txt") as text
--
--set OutputFile to open for access OutputFile with write permission
--set eof of OutputFile to 0
--
--set count1 to count of ExtList
--tell application "ASObjC Runner"
-- reset progress
-- set properties of progress window to {bar two visible:false, button visible:true, max value:count1, current value:1, message:"writing output"}
--end tell
--set ExtList to hTable's keys()
--sort(ExtList, 1, -1, {})
--set i to 1
--repeat with TheExt in ExtList
-- tell application "ASObjC Runner"
-- set properties of progress window to {current value:i, detail:"line " & i & " of " & count1}
-- if button was pressed of progress window then
-- close access OutputFile
-- hide progress
-- return 0
-- end if
-- end tell
-- write ("<string>" & TheExt & "</string>" & return) as text to OutputFile
-- set i to i + 1
--end repeat
--tell application "ASObjC Runner"
-- hide progress
--end tell
--close access OutputFile
end if
end GetExtsFromFolders
on GetList()
local ListPath, ListFile
set ListPath to (((path to me) as text) & "Contents:Resources:" & DefaultAppListFile)
set ListFile to open for access ListPath -- with write permission
--set eof of ListFile to 0
--write SampleList to ListFile as list
set TheList to {}
try
set TheList to read ListFile from 0 as list
end try
close access ListFile
end GetList
on SaveList()
local ListPath, ListFile
set ListPath to (((path to me) as text) & "Contents:Resources:" & DefaultAppListFile)
set ListFile to open for access ListPath with write permission
set eof of ListFile to 0
write TheList to ListFile as list
close access ListFile
set ListPath to (((path to me) as text) & "Contents:Resources:" & SummaryFile)
set ListFile to open for access ListPath with write permission
set eof of ListFile to 0
if (count of TheList) > 0 then
repeat with AnApp in TheList
write (name of AnApp & " (" & name of AnApp & ", located at " & path of AnApp & ")" & return & "File types: ") as text to ListFile
repeat with i from 1 to count of TypeExtList of AnApp
write (item i of TypeExtList of AnApp) as text to ListFile
if i < (count of TypeExtList of AnApp) then write ", " as text to ListFile
end repeat
write (return & return) as text to ListFile
end repeat
else
write "There are no file associations in effect." to ListFile
end if
close access ListFile
end SaveList
on SelectDefaultApp(NewAppName, FileExts)
local SelectedApp, AppPath, AppName, AppBundleID, NewApp, ListModified
if NewAppName is missing value then
set SelectNewApp to true
set SelectedApp to choose file with prompt "Select application to add file association to" default location (path to applications folder) of type {"app"} without multiple selections allowed
if SelectedApp is false then return false
set AppPath to SelectedApp as text
if text item -1 of AppPath is ":" then repeat until text item -1 of AppPath is not ":"
set AppPath to (text items 1 through -2 of AppPath) as text
end repeat
else
set SelectNewApp to false
end if
repeat with AnApp in TheList
if (SelectNewApp and AppPath is path of AnApp) or (not SelectNewApp and NewAppName as string is name of AnApp as string) then
set ListModified to false
repeat with AnExt in FileExts
if TypeExtList of AnApp does not contain AnExt then
set end of TypeExtList of AnApp to AnExt
set ListModified to true
end if
end repeat
if ListModified then
SaveList()
return AnApp
end if
end if
end repeat
if SelectNewApp then
tell application "System Events" to set AppName to displayed name of SelectedApp
set AppBundleID to missing value
try
tell application "Finder" to set AppBundleID to id of application file AppPath
end try
set NewApp to {path:AppPath, name:AppName, BundleID:AppBundleID, TypeExtList:FileExts}
set end of TheList to NewApp
SaveList()
return NewApp
end if
end SelectDefaultApp
on RemoveFromList(OldList, RemItemList)
local NewList
set NewList to {}
if class of beginning of RemItemList is record then
repeat with i from 1 to count of OldList
set RemItem to false
repeat with j from 1 to count of RemItemList
if name of item i of OldList is name of item j of RemItemList then
set RemItem to true
exit repeat
end if
end repeat
if not RemItem then set end of NewList to item i of OldList
end repeat
else
repeat with i from 1 to count of OldList
if item i of OldList is not in RemItemList then set end of NewList to item i of OldList
end repeat
end if
return NewList
end RemoveFromList
on GetTmpList(MakeExtList)
local TmpList, Tmp
set TmpList to {}
if not MakeExtList then
repeat with AnApp in TheList
set end of TmpList to name of AnApp
end repeat
else
repeat with AnApp in TheList
repeat with AnExt in TypeExtList of AnApp
if TmpList does not contain AnExt as string then set end of TmpList to AnExt as string
end repeat
end repeat
end if
sort(TmpList, 1, -1, {})
return TmpList
end GetTmpList
on SetProgress(P)
try
set progress completed steps to current of P
end try
try
set progress total steps to max of P
end try
try
set progress description to message of P
end try
try
set progress additional description to detail of P
end try
end SetProgress
on parseLine(theLine, delimiter)
-- This came from Nigel Garvey
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to {delimiter}
set theTextItems to theLine's text items
set AppleScript's text item delimiters to astid
repeat with i from 1 to (count theTextItems)
if (item i of theTextItems is "") then set item i of theTextItems to missing value
end repeat
return theTextItems's every text
end parseLine
--c-- replaceString(theText, oldString, newString)
--d-- Case-sensitive find and replace of all occurrences.
--a-- theText : string -- the string to search
--a-- oldString : string -- the find string
--a-- newString : string -- the replacement string
--r-- string
--x-- replaceString("Hello hello", "hello", "Bye") --> "Hello Bye"
--u-- ljr (http://applescript.bratis-lover.net/library/string/)
on replaceString(theText, oldString, newString)
local astid, theText, oldString, newString, lst
set astid to AppleScript's text item delimiters
try
considering case
set AppleScript's text item delimiters to oldString
set lst to every text item of theText
set AppleScript's text item delimiters to newString
set theText to lst as string
end considering
set AppleScript's text item delimiters to astid
return theText
on error eMsg number eNum
set AppleScript's text item delimiters to astid
error "Can't replaceString: " & eMsg number eNum
end try
end replaceString
on CustomMergeSort(ListToSort, l, r, customiser) -- Sort items l thru r of theList.
script o
property comparer : me
property slave : me
property lst : ListToSort
property rangeB : missing value
on msrt(l, r)
-- Get indices for the end of the left half of this range and the beginning of the right.
set m1 to (l + r) div 2
set m2 to m1 + 1
sortHalf(l, m1) -- Sort the left half.
sortHalf(m2, r) -- Sort the right half.
-- Extract a copy of the whole range.
set rangeB to items l thru r of my lst
slave's extract(l, r)
-- Merge the already sorted halves of the extract back into the original: for each slot
-- in the original, compare the lowest unassigned item from the left half of the extract
-- with that from the right half and assign the lower of the two to the slot.
-- Indices relative to the extract.
set i to 1 -- Beginning of left half.
set endLeft to m2 - l -- End of left half.
set j to endLeft + 1 -- Beginning of right half.
set endRight to r - l + 1 -- End of right half.
-- Get the first item from the left and right halves of the extract.
set lv to beginning of my rangeB
set rv to item j of my rangeB
repeat with k from l to r
if (comparer's isGreater(lv, rv)) then
-- The right value's less than the left. Assign it to this slot in the original list.
set item k of my lst to rv
slave's merge(j, k)
-- Get the next right value. If none, assign the remaining left values to the remaining slots and exit this repeat and recursion level.
if (j < endRight) then
set j to j + 1
set rv to item j of my rangeB
else
repeat with k from (k + 1) to r
set item k of my lst to item i of my rangeB
slave's merge(i, k)
set i to i + 1
end repeat
exit repeat
end if
else
-- The left value's less than or equal to the right. Assign it to this slot in the original list.
set item k of my lst to lv
slave's merge(i, k)
-- Get the next left value. If none, simply exit this repeat and recursion level, as the remaining right values are from the remaining slots anyway.
if (i < endLeft) then
set i to i + 1
set lv to item i of my rangeB
else
exit repeat
end if
end if
end repeat
end msrt
-- Sort the "half-range" l thru r.
on sortHalf(l, r)
-- If there are more than two items in this half, recurse further to merge-sort them.
-- If there are two, simply swap them (if necessary). If one, do nothing.
if (r - l > 1) then
msrt(l, r)
else if (r > l) then
set lv to item l of my lst
set rv to item r of my lst
if (comparer's isGreater(lv, rv)) then
set item l of my lst to rv
set item r of my lst to lv
slave's swap(l, r)
end if
end if
end sortHalf
-- Default comparison and slave handlers, for a normal, mergeSort-style sort.
on isGreater(a, b)
(a > b)
end isGreater
on extract(a, b)
end extract
on merge(a, b)
end merge
on swap(a, b)
end swap
end script
-- Process the input parmeters.
set listLen to (count ListToSort)
if (listLen > 1) then
-- Negative and/or transposed range indices.
if (l < 0) then set l to listLen + l + 1
if (r < 0) then set r to listLen + r + 1
if (l > r) then set {l, r} to {r, l}
-- Supplied or default customisation scripts.
if (customiser's class is record) then set {comparer:o's comparer, slave:o's slave} to (customiser & {comparer:o, slave:o})
-- Do the sort.
o's msrt(l, r)
end if
return -- nothing
end CustomMergeSort
property sort : CustomMergeSort
--Hash table implementation by DJ Bazzie Wazzie at Macscripter:
--http://macscripter.net/viewtopic.php?id=39424
script hashTable
on newInstance()
script hashTableInstance
property parent : hashTable
property size : missing value
property hashList : missing value
property keyList : missing value
property valueList : missing value
end script
end newInstance
on init()
set my size to 0
set my hashList to {}
repeat HTableSize times
set end of my hashList to {}
end repeat
set my keyList to {}
set my valueList to {}
return me
end init
on initWithKeysAndValues(_keys, _values)
set my keyList to _keys
set my valueList to _values
set my size to count my keyList
set my hashList to {}
repeat HTableSize times
set end of my hashList to {}
end repeat
repeat with x from 1 to my size
set end of item hashFunction(item x of my keyList) of my hashList to x
end repeat
return me
end initWithKeysAndValues
on setValueforKey(_key, _value)
set x to hashFunction(_key)
repeat with node in item x of my hashList
if id of item node of my keyList = id of _key then
set item node of my valueList to _value
return true
end if
end repeat
set my size to (my size) + 1
set end of my valueList to _value
set end of my keyList to _key
set end of item x of my hashList to my size
end setValueforKey
on valueForKey(_key)
set x to hashFunction(_key)
repeat with node in item x of my hashList
if id of item node of my keyList = id of _key then
return item node of my valueList
end if
end repeat
return missing value
end valueForKey
on keyExists(_key)
considering case
return my keyList contains _key
end considering
end keyExists
on keys()
return my keyList
end keys
on setKeys(_keys)
set _keys to every text of _keys
if (count _keys) = (count my keyList) then
set my keyList to _keys
--need to re-hash
set my hashList to {}
repeat HTableSize times
set end of my hashList to {}
end repeat
repeat with x from 1 to my size
set end of item hashFunction(item x of my keyList) of my hashList to x
end repeat
end if
end setKeys
on valueExists(_value)
return my valueList contains _value
end valueExists
on values()
return my valueList
end values
on setValues(_values)
if (count _values) = (count my valueList) then set my valueList to _values
end setValues
on count
return my size
end count
on hashFunction(_key)
set _hash to count _key
--Credits to Shane Stanly here, supports single character keys now.
repeat with char in (id of _key as list)
set _hash to _hash + char
end repeat
return _hash mod HTableSize + 1
end hashFunction
end script