Copying a tab-indented outline from OmniPlan

OmniPlan Edit > Copy (when tasks are selected) places XML and URLs in the clipboard, but not a pasteable text outline.

Here is a very basic Copy as Tab-Indented version (which copies only task names in this draft, but is easily extended).

Would have liked to write it in omniJS, but couldn’t immediately find:

  1. An evaluateJavaScript method (as in omniGraffle and omniOutliner) for osascript ⇄ omniJS cross-traffic, or
  2. omniJS access to the clipboard from omniPlan (as in the OO omniJS PasteBoard object)

So here, for the moment, and out of reach for iOS, is a JavaScript for Automation draft, for use from Script Editor, Keyboard Maestro etc

Copy whole script, all the way down to the line after return main()

(() => {
    'use strict';

    // macOS omniplan :: 
    // Copy selected tasks as tab-indented outline.

    // Rob Trew 2019
    // Ver 0.02

    // 0.02 Allows for selection of sub-trees at or below level 1.
    //      Descendants of selected nodes are also copied. 

    ObjC.import('AppKit');

    // main :: IO ()
    const main = () => {
        const
            plan = Application('OmniPlan'),
            ws = plan.windows;
        return either(identity)(
            outline => (
                copyText(outline),
                outline
            )
        )(bindLR(
            0 < ws.length ? (() => {
                const ts = ws.at(0).selectedTasks();
                return null !== ts && 0 < ts.length ? (
                    Right([minimum(map(x => x.outlineDepth())(ts)), ts])
                ) : Left('No tasks selected in OmniPlan');
            })() : Left('No window open in OmniPlan.')
        )(([level, tasks]) => Right(
            tabIndentFromForest(root)(
                concatMap(
                    task => level === task.outlineDepth() ? (
                        [fmapPureOP(x => x.name())(task)]
                    ) : []
                )(tasks)
            )
        )));
    };

    // OMNIPLAN -------------------------------------------

    // fmapPureOP :: (OPTask -> a) -> OPTask -> Tree OPTask
    const fmapPureOP = f => task => {
        const go = x => {
            const xs = x.childTasks;
            return Node(f(x))(
                0 < xs.length ? (
                    xs().map(go)
                ) : []
            );
        };
        return go(task);
    };

    // TABBED OUTLINE -------------------------------------

    // tabIndentFromForest :: (Tree -> String) ->
    //      [Tree] -> String
    const tabIndentFromForest = fnText => trees => {
        const go = indent => tree =>
            indent + fnText(tree) +
            (tree.nest.length > 0 ? (
                '\n' + tree.nest
                .map(go('\t' + indent))
                .join('\n')
            ) : '');
        return trees.map(go('')).join('\n');
    };

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

    // copyText :: String -> IO Bool
    const copyText = s => {
        // String copied to general pasteboard.
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

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

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

    // Node :: a -> [Tree a] -> Tree a
    const Node = v => xs => ({
        type: 'Node',
        root: v, // any type of value (consistent across tree)
        nest: xs || []
    });

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

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

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        x => fs.reduceRight((a, f) => f(a), x);

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

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl => fr => e =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;


    // identity :: a -> a
    const identity = x => x;

    // Returns Infinity over objects without finite length.
    // This enables zip and zipWith to choose the shorter
    // argument when one is non-finite, like cycle, repeat etc

    // length :: [a] -> Int
    const length = xs =>
        (Array.isArray(xs) || 'string' === typeof xs) ? (
            xs.length
        ) : Infinity;

    // map :: (a -> b) -> [a] -> [b]
    const map = f => xs =>
        (Array.isArray(xs) ? (
            xs
        ) : xs.split('')).map(f);

    // minimum :: Ord a => [a] -> a
    const minimum = xs =>
        0 < xs.length ? (
            xs.slice(1)
            .reduce((a, x) => x < a ? x : a, xs[0])
        ) : undefined;

    // root :: Tree a -> a
    const root = tree => tree.root;

    // MAIN ---
    return main();
})();