BIKE Outliner – Open all links in selected lines

A macro for Jesse Grosjean's Bike Outliner.

Bike's GUI allows us to "look before we leap", and check a URL visuallly before we follow a link.

This is macro is for moments when we want to open a set of links quickly, are confident that we know where they all point and, are happy to leap before we look.


BIKE – Open all links in selected rows.kmmacros (28 KB)


Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    ObjC.import("AppKit");

    // Directly open (leap without look)
    // any links in the selected Bike rows.

    // main : IO ()
    const main = () => {
        const doc = Application("Bike").documents.at(0);

        return doc.exists() ? (() => {
            const
                message = alert(
                    "Open links in selected rows"
                );

            return either(message)(
                xs => 0 < xs.length ? (() => {
                    const
                        links = nub(xs.map(
                            x => x.attributes.href
                        ));

                    return Object.assign(
                        Application.currentApplication(), {
                            includeStandardAdditions: true
                        }
                    )
                    .doShellScript(
                        links.map(x => `open "${x}"`)
                        .join("\n")
                    ),
                    links.join("\n");
                })() : message(
                    "No links found in selected rows."
                )
            )(
                fmapLR(
                    filterTree(
                        x => Boolean(x.attributes.href)
                    )
                )(
                    dictFromHTML(
                        doc.export({
                            from: doc.rows.where({
                                selected: true
                            }),
                            as: "bike format",
                            all: false
                        })
                    )
                )
            );
        })() : "No documents open in Bike";
    };


    // ----------------------- XML -----------------------

    // dictFromHTML :: String -> Either String Tree Dict
    const dictFromHTML = html => {
        const
            error = $(),
            node = $.NSXMLDocument.alloc
            .initWithXMLStringOptionsError(
                html, 0, error
            );

        return Boolean(error.code) ? (
            Left("Not parseable as XML: " + (
                `${html}`
            ))
        ) : Right(xmlNodeDict(node));
    };

    // xmlNodeDict :: NSXMLNode -> Node Dict
    const xmlNodeDict = xmlNode => {
        const
            unWrap = ObjC.unwrap,
            blnChiln = 0 < parseInt(
                xmlNode.childCount, 10
            );

        return Node({
            name: unWrap(xmlNode.name),
            content: blnChiln ? (
                undefined
            ) : (unWrap(xmlNode.stringValue) || " "),
            attributes: (() => {
                const attrs = unWrap(xmlNode.attributes);

                return Array.isArray(attrs) ? (
                    attrs.reduce(
                        (a, x) => Object.assign(a, {
                            [unWrap(x.name)]: unWrap(
                                x.stringValue
                            )
                        }),
                        {}
                    )
                ) : {};
            })()
        })(
            blnChiln ? (
                unWrap(xmlNode.children)
                .reduce(
                    (a, x) => a.concat(xmlNodeDict(x)),
                    []
                )
            ) : []
        );
    };

    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: 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 || []
        });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // 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 => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // filterTree (a -> Bool) -> Tree a -> [a]
    const filterTree = p =>
    // List of all values in the tree
    // which match the predicate p.
        foldTree(x => xs =>
            (
                p(x) ? [
                    [x], ...xs
                ] : xs
            ).flat(1)
        );


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
    // Either f mapped into the contents of any Right
    // value in e, or e unchanged if is a Left value.
        e => "Left" in e ? (
            e
        ) : Right(f(e.Right));


    // 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;
    };

    // nub :: Eq a => [a] -> [a]
    const nub = xs =>
        [...new Set(xs)];


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


Other Keyboard Maestro macros for BIKE Outliner

1 Like

Thanks! This is a very nice addition to the other macros you've created for Bike. I can see myself using this one a lot.

1 Like