Get JXA Plist for a Macro UUID, With VSCode "Intellisense" Documentation Examples

NOTE: This code only works with KM version 10 (and above)

This JXA code retrieves the XML for a specified Macro UUID, converts it into a "Plist" object, and displays some properties.

This also includes some examples of how to use VSCode's support for Intellisense based on JSDoc, which I think is pretty cool.

image

NOTE:

I am well aware that this code could be written more concisely. The point of this example is to be semi-self-documenting, and be more likely to be understood by non-professionals. I beg y'all to not let this devolve into discussions/arguments about programming styles - if you feel you must make those kinds of comments, please start another topic.

This example uses the UUID for a KMFAM macro, so if you use KMFAM, this will run as is in VSCode. Otherwise, change the UUID.

(function() {
    'use strict';

	ObjC.import('AppKit');

    const _kmEditor = Application("Keyboard Maestro");
    const _kmEngine  = Application("Keyboard Maestro Engine");

    // #region ======== Plist Stuff =============================

    // These are to work around a bug in VS Code's JSDoc.
    // The can be deleted with no adverse affect.
    class Plist {}
    class nsErrorObject {}

    /**
     * Returns an error message for an ObjC "nsError" object.
     * @param {nsErrorObject} nsError The nsError object.
     * @param {string} message Message about the context of the error.
     * @returns {string} "message" if nsError can't be decoded, otherwise "{message}. Error: " followed by the decoded error text.
     */
	function getErrorMessage(nsError, message) {
		try {
			return `${message}. Error: ${ObjC.unwrap(nsError.localizedDescription)}`;
		} catch (e) {
			return message;
		}
	}

    /**
     * Returns a plist from an XML string.
     * @param {string} xml An XML string to create the plist object from.
     * @returns {Plist} The plist object.
     * @throws An Error object if the XML can't be converted.
     */
	function createPlistFromXml(xml) {
		var nsError = $();
		var result = ObjC.deepUnwrap(
			$.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(
				$(xml).dataUsingEncoding($.NSUTF8StringEncoding), 0, 0, nsError));
		if (!result)
			throw Error(getErrorMessage(nsError, "Could not convert xml string to plist. xml:\n${xml}"));
		return result;
	}

    /**
     * Creates a Plist object for the specified Macro UUID.
     * @param {string} uuid The macro's UUID.
     * @returns {Plist} The plist.
     * @throws An Error object if the UUID can't be found, or multiple objects are returned (not likely).
     */
	function createPlistForMacroUuid(uuid) {
		try {
			const xml = _kmEditor.macros.whose({id: {"=": uuid}}).xml();
			if (xml.length == 0)
				throw new Error("Not found")
			if (xml.length > 1)
				throw new Error(`Expected 1 result, found ${xml.length}`)
			return createPlistFromXml(xml[0]);
		} catch (error) {
			const macroName = _kmEngine.processTokens(`%MacroNameForUUID%${uuid}%`);
			throw new Error(`Error trying to create a plist for macro UUID ${uuid} (${macroName}):\n${error.message}`);
		}
	}
    // #endregion


    // #region ======== Usage ===================================
    
	const plist = createPlistForMacroUuid("C9E7EDDB-F0D2-4F45-8DCC-A403B74EF347");

	console.log(`Created plist for macro "${plist.Name}" (${plist.UID}).`);
	console.log(`Number of top-level actions: ${plist.Actions.length}.`);

	if (plist.Actions.length > 0) {
		const action = plist.Actions[0];
		const name = action.ActionName || action.Title || "not defined";
		console.log(`First action: MacroActionType: ${action.MacroActionType}; name: "${name}".`);
	}

	console.log(`\nExamine this JSON to see what the plist property names are:`);
	console.log(JSON.stringify(plist, null, 2));

    // #endregion
})();

2 Likes

FWIW It might simplify things for readers to know that you don't actually have to write:

_kmEditor.macros.whose({id: {"=": uuid}})

The query already works as:

_kmEditor.macros.where({id: uuid})

Incidentally, as a non-professional reader, could you give us slightly narrower lines ?

(Probably just age, but having to scroll horizontally back and forth to see the end of the lines is upping the cognitive effort a bit :slight_smile: )

I did not know that. Thanks!

Incidentally, as a non-professional reader, could you give us slightly narrower lines ?

No, I don't think so. I wouldn't even know where to start (or stop), and to me, this is easier to understand. Hmm, where have I heard that defence before?

A worth-while goal, I think :slight_smile:

(Forgive me for quoting you)

(And even without the need to scroll the panel back and forth,
horizontal eye-scanning already turns out, experimentally,
to impose higher cognitive costs than vertical scanning).

Posted by mistake. Still editing.

I'm actually going to reply to this, because I fundamentally disagree with this, from a programming point of view. And if there are studies that disagree with me, I honestly don't care. It's my opinion.

The following isn't meant as criticism. This is just how things look to me.

When I read your code, it feels like this:

I
went
to
the
market
today.

Again, this is my perception. It doesn't have to be reality - it's just how I see it.

My goal in writing longer lines is, as much as possible anyway, to have one logical thought per line. I'll readily admit there are times I don't do this, but that's my goal.

So, for example:

```javascript
1	if (plist.Actions.length > 0) {
2		const action = plist.Actions[0];
3		const name = action.ActionName || action.Title || "not defined";
4		console.log(`First action: MacroActionType: ${action.MacroActionType}; name: "${name}".`);
5	}

As I'm scanning the code, line by line, this is what I see:

Line 1: OK, here's what happens if there's something in the Actions array.
Line 2: It sets action to the first item in the array.

Here's the relevant part:

Line 3: It sets name. I can see it sets it to action.ActionName, or some other defaults. If I care about what the other defaults are, I can look closer, but just knowing it sets name is enough to keep reading.

Line 4: It logs some stuff to the console. OK, good to know. If I care what gets logged, I can look closer. Otherwise I keep reading.

And this is my goal. To be able to understand the gist of what's happening without having to delve into the details. Each line is (usually) one logical, um, thing.

I'm guessing this isn't something you'd necessarily agree with. But, and this is going to sound like "I think I know better because I'm old", but it really has been my experience as a professional developer in a business environment (which is an important distinction to, say, a scientific environment) that this is a good philosophy.

Not everyone will agree with me, of course. But I wanted to show that there's a well-thought-out reason to my madness, regardless of whether it works for you or not.

You're talking about something completely different there – cognitive optimisation for the writer.

My goal in writing longer lines is, as much as possible anyway, to have one logical thought per line.

There is no reason at all for us to expect that the cognitive needs of readers (varied) are likely to correspond to those of the writer (singular and particular).

In fact, we know from experiment (which is, of course the only way we can know anything at all :slight_smile: ) that cognitive optima for writers and readers typically differ – rather strongly.

Writers and speakers have to make an effort to bridge that gap, if they want things to be Quoting your manifesto above: "more likely to be understood" by a particular audience.

We all expect writers and speakers to:

  • maximise the value made available to the reader/listener
  • and minimize the cognitive processing effort imposed on them.

Optimising for ourselves doesn't do that.

You are quite right – the one 'morpheme' / 'atom of meaning' per line of your caricature wouldn't work, and I don't think either of us has really seen code like that.

But if you want a rule of thumb:

  • 3-5 units of meaning per line turn out to be optimal for understanding at a glance in many contexts, and
  • having to scroll the UI left and right all the time is never optimal : -)
1 Like

PS It's no accident that hundreds of years of book-printing experience have converged on line lengths of about 60 characters.

(That's really a more plausible upper limit, for optimal legibility to most readers, than the 80 characters of punched cards)

1 Like

I guess I didn't make it clear, but when I talked about how I read the code, I meant that's how I see people reading code written in my style. I didn't mean me as the writer.

But as I said, I've stated my opinion, and the fact that you disagree with me doesn't surprise me in the least.

You actually had a chance to help me learn your coding style, and who knows, maybe I'd have ended up agreeing with you. However, you chose to belittle me and be pedantic about my question, rather than share your infinite wisdom. You will, of course, disagree with my version of that conversation, but I don't care. You lost any respect I had for you at that point, so now you're really just wasting electrons.

The only reason I responded at all was to have a chance to state my philosophy, in case anyone else reads this. Imagine my surprise when you were critical of my post.

I'd done here.

Good heavens !

You are talking about the time when you told me what my approach was and I suggested that it might be more productive to ask ?

I would have been delighted to continue that conversation, and explain my approach, but you seemed to find sudden exits a bit easier or more familiar than conversations.

A pity. Let's resume.

Surely you have a macro for opening a code block in your favorite editor...

Do so and have done.

I agree – the tooling is the answer.

Visual Studio Code, for example, has plugins for ESLint + (JS) Beautify, which can catch (and even fix) these things upstream, before posting.

(Saving everyone, the writer and all the readers, a lot of cognitive effort and unneeded processing)