Here is a cross-platform (iOS/Mac) OmniJS Plug-In. Does it solve your problem ? If you really need an AppleScript version, I can look into it later.
Save this code into a file with .omnijs extension and save it in your OmniFocus iCloud folder. It should appear automatically in the Automation
menu. The script assumes there is a project named ‘Example’ in the database.
Full code:
/*{
"type": "action",
"name": "Remove tag from tasks and mark complete in specific project",
"author": "@unlocked2412",
"version": "0.10",
"description": "Remove tag from tasks and mark complete in specific project",
"mediumLabel": "Remove tag",
"paletteLabel": "Remove tag",
}*/
// Twitter: @unlocked2412
(() => {
// ---------------------- PLUGIN -----------------------
return Object.assign(
new PlugIn.Action(selection => {
'use strict';
// OMNI JS CODE ---------------------------------------
const omniJSContext = () => {
// main :: IO ()
const main = () => {
const projName = 'Example'
const tagName = 'next'
const lrTasks = bindLR(
projectLR(projName)
)(
proj => {
const tasks = filter(
x => x.taskStatus !== Task.Status.Completed &&
elem(tagName)(map(x => x.name)(x.tags))
)(proj.flattenedTasks);
return 0 === tasks.length ? (
Left('No tasks in project: ' + projName)
) : Right(tasks)
}
)
return new Alert(
'Complete and remove tag',
either(identity)(
xs => `Completed ${map(task => {
return (
task.removeTag(tagFoundOrCreated(tagName)),
task.markComplete(),
task
)
})(xs).length} tasks`
)(lrTasks)
).show()
};
// projectLR :: String -> Either String OFProject
const projectLR = strName => {
const xs = filter(x => strName === x.name)(
flattenedProjects
);
return 0 === xs.length ? (
Left(`No projects named '${strName}' in the database.`)
) : Right(xs[0])
};
// FUNCTIONS --
// https://github.com/RobTrew/prelude-jxa
// 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
});
// 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
});
// and :: [Bool] -> Bool
const and = xs =>
// True unless any value in xs is false.
[...xs].every(Boolean);
// append (++) :: [a] -> [a] -> [a]
const append = xs =>
// A list defined 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]
);
};
// concat :: [[a]] -> [a]
// concat :: [String] -> String
const concat = xs => (
ys => 0 < ys.length ? (
ys.every(Array.isArray) ? (
[]
) : ''
).concat(...ys) : ys
)(list(xs));
// 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 -> [Char]
const dropWhileEnd = p =>
// xs without the longest suffix for which
// p returns true for all elements.
xs => {
let i = xs.length;
while (i-- && p(xs[i])) {}
return xs.slice(0, i + 1);
};
// 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;
// elem :: Eq a => a -> [a] -> Bool
const elem = x =>
// True if xs contains an instance of x.
xs => {
const t = xs.constructor.name;
return 'Array' !== t ? (
xs['Set' !== t ? 'includes' : 'has'](x)
) : xs.some(eq(x));
};
// 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));
};
// 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]));
})();
};
// filter :: (a -> Bool) -> [a] -> [a]
const filter = p =>
// The elements of xs which match
// the predicate p.
xs => [...xs].filter(p);
// 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.
x;
// 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, enabling zip and zipWith
// to choose the shorter argument when one
// is non-finite, like a cycle or repeat.
'GeneratorFunction' !== (
xs.constructor.constructor.name
) ? xs.length : Infinity;
// 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
) : Array.isArray(a) ? (
append
) : 'function' === typeof a ? (
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 = x =>
y => 0 !== x ? (
x
) : y;
// 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;
// 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
);
// 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
);
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = p => f => x => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// OmniFocus OmniJS --------------------------------------------
// removeTag :: Tag Object -> OFItem -> OFItem
const removeTag = oTag => item => {
item.removeTag(oTag)
return item
}
// tagFoundOrCreated :: Tag Name -> Tag Object
const tagFoundOrCreated = strTag =>
tagNamed(strTag) || new Tag(strTag)
return main();
};
return omniJSContext()
}), {
validate: selection => true
});
})();