Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 362 additions & 0 deletions scripts/generate_menu_sdef.applescript
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
-- Menu Item SDEF Generator
-- Prompts for an app, enumerates menu items, and writes an .sdef and plist.

use scripting additions

on run
set appName to my promptForAppName()
if appName is "" then return

if my ensureAppRunning(appName) is false then return

set outputFolder to my promptForOutputFolder()
if outputFolder is missing value then return

set sdefFileName to my promptForText("SDEF file name:", appName & ".sdef")
if sdefFileName is "" then return

set plistFileName to my promptForText("Plist file name:", "ScriptingInfo.plist")
if plistFileName is "" then return

set menuRecords to my fetchMenuItems(appName)
if (count of menuRecords) is 0 then
display dialog "No menu items found for " & appName buttons {"OK"} default button "OK"
return
end if

set sdefText to my buildSdef(appName, menuRecords)
set sdefPath to my joinPath(outputFolder, sdefFileName)
my writeTextFile(sdefPath, sdefText)

set bundleId to my getBundleId(appName)
set plistText to my buildPlist(appName, bundleId, sdefFileName)
set plistPath to my joinPath(outputFolder, plistFileName)
my writeTextFile(plistPath, plistText)

display dialog "Generated:" & linefeed & sdefPath & linefeed & plistPath buttons {"OK"} default button "OK"
end run

on promptForAppName()
repeat
try
set dialogResult to display dialog "App to scan for menu items:" default answer "Finder" buttons {"Cancel", "OK"} default button "OK"
if button returned of dialogResult is "Cancel" then return ""
set appName to text returned of dialogResult
on error number -128
return ""
end try

if appName is "" then return ""

try
set _appAlias to path to application appName
return appName
on error
display dialog "App not found: " & appName buttons {"OK"} default button "OK"
end try
end repeat
end promptForAppName

on promptForOutputFolder()
try
return choose folder with prompt "Choose output folder for the .sdef and .plist files:"
on error number -128
return missing value
end try
end promptForOutputFolder

on promptForText(promptText, defaultValue)
try
set dialogResult to display dialog promptText default answer defaultValue buttons {"Cancel", "OK"} default button "OK"
if button returned of dialogResult is "Cancel" then return ""
return text returned of dialogResult
on error number -128
return ""
end try
end promptForText

on ensureAppRunning(appName)
set isRunning to false
tell application "System Events"
set isRunning to (exists process appName)
end tell
if isRunning then return true

try
tell application appName to activate
on error errMsg
display dialog "Unable to launch " & appName & "." & linefeed & errMsg buttons {"OK"} default button "OK"
return false
end try

repeat 20 times
delay 0.2
tell application "System Events"
set isRunning to (exists process appName)
end tell
if isRunning then return true
end repeat

display dialog appName & " did not become ready in time." buttons {"OK"} default button "OK"
return false
end ensureAppRunning

on fetchMenuItems(appName)
set results to {}
try
tell application "System Events"
if not (exists process appName) then return results
tell process appName
if (count of menu bars) is 0 then return results
set menuBarItems to menu bar items of menu bar 1
repeat with menuBarItem in menuBarItems
set barName to name of menuBarItem
try
set topMenu to menu of menuBarItem
set results to results & my collectMenuItems(topMenu, {barName})
end try
end repeat
end tell
end tell
on error errMsg number errNum
display dialog "Failed to read menus. Ensure Accessibility permissions for System Events." & linefeed & errMsg buttons {"OK"} default button "OK"
end try
return results
end fetchMenuItems

on collectMenuItems(menuRef, pathList)
set collected to {}
set menuItems to menu items of menuRef
repeat with menuItem in menuItems
set itemName to name of menuItem
set itemPath to pathList & {itemName}
set itemEnabled to enabled of menuItem
set end of collected to {pathList:itemPath, enabled:itemEnabled}
if (exists menu of menuItem) then
set subMenu to menu of menuItem
set collected to collected & my collectMenuItems(subMenu, itemPath)
end if
end repeat
return collected
end collectMenuItems

on buildSdef(appName, menuRecords)
set usedNames to {}
set lines to {}
set end of lines to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
set end of lines to "<!DOCTYPE sdef SYSTEM \"sdef.dtd\">"
set end of lines to "<sdef>"
set suiteName to "Menu Items"
set suiteDesc to "Menu item commands generated from " & appName & "."
set end of lines to " <suite name=\"" & my escapeXml(my asciiOnly(suiteName)) & "\" code=\"MENU\" description=\"" & my escapeXml(my asciiOnly(suiteDesc)) & "\">"

set indexNumber to 0
repeat with itemRecord in menuRecords
set indexNumber to indexNumber + 1
set pathList to pathList of itemRecord
set sanitizedParts to my sanitizePathList(pathList)
set commandBaseName to my joinWith(sanitizedParts, " ")
set commandName to commandBaseName
set suffixCount to 1
repeat while (commandName is in usedNames)
set suffixCount to suffixCount + 1
set commandName to commandBaseName & " " & suffixCount
end repeat
set end of usedNames to commandName

set menuPathString to my joinWith(sanitizedParts, " > ")
set eventCode to "MNU1" & my fourCharCode(indexNumber)
set descText to "Menu path: " & menuPathString

set lineText to " <command name=\"" & my escapeXml(commandName) & "\" code=\"" & eventCode & "\" description=\"" & my escapeXml(descText) & "\"/>"
set end of lines to lineText
end repeat

set end of lines to " </suite>"
set end of lines to "</sdef>"
return my joinLines(lines)
end buildSdef

on buildPlist(appName, bundleId, sdefFileName)
set safeName to my escapeXml(my asciiOnly(appName))
set safeBundleId to my escapeXml(my asciiOnly(bundleId))
set safeSdef to my escapeXml(my asciiOnly(sdefFileName))
set lines to {}
set end of lines to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
set end of lines to "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
set end of lines to "<plist version=\"1.0\">"
set end of lines to "<dict>"
set end of lines to " <key>CFBundleIdentifier</key>"
set end of lines to " <string>" & safeBundleId & "</string>"
set end of lines to " <key>CFBundleName</key>"
set end of lines to " <string>" & safeName & "</string>"
set end of lines to " <key>NSScriptingDefinition</key>"
set end of lines to " <string>" & safeSdef & "</string>"
set end of lines to "</dict>"
set end of lines to "</plist>"
return my joinLines(lines)
end buildPlist

on getBundleId(appName)
try
set bundleId to id of application appName
if bundleId is missing value then error "missing bundle id"
return bundleId as text
on error
return "com.example." & my slugify(appName)
end try
end getBundleId

on slugify(rawText)
set cleaned to my normalizeLabel(rawText)
set cleaned to my replaceText(cleaned, " ", "")
set cleaned to my replaceText(cleaned, "-", "")
set cleaned to my replaceText(cleaned, "_", "")
if cleaned is "" then set cleaned to "app"
return cleaned
end slugify

on sanitizePathList(pathList)
set sanitizedParts to {}
repeat with partItem in pathList
set safePart to my normalizeLabel(partItem)
if safePart is "" then set safePart to "Menu"
set end of sanitizedParts to safePart
end repeat
return sanitizedParts
end sanitizePathList

on normalizeLabel(rawText)
set cleaned to my asciiOnly(rawText)
set cleaned to my replaceText(cleaned, ":", " ")
set cleaned to my replaceText(cleaned, "/", " ")
set cleaned to my replaceText(cleaned, "&", " and ")
set cleaned to my replaceText(cleaned, ">", " ")
set cleaned to my replaceText(cleaned, "<", " ")
set cleaned to my replaceText(cleaned, "?", " ")
set cleaned to my replaceText(cleaned, "*", " ")
set cleaned to my replaceText(cleaned, quote, "")
set cleaned to my replaceText(cleaned, "'", "")
set cleaned to my collapseWhitespace(cleaned)
if cleaned is "" or cleaned is "-" then set cleaned to "Separator"
return cleaned
end normalizeLabel

on asciiOnly(inputText)
set outputText to ""
set charCount to count of characters of inputText
repeat with i from 1 to charCount
set ch to character i of inputText
set chCode to ASCII number of ch
if chCode is 9 then
set outputText to outputText & " "
else if chCode >= 32 and chCode <= 126 then
set outputText to outputText & ch
else if chCode is 8230 then
set outputText to outputText & "..."
else if chCode is 8216 or chCode is 8217 then
set outputText to outputText & "'"
else if chCode is 8220 or chCode is 8221 then
set outputText to outputText & quote
else if chCode is 8211 or chCode is 8212 then
set outputText to outputText & "-"
else
set outputText to outputText & " "
end if
end repeat
return outputText
end asciiOnly

on collapseWhitespace(inputText)
set AppleScript's text item delimiters to {space, tab, return, linefeed}
set parts to text items of inputText
set AppleScript's text item delimiters to space
set joined to parts as text
set AppleScript's text item delimiters to ""
return my trimSpaces(joined)
end collapseWhitespace

on trimSpaces(inputText)
set outputText to inputText
repeat while outputText begins with " "
if (count of characters of outputText) is 1 then
set outputText to ""
else
set outputText to text 2 thru -1 of outputText
end if
end repeat
repeat while outputText ends with " "
if (count of characters of outputText) is 1 then
set outputText to ""
else
set outputText to text 1 thru -2 of outputText
end if
end repeat
return outputText
end trimSpaces

on escapeXml(inputText)
set escapedText to inputText
set escapedText to my replaceText(escapedText, "&", "&amp;")
set escapedText to my replaceText(escapedText, "<", "&lt;")
set escapedText to my replaceText(escapedText, ">", "&gt;")
set escapedText to my replaceText(escapedText, quote, "&quot;")
set escapedText to my replaceText(escapedText, "'", "&apos;")
return escapedText
end escapeXml

on joinWith(itemList, separatorText)
set AppleScript's text item delimiters to separatorText
set joinedText to itemList as text
set AppleScript's text item delimiters to ""
return joinedText
end joinWith

on joinLines(lineList)
set AppleScript's text item delimiters to linefeed
set joinedText to lineList as text
set AppleScript's text item delimiters to ""
return joinedText
end joinLines

on replaceText(inputText, findText, replaceWith)
set AppleScript's text item delimiters to findText
set parts to text items of inputText
set AppleScript's text item delimiters to replaceWith
set outputText to parts as text
set AppleScript's text item delimiters to ""
return outputText
end replaceText

on writeTextFile(filePath, fileContents)
set fileRef to open for access (POSIX file filePath) with write permission
try
set eof of fileRef to 0
write fileContents to fileRef starting at eof
close access fileRef
on error errMsg number errNum
try
close access fileRef
end try
error errMsg number errNum
end try
end writeTextFile

on joinPath(folderAlias, fileName)
set folderPath to POSIX path of folderAlias
if folderPath does not end with "/" then set folderPath to folderPath & "/"
return folderPath & fileName
end joinPath

on fourCharCode(indexNumber)
set alphabet to "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
set base to 26
set n to indexNumber - 1
set codeText to ""
repeat 4 times
set charIndex to (n mod base) + 1
set codeText to (character charIndex of alphabet) & codeText
set n to n div base
end repeat
return codeText
end fourCharCode