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.

hey @unlocked2412, a question for you - what is the best resource to learn your approach to javascript which lends itself to reuse of generic functions (such as the ones found here https://github.com/RobTrew/prelude-jxa)? thanks!

Hello, @sam70. Taking inspiration from @draft8, I think good options are:

Learning JS:

  • First 10 chapters of Eloquent Javascript book, with special attention to filter, map, reduce.
  • Mozilla pages on map, filter, reduce, flatmap.

Learning Haskell:

  • Programming in Haskell - 2nd Edition by Graham Hutton.
  • Haskell for Mac application.

Perhaps, you could start experimenting with the wonderful jxa-prelude library. Here are some basic examples:

Adding one to a list of integers

(() => {
    // GENERIC FUNCTIONS ----------------
    // 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);

    return map(x => x + 1)([1, 2, 3])
    //--> [2,3,4]
})();

Leaving only even integers

(() => {
    // GENERIC FUNCTIONS ----------------
    // even :: Int -> Bool
    const even = n => 0 === n % 2;
    
    // filter :: (a -> Bool) -> [a] -> [a]
    const filter = p =>
        // The elements of xs which match
        // the predicate p.
        xs => [...xs].filter(p);

    return filter(even)([1, 2, 3])
    //--> [2]
})();

Summing a list of integers

(() => {
    // GENERIC FUNCTIONS ----------------
    // add (+) :: Num a => a -> a -> a
    const add = a =>
        // Curried addition.
        b => a + b;

    // foldr :: (a -> b -> b) -> b -> [a] -> b
    const foldr = f =>
        // Note that that the Haskell signature of foldr differs from that of
        // foldl - the positions of accumulator and current value are reversed
        a => xs => [...xs].reduceRight(
            (a, x) => f(x)(a),
            a
        );

    return foldr(add)(0)([1, 2, 3])
    //--> 6
})();
2 Likes

Thank you!

Now I have no reason to not understand the code which feels so arcane as of now. Of course, there is a distance to cover between that day and today :)

Good! You’re welcome. Enjoy the process !

1 Like

Wow! Nice book. Got the first few chapters as a free sample. Breezed through based on past experience in coding (and now I know what the === means, and I can just about guess what => means too).

Thanks!


JJW

You are welcome, Jeffrey. Glad you found it useful.

Just out of curiosity, can you point out where are you looking ? Is there a specific line of code ?

In plain English … the constant (function) “even” becomes n when the result of n / 2 is identically zero.


JJW

or

// even :: Int -> Bool
const even = n => 
    0 === n % 2;

the function even, defined over n, yields true if (and only if) n modulo 2 leaves no remainder.

(i.e. if the value of 0 is exactly the same, without type coercion, as that of n % 2

=> is a light-weight function definition, which imposes less internal baggage on the JavaScript engine, is quicker to write, and is more than enough for most purposes.

In the example above, even is defined as the name of a simple function which maps from the integer n to a Boolean (true | false) expression whose value depends on the specific value of n.

PS you can think of a function as a value with a gap that has to be filled.

even is the name of a Boolean value with an integer gap named n.

When the n gap is filled with a specific integer, even evaluates to a specific truth value.

Now we can define odd :-)

// odd :: Int -> Bool
const odd = n =>
    !even(n);

(Where ! is logical negation – not – in JS).

Comparing with AppleScript:

-- even :: Int -> Bool
on even(x)
    0 = x mod 2
end even

-- odd :: Int -> Bool
on odd(x)
    not even(x)
end odd

and applied with a filter function:

on run
    
    filter(even, {1, 2, 3, 4})
    
    --> {2, 4}
    
end run


-------------------------- GENERIC -------------------------


-- even :: Int -> Bool
on even(x)
    0 = x mod 2
end even


-- odd :: Int -> Bool
on odd(x)
    not even(x)
end odd


-- filter :: (a -> Bool) -> [a] -> [a]
on filter(p, xs)
    tell mReturn(p)
        set lst to {}
        set lng to length of xs
        repeat with i from 1 to lng
            set v to item i of xs
            if |λ|(v, i, xs) then set end of lst to v
        end repeat
        return lst
    end tell
end filter

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

Thanks @unlocked2412 and @draft8.

Someday, I will have a grand time comparing the level of conversational intelligence that I have garnered across the multitude of programming languages that I’ve used in my career. If I might use an analogy, the references here have just upped my comfort level for when I need to try to find my way around Prague (which is a place that I greatly enjoyed but rarely can actually visit for long periods of time).


JJW