Script – Select Next Uncompleted Task

Selects next uncompleted task in the active Bike document:

  • searching first in the outline subsection containing the selection cursor
  • then more broadly, top-down.

To test in Script Editor, set the language selector at top left to JavaScript rather than AppleScript.

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

    // --------- NEXT INCOMPLETE TASK SELECTED, ----------
    // ---- SEARCHING FIRST IN LOCAL SUBTREE CONTEXTS ----

    // Rob Trew @2024
    // Ver 0.1

    const nextTask = "//@type=task and not @done[1]";

    const main = () => {
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return doc.exists()
            ? (() => {
                const
                    rootID = doc.id(),
                    selnID = doc.selectionRow.id(),
                    rows = doc.rows,
                    selnPath = `//@id="${selnID}"/ancestor-or-self::*`;

                return either(
                    ([matchID, matchName]) => (
                        doc.select({ at: rows.byId(matchID) }),
                        matchName
                    )
                )(
                    rowIDs => alert("Next task")(
                        unlines([
                            "All tasks in document completed.\n",
                            "Searched widening context:",
                            ...init(rowIDs).map(
                                k => `\t- ${rows.byId(k).name()}`
                            ),
                            `\t- ${doc.name()}`
                        ])
                    )
                )(
                    traverseListLR(rowID => {
                        const
                            match = doc.query({
                                outlinePath: (
                                    rootID !== rowID
                                        ? `//@id="${rowID}"`
                                        : ""
                                ) + nextTask
                            })[0];

                        return match
                            ? Left([match.id(), match.name()])
                            : Right(rowID)
                    })(
                        doc.query({ outlinePath: selnPath })
                            .map(x => x.id())
                            .reverse()
                    )
                );
            })()
            : "No document open in Bike."
    };

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


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


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


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


    // init :: [a] -> [a]
    const init = xs =>
        // All elements of a list except the last.
        0 < xs.length
            ? xs.slice(0, -1)
            : null;


    // traverseListLR (a -> Either b c) ->
    // [a] -> Either b [c]
    const traverseListLR = flr =>
        // Traverse over [a] with (a -> Either b c)
        // Either Left b or Right [c]
        xs => {
            const n = xs.length;

            return 0 < n
                ? until(
                    ([i, lr]) => (n === i) || ("Left" in lr)
                )(
                    ([i, lr]) => {
                        // Passing an optional index argument
                        // which flr can ignore or use.
                        const lrx = flr(xs[i], i);

                        return [
                            1 + i,
                            "Right" in lrx
                                ? Right(
                                    lr.Right.concat([
                                        lrx.Right
                                    ])
                                )
                                : lrx
                        ];
                    }
                )(
                    Tuple(0)(Right([]))
                )[1]
                : Right([]);
        };


    // unlines :: [String] -> String
    const unlines = xs =>
        // A single string formed by the intercalation
        // of a list of strings with the newline character.
        xs.join("\n");


    // until :: (a -> Bool) -> (a -> a) -> a -> a
    const until = p =>
        // The value resulting from successive applications
        // of f to f(x), starting with a seed value x,
        // and terminating when the result returns true
        // for the predicate p.
        f => x => {
            let v = x;

            while (!p(v)) {
                v = f(v);
            }

            return v;
        };

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

See: Using Scripts | Bike


For a Keyboard Maestro binding to a keystroke:

1 Like