Exporting AppleScript to OmniOutliner

As I’ve said several times in the past, I use OmniOutliner on the Mac and iPad for a vast array of tasks. OmniOutliner’s scriptability on the mac is outstanding and demonstrates what’s possible when a developer commits to making automation a key feature of their product.

A while ago I had the need to create an OmniOutliner outline of a number of scripts I had written. These outlines include the names of each property and each handler defined in the script. Improvements to Script Debugger 7’s scripting interface finally made this possible.

I think this is a great example of how application scriptability, combined with AppleScript, makes very personal and idiosyncratic automations possible and valuable. This could have been done by hand, but now its something I can repeat at will without errors creeping in. There is no way this specific feature would appear in either OmniOutliner or Script Debugger, but I can create it for myself.

This example script demonstrates the following technologies:

  • OmniOutliner’s scripting interface for creating outlines
  • Script Debugger 7’s ability to introspect a script
  • Script Debugger 7’s enhanced applets for creating attractive droplets
  • Script Debugger 7’s ability to control the types of files a droplet accepts

NOTE: MarksLib is needed to compile this script.

As an example of how this works, if I drop my MarksLib script onto my Export To OmniOutliner droplet this is the result:

Here’s the AppleScript code for an enhanced droplet which accepts a script and generates a new OmniOutliner document. Note that the sourceForHandlerName handler contains the logic needed to reconstruct a handler’s source declaration from the meta-data offered by Script Debugger 7’s scripting interface.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use MarksLib : script "MarksLib" version "1.0"


--	SD7 Enhanced applet properties
property appletDropName : "Drop your scripts here"
property appletSearchName : "Find scripts"


on open theFiles
	set progress description to "Generate OmniOutliner Outline"
	set progress total steps to count theFiles
	set progress completed steps to 0
	
	repeat with aFile in theFiles
		tell application "Finder"
			set aFileName to name of item aFile
			set outputFolder to container of item aFile as alias
		end tell
		set progress additional description to "Processing " & aFileName & "..."
		
		processFile(contents of aFile)
		set my progress completed steps to (my progress completed steps) + 1
	end repeat
end open

on processFile(theFile)
	local ooDocument
	
	tell application "OmniOutliner"
		set ooDocument to make new document
	end tell
	
	tell application "Script Debugger"
		local numOpenDocuments, documentAlreadyOpen, ooProperties, propertyName, propertyValue, ooHandlers, handlerName, handlerCode
		
		set numOpenDocuments to count of documents
		tell (open theFile)
			set documentAlreadyOpen to (count of current application's documents) = numOpenDocuments
			
			--	Inventory the script's properties...
			if (count of script properties) > 0 then
				tell application "OmniOutliner"
					set ooProperties to make new row in ooDocument with properties {topic:"Properties"}
				end tell
				
				repeat with aProperty in script properties
					set propertyName to aProperty's name
					set propertyValue to aProperty's source text
					
					tell application "OmniOutliner"
						make new row in ooProperties with properties {topic:propertyName, note:propertyValue}
					end tell
				end repeat
			end if
			
			--	Inventory the script's handlers
			if (count of script handlers) > 0 then
				tell application "OmniOutliner"
					set ooHandlers to make new row in ooDocument with properties {topic:"Handers"}
				end tell
				
				repeat with aHandler in script handlers
					set handlerName to my sourceForHandlerName(aHandler)
					set handlerCode to aHandler's source text
					
					tell application "OmniOutliner"
						make new row in ooHandlers with properties {topic:handlerName, note:handlerCode}
					end tell
				end repeat
			end if
			
			if not documentAlreadyOpen then
				close saving no
			end if
		end tell
	end tell
end processFile

on sourceForHandlerName(theHandler)
	tell application "Script Debugger"
		local handlerName
		
		set handlerName to theHandler's name
		
		if handlerName contains "_" then -- interlieved syntax/ASObjC
			--	handler: varName parameter: varName ...
			local handlerNameParts, numParts, formattedHandlerName, formattedHandlerNameParts
			
			set handlerNameParts to MarksLib's split(handlerName, "_")
			if last item of handlerNameParts is "" then ¬
				set handlerNameParts to items 1 thru -2 of handlerNameParts
			set numParts to count handlerNameParts
			set formattedHandlerNameParts to {}
			repeat with i from 1 to numParts
				set end of formattedHandlerNameParts to (handlerNameParts's item i) & ":" & (theHandler's handler parameter i's name)
			end repeat
			
			return MarksLib's |join|(formattedHandlerNameParts, " ")
		else
			--
			local positionalParameters, keywordParameterNames, keywordParameterVariables, givenParameterNames, givenParameterVariables, eventParameters
			
			set positionalParameters to name of (every handler parameter whose parameter type is positional parameter) of theHandler
			set keywordParameterNames to name of (every handler parameter whose parameter type is keyword parameter) of theHandler
			set keywordParameterVariables to variable name of (every handler parameter whose parameter type is keyword parameter) of theHandler
			set givenParameterNames to name of (handler parameters whose parameter type is named parameter) of theHandler
			set givenParameterVariables to variable name of (handler parameters whose parameter type is named parameter) of theHandler
			set eventParameters to name of (handler parameters whose parameter type is direct parameter) of theHandler
			
			if (count of eventParameters) = 1 then
				if (count of keywordParameterNames) > 0 then
					--	handler of p1 ...
					set formattedHandlerName to handlerName & " of " & (first item of eventParameters)
				else
					--	handler p1
					set formattedHandlerName to handlerName & " " & (first item of eventParameters)
				end if
			else if (count of eventParameters) > 1 then
				--	handler {p1, ...}
				set formattedHandlerName to handlerName & " {" & (MarksLib's |join|(eventParameters, ", ")) & "}"
			else if (count of positionalParameters) > 0 then
				--	handler(p1, ...)
				set formattedHandlerName to handlerName & "(" & (MarksLib's |join|(positionalParameters, ", ")) & ")"
			else
				--	Use `script handler`'s `handler type` property to determin this once 7.0.1 ships
				if handlerName = "run" or handlerName = "idle" or handlerName = "quit" then
					--	handler
					set formattedHandlerName to handlerName
				else
					--	handler()
					set formattedHandlerName to handlerName & "()"
				end if
			end if
			if (count of keywordParameterNames) > 0 then
				--	handler ... given {name: varName, ...}
				set params to {}
				
				repeat with i from 1 to count of keywordParameterNames
					set end of params to (item i of keywordParameterNames) & " " & (item i of keywordParameterVariables)
				end repeat
				
				set formattedHandlerName to formattedHandlerName & " " & (MarksLib's |join|(params, " "))
			end if
			if (count of givenParameterNames) > 0 then
				--	handler of p1 name varName ...
				set params to {}
				
				repeat with i from 1 to count of givenParameterNames
					set end of params to (item i of givenParameterNames) & ": " & (item i of givenParameterVariables)
				end repeat
				
				set formattedHandlerName to formattedHandlerName & " given " & (MarksLib's |join|(params, ", "))
			end if
			return formattedHandlerName
		end if
	end tell
end sourceForHandlerName

And here is the code packaged as an enhanced applet:

Export To OmniOutliner.zip (2.3 MB)

3 Likes

Mark, I totally agree! Thanks for sharing such a great idea, great workflow, and great script! :+1: