Assign a tag to items matching regex - plug-in

Hi all,

I use Mail Drop at work (Windows) to add agendas actions to OmniFocus and I don’t want to process them in the Inbox.

I would like to create a plug-in (iOS) to assign a tag (Agendas) and a project (MyProject) to all items in the Inbox matching this regex ^[A-Z]{2}[a-z]:(*)

I don’t know where to start ? Any idea ?

Thanks.

Marco, Les Sables d’Olonne, France

I can’t help you with the script here, but what could work if you have a mac somewhere is what is described here:

You’d send the items to the omnifocus email with the “@Agendas“ for the tag and “::MyProject” for the project.

@unlocked2412 is the expert on this kind of thing, but in the meanwhile, as a rough sketch, you could experiment with saving this draft in a file with the extension .omnijs

/*{
    "author": "Author Name",
    "targets": ["omnifocus"],
    "type": "action",
    "identifier": "com.mycompany.Untitled Action",
    "version": "0.1",
    "description": "A plug-in that...",
    "label": "TagInboxMatches",
    "mediumLabel": "TagInboxMatches",
    "paletteLabel": "TagInboxMatches",
}*/
(() => Object.assign(
    new PlugIn.Action(selection => {

        const tagName = 'Agendas';
        const projectName = 'MyProject';

        const rgxNamePattern = /^[A-Z]{2}[a-z]:*/;

        // main :: IO ()
        const main = () => {
            const
                nameMatch = x => rgxNamePattern.test(
                    x.name
                ),
                db = selection.database;

            // Either an explanatory message, 
            // or a list of the Inbox items tagged 
            // and moved to the project.
            return either(
                x => showLog('Left', x)
            )(
                x => showLog('Right', x)
            )(
                bindLR(
                    ofItemFoundOrCreatedLR(db)(
                        'tag'
                    )(tagName)
                )(tag => bindLR(
                    ofItemFoundOrCreatedLR(db)(
                        'project'
                    )(projectName)
                )(project => Right(
                    db.inbox.map(pureOF)
                    .flatMap(filterTree(nameMatch))
                    .map(x => (
                        x.addTags([tag]),
                        // `assignedContainer`, which 
                        // leaves the moving to 'Clean Up'
                        // doesn't work for Inbox subtasks,
                        // so a direct move is applied here.
                        db.moveTasks([x], project),
                        x.name
                    ))
                )))
            );
        };

        // ------------------ OMNIFOCUS ------------------

        // pureOF :: OFItem -> Tree OFItem
        const pureOF = item => {
            // The item and its descendants
            // lifted into a generic Tree functor.
            // (Allowing use of generic tree functions).
            const go = x => {
                const v = 'Project' !== x
                    .constructor.name ? (
                        x
                    ) : x.task;
                return Node(v)(
                    v.children.map(go)
                );
            };
            return go(item);
        };


        // ofItemFoundOrCreatedLR :: OF Database -> 
        // String -> String -> Either String OF DatabaseObject
        const ofItemFoundOrCreatedLR = db =>
            // Either an explanatory message, or 
            // the first OF folder, project, tag 
            // or task of the given name that is found 
            // (or successfully created if not found).
            // NB
            // The typeName argument should be drawn from the 
            // set of known names listed below.
            // If a 'task' is specified, but not found by the 
            // given name, a new task of that name will be
            // created at the start of the OF Inbox.
            typeName => itemName => {
                const
                    knownNames = [
                        'folder', 'project', 'tag', 'task'
                    ],
                    methodName = k => `flattened${toSentence(k)}s`;
                return bindLR(
                    knownNames.includes(typeName) ? (
                        Right(typeName)
                    ) : Left('OF item type not recognized')
                )(
                    k => {
                        const
                            mb = db[methodName(k)].find(
                                x => itemName === x.name
                            );
                        return undefined !== mb ? (
                            Right(mb)
                        ) : (() => {
                            try {
                                return Right(
                                    new(db[toSentence(k)])(
                                        // Name:
                                        itemName,
                                        // Position:
                                        'task' !== typeName ? (
                                            undefined
                                        ) : inbox.beginning
                                    )
                                );
                            } catch (e) {
                                return Left(e.message);
                            }
                        })();
                    }
                );
            };

        // ------------- GENERIC PRIMITIVES --------------
        // https://github.com/RobTrew/prelude-jxa

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

        // Right :: b -> Either a b
        const Right = x => ({
            type: 'Either',
            Right: 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 || []
            });

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


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


        // either :: (a -> c) -> (b -> c) -> Either a b -> c
        const either = fl =>
            // Application of the function fl to the
            // contents of any Left value in e, or
            // the application of fr to its Right value.
            fr => e => 'Either' === e.type ? (
                undefined !== e.Left ? (
                    fl(e.Left)
                ) : fr(e.Right)
            ) : undefined;


        // filterTree (a -> Bool) -> Tree a -> [a]
        const filterTree = p =>
            // List of all values in the tree
            // which match the predicate p.
            foldTree(x => xs => concat(
                p(x) ? [
                    [x], ...xs
                ] : xs
            ));


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

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

        // toSentence :: String -> String
        const toSentence = s =>
            // Sentence case - initial char capitalized 
            // and rest lowercase.
            (0 < s.length) ? (
                s[0].toUpperCase() + s.slice(1)
                .toLowerCase()
            ) : s;

        // MAIN ---
        return main();
    }), {
        validate: selection => true
    }
))();
3 Likes

I am so impressed and grateful for this script. It works great and improves my GTD workflow so much!
I would never have been able to write it even spending time in it. Thanks again it’s amazing.
Marco

2 Likes

Thanks for your reply. @draft8 did an amazing script for me.