Running AppleScript record lists as Keyboard Maestro macros

Now that Keyboard Maestro 8 makes it so easy to copy an action or macro in XML (plist) format,

one way of executing actions and action lists is to specify their details in scripting language record (Applescript) or dictionary (JavaScript) format, and then pass the record, or list of records, to a function which executes them as KM actions or action lists.

In what context might this be useful ?

Simply when you want to be able to change any action settings at runtime – easier to make changes to a record or dictionary than to an XML string.
(and, of course, a little more readable).

I personally tend to do this in JavaScript, but to show how it can be done in AppleScript too, here is a "Hello world" example – a list of two Keyboard Maestro Play Sound actions, in the form of an Applescript list of two records, and executed by a kmEvalASMay function, which:

  1. Executes the AS list, if it can be interpreted as a sequence of KM actions,
  2. returns the XML version of the list (if ditto)

(The final May in the function name just means Maybe)

use framework "Foundation"
use scripting additions

-- EXECUTE APPLESCRIPT LISTS OF RECORDS AS KEYBOARD MAESTRO MACROS -----------

-- kmEvalASMay :: (Array | Record) a => a -> Maybe XML String
on kmEvalASMay(asObject)
    set mbXML to plistMay(asObject)
    if nothing of mbXML then
        mbXML
    else
        set strXML to just of mbXML
        
        -- Effects
        tell application "Keyboard Maestro Engine" to do script strXML
        -- Value
        mbXML
    end if
end kmEvalASMay


-- TEST ------------------------------------------------------------------
-- Execute two KM Play Sound actions
on run
    -- First Copy as XML in KM8, 
    -- then ...
    -- set xs to Actions of item 1 of Macros of item 1 of just of 
    --   asRecFromPlistMay(strXML)
    
    -- (Check that keys like |Path| begin Titlecased)
    
    set lstActions to {{Volume:50.0, MacroActionType:¬
        "PlaySound", TimeOutAbortsMacro:true, |Path|:¬
        "/System/Library/Sounds/Glass.aiff", DeviceID:"SOUNDEFFECTS"} ¬
        , ¬
        {Volume:100.0, MacroActionType:¬
            "PlaySound", TimeOutAbortsMacro:true, |Path|:¬
            "/System/Library/Sounds/Submarine.aiff", DeviceID:"SOUNDEFFECTS"}}
    
    -- Run records as KM macro -----------------------------------------------
    set mbXML to kmEvalASMay(lstActions)
    
    -- and return XML --------------------------------------------------------
    if nothing of mbXML then
        "plist XML not generated ..."
    else
        set strXML to just of mbXML
        writeFile("~/Desktop/asPlist.txt", strXML)
        strXML
    end if
end run



-- PLIST XML TO AND FROM AS RECORDS ------------------------------------------

-- asRecFromPlistMay :: String -> Maybe Record
on asRecFromPlistMay(s)
    set ca to current application
    set strTempPath to POSIX path of (path to temporary items as alias) & ¬
        drop(3, (random number) as string) & ".plist"
    writeFile(strTempPath, s)
    try
        set v to unwrap(ca's NSArray's ¬
            arrayWithContentsOfFile:wrap(strTempPath))
        if v is missing value then
            {nothing:true, msg:"Clipboard did not contain plist XML"}
        else
            {nothing:false, just:v}
        end if
    on error e
        {nothing:true, msg:"Clipboard did not contain plist XML"}
    end try
end asRecFromPlistMay

-- CONVERT AN APPLESCRIPT RECORD BACK TO AN XML FORMAT WHICH 
-- THE KEYBOARD MAESTRO ENGINE CAN EXECUTE

-- plistMay :: AS Object -> Maybe Plist String
on plistMay(arrayOrRec)
    set ca to current application
    set {v, e} to ca's NSPropertyListSerialization's ¬
        ¬
            dataWithPropertyList_format_options_error_(arrayOrRec, (ca's NSPropertyListXMLFormat_v1_0), 0, (reference))
    if v is missing value then
        {nothing:true, msg:theError's localizedDescription() as text}
    else
        {nothing:false, just:¬
            unwrap(ca's NSString's alloc()'s ¬
                initWithData:v encoding:(ca's NSUTF8StringEncoding))}
    end if
end plistMay


-- GENERIC FUNCTIONS ---------------------------------------------------------

--  drop :: Int -> a -> a
on drop(n, a)
    if n < length of a then
        if class of a is text then
            text (n + 1) thru -1 of a
        else
            items (n + 1) thru -1 of a
        end if
    else
        {}
    end if
end drop

-- unwrap :: NSObject -> AS value
on unwrap(objCValue)
    if objCValue is missing value then
        return missing value
    else
        set ca to current application
        item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

-- wrap :: AS value -> NSObject
on wrap(v)
    set ca to current application
    ca's (NSArray's arrayWithObject:v)'s objectAtIndex:0
end wrap

-- writeFile :: FilePath -> String -> IO ()
on writeFile(strPath, strText)
    set ca to current application
    script wrap
        on |λ|(x)
            ca's (NSArray's arrayWithObject:x)'s objectAtIndex:0
        end |λ|
    end script
    tell wrap
        |λ|(strText)'s writeToFile:(|λ|(strPath)'s ¬
            stringByStandardizingPath()) atomically:true
    end tell
end writeFile

Exercise for the reader - rewrite this so that the records are generated by the script, as variations of a single original record, and play, in sequence, each of the sounds in "/System/Library/Sounds"

One of the many ways in which we can use Keyboard Maestro 8 is as a script library for JavaScript or Applescript.

3 Likes

For reference, analogous code in JavaScript for Automation:

(() => {
    'use strict';

    // kmEvalJSOMay :: JS Object -> Maybe String
    const kmEvalJSOMay = jso => {

        // plistMay :: JS Object -> Maybe Plist String
        const plistMay = jso => {
                var error = $();
                const maybeString = $.NSString.alloc.initWithDataEncoding(
                    $.NSPropertyListSerialization
                    .dataWithPropertyListFormatOptionsError(
                        $(jso), $.NSPropertyListXMLFormat_v1_0, 0, error
                    ), $.NSUTF8StringEncoding
                );
                return Boolean(error.code) ? {
                    nothing: true,
                    msg: error.localizedDescription
                } : {
                    nothing: false,
                    just: ObjC.unwrap(maybeString)
                };
            },
            mbXML = plistMay(jso);
        return mbXML.nothing ? mbXML : (() => {
            try {
                // Effect
                Application('Keyboard Maestro Engine')
                    .doScript(mbXML.just);
                // Value
                return mbXML;
            } catch (e) {
                return {
                    nothing: true,
                    msg: e.message
                }
            }
        })()
    };

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            $(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );


    // TEST -------------------------------------------------------------------
    const xs = [{
            "Volume": 50,
            "MacroActionType": "PlaySound",
            "TimeOutAbortsMacro": true,
            "Path": "/System/Library/Sounds/Glass.aiff",
            "DeviceID": "SOUNDEFFECTS"
        },
        {
            "Volume": 100,
            "MacroActionType": "PlaySound",
            "TimeOutAbortsMacro": true,
            "Path": "/System/Library/Sounds/Submarine.aiff",
            "DeviceID": "SOUNDEFFECTS"
        }
    ];


    const strXML = kmEvalJSOMay(xs)
        .just;
    return (
        // Effect ------------------------------------------------------------
        writeFile('~/Desktop/jsPlist.txt', strXML),

        // Value -------------------------------------------------------------
        strXML
    );
})();

1 Like