Enhancement Wish -- Copy KM Action as plist, JSON, XML

I call this a “wish” instead of a “request” because you’ve got enough on your plate.

I wish I could paste an action(s) plist text into the KM editor, rather than having to import it. Importing it causes a notification message which has to be clicked, if I’ve got the KM Editor set up for Alerts.

And while we’re at it, I wish I could “copy as plist” selected action(s). Yes, I can make a macro for this, in fact I did. But still, copying and pasting as plist would be kind of nice.

Like I said, a wish, not a request.

2 Likes

It would also be nice if you could drag and drop things out of the Keyboard Maestro to the Finder and vice versa.

-Chris

1 Like

copy as plist

So much more tractable as JSON :wink:

function run() {
    "use strict";

    function kmMacrosByUID(lstUID, strPlistPath) {

        // concatMap :: (a -> [b]) -> [a] -> [b]
        function concatMap(f, xs) {
            return [].concat.apply([], xs.map(f));
        }

        var uids = (
                lstUID && lstUID instanceof Array
        ) ? lstUID : [];

        return uids.length ? concatMap(
            function (group) {
                return (group.Macros || []).filter(function (x) {
                    return uids.indexOf(x.UID) !== -1;
                });
            }, ObjC.deepUnwrap(
                $.NSDictionary.dictionaryWithContentsOfFile(strPlistPath)
            ).MacroGroups
        ) : [];
    }

    /******************************************************/

    var a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true && a);

    return kmMacrosByUID(
        Application("Keyboard Maestro")
        .selectedmacros(),
        sa.pathTo(
            'application support', {
                from: 'user domain'
            }
        ) + "/Keyboard Maestro/Keyboard Maestro Macros.plist"
    );
}

How do I use this?

1 Like

How do I use this ?

The simplest use, if you want to paste JSON versions of the selected macro(s) into a text editor, might be an Execute JavaScript for Automation action to put the JSON in the clipboard.

(see the extra lines at the end)

function run() {
    "use strict";

    function kmMacrosByUID(lstUID, strPlistPath) {

        // concatMap :: (a -> [b]) -> [a] -> [b]
        function concatMap(f, xs) {
            return [].concat.apply([], xs.map(f));
        }

        var uids = (
            lstUID && lstUID instanceof Array
        ) ? lstUID : [];

        return uids.length ? concatMap(
            function (group) {
                return (group.Macros || [])
                    .filter(function (x) {
                        return uids.indexOf(x.UID) !== -1;
                    });
            }, ObjC.deepUnwrap(
                $.NSDictionary.dictionaryWithContentsOfFile(strPlistPath)
            )
            .MacroGroups
        ) : [];
    }

    /******************************************************/

    var a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true && a),

        jso = kmMacrosByUID(
            Application("Keyboard Maestro")
            .selectedmacros(),
            sa.pathTo(
                'application support', {
                    from: 'user domain'
                }
            ) + "/Keyboard Maestro/Keyboard Maestro Macros.plist"
        ),
        strJSON = JSON.stringify(jso, null, 2);

    sa.setTheClipboardTo(
        JSON.stringify(jso, null, 2)
    );
}

To write things back to a .plist file

You can convert the edited or run-time updated js object (either loaded as literal json, or parsed from a string using jso = JSON.parse(strJSON);

and then write it out to file as a .plist with code like:

 $([jso]).writeToFileAtomically(
            $(strPath)
            .stringByStandardizingPath, true
        );

Got it. I can see that if JSON was something you’re familiar with, this would be pretty cool. Doing things in a more structured way always appeals to me.

Do you have a utility you use to view/edit JSON files, or do you just do it in an editor?

I just edit in Atom, and for evaluation as JavaScript for Automation (inside Atom) I use these two plugins:

1 Like

Incidentally, you can run a macro directly from its JS Object / JSON description
(which I personally find a little less noisy, visually, than plist XML,
and which is, of course, very scriptable)

If we have a simple two-action macro like this (SpeakText, then PlaySound)

[{
    'MacroActionType': 'SpeakText',
    'Rate': 'Default',
    'IsDisclosed': true,
    'TimeOutAbortsMacro': true,
    'Text': 'This is a simple example',
    'IsActive': true,
    'Voice': ''
}, {
    'MacroActionType': 'PlaySound',
    'Volume': 74,
    'IsDisclosed': true,
    'TimeOutAbortsMacro': true,
    'IsActive': true,
    'Path': '/System/Library/Sounds/Glass.aiff',
    'DeviceID': 'SOUNDEFFECTS'
}]

We can use a jsoDoScript() function to execute it directly with the Keyboard Maestro Engine like this:

// OSX JavaScript for Applications function with example of use:

// Executing a (JSON) JavaScript object version of a Keyboard Maestro action

// Rob Trew Twitter @ComplexPoint MIT License 2016
// Ver 0.8

function run() {

    function jsoDoScript(jso) {

        // plistSnippets :: Object -> [String]
        function plistSnippets(jso) {
            var a = Application.currentApplication(),
                sa = (a.includeStandardAdditions = true, a);

            // Array of executable plist translations of JS objects
            return (jso instanceof Array ? jso : [jso])
                .reduce(function (a, dctAction) {
                    var strPath = sa.pathTo('temporary items') +
                        '/' + sa.randomNumber()
                        .toString()
                        .substring(3);

                    return (
                        $(dctAction)
                        .writeToFileAtomically(
                            $(strPath)
                            .stringByStandardizingPath, true
                        ),

                        a.concat(
                            readFile(strPath)
                            .split(/[\n\r]/)
                            .slice(3, -2)
                            .join('')
                        )
                    );
                }, []);
        }

        // readFile :: FilePath -> maybe String
        function readFile(strPath) {
            var error = $(),
                str = ObjC.unwrap(
                    $.NSString.stringWithContentsOfFileEncodingError(
                        $(strPath)
                        .stringByStandardizingPath,
                        $.NSUTF8StringEncoding,
                        error
                    )
                );
            return error.code || str;
        }

        var kme = Application('Keyboard Maestro Engine'),
            lstXML = plistSnippets(jso);

        return (
            lstXML.forEach(function (plist) {
                kme.doScript(plist);
            }),
            lstXML
        );
    }

    jsoDoScript([{
        'MacroActionType': 'SpeakText',
        'Rate': 'Default',
        'IsDisclosed': true,
        'TimeOutAbortsMacro': true,
        'Text': 'This is a simple example',
        'IsActive': true
    }, {
        'MacroActionType': 'PlaySound',
        'Volume': 74,
        'IsDisclosed': true,
        'TimeOutAbortsMacro': true,
        'IsActive': true,
        'Path': '/System/Library/Sounds/Glass.aiff',
        'DeviceID': 'SOUNDEFFECTS'
    }]);

}

Or, through a JSON string variable:

( note that JSON needs double quotes – in a JS script the quotes can be single or double)

Run Macro directly from JSON description.kmmacros (20.5 KB)

Done for the next version.

* Added Copy as XML to copy macro groups, macros or actions as XML.
* Support pasting XML actions.
2 Likes

FWIW a parallel Copy as JSON (for Groups, Macros or Actions selected in the Keyboard Maestro editor)

Copy as JSON for Keyboard Maestro Editor.kmmacros (20.2 KB)

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

    var p = $.NSPasteboard.generalPasteboard,
        js = ObjC.deepUnwrap,
        jso = js(p.pasteboardItems.js[0].types)
        .reduce(function (a, uti) {
            return uti.startsWith(
                "com.stairways.keyboardmaestro."
             ) ? (
                a[uti] = js(p.propertyListForType(uti)),
                a
            ) : a;
        }, {});

    return JSON.stringify(jso, null, 2);
})();

thanks for sharing this tool. Looks interesting.

But I’m not sure I understand the use case. Since KM8 now provides with with “Copy as XML”, and “Paste XML as Action”, how to you see using JSON in this context?

As a minimum wouldn’t we need a JSON-to-XML function to use with KM?

Use of KM as a 'script library'. For the basic mechanism, see, earlier in this thread:

(Slightly updating the code in the earlier macro):

Once you have:

  • copied some actions as a JSON string,
  • obtained some JavaScript object(s) from the string with JSON.parse(strJSON)
  • made any run-time adjustments to the parameter values in those objects

You can then execute it/them as a JavaScript Array (or Dictionary, for a single action), with a piece of code like:

// jsoDoScript :: Object (Dict | Array) -> IO ()
const jsoDoScript = jso => {
    const strPath = tempFilePath('tmp.plist');
    return (
        Application('Keyboard Maestro Engine')
        .doScript((
            $(Array.isArray(jso) ? jso : [jso])
            .writeToFileAtomically(
                $(strPath)
                .stringByStandardizingPath,
                true
            ),
            readFile(strPath)
        )),
        true
    );
};

For example, to execute this sequence of two Keyboard Maestro actions, copied as JSON, or constructed / adjusted at run-time.

[{
    "MacroActionType": "SpeakText",
    "Rate": "Default",
    "IsDisclosed": true,
    "TimeOutAbortsMacro": true,
    "Text": "This is a simple example",
    "IsActive": true
}, {
    "MacroActionType": "PlaySound",
    "Volume": 74,
    "IsDisclosed": true,
    "TimeOutAbortsMacro": true,
    "IsActive": true,
    "Path": "/System/Library/Sounds/Glass.aiff",
    "DeviceID": "SOUNDEFFECTS"
}]

The whole script might look like this:

(() => {
    'use strict';

    // GENERIC ---------------------------------------------------------------

    // readFile :: FilePath -> IO String
    const readFile = strPath => {
        var error = $(),
            str = ObjC.unwrap(
                $.NSString.stringWithContentsOfFileEncodingError(
                    $(strPath)
                    .stringByStandardizingPath,
                    $.NSUTF8StringEncoding,
                    error
                )
            );
        return typeof error.code !== 'string' ? (
            str
        ) : 'Could not read ' + strPath;
    };

    // takeExtension :: FilePath -> String
    const takeExtension = strPath => {
        const
            xs = strPath.split('.'),
            lng = xs.length;
        return lng > 1 ? (
            '.' + xs[lng - 1]
        ) : '';
    };

    // takeBaseName :: FilePath -> String
    const takeBaseName = strPath =>
        strPath !== '' ? (
            strPath[strPath.length - 1] !== '/' ? (
                strPath.split('/')
                .slice(-1)[0].split('.')[0]
            ) : ''
        ) : '';

    // File name template -> temporary path
    // (Random digit sequence inserted between template base and extension)
    // tempFilePath :: String -> IO FilePath
    const tempFilePath = template =>
        ObjC.unwrap($.NSTemporaryDirectory()) +
        takeBaseName(template) + Math.random()
        .toString()
        .substring(3) + takeExtension(template)


    // JXA -------------------------------------------------------------------

    // actions :: [Dict]
    const actions = [{
        "MacroActionType": "SpeakText",
        "Rate": "Default",
        "IsDisclosed": true,
        "TimeOutAbortsMacro": true,
        "Text": "This is a simple example",
        "IsActive": true
    }, {
        "MacroActionType": "PlaySound",
        "Volume": 74,
        "IsDisclosed": true,
        "TimeOutAbortsMacro": true,
        "IsActive": true,
        "Path": "/System/Library/Sounds/Glass.aiff",
        "DeviceID": "SOUNDEFFECTS"
    }];

    // jsoDoScript :: Object (Dict | Array) -> IO ()
    const jsoDoScript = jso => {
        const strPath = tempFilePath('tmp.plist');
        return (
            Application('Keyboard Maestro Engine')
            .doScript((
                $(Array.isArray(jso) ? jso : [jso])
                .writeToFileAtomically(
                    $(strPath)
                    .stringByStandardizingPath,
                    true
                ),
                readFile(strPath)
            )),
            true
        );
    };

    // MAIN ------------------------------------------------------------------

    jsoDoScript(actions);
})();


As ever:

Je n’ai fait celle-ci plus longue que parce que je n’ai pas eu le loisir de la faire plus courte.

Blaise Pascal (Lettres Provinciales) 1657

(This (script) is long, simply for want of the leisure to shorten it.)

I also find that snapping things together from a set of unpruned Lego bricks (pre-fab functions, automatically pasted from a library), is worth, not just in speed of preparation but also in ease of maintenance, whatever it happens to cost in length and redundancy : -)

(Brevity is certainly nothing to be ashamed of, but perhaps the expenditure of time which it takes to create is not always something to be proud of either : -)

did pasting XML actions ever make it in? on version 9 i can't seem to find it .... copy as XML is here, ... paste XML actions seems to have evaded me ...

Thanks!