Share tasks with specific tag: OmniJS Script (Mac/iOS)

This script opens the share sheet with a text representation of the Database in the Taskpaper format documented here:

https://support.omnigroup.com/omnifocus-taskpaper-reference/

In order to set desired tag, change the value in USER OPTIONS.

Used some of Rob Trew’s wonderful generics available at: https://github.com/RobTrew/prelude-jxa

Link:

Code:

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

        // USER OPTIONS --------------------------------------
        const options = {
            tag: 'Communications'
        };

        // OMNI JS CODE ---------------------------------------
        const omniJSContext = () => {
            // main :: IO ()
            const main = () => {
                const 
                    tagName = options.tag,
                    tpText = ofTaskPaperFromTree(
                    fmapTree(
                        either(dictFromLeft)(
                            dictFromRight
                        )
                    )(
                        filteredTree(
                            either(constant(true))(
                                x => elem(tagName)(
                                    x.tags.map(x => x.name)
                                )
                            )
                        )(
                            pureOFdbLR(library)
                        )
                    )
                )
                return new SharePanel([tpText]).show()
            };

            // OMNIFOCUS FUNCTIONS ------------------------------
            const dictFromLeft = x => ({
                text: x
            })

            const dictFromRight = x => {
                const v = 'Project' !== x.constructor.name ? (
                    x
                ) : x.task
                return ({
                    text: v.name,
                    note: v.note,
                    tags: v.tags.map(x => x.name)
                })
            }

            // pureOFdbLR :: OF Item -> Tree Either String OF Item
            const pureOFdbLR = item => {
                const go = x => {
                    const k = x.constructor.name;
                    return ['Project', 'Task'].includes(k) ? (
                        fmapPureOF(Right)(x)
                    ) : 'Folder' !== k ? (
                        Node(Left(k))((
                            'Database' !== k ? (
                                x
                            ) : [x.inbox, x.library]
                        ).map(go))
                    ) : Node(Left('Folder: ' + x.name))(
                        x.children.map(go)
                    );
                };
                return go(item);
            };

            // fmapPureOF :: (OF Item -> a) -> OF Item -> Tree a
            const fmapPureOF = f => item => {
                const go = x => Node(f(x))((
                    'Project' !== x.constructor.name ? (
                        x
                    ) : x.task
                ).children.map(go));
                return go(item);
            };

            // ofTaskPaperFromTree :: Tree -> String
            const ofTaskPaperFromTree = 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' ? (
                                '- ' + txt
                            ) : txt + ':') +
                            (
                                blnTags ? ` @tags(${
                                    intercalateS(',')(tags)
                                })` : ''
                            ) +
                            (blnNotes ? (
                                '\n' + unlines(map(
                                    s => strNext + s
                                )(lines(note)))
                            ) : '') + (blnNest ? (
                                '\n' + unlines(map(go(strNext))(nest))
                            ) : '')
                        ) : '';
                    };
                return go('')(x);
            };

            // GENERIC FUNCTIONS --------------------------------------------
            // https://github.com/RobTrew/prelude-jxa
            // 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 || []
                });

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

            // constant :: a -> b -> a
            const constant = k =>
                _ => k;

            // 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;

            // elem :: Eq a => a -> [a] -> Bool
            // elem :: Char -> String -> Bool
            const elem = x =>
                xs => {
                    const t = xs.constructor.name;
                    return 'Array' !== t ? (
                        xs['Set' !== t ? 'includes' : 'has'](x)
                    ) : xs.some(eq(x));
                };

            // eq (==) :: Eq a => a -> a -> Bool
            const eq = a =>
                // True when a and b are equivalent in the terms
                // defined below for their shared data type.
                b => {
                    const t = typeof a;
                    return t !== typeof b ? (
                        false
                    ) : 'object' !== t ? (
                        'function' !== t ? (
                            a === b
                        ) : a.toString() === b.toString()
                    ) : (() => {
                        const kvs = Object.entries(a);
                        return kvs.length !== Object.keys(b).length ? (
                            false
                        ) : kvs.every(([k, v]) => eq(v)(b[k]));
                    })();
                };

            // filteredTree (a -> Bool) -> Tree a -> Tree a
            const filteredTree = p =>
                // A tree including only those children
                // which either match the predicate p, or have
                // descendants which match the predicate p.
                foldTree(x => xs =>
                    Node(x)(xs.filter(
                        tree => (0 < tree.nest.length) || (
                            p(tree.root)
                        )
                    ))
                );

            // fmapTree :: (a -> b) -> Tree a -> Tree b
            const fmapTree = f => {
                // A new tree. The result of a structure-preserving
                // application of f to each root in the existing tree.
                const go = tree => Node(f(tree.root))(
                    tree.nest.map(go)
                );
                return go;
            };

            // foldTree :: (a -> [b] -> b) -> Tree a -> b
            const foldTree = f => {
                // The catamorphism on trees. A summary
                // value obtained by a depth-first fold.
                const go = tree => f(tree.root)(
                    tree.nest.map(go)
                );
                return go;
            };

            // intercalateS :: String -> [String] -> String
            const intercalateS = s =>
                // The concatenation of xs
                // interspersed with copies of s.
                xs => xs.join(s);

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

            // 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 => (
                    Array.isArray(xs) ? (
                        xs
                    ) : xs.split('')
                ).map(f);

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

            // 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');

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

        return omniJSContext()
        
        }), {
            validate: selection => true
        }
))();
2 Likes

As one entirely uninitiated in JXA, I am astounded by its focus and utility. In truth, as much as I think that I know enough to read almost any coding language, I am lost in the bright sunshine here.

With this respect in mind, might I ask one entirely naive question? Would it be better to put the tag that is to be changed within its own “handler” function at the very top of the code? I am thinking of an equivalent to an AppleScript property done with JXA conventions.

While the tag resides within the first few lines and while it is clearly denoted, I fear that an approach of “hard-coding” a changeable parameter within a function call violates some “keep it easy to change” guideline somewhere.

Thanks for the effort!


JJW

Good observation, @DrJJWMac. I thought about that. Just made a minor modification to the script.

I used that feature many times when coding in that language. I think it was useful.

Only a clarification, this specific script (being OmniJS) do not have access to Automation object. Once we step into OmniJS context, we lose access to Automation object methods and properties. We could, if wanted, pass information from JXA to OmniJS context, though.