Calling plugin library functions from JXA?

I’m probably missing something obvious here, but I thought I remembered reading something somewhere that illustrated how to call plugins from JXA automation.

How can this be done?

I’ve been searching for quite a while now, and I can’t seem to find where I read it. I’ve tried several things, and I looked at @draft8’s text editor launch, but I’m not sure that will do what I want.

I’m trying to initiate two-way communication between arbitrary applications basically in the same way the Omni App-to-App stuff works using the omnijs-run URLs.

Ideally, I want to script this from the command line.

What am I missing?

Thanks in advance.

goeie more,

Not sure that calling plugins from JXA has yet been described here but for omniJS code in general:

  1. Ken Case has mentioned that work has progressed on an evaluateOmniJS function for JXA to which an omniJS function can be passed. (Not yet in release, I think)
  2. I’ve sketched a couple of very bush versions of a hack which relies on the omniJS function writing a JSON string to a user data key in the background canvas. A JXA calling function can then read and JSON.parse that userData string to get a return value.

Here’s one version of that:

// evaluateOmniJS :: (Options -> OG Maybe JSON String) -> {KeyValues}
    //      -> Maybe JSON String
    const evaluateOmniJS = (f, dctOptions) => {
        const
            a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a);

        ap([sa.openLocation, sa.setTheClipboardTo], //
            ['omnigraffle:///omnijs-run?script=' +
                encodeURIComponent(
                    '(' + f.toString() + ')(' +
                    (dctOptions && Object.keys(dctOptions)
                        .length > 0 ? show(dctOptions) : '') + ')'
                )
            ]);
        // Possible harvest of return value from canvasbackground.userData
        const
            og = Application('OmniGraffle'),
            ws = (og.activate(), og.windows.where({
                _not: [{
                    name: {
                        _beginsWith: 'Automation Console'
                    }
                }]
            })),
            mw = ws.length > 0 ? {
                just: ws.at(0),
                nothing: false
            } : {
                nothing: true
            },
            mResult = mw.nothing ? mw : (() => {
                const v = mw.just.canvas()
                    .canvasbackground.userDataItems.byName('omniJSON')
                    .value()
                return (v === undefined || v === null) ? ({
                    nothing: true,
                    msg: "No JSON found in " +
                        ".canvasbackground.userDataItems.byName('omniJSON')"
                }) : {
                    just: v,
                    nothing: false
                };
            })();
        return mResult.nothing ? mResult : (
            mw.just.canvas()
            .canvasbackground.userDataItems.byName('omniJSON')
            .value = null,
            mResult
        );
    };

Calling plugins from JXA I haven’t personally experimented with so far – might possibly have a reason for doing that later this week. Let me know if you get there first.

Ah, I notice that that depends on at least one generic function that I haven’t included:

// e.g. [(*2),(/2), sqrt] <*> [1,2,3]
// -->  ap([dbl, hlf, root], [1, 2, 3])
// -->  [2,4,6,0.5,1,1.5,1,1.4142135623730951,1.7320508075688772]
    
// A list of functions applied to a list of arguments
// <*> :: [(a -> b)] -> [a] -> [b]
const ap = (fs, xs) => //
    [].concat.apply([], fs.map(f => //
        [].concat.apply([], xs.map(x => [f(x)]))));

Hi,

Thanks for that. Ok, so well, calling library modules works fine. My issue was that I was stupidly expecting to get a return value back to the JXA. Your approach above is a little cryptic for my out-of-date, old-school JS brain (but I’m getting there again), but that was the part I was missing: how to get the data back.

e.g.:

var plugin = PlugIn.find("...");
var MyLib = plugin.library("MyLib");
console.log(Mylib.myFn("hello"));

and using the https://omni-automation.com/script-links.html page for testing writes the output, so the calling works fine. I just needed a delivery vehicle for the output.

I guess you could always use the clipboard too, but your solution, while “very bush” indeed, does the job of at least allowing things to go back and forth.

I’m a little surprised they didn’t make a more straightforward way to do this yet. Maybe it’s just for Omni Apps… ;)

I’ll give this a go. Thanks!

I think the return value is coming soon, with their own evaluateOmniJS function as an addition to the AppleScript / JXA library.

kunjani, a PS

omniJS access to the clipboard is certainly on my wish-list, but I don’t think it’s there yet, is it ?

This isn’t working for me on 7.4 for some reason.

I get an error when trying to set values on the canvas from JXA using the path you have above, and I’m also seeing the same thing from the console.

document.windows[0].selection.canvas.background.userData.xfer = "XXX"; XXX document.windows[0].selection.canvas.background.userData.xfer; undefined

I’d tried your version first, but the ScriptEditor choked on the modern JS, or there were some copy/paste formatting issues with the site. Trying to get it back to basics to see what the issue is.

using this script, I get errors on the set of the property:

app = Application("OmniGraffle"); app.includeStandardAdditions = true; app.windows[0].canvas().canvasbackground.userDataItems.byName('xfer').value = "xyzzy"; app.windows[0].canvas().canvasbackground.userDataItems.byName('xfer').value();

$ osascript -l JavaScript test.js test.js:71:156: execution error: Error on line 3: Error: AppleEvent handler failed. (-10000)

Build details:

Product: OmniGraffle-7.4.x Tag: Date: 2017-06-29 13:27:13 -0700 Builder: omnibuild Host: tb1010h.private.omnigroup.com Revision: 291208

So I can’t figure out whether the path is wrong, we’re on different versions, or there’s more magic happening under the covers.

First thing to check - which OS version are you using ?

If pre-Sierra we need to convert the JS from ES6 to ES5 by, for example, pasting it into the REPL at Babel JS

Then, on the omniJS side, you need the .setUserData() method:

I’m still on El Capitan, so probably that’s the issue there. Too many deadlines and ongoing development projects to take the time to upgrade right now.

More interesting things below, however. I finally got the push to work, but not pull, nor delete from the OmniJS side of the house. I also solved the error from earlier.

The primary issue above was that the document window wasn’t active (the console was), so it puked. That’s nice… from the OJS console, at least it doesn’t need to be active…or visible, but I guess the cleverness of JXA isn’t so clever. It’s worse than doing automated testing with QA Partner in the '90s where “window not found” was the bane of 24hrs of GUI automated testing… of course, it always happened 5 minutes after you left for the evening.

AAAAAAAnyway…

This is interesting, no?

document.windows[0].selection.canvas.background.userData.xfer shell delete document.windows[0].selection.canvas.background.userData.xfer true delete document.windows[0].selection.canvas.background.userData.xfer true document.windows[0].selection.canvas.background.userData.xfer shell document.windows[0].selection.canvas.background.userData.xfer = 9 9 document.windows[0].selection.canvas.background.userData.xfer shell

The “shell” value comes from this script:

app = Application("OmniGraffle");
app.includeStandardAdditions = true;

console.log("hidden? " + app.windows[0].visible());
console.log("minimized? " + app.windows[0].miniaturized());

if(!app.windows[0].visible())
{   
    app.activate();
    console.log("can't delay in an interactive script; run it again now.");
}
else
{   
    app.windows[0].canvas().name();
//  app.windows[0].canvas().canvasbackground.userDataItems.byName('xfer').value = "shell";
    app.windows[0].canvas().canvasbackground.userDataItems.byName('xfer').value();      
//  app.windows[0].canvas().canvasbackground.userDataItems.byName('xfer').value = null; 
}

And it appears that if I try and set a value on the OmniJS side, it never is visible to the JXA side, nor can I delete it from OJS.

Delete from JXA works, however.

At this stage, I’m just going to punt for now. I’ve wasted most of a day arsing around trying to exchange data, and if the window needs to be active (there’s too much of a delay for #activate to work), then it won’t really suit what I’m trying to do anyway.

I need a way that I can reliably poke OG from effectively a command line script run with osascript so I can scrape the information there and not have to bundle a bunch of JS code into the plugin. I’m also not crazy about modifying existing code to fit the library module loading approach, and even then, it still makes it difficult to push out the results of the analysis in batch mode rather than interactively.

While this is pretty cool, and I now have a visualization tool I didn’t expect as a result of the last couple of days, if I’d gone ahead and just wrote a utility to parse the XML of the document directly (using libraries I’ve already written and proven in production for quite some time), I’d be more-or-less done with the original issue.

Much, much, much potential here, though, so will keep playing.

Oh, I know, I could generate text in a document and then automate the copy and paste to the clipboard with JXA and then figure out how to interact with the clipboard through the osascript command so that I could finally end up with the data I want in a nodejs world that I can do other, much more complex and interesting things with. ;)

Not today, methinks…

Thanks for all your help over the last couple of days, though. Really appreciate it!

Cheers!