Automation - Parent and child summary

So I’m very new to omni outliner. But want to use it as the following:

  • create and outline
  • run an automation where it adds all the number of children to the parent line example
    -parent1
    –child1
    –child2

result
parent1 - 2
–child1
–child2

Any example so that or even a tutorial to get started with the automation or even documents with examples would be helpful…

Lots of examples at omni-automation.com.

Also see this thread for simple examples of things that can be done that are immediately useful.

SG

All descendants (grandchildren and beyond) ?

  • Leaf counts ? (3, for Alpha, in the example below :: [Delta, Epsilon Gamma])
  • Descendant node counts ? (4, for Alpha, in the same example)
  • Immediate children only ? (2, for Alpha, below)
- Alpha
    - Beta
        - Delta
        - Epsilon
    - Gamma 

Here is an example which adds three different descendant counts (for the selected row).

(In this version it adds them as column values).

There are a number of different approaches, but I personally use a general-purpose foldTree function for any kind of bottom-to-top counting or summation which produces a single summary value for a whole nested structure like an outline.

It looks like this:

// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f => {
    // A summary value obtained 
    // by a depth-first fold.
    const go = tree => f(tree.root)(
        tree.nest.map(go)
    );
    return go;
};

and it assumes that the tree structure has been wrapped up in a standard format with each parent node connected to a (possibly empty) list of its child nodes. The ‘constructor’ which I use to manufacture these nodes looks like this:

// 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 || []
    });

We can wrap up the sub-tree of an OmniOutliner row in a nest of these nodes with this function:

// pureTreeOO :: OOItem  -> Tree OOItem
const pureTreeOO = item => {
    const go = x =>
        Node(x)(x.hasChildren ? x.children.map(go) : []);
    return go(item);
};

Once the sub-tree of a selected row has been captured in a standard Tree structure of these nested node objects, we can get various kinds of sum by writing things like this:

const
    row = selectedItems[0],
    tree = pureTreeOO(row),
    childCount = tree.nest.length,
    leafCount = 0 < childCount ? (
        foldTree(
            _ => xs => 0 !== xs.length ? (
                sum(xs)
            ) : 1
        )(tree)
    ) : 0,
    descendantCount = 0 < childCount ? (
        foldTree(
            _ => xs => 1 + sum(xs)
        )(tree)
    ) : 0;

and we could either add one or more of these values to the .topic string of the selected row, or add them as separate column values.

To create or find a numeric column with a given name:

// columnFoundOrCreated :: [Column] ->
// Column.Type -> String -> Column
const columnFoundOrCreated = editor => outline =>
    type => name => {
        const
            columns = outline.columns,
            iFound = columns.findIndex(
                col => (type === col.type) && (
                    name === col.title
                )
            );
        return -1 !== iFound ? (
            columns[iFound]
        ) : outline.addColumn(
            type,
            editor.afterColumn(columns[columns.length]),
            col => col.title = name
        );
    };

and to assign a value to a column entry for a given row:

row.setValueForColumn(n, col)

Pulling it all together, to add counts to columns (for selected rows) in this kind of pattern:

We could write something like the source code (for a file with an .omnijs extension), behind the disclosure triangle below:

JS Source
/*{
    "author": "Rob Trew",
    "targets": ["omnioutliner"],
    "type": "action",
    "identifier": "com.robtrew.nodecounts",
    "version": "0.1",
    "description": "Updated columns of child, descendant, and leaf counts",
    "label": "nodeCounts",
    "mediumLabel": "nodeCounts",
    "paletteLabel": "nodeCounts",
}*/
(() => Object.assign(
    new PlugIn.Action(selection => {
        const main = () => {

            const
                outline = selection.outline,
                editor = selection.editor,
                selectedItems = selection.items;

            return 0 < selectedItems.length ? (
                (() => {
                    const
                        row = selectedItems[0],
                        tree = pureTreeOO(row),
                        childCount = tree.nest.length,
                        leafCount = 0 < childCount ? (
                            foldTree(
                                _ => xs => 0 !== xs.length ? (
                                    sum(xs)
                                ) : 1
                            )(tree)
                        ) : 0,
                        descendantCount = foldTree(
                            _ => xs => 1 + sum(xs)
                        )(tree) - 1;

                    console.log(
                        zipWith(columnName => n => {
                            const
                                col = columnFoundOrCreated(
                                    editor
                                )(outline)(
                                    Column.Type.Number
                                )(columnName);
                            return (
                                row.setValueForColumn(n, col),
                                [columnName, n]
                            );
                        })([
                            'Children', 'Leaves', 'Descendants'
                        ])([
                            childCount, leafCount, descendantCount
                        ])
                    );
                })()
            ) : (
                new Alert(
                    'Count of descendants',
                    '\nSelect a row in OmniOutliner, and try again.'
                ).show()
            );
        };

        // ----------- OMNIOUTLINER FUNCTIONS ------------

        // pureTreeOO :: OOItem  -> Tree OOItem
        const pureTreeOO = item => {
            const go = x =>
                Node(x)(x.hasChildren ? x.children.map(go) : []);
            return go(item);
        };

        // columnFoundOrCreated :: [Column] ->
        // Column.Type -> String -> Column
        const columnFoundOrCreated = editor => outline =>
            type => name => {
                const
                    columns = outline.columns,
                    iFound = columns.findIndex(
                        col => (type === col.type) && (
                            name === col.title
                        )
                    );
                return -1 !== iFound ? (
                    columns[iFound]
                ) : outline.addColumn(
                    type,
                    editor.afterColumn(columns[columns.length]),
                    col => col.title = name
                );
            };

        // ------------------- GENERIC -------------------

        // 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 || []
            });


        // foldTree :: (a -> [b] -> b) -> Tree a -> b
        const foldTree = f => {
            // A summary value obtained 
            // by a depth-first fold.
            const go = tree => f(tree.root)(
                tree.nest.map(go)
            );
            return go;
        };

        // length :: [a] -> Int
        const length = xs =>
            // 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
            'GeneratorFunction' !== xs.constructor
            .constructor.name ? (
                xs.length
            ) : Infinity;

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

        // sum :: [Num] -> Num
        const sum = xs =>
            // The numeric sum of all values in xs.
            xs.reduce((a, x) => a + x, 0);


        // take :: Int -> [a] -> [a]
        // take :: Int -> String -> String
        const take = n =>
            // The first n elements of a list,
            // string of characters, or stream.
            xs => 'GeneratorFunction' !== xs
            .constructor.constructor.name ? (
                xs.slice(0, n)
            ) : [].concat.apply([], Array.from({
                length: n
            }, () => {
                const x = xs.next();
                return x.done ? [] : [x.value];
            }));

        // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
        const zipWith = f =>
            // A list constructed by zipping with a
            // custom function, rather than with the
            // default tuple constructor.
            xs => ys => ((xs_, ys_) => {
                const lng = Math.min(length(xs_), length(ys_));
                return take(lng)(xs_).map(
                    (x, i) => f(x)(ys_[i])
                );
            })(xs, ys);

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

and for other routes to such counts through API methods:

  • item.descendants.length
  • item.leaves.length
  • item.children.length

So, again in full:

JS Source B
/*{
    "author": "Rob Trew",
    "targets": ["omnioutliner"],
    "type": "action",
    "identifier": "com.robtrew.nodecountsB",
    "version": "0.1",
    "description": "Updated columns of child, descendant, and leaf counts",
    "label": "nodeCountsB",
    "mediumLabel": "nodeCountsB",
    "paletteLabel": "nodeCountsB",
}*/
(() => Object.assign(
    new PlugIn.Action(selection => {
        const main = () => {

            const
                editor = selection.editor,
                outline = selection.outline, 
                selectedItems = selection.items;

            return 0 < selectedItems.length ? (
                (() => {
                    const
                        row = selectedItems[0],
                        childCount = row.children.length,
                        leafCount = row.leaves.length,
                        descendantCount = row.descendants.length;

                    console.log(
                        zipWith(columnName => n => {
                            const
                                col = columnFoundOrCreated(
                                    editor
                                )(outline)(
                                    Column.Type.Number
                                )(columnName);
                            return (
                                row.setValueForColumn(n, col),
                                [columnName, n]
                            );
                        })([
                            'Children', 'Leaves', 'Descendants'
                        ])([
                            childCount, leafCount, descendantCount
                        ])
                    );
                })()
            ) : (
                new Alert(
                    'Count of descendants',
                    '\nSelect a row in OmniOutliner, and try again.'
                ).show()
            );
        };

        // ----------- OMNIOUTLINER FUNCTIONS ------------

        // columnFoundOrCreated :: [Column] ->
        // Column.Type -> String -> Column
        const columnFoundOrCreated = editor => outline =>
            type => name => {
                const
                    columns = outline.columns,
                    iFound = columns.findIndex(
                        col => (type === col.type) && (
                            name === col.title
                        )
                    );
                return -1 !== iFound ? (
                    columns[iFound]
                ) : outline.addColumn(
                    type,
                    editor.afterColumn(columns[columns.length]),
                    col => col.title = name
                );
            };

        // ------------------- GENERIC -------------------

        // length :: [a] -> Int
        const length = xs =>
            // 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
            'GeneratorFunction' !== xs.constructor
            .constructor.name ? (
                xs.length
            ) : Infinity;

        // take :: Int -> [a] -> [a]
        // take :: Int -> String -> String
        const take = n =>
            // The first n elements of a list,
            // string of characters, or stream.
            xs => 'GeneratorFunction' !== xs
            .constructor.constructor.name ? (
                xs.slice(0, n)
            ) : [].concat.apply([], Array.from({
                length: n
            }, () => {
                const x = xs.next();
                return x.done ? [] : [x.value];
            }));

        // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
        const zipWith = f =>
            // A list constructed by zipping with a
            // custom function, rather than with the
            // default tuple constructor.
            xs => ys => ((xs_, ys_) => {
                const lng = Math.min(length(xs_), length(ys_));
                return take(lng)(xs_).map(
                    (x, i) => f(x)(ys_[i])
                );
            })(xs, ys);

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

Installing an .omnijs action:

  • Save the source code in a file with the extension .omnijs
  • in the Omni app, open Automation > Configure...
  • drag the .omnijs file onto the window which appears.

Using the action:

Two options:

  1. The action, once installed, appears by name in the apps Automation menu
  2. You can optionally Ctrl-click the (app or Console window) toolbar and then
    • choose Customize Toolbar,
    • visually hunt for a green icon with the name of the action,
    • drag it onto the toolbar, where it can be clicked whenever you need it.