Export Perspective on Mac to readable text


#1

Continuing the discussion from How do I export from OF3?:

My goal is to automate getting a well formatted perspective out of OmniFocus.

WorkFlow

@heyscottyj and @psidnell have proposed workflows here and here. Thank you! These are very helpful.

OSX

@dfay, has a really interesting solution here to export as a PDF.

Could someone help me create an AppleScript or some automation for the Mac that would do the following:

  1. open a named perspective
  2. prepare readable text which is put on the clipboard to be pasted into an email app or document.

Perspective Title

  • task one @tag due
    notes: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
  • task two @tag due
    Reference notes: How do I export from OF3?:

Thanks!


#2

First draft. Change this line to suit your needs:

const strPerspectiveName = 'Checklists'

Code:

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        // USER DATA
        const strPerspectiveName = 'Checklists'
        const
            appOF = Application('OmniFocus'),
            win = appOF.defaultDocument.documentWindows()[0];

        win.perspectiveName = strPerspectiveName
        const trees = win.content.trees();

        const treeNodes = fmapPureTreesOF(x => {
            const item = x.value()
            const isTask = ObjectSpecifier.classOf(item) === 'task'
            const taskTags = isTask ? item.tags().map(x => x.name()) : []
            return ({
                text: isTask ? item.name() : x.name(),
                note: isTask ? item.note() : '',
                tags: {
                    tags: taskTags.length > 0 ? taskTags : '',
                    due: isTask ? isoLocal(item.dueDate()) : ''
                    // flag: isTask ? item.flagged() : ''
                }
            })
        }, trees)

        return taskPaperFromTrees(treeNodes)
    };

    // GENERIC -----------------------------------------------------------------
    // https://github.com/RobTrew/prelude-jxa
    // JS - Apps

    // fmapPureTreesOF :: (OFTree -> a) -> [OFTree] -> [Tree OFItem]
    const fmapPureTreesOF = (f, xs) => {
        const go = x => Node(f(x),
            x.trees.length > 0 ? (
                x.trees().map(go)
            ) : []);
        return concatMap(go, xs);
    };

    // isoLocal :: Date -> String
    const isoLocal = dte =>
        dte instanceof Date ? (() => {
            const xs = ['FullYear', 'Month', 'Date',
                    'Hours', 'Minutes' //, 'Seconds', 'Milliseconds'
                ]
                .map((k, i) => {
                    const s = (dte['get' + k]() + (i === 1 ? 1 : 0))
                        .toString();
                    return (s.length === 1 ? '0' : '') + s;
                });
            return xs.slice(0, 3)
                .join('-') + ((xs[3] !== '00' || xs[4] !== '00') ? (
                    ' ' + xs.slice(3)
                    .join(':')
                ) : '');
        })() : undefined;

    // taskPaperFromTrees :: [Tree] -> String
    const taskPaperFromTrees = xs => {
        const
            rgxSpace = /\s+/g,
            go = (strIndent, ts) =>
            foldl((a, x) => {
                const
                    nest = x.nest,
                    root = x.root,
                    txt = root.text,
                    name = root.name,
                    tags = root.tags,
                    ks = Boolean(tags) ? (
                        Object.keys(tags)
                    ) : [],
                    note = root.note,
                    blnName = Boolean(name),
                    blnNotes = Boolean(note),
                    blnTags = ks.length > 0,
                    blnNest = nest.length > 0,
                    blnData = Boolean(txt) ||
                    (ks.length > 0) || blnNotes ||
                    blnNest || blnName,
                    strNext = '\t' + strIndent;

                return blnData ? (
                    a + strIndent + '- ' + (root.text || '') +
                    (blnTags ? (
                        foldl(
                            (t, k) => {
                                const v = tags[k];
                                return t + (Boolean(v) ? (
                                    ' @' + k.replace(rgxSpace, '_') +
                                    (Boolean(v) && v !== true ? (
                                        '(' + v + ')'
                                    ) : '')
                                ) : '');
                            },
                            '',
                            ks
                        )
                    ) : '') + '\n' +
                    (blnNotes ? (
                        unlines(
                            map(s => strNext + s, lines(note))
                        ) + '\n'
                    ) : '') + (blnNest ? (
                        go(strNext, nest)
                    ) : '')
                ) : a;
            }, '', ts);
        return go('', xs);
    };

    // JS - Prelude

    // Node :: a -> [Tree a] -> Tree a
    const Node = (v, xs) => ({
        type: 'Node',
        root: v, // any type of value (but must be consistent across tree)
        nest: xs || []
    });

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = (f, xs) => [].concat.apply([], xs.map(f));

    // foldl :: (a -> b -> a) -> a -> [b] -> a
    const foldl = (f, a, xs) => xs.reduce(f, a);

    // lines :: String -> [String]
    const lines = s => s.split(/[\r\n]/);

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // JXA MAIN ----------------------------------------------------------------
    return main();
})();

Used some of Rob Trew’s wonderful generics.


#3

@unlocked2412, WOW-- it feels like magic! This is amazing! Thank you for all of your work and effort! This is so much better than I had hoped for!

How do I learn more about this or find other code snippets to better harness javascript for Omnifocus. I’ve used AppleScript for years-- this is the first time I’ve seen this code.

If anyone comes along and wonders how to use this, I recommend the application Keyboard Maestro. Then again, it’s the only way I know how to invoke this script.


#4

You’re very welcome.

Some useful links from Apple:

Mac Automation Scripting Guide

OS X 10.10 Release Notes

OmniFocus scripting dictionary (Javascript) is available through Script Editor. It’s a good place to start familiarizing with OmniFocus methods and properties (Javascript).

You can execute it in Script Editor setting language to Javascript. Additionaly, with Keyboard Maestro (as you found out), Alfred, etc…


Script update: now handles more perspectives and it is a little more polished. It returns a string with a plain text representation of a perspective. It can be used to set the clipboard to this string with Keyboard Maestro (output of an Execute a Javascript for Automation), for example.

33%20copy


#5

I must be doing something wrong. The javascript will not compile on 10.13 here: Error on line 101: SyntaxError: Unexpected token ','


#6

Thanks for letting me know, @TheWart. It’s fixed, now. Does it work for you?


#7

Yes, thanks!


#8

works like a charm, kudos!
1 question though:

Can I use a variable as input for this script to:

  1. ask user for which perspective to output
  2. have the jsa script use that for the "const strPerspectiveName = ‘Checklists’ "

Do you have an idea about this?


#9

Of course, one possible route would be through Keyboard Maestro.

OmniFocus 3 Perspective As Text.kmmacros


#10

Kudos again! :-)


#11

Love possibility here. However, it prompts me as expected but only returns the Inbox items despite naming a stock or custom perspective.

I simply downloaded the macro and placed it in my enabled OmniFocus macro group, running it with the indicated shortcut.


#12

Thanks for letting me know. I think it’s fixed. Does it work for you, now?


#13

It does, thanks! Could it be further tweaked to toggle the task notes on/off when run?


#14

Far from my desk now but will look into it.


#15

Received great ideas from user @draft8 on passing information between KM prompt for user input action and a Execute a JavaScript For Automation action and creating specialised versions of a function to allow options.

Just implemented task notes on/off toggle. Is this what you had in mind, @TheWart ?


#16

Can you share the new link? Or is it still the same?


#17

It’s the same link. I updated the file.


#18

So, so good! Thank you very much for all the tweaking!