Share selected tasks: OmniJS Script (Mac/iOS)

The following OmniJS (cross-platform) script opens the share sheet with a text representation of selected tasks preserving only task name and notes .

Screenshots:

Screen Shot 2020-08-31 at 11.20.27
Output:

- Plant a tree
	- Buy seeds
		Research types of seeds
	- Prepare garden
	- Plant seeds
- Vacation in Rome
	- Buy tickets
		Compare prices per season
	- Search hotel
	- Pack for trip

Full code:

/*{
	"type": "action"
}*/
// Twitter: @unlocked2412
(() => Object.assign(
    new PlugIn.Action(selection => {

        // OMNI JS CODE ---------------------------------------
        const omniJSContext = () => {
            // main :: IO ()
            const main = () => {
                const
                    ts = selection.tasks,
                    strText = compose(
                        unlines,
                        map(taskPaperFromTree),
                        map(
                            fmapPureOF(x => ({
                                text: x.name,
                                note: x.note
                            }))
                        )
                    )(ts)
                return (
                    new SharePanel([strText]).show(),
                    strText
                )
            };


            // FUNCTIONS --
            // JS Prelude --------------------------------------------------
            // Just :: a -> Maybe a
            const Just = x => ({
                type: 'Maybe',
                Nothing: false,
                Just: x
            });

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

            // Node :: a -> [Tree a] -> Tree a
            const Node = v =>
                // Constructor for a Tree node which connects a
                // value of some kind to a list of zero or
                // more child trees.
                xs => ({
                    type: 'Node',
                    root: v,
                    nest: xs || []
                });

            // Nothing :: Maybe a
            const Nothing = () => ({
                type: 'Maybe',
                Nothing: true,
            });

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

            // Tuple (,) :: a -> b -> (a, b)
            const Tuple = a =>
                b => ({
                    type: 'Tuple',
                    '0': a,
                    '1': b,
                    length: 2
                });

            // append (++) :: [a] -> [a] -> [a]
            const append = xs =>
                // A list obtained by the
                // concatenation of two others.
                ys => xs.concat(ys);

            // bind (>>=) :: Monad m => m a -> (a -> m b) -> m b
            const bind = m =>
                mf => (Array.isArray(m) ? (
                    bindList
                ) : (() => {
                    const t = m.type;
                    return 'Either' === t ? (
                        bindLR
                    ) : 'Maybe' === t ? (
                        bindMay
                    ) : 'Tuple' === t ? (
                        bindTuple
                    ) : ('function' === typeof m) ? (
                        bindFn
                    ) : undefined;
                })()(m)(mf));

            // bindFn (>>=) :: (a -> b) -> (b -> a -> c) -> a -> c
            const bindFn = f =>
                // Binary operator applied over f x and x.
                bop => x => bop(f(x))(x);

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

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

            // bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
            const bindMay = mb =>
                // Nothing if mb is Nothing, or the application of the
                // (a -> Maybe b) function mf to the contents of mb.
                mf => mb.Nothing ? (
                    mb
                ) : mf(mb.Just);

            // bindTuple (>>=) :: Monoid a => (a, a) -> (a -> (a, b)) -> (a, b)
            const bindTuple = tpl =>
                f => {
                    const t2 = f(tpl[1]);
                    return Tuple(mappend(tpl[0])(t2[0]))(
                        t2[1]
                    );
                };

            // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
            const compose = (...fs) =>
                // A function defined by the right-to-left
                // composition of all the functions in fs.
                fs.reduce(
                    (f, g) => x => f(g(x)),
                    x => x
                );

            // concat :: [[a]] -> [a]
            // concat :: [String] -> String
            const concat = xs => (
                ys => 0 < ys.length ? (
                    ys.every(Array.isArray) ? (
                        []
                    ) : ''
                ).concat(...ys) : ys
            )(list(xs));

            // cons :: a -> [a] -> [a]
            const cons = x =>
                // A list constructed from the item x,
                // followed by the existing list xs.
                xs => Array.isArray(xs) ? (
                    [x].concat(xs)
                ) : 'GeneratorFunction' !== xs
                .constructor.constructor.name ? (
                    x + xs
                ) : ( // cons(x)(Generator)
                    function* () {
                        yield x;
                        let nxt = xs.next();
                        while (!nxt.done) {
                            yield nxt.value;
                            nxt = xs.next();
                        }
                    }
                )();

            // dropAround :: (a -> Bool) -> [a] -> [a]
            // dropAround :: (Char -> Bool) -> String -> String
            const dropAround = p =>
                xs => dropWhile(p)(
                    dropWhileEnd(p)(xs)
                );

            // dropWhile :: (a -> Bool) -> [a] -> [a]
            // dropWhile :: (Char -> Bool) -> String -> String
            const dropWhile = p =>
                xs => {
                    const n = xs.length;
                    return xs.slice(
                        0 < n ? until(
                            i => n === i || !p(xs[i])
                        )(i => 1 + i)(0) : 0
                    );
                };

            // dropWhileEnd :: (a -> Bool) -> [a] -> [a]
            // dropWhileEnd :: (Char -> Bool) -> String -> String
            const dropWhileEnd = p =>
                xs => {
                    let i = xs.length;
                    while (i-- && p(xs[i])) {}
                    return xs.slice(0, i + 1);
                };

            // enumFromTo :: Int -> Int -> [Int]
            const enumFromTo = m =>
                n => !isNaN(m) ? (
                    Array.from({
                        length: 1 + n - m
                    }, (_, i) => m + i)
                ) : enumFromTo_(m)(n);

            // enumFromTo_ :: Enum a => a -> a -> [a]
            const enumFromTo_ = m => n => {
                const [x, y] = [m, n].map(fromEnum),
                    b = x + (isNaN(m) ? 0 : m - x);
                return Array.from({
                    length: 1 + (y - x)
                }, (_, i) => toEnum(m)(b + i));
            };

            // foldl :: (a -> b -> a) -> a -> [b] -> a
            const foldl = f =>
                a => xs => [...xs].reduce(
                    (x, y) => f(x)(y),
                    a
                );

            // fromEnum :: Enum a => a -> Int
            const fromEnum = x =>
                typeof x !== 'string' ? (
                    x.constructor === Object ? (
                        x.value
                    ) : parseInt(Number(x))
                ) : x.codePointAt(0);

            // identity :: a -> a
            const identity = x =>
                // The identity function. (`id`, in Haskell)
                x;

            // isDigit :: Char -> Bool
            const isDigit = c => {
                const n = c.codePointAt(0);
                return 48 <= n && 57 >= n;
            };

            // join :: Monad m => m (m a) -> m a
            const join = x =>
                bind(x)(identity);

            // keys :: Dict -> [String]
            const keys = Object.keys;

            // length :: [a] -> Int
            const length = xs =>
                // 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
                'GeneratorFunction' !== xs.constructor.constructor.name ? (
                    xs.length
                ) : Infinity;

            // lines :: String -> [String]
            const lines = s =>
                // A list of strings derived from a single
                // newline-delimited string.
                0 < s.length ? (
                    s.split(/[\r\n]/)
                ) : [];

            // list :: StringOrArrayLike b => b -> [a]
            const list = xs =>
                // xs itself, if it is an Array,
                // or an Array derived from xs.
                Array.isArray(xs) ? (
                    xs
                ) : Array.from(xs || []);

            // map :: (a -> b) -> [a] -> [b]
            const map = f =>
                // The list obtained by applying f
                // to each element of xs.
                // (The image of xs under f).
                xs => [...xs].map(f);

            // mappend (<>) :: Monoid a => a -> a -> a
            const mappend = a =>
                // Associative operation 
                // defined for various monoid types.
                b => (t => (Boolean(t) ? (
                    'Maybe' === t ? (
                        mappendMaybe
                    ) : mappendTuple
                ) : 'function' !== typeof a ? (
                    append
                ) : a.toString() !== 'x => y => f(y)(x)' ? (
                    mappendFn
                ) : mappendOrd)(a)(b))(a.type);

            // mappendFn :: Monoid b => (a -> b) -> (a -> b) -> (a -> b)
            const mappendFn = f =>
                g => x => mappend(f(x))(
                    g(x)
                );

            // mappendMaybe (<>) :: Maybe a -> Maybe a -> Maybe a
            const mappendMaybe = a =>
                b => a.Nothing ? (
                    b
                ) : b.Nothing ? (
                    a
                ) : Just(
                    mappend(a.Just)(
                        b.Just
                    )
                );

            // mappendOrd (<>) :: Ordering -> Ordering -> Ordering
            const mappendOrd = cmp =>
                cmp1 => a => b => {
                    const x = cmp(a)(b);
                    return 0 !== x ? (
                        x
                    ) : cmp1(a)(b);
                };

            // mappendTuple (<>) :: (a, b) -> (a, b) -> (a, b)
            const mappendTuple = t => t2 =>
                Tuple(
                    mappend(t[0])(
                        t1[0]
                    )
                )(mappend(t[1])(
                    t1[1]
                ));

            // min :: Ord a => a -> a -> a
            const min = a => b => b < a ? b : a;

            // negate :: Num -> Num
            const negate = n =>
                -n;

            // nest :: Tree a -> [a]
            const nest = tree => {
                // Allowing for lazy (on-demand) evaluation.
                // If the nest turns out to be a function –
                // rather than a list – that function is applied
                // here to the root, and returns a list.
                const xs = tree.nest;
                return 'function' !== typeof xs ? (
                    xs
                ) : xs(root(x));
            };

            // or :: [Bool] -> Bool
            const or = xs =>
                xs.some(Boolean);

            // read :: Read a => String -> a
            const read = JSON.parse;

            // replace :: String -> String -> String -> String
            // replace :: Regex -> String -> String -> String
            const replace = needle => strNew => strHaystack =>
                strHaystack.replace(
                    'string' !== typeof needle ? (
                        needle
                    ) : new RegExp(needle, 'g'),
                    strNew
                );

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

            // show :: a -> String
            // show :: a -> Int -> Indented String
            const show = x => {
                const
                    e = ('function' !== typeof x) ? (
                        x
                    ) : {
                        type: 'Function',
                        f: x
                    };
                return JSON.stringify(e, (_, v) => {
                    const
                        f = ((null !== v) && (undefined !== v)) ? (() => {
                            const t = v.type;
                            return 'Either' === t ? (
                                showLR
                            ) : 'Function' === t ? (
                                dct => 'λ' + dct.f.toString()
                            ) : 'Maybe' === t ? (
                                showMaybe
                            ) : 'Ordering' === t ? (
                                showOrdering
                            ) : 'Ratio' === t ? (
                                showRatio
                            ) : 'string' === typeof t && t.startsWith('Tuple') ? (
                                showTuple
                            ) : Array.isArray(v) ? (
                                showList
                            ) : undefined;
                        })() : showUndefined;
                    return Boolean(f) ? (
                        f(v)
                    ) : 'string' !== typeof v ? (
                        v
                    ) : v;
                })
            };

            // showLR :: Either a b -> String
            const showLR = lr => {
                const k = undefined !== lr.Left ? (
                    'Left'
                ) : 'Right';
                return k + '(' + unQuoted(show(lr[k])) + ')';
            };

            // showList :: [a] -> String
            const showList = xs =>
                '[' + xs.map(show)
                .join(', ')
                .replace(/[\"]/g, '') + ']';

            // showMaybe :: Maybe a -> String
            const showMaybe = mb =>
                mb.Nothing ? (
                    'Nothing'
                ) : 'Just(' + unQuoted(show(mb.Just)) + ')';

            // showOrdering :: Ordering -> String
            const showOrdering = e =>
                0 < e.value ? (
                    'GT'
                ) : 0 > e.value ? (
                    'LT'
                ) : 'EQ';

            // showRatio :: Ratio -> String
            const showRatio = r =>
                'Ratio' !== r.type ? (
                    r.toString()
                ) : r.n.toString() + (
                    1 !== r.d ? (
                        '/' + r.d.toString()
                    ) : ''
                );

            // showTuple :: Tuple -> String
            const showTuple = tpl =>
                '(' + enumFromTo(0)(tpl.length - 1)
                .map(x => unQuoted(show(tpl[x])))
                .join(',') + ')';

            // showUndefined :: () -> String
            const showUndefined = () => '(⊥)';

            // toEnum :: a -> Int -> a
            const toEnum = e =>
                // The first argument is a sample of the type
                // allowing the function to make the right mapping
                x => ({
                    'number': Number,
                    'string': String.fromCodePoint,
                    'boolean': Boolean,
                    'object': v => e.min + v
                } [typeof e])(x);

            // unQuoted :: String -> String
            const unQuoted = s =>
                dropAround(x => 34 === x.codePointAt(0))(
                    s
                );

            // unlines :: [String] -> String
            const unlines = xs =>
                // A single string formed by the intercalation
                // of a list of strings with the newline character.
                xs.join('\n');

            // until :: (a -> Bool) -> (a -> a) -> a -> a
            const until = p => f => x => {
                let v = x;
                while (!p(v)) v = f(v);
                return v;
            };

            // JS Trees ----------------------------------------------------
            // taskPaperFromTree :: Tree -> String
            const taskPaperFromTree = x => {
                const
                    rgxSpace = /\s+/g,
                    go = strIndent => x => {
                        const
                            nest = x.nest,
                            root = x.root,
                            txt = root.text || '',
                            tags = root.tags,
                            ks = Boolean(tags) ? (
                                Object.keys(tags)
                            ) : [],
                            note = root.note,
                            blnNotes = Boolean(note),
                            blnTags = ks.length > 0,
                            blnNest = nest.length > 0,
                            strNext = '\t' + strIndent;

                        return or([Boolean(txt), blnTags, blnNotes, blnNest]) ? (
                            strIndent + (root.type !== 'project' ? (
                                root.type !== 'note' ? (
                                    '- ' + txt
                                ) : txt
                            ) : txt + ':') +
                            (blnTags ? foldl(
                                t => k => {
                                    const v = tags[k];
                                    return t + (Boolean(v) ? (
                                        ' @' + k.replace(rgxSpace, '_') +
                                        (Boolean(v) && v !== true ? (
                                            '(' + v + ')'
                                        ) : '')
                                    ) : '');
                                })('')(
                                ks
                            ) : '') +
                            (blnNotes ? (
                                '\n' + unlines(map(
                                    s => strNext + s
                                )(lines(note)))
                            ) : '') + (blnNest ? (
                                '\n' + unlines(map(go(strNext))(nest))
                            ) : '')
                        ) : '';
                    };
                return go('')(x);
            };

            // OmniFocus OmniJS --------------------------------------------
            // fmapPureOF :: (OF Item -> a) -> OF Item -> Tree a
            const fmapPureOF = f => item => {
                const go = x => Node(f(x))(
                    x.children.map(go)
                );
                return go(item);
            };

            return main();
        };

        return omniJSContext()

    }), {
        validate: selection => selection.tasks.length > 0
    }
))();

Link:

2 Likes

Thanks for the script! But… I tried it on my iPad Pro and it just crashed OmniFocus…
Probably me, not really knowing how to install/use scripts. So my question is: how do I select tasks and then run the script ?
Sorry for this kind of basic question :)

Good question. You’re not doing anything wrong. I noticed the same behaviour on iPadOS. Apparently, there is a bug in show method of SharePanel class. Would be good if you could send a note to support to let them know about this issue. I will send it, too.

Just to let everyone know. This bug is solved in latest version iOS version.