OmniPlan Edit > Copy
(when tasks are selected) places XML and URLs in the clipboard, but not a pasteable text outline.
Here is a very basic Copy as Tab-Indented
version (which copies only task names in this draft, but is easily extended).
Would have liked to write it in omniJS, but couldn’t immediately find:
- An
evaluateJavaScript
method (as in omniGraffle and omniOutliner) for osascript ⇄ omniJS cross-traffic, or - omniJS access to the clipboard from omniPlan (as in the OO omniJS PasteBoard object)
So here, for the moment, and out of reach for iOS, is a JavaScript for Automation draft, for use from Script Editor, Keyboard Maestro etc
Copy whole script, all the way down to the line after return main()
(() => {
'use strict';
// macOS omniplan ::
// Copy selected tasks as tab-indented outline.
// Rob Trew 2019
// Ver 0.02
// 0.02 Allows for selection of sub-trees at or below level 1.
// Descendants of selected nodes are also copied.
ObjC.import('AppKit');
// main :: IO ()
const main = () => {
const
plan = Application('OmniPlan'),
ws = plan.windows;
return either(identity)(
outline => (
copyText(outline),
outline
)
)(bindLR(
0 < ws.length ? (() => {
const ts = ws.at(0).selectedTasks();
return null !== ts && 0 < ts.length ? (
Right([minimum(map(x => x.outlineDepth())(ts)), ts])
) : Left('No tasks selected in OmniPlan');
})() : Left('No window open in OmniPlan.')
)(([level, tasks]) => Right(
tabIndentFromForest(root)(
concatMap(
task => level === task.outlineDepth() ? (
[fmapPureOP(x => x.name())(task)]
) : []
)(tasks)
)
)));
};
// OMNIPLAN -------------------------------------------
// fmapPureOP :: (OPTask -> a) -> OPTask -> Tree OPTask
const fmapPureOP = f => task => {
const go = x => {
const xs = x.childTasks;
return Node(f(x))(
0 < xs.length ? (
xs().map(go)
) : []
);
};
return go(task);
};
// TABBED OUTLINE -------------------------------------
// tabIndentFromForest :: (Tree -> String) ->
// [Tree] -> String
const tabIndentFromForest = fnText => trees => {
const go = indent => tree =>
indent + fnText(tree) +
(tree.nest.length > 0 ? (
'\n' + tree.nest
.map(go('\t' + indent))
.join('\n')
) : '');
return trees.map(go('')).join('\n');
};
// JXA ------------------------------------------------
// copyText :: String -> IO Bool
const copyText = s => {
// String copied to general pasteboard.
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
);
};
// GENERIC --------------------------------------------
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = v => xs => ({
type: 'Node',
root: v, // any type of value (consistent across tree)
nest: xs || []
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = m => mf =>
undefined !== m.Left ? (
m
) : mf(m.Right);
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
x => fs.reduceRight((a, f) => f(a), x);
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = f =>
xs => xs.flatMap(f);
// 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;
// identity :: a -> a
const identity = x => x;
// Returns Infinity over objects without finite length.
// This enables zip and zipWith to choose the shorter
// argument when one is non-finite, like cycle, repeat etc
// length :: [a] -> Int
const length = xs =>
(Array.isArray(xs) || 'string' === typeof xs) ? (
xs.length
) : Infinity;
// map :: (a -> b) -> [a] -> [b]
const map = f => xs =>
(Array.isArray(xs) ? (
xs
) : xs.split('')).map(f);
// minimum :: Ord a => [a] -> a
const minimum = xs =>
0 < xs.length ? (
xs.slice(1)
.reduce((a, x) => x < a ? x : a, xs[0])
) : undefined;
// root :: Tree a -> a
const root = tree => tree.root;
// MAIN ---
return main();
})();