diff --git a/scripts/generate_menu_sdef.applescript b/scripts/generate_menu_sdef.applescript new file mode 100644 index 0000000..71247f6 --- /dev/null +++ b/scripts/generate_menu_sdef.applescript @@ -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 "" + set end of lines to "" + set end of lines to "" + set suiteName to "Menu Items" + set suiteDesc to "Menu item commands generated from " & appName & "." + set end of lines to " " + + 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 " " + set end of lines to lineText + end repeat + + set end of lines to " " + set end of lines to "" + 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 "" + set end of lines to "" + set end of lines to "" + set end of lines to "" + set end of lines to " CFBundleIdentifier" + set end of lines to " " & safeBundleId & "" + set end of lines to " CFBundleName" + set end of lines to " " & safeName & "" + set end of lines to " NSScriptingDefinition" + set end of lines to " " & safeSdef & "" + set end of lines to "" + set end of lines to "" + 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, "&", "&") + set escapedText to my replaceText(escapedText, "<", "<") + set escapedText to my replaceText(escapedText, ">", ">") + set escapedText to my replaceText(escapedText, quote, """) + set escapedText to my replaceText(escapedText, "'", "'") + 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