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]:(*)
@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
}
))();
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