Pasting from an Excel clipboard to an OG 7.5 Table object (omniJS)


#1

An experiment in using omniJS, JXA and Keyboard Maestro to paste from a range of copied Excel cells (or any other tab-delimited utf8 lines in the clipboard) to an editable and formattable OmniGraffle 7.5 Table object.

From (Excel):

To (OG 7.5 test)

Keyboard Maestro (and a required plugin for using omniJS) at:

Code in omniJS plugin action (json -> Table)

const mbTable = JSON.parse(kmvar.jsonMaybeTable);

return mbTable.nothing ? (
    "No table found in clipboard"
) : (() => {
    const
        canvas = document.windows[0].selection.canvas,
        table = mbTable.just;
    return (
        Table.withRowsColumns(
            table.rows,
            table.columns,
            table.values.map(
                x => Object.assign(canvas.newShape(), {
                    textSize: 12,
                    text: x,
                    geometry: new Rect(0, 0, 50, 20),
                    strokeThickness: 0.5,
                    strokeColor : Color.RGB(0.5, 0.6, 0.75)
                })
            )
        ),
        table.values.length + "  cells in " + 
        table.rows + " rows of " + table.columns + " columns."
    );
})();

Code in JXA action (clipboard -> maybe json)

(() => {
    'use strict';

    // APPKIT IMPORTED FOR NSPasteboard --------------------------------------

        ObjC.import('AppKit');

        // GENERIC FUNCTIONS -----------------------------------------------------

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        // concat :: [[a]] -> [a] | [String] -> String
        const concat = xs =>
            xs.length > 0 ? (() => {
                const unit = typeof xs[0] === 'string' ? '' : [];
                return unit.concat.apply(unit, xs);
            })() : [];

        // curry :: Function -> Function
        const curry = (f, ...args) => {
            const go = xs => xs.length >= f.length ? (f.apply(null, xs)) :
                function () {
                    return go(xs.concat(Array.from(arguments)));
                };
            return go([].slice.call(args, 1));
        };

        // length :: [a] -> Int
        const length = xs => xs.length;

        // lines :: String -> [String]
        const lines = s => s.split(/[\r\n]/);

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

        // map :: (a -> b) -> [a] -> [b]
        const map = (f, xs) => xs.map(f);

        // maximumBy :: (a -> a -> Ordering) -> [a] -> a
        const maximumBy = (f, xs) =>
            xs.reduce((a, x) => a === undefined ? x : (
                f(x, a) > 0 ? x : a
            ), undefined);

        // show :: Int -> a -> Indented String
        // show :: a -> String
        const show = (...x) =>
            JSON.stringify.apply(
                null, x.length > 1 ? [x[1], null, x[0]] : x
            );

        // splitOn :: a -> [a] -> [[a]]
        // splitOn :: String -> String -> [String]
        const splitOn = curry((needle, haystack) =>
            typeof haystack === 'string' ? (
                haystack.split(needle)
            ) : (function sp_(ndl, hay) {
                const mbi = findIndex(x => ndl === x, hay);
                return mbi.nothing ? (
                    [hay]
                ) : append(
                    [take(mbi.just, hay)],
                    sp_(ndl, drop(mbi.just + 1, hay))
                );
            })(needle, haystack));

        // UTF8, IF ANY, IN CLIPBOARD --------------------------------------------

        // textClipMay :: Maybe String
        const textClipMay = () => {
            const
                utf8 = "public.utf8-plain-text",
                pb = $.NSPasteboard.generalPasteboard;
            return ObjC.deepUnwrap(pb.pasteboardItems.js[0].types)
                .indexOf(utf8) !== -1 ? {
                    nothing: false,
                    just: ObjC.unwrap(
                        pb.stringForType(utf8)
                    )
                } : {
                    nothing: true
                };
        };

        // ROW-COL DIMENSIONS AND CELL-ARRAY OF TABLE IN CLIPBOARD -----------
        const
            mbText = textClipMay(),
            mbTable = mbText.nothing ? mbText : (() => {
                const clipRows = map(splitOn('\t'), lines(mbText.just));
                return {
                    just: {
                        columns: length(
                            maximumBy(comparing(length), clipRows)
                        ),
                        rows: length(clipRows),
                        values: concat(clipRows)
                    },
                    nothing: false
                };
            })();
        return JSON.stringify(mbTable);
    })();


#2

Hiya,

Sticking my nose back into OmniJS, and when trying to figure out how to set the clipboard text from the Mac version, I found all these cool posts I’d missed…

However, I guess I’m being thick, but I don’t see how to actually set the clipboard from OmniJS from within a plugin. I saw that one of the Omni tools has a Pasteboard object, but that’s not in my version.

I also saw a couple other people asking about this, but I didn’t see a reply. You also mentioned you’ve done this in another thread, I think.

What am I missing?

Cheers,

ast


#3

Good question – perhaps Tim Wood, or the Automation section of the https://omnigrouphq.slack.com discussions might be able to tell us what the thinking on the OG omniJS APIs and the clipboard.

As far as I can recall, I have been doing this kind of thing through JXA (either the .setTheClipboard(...) method in standardAdditions, or through $.NSPasteBoard in AppKit)

That does, of course, limit you to returning a value to JXA first …


#4

(on the iOS side, there didn’t, for example, seem to be a ready answer to this question:

So I imagine that some difficulty may have been experienced …


#5

Thanks for the reply. I tried to check out the slack channel, but seems to be closed to outsiders…

Quite annoying there’s no really good way to push data out of OG exposed by the native APIs. :(


#6

Worth signalling that through support, I think.

(Also worth asking them for an invite to the Slack channel, though I raised your question there and there didn’t seem to be a response)

(I generally return a value from the JXA-side application.execute(strOmniJS) method).


#7

I may have to figure out how to resort to this approach. I really want the workflow to be driven within OG, however, e.g., you’re doing something, then poke the plugin as part of a workflow step, then you paste to the next tool, then continue the task.

I actually never really had much luck with the JXA-driven approach for what I wanted, because I need to use the command-line tools given the only other alternative I can drive the workflow.

I’d put this on hold for a while, but I’m getting back to it now. Trying to remember where I was last year and really make sure what I had in place works as I remember. As I work through, I’m planning on going back through your posts in particular since you seem to have really done a lot in this area.

Really appreciate you sharing what you’ve done!