Swift example script: Get macros and uuids from clipboard

Swift example script: Get macros and uuids from clipboard

Get Macros and UUIDs from the clipboard.kmmacros (7.6 KB)

Here's an example Swift script I just finished, and a simple macro that runs it. The script is highly commented..

The Swift script does the same thing the AppleScript does, in

/*****************************************************************
GetKMMacrosAndUUIDsFromClipboard

Author:		@DanThomas at the Keyboard Maestro forum
Updated:	2016/06/07 19:52 PDT
Language:	Swift v2.2.1

PURPOSE:

This script expects the clipboard to contain one or more Keyboard
Maestro Macros. It will return a multi-line string, each line
containing the KM Macro name and UUID of one of these Macros.

If the information isn't on the clipboard, this script returns an
empty string.


NOTES:

1) You can place this script in a Keyboard Maestro "Execute a
Swift script" Action, or you can use it in Xcode. One easy way
to use it in Xcode is to use a Playground, but since you can't
use breakpoints in Playgrounds, I recommend making an OS/X
Console application, and using this there.

2) This script is not only functional, but hopefully will be used
as a learning experience for others who might be new to Swift.
As such, I'm including logic-flow comments that, to more
experienced Swift developers, will seem extreme. Also, since I'm
also new at Swift, cut me some slack. :)

3) I'm well aware that the standard way to write Swift code is to
place the opening script brace ( "{" ) at the end of a line.
However, this script is meant to be used as a learning tool, and
I felt that a little more line spacing would help. Also, I prefer
it this way. So there.

4) I'm also well aware that most people don't name variables with
an underscore prefix. Old habits die hard. This is to distinguish
local variables from parameters.
*******************************************************************/


import Cocoa

// I'm wrapping everything in a class, although all the fuctions
// are static functions ("class" functions) so it isn't strictly
// necessary, but I'm doing it because, well, it just seems right.

class GetKMMacrosAndUUIDsFromClipboard
{
	// This method is called from the last line in the script.
	// It doesn't need to be called "execute", it's just what I
	// decided to name it.
	//
	// This is a function which returns a string.
	class func execute() -> String
	{
		// This calls a routine (below) to get the KM clipboard
		// data we're looking for, or else it returns nil.
		let _clipboardResult = getKMMacroArrayFromClipboard()
		
		// This attepts to type cast the result as an NSArray,
		// which is what it will be if it was Macros copied
		// from KM. If "_aray" was already nil, or if the cast
		// fails, the "else" is executed.
		if let _array = _clipboardResult as? NSArray
		{
			return getMacrosAndUUIDs(_array)
		}
		else
		{
			return ""
		}
	}
	
	// Attempts to get the KM Macros from the clipboard. It
	// returns nil if they aren't on the clipboard, otherwise
	// it returns the deserialized NS object.
	class func getKMMacroArrayFromClipboard() -> AnyObject?
	{
		let _pasteboard = NSPasteboard.generalPasteboard()
		
		// Rather than querying the clipboard for what's on it,
		// then extractin it if it's the corrrect format, we go
		// ahead and try and grap what we want. But we wrap
		// these statements in "guard" statements, so if the
		// result is nil, the "else" is executed, and we exit
		// this function returning nil.
		guard let _clipping = _pasteboard.stringForType("com.stairways.keyboardmaestro.macrosarray") else {return nil}
		guard let _data = _clipping.dataUsingEncoding(NSUTF8StringEncoding) else {return nil}
		
		// The next statement we call is marked as being able to
		// throw exceptions, so we have to wrap it up to catch
		// the exceptions. Since we don't care what the exception
		// is, we just return nil.
		do
		{
			return try NSPropertyListSerialization.propertyListWithData(_data, options: NSPropertyListReadOptions.MutableContainersAndLeaves, format: nil)
		}
		catch
		{
			return nil
		}
	}
	
	// This function takes the NSArray data we got from the
	// clipboard, parses it and returns a string with info
	// about one macro per line.
	class func getMacrosAndUUIDs(macros: NSArray) -> String
	{
		// This is where we'll store our results. Obviously.
		var _result = [String]()

		// Enumerate through the items in the array. Since
		// NSArray is not type-specific, we have to use a
		// variable of "AnyObject" for each item of the array.
		for _macroObject: AnyObject in macros
		{
			// From this part on, we pretty-much expect everything
			// to be in a specific format. So no more type-checking.
			// If something fails it will cause an exception,
			// but at that point we'd prefer the exception to
			// bubble up so we have some idea of exactly what
			// went wrong.
			//
			// So we use the "as!" constrtuct to say "we know
			// what's in here, so give us what we want".
			//
			// First, we cast the array object as an NSDictionary
			let _macro = _macroObject as! NSDictionary
			
			// Then we get the Macro's Name and UUID (UID)
			let _name = _macro.valueForKey("Name") as! String
			let _uuid = _macro.valueForKey("UID") as! String
			
			// And finally, we stick the info in our result array.
			_result.append(_name + "\t" + _uuid)
		}
		
		// Return the results as a multi-line string
		return _result.joinWithSeparator("\n")
	}
}

// Execute our code, and return (display) our results
print(GetKMMacrosAndUUIDsFromClipboard.execute())

//*******************************************************************
4 Likes

Useful work, and I think that Swift may have fuller (and certainly better documented) access to the NS classes than JSA.

If there are KM arrays in the clipboard, we can get the plist XML in JSA with:

function run() {
    ObjC.import('AppKit');
	
    return ObjC.unwrap(
        $.NSPasteboard.generalPasteboard.stringForType(
            'com.stairways.keyboardmaestro.macrosarray'
        )
    )
}

but using the NSPropertyListSerialization functions seems more opaque (or less available) from JSA, so we would have to write out the XML to a temporary file, and then bring it back in as a JavaScript array with something like:

ObjC.deepUnwrap(
    $.NSArray.arrayWithContentsOfFile(strFullPath)
)

Thanks. Yes, it looks like, in Swift, I can use the NS classes without having to convert them. Of course, NSDictionary returns untyped data. But with plist nesting, this is how you would need to do it in Swift even with native types, so it's no loss. At least I think this is true - still learning of course.

I'd like to figure out if it's possible to build and launch a UI in code in Swift. That would be really cool. Haven't figured out what to Google for to find that yet, if it exists.

For ā€˜hello worldā€™, I might start by searching for swift UIAlertView

I think the ā€œUIā€ classes are only for iOS, arenā€™t they? I think for OS/X, it would have to be NSAlertView? I think. EDIT: NSAlert in Swift. I think. :slight_smile:

1 Like

( As you can see, I have yet to reach the pre-beginner stage : - )

1 Like

FWIW here is a JavaScript for Automation equivalent of that code.

Notes:

  • The explicit import of AppKit is needed to bring NSPasteboard into the context
  • The JSExport naming convention for using NS classes which is used in JSA appends capitalised parameter names to the function name, also appending ā€˜Errorā€™ for functions which can throw an Error value
function run() {
    ObjC.import('AppKit');

    var mbXML = ObjC.unwrap(
        $.NSPasteboard.generalPasteboard.stringForType(
            'com.stairways.keyboardmaestro.macrosarray'
        )
    );

    return (
            (mbXML && ObjC.deepUnwrap(
                $.NSPropertyListSerialization
                .propertyListWithDataOptionsFormatError(
                    $(mbXML)
                    .dataUsingEncoding($.NSUTF8StringEncoding),
                    0, 0, null
                )
            )) || []
        )
        .map(function (dct) {
            return dct.Name + '\t' + dct.UID;
        })
        .join('\n');
}
1 Like

Hereā€™s the above code, broken down step by step. I canā€™t be the only one who needs to see this!

function run() {
	ObjC.import('AppKit');

	var nsPasteboardData = $.NSPasteboard.generalPasteboard.stringForType('com.stairways.keyboardmaestro.macrosarray');
	if (nsPasteboardData == null) {return ""}
	
	var jsPasteboardData = ObjC.unwrap(nsPasteboardData);
	if (jsPasteboardData == null) {return ""}

	var nsPasteboardString = $(jsPasteboardData).dataUsingEncoding($.NSUTF8StringEncoding);
	var nsDict = $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(nsPasteboardString, 0, 0, null);
	var jsDict = ObjC.deepUnwrap(nsDict);
	if (jsDict == null) {return ""}
	
	var jsLines = 
		jsDict
	            .map(function (dct) {
	    	        return dct.Name + '\t' + dct.UID;
          	    });
	
	var jsString = jsLines.join('\n');
	
	return jsString;
}
3 Likes