Passing user input from JXA to OmniJS

I am trying to prompt the user for a shape name and color using JXA and then pass that info along to an OmniJS script to alter the shape’s fillColor to match.

Here’s my code:

const og = Application('OmniGraffle 7')
og.includeStandardAdditions = true

const deviceColors = {
    'red' : [1,0,0],
    'green' : [0,1,0],
    'blue' : [0,0,1]
}

const newDeviceColor = og.chooseFromList( Object.keys(deviceColors).sort() )[0]

const deviceName = og.displayDialog( 'Device Name:', {defaultAnswer: ''} ).textReturned

if ( newDeviceColor && deviceName ) {
    let newColor = deviceColors[newDeviceColor]

    const ogJSContext = (deviceName, newColor) => {
        const newDevice = canvases[0].graphicWithName(deviceName)
        newDevice.fillColor = Color.RGB( ...newColor )
    }

    og.evaluateJavascript(
        "(" + ogJSContext(deviceName, newColor) + ")()"
    )
}

It gets to this line:

const newDevice = canvases[0].graphicWithName(deviceName)

then I get: ReferenceError: Can't find variable: canvases.

I know it’s a matter of the difference between JXA and OmniJS contexts and that JXA doesn’t know about OmniJS objects like canvases but I can’t for the life of me figure out how to do it. I’ve tried several different approaches besides the one above and none have worked.

Atomic arguments can, of course, be stringified, with .toString() or just concatenation,
but things get easier if we pass a JSON.stringified version of a single argument (a dictionary of key-value pairs),

So while stage one might be as simple as:

(() => {
    'use strict';

    const og = Application('OmniGraffle');

    const ogJSContext = options => {
        return options.toString();
    };

    return og.evaluateJavascript(
        '(' + ogJSContext + ')(' + 7 + ')'
    );

})();

For most purposes it works better to write things like:

(() => {
    'use strict';

    const og = Application('OmniGraffle');

    const ogJSContext = options => {
        return JSON.stringify(options.color) + '\n' +
        options.deviceName;
    };

    return og.evaluateJavascript(
        '(' + ogJSContext + ')(' + JSON.stringify({
            color: [0, 0, 1],
            deviceName : 'BlueBottle'
        }) + ')'
    );

})();

And fleshing that out a little in this context, one variant might be something like:

(() => {
    'use strict';

    // ogJSContext = { color :: (Int, Int, Int), deviceName :: String }
    //                  -> String
    const ogJSContext = options => {
        const main = () => {
            const
                cnv = canvases[0],
                strName = options.deviceName,
                shp = cnv.graphicWithName(strName),
                lrResult = bindLR(
                    shp !== null ? Right(
                        shp
                    ) : Left('Shape not found by name: ' + strName),
                    shp => Right(
                        (shp.fillColor = Color.RGB(
                                ...options.color
                            ),
                            'Shape named ' + strName +
                            ' now has fillColor:' +
                            (() => {
                                const newColor = shp.fillColor;
                                return JSON.stringify(
                                    ['red', 'green', 'blue'].map(
                                        k => newColor[k]
                                    )
                                );
                            })()
                        )
                    )
                );

            return JSON.stringify(lrResult.Left || lrResult.Right);
        };

        // GENERICS FOR OMNIJS CONTEXT ---------------------------------------

        // Left :: a -> Either a b
        const Left = x => ({
            type: 'Either',
            Left: x
        });

        // Right :: b -> Either a b
        const Right = x => ({
            type: 'Either',
            Right: x
        });

        // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
        const bindLR = (m, mf) =>
            m.Right !== undefined ? (
                mf(m.Right)
            ) : m;

        // OMNIJS MAIN
        return main();
    };

    const jxaMain = () => {

        const main = () => {
            const
                og = Application('OmniGraffle'),
                sa = (og.includeStandardAdditions = true, og),
                deviceColors = {
                    'red': [1, 0, 0],
                    'green': [0, 1, 0],
                    'blue': [0, 0, 1]
                },
                colors = Object.keys(deviceColors).sort(),
                color = (
                    sa.activate(),
                    sa.chooseFromList(colors, {
                        withTitle: 'Colors',
                        defaultItems: colors[0]
                    })[0]
                );

            const lrResult = bindLR(
                bindLR(
                    Boolean(color) ? Right(
                        color
                    ) : Left('No color selected'),
                    color => {
                        try {
                            return Right({
                                color: deviceColors[color],
                                deviceName: og.displayDialog(
                                    'Name of device:', {
                                        defaultAnswer: '',
                                        withTitle: 'Device'
                                    }
                                ).textReturned
                            });
                        } catch (e) {
                            return Left(e.message)
                        }
                    }),
                options => Right(
                    og.evaluateJavascript(
                        '(' + ogJSContext + ')(' +
                        JSON.stringify(options) +
                        ')'
                    )
                )
            );
            return lrResult.Left || lrResult.Right;
        };

        // GENERICS FOR JXA CONTEXT ------------------------------------------
        // Left :: a -> Either a b
        const Left = x => ({
            type: 'Either',
            Left: x
        });

        // Right :: b -> Either a b
        const Right = x => ({
            type: 'Either',
            Right: x
        });

        // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
        const bindLR = (m, mf) =>
            m.Right !== undefined ? (
                mf(m.Right)
            ) : m;

        // showJSON :: a -> String
        const showJSON = x => JSON.stringify(x, null, 2);

        // showLog :: a -> IO ()
        const showLog = (...args) =>
            console.log(
                args
                .map(JSON.stringify)
                .join(' -> ')
            );

        // JXA
        return main();
    };

    return jxaMain();
})();

Incidentally, I’m not sure how many named shapes there are on your canvases, but if the number is manageable, you could, of course, get a list of shape names before running your main code, and choose from a menu of them:

(() => {
    'use strict';

    // ogJSContextNames :: OG () -> JSON String
    const ogJSContextNames = () =>
        JSON.stringify(
            canvases[0].graphics
            .reduce((a, x) => {
                const name = x.name;
                return null !== name ? (
                    a.concat(name)
                ) : a;
            }, [])
        );

    const
        og = Application('OmniGraffle'),
        sa = (og.includeStandardAdditions = true, og),
        names = JSON.parse(og.evaluateJavascript(
            '(' + ogJSContextNames + ')()'
        )).sort();

    return (
        sa.activate(),
        sa.chooseFromList(names, {
            withTitle: 'Named shapes on canvas',
            defaultItems: names[0]
        })
    );
})();