Cycling Bike flags with traffic-light colors

:vertical_traffic_light: :woman_biking:

I keep an overview file of ongoing projects, and each day flag three things to work on.

At start of day:

  • one green flag for immediate work,
  • one orange for whatever comes next
  • one red for the third in line.

Once I’ve worked for a while, made some progress, and need a change, I cycle the traffic-light colors, and switch work to whatever is now green.

RED → ORANGE
ORANGE → GREEN
GREEN → RED

Most days, about 2-3 cycles.

(The current configuration of flag colors for three items also shows up on my Desktop as a TerminalWidget panel – see Links to flagged lines in a Hogbay Software Bike 2.0 outline)

Not an extension – perhaps too small for that – just a first sketch as a JavaScript for Automation script which sends extension code to the Bike JS Context.

I personally bind it, in Keyboard Maestro, to ⌘R – in my home town traffic-lights were called robots.

( and the code can be adjusted, in the options at the top, to cycle the values of other Bike 2 attributes)

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

    // Cycle values of a Bike 2 Preview attribute

    // --------------------- OPTIONS ---------------------
    const options = {
        attribName: "flagged",
        valueSequence: ["red", "orange", "green"]
    };

    // Rob Trew @2026
    // Ver 0.1

    // -------------------- JXA MAIN ---------------------
    const main = () =>
        either(
            alert("JSON Parse")
        )(
            either(
                alert("JS Context :: Cycled attribute value")
            )(
                xs => xs
                    .map(pair => pair.join(": "))
                    .join("\n")
            )
        )(
            jsonParseLR(
                Application("Bike").evaluate({
                    script,
                    input: JSON.stringify(options)
                })
            )
        );


    // ----------------- BIKE JS CONTEXT -----------------
    const script = (inputString => {

        const bikeMain = () =>
            bindLR(
                jsonParseLR(inputString)
            )(
                options => {
                    const
                        { attribName, valueSequence } = options,
                        outline = bike.frontmostOutlineEditor.outline,
                        matchingRows = valueSequence.flatMap(
                            k => outline.query(`//@${attribName}=${k}`).value
                        );

                    return 0 < matchingRows.length
                        ? traverseListLR(
                            row => fmapLR(
                                newValue => (
                                    row.setAttribute(attribName, newValue),
                                    [newValue, row.text.string]
                                )
                            )(
                                nextValueLR(valueSequence)(
                                    row.getAttribute(attribName)
                                )
                            )
                        )(
                            matchingRows
                        )
                        : Left(
                            [
                                "No matching attribute values found for:",
                                JSON.stringify(options, null, 2)
                            ]
                                .join("\n")
                        )
                }
            );

        // ----------------- NEXT VALUE ------------------

        // nextValueLR :: [String] -> String -> Either String String
        const nextValueLR = ks =>
            k => {
                const n = ks.length;

                return fmapLR(
                    i => -1 === i
                        ? ks[0]
                        : ks[(1 + i) % n]
                )(
                    0 < n
                        ? Right(ks.indexOf(k))
                        : Left("Empty value list in nextValueLR(valueSequence)(value)")
                );
            };


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


        // bindLR (>>=) :: Either a ->
        // (a -> Either b) -> Either b
        const bindLR = lr =>
            // Bind operator for the Either option type.
            // If lr has a Left value then lr unchanged,
            // otherwise the function mf applied to the
            // Right value in lr.
            mf => "Left" in lr
                ? lr
                : mf(lr.Right);


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


        // jsonParseLR :: String -> Either String a
        const jsonParseLR = s => {
            try {
                return Right(JSON.parse(s));
            } catch (e) {
                return Left(
                    [
                        e.message,
                        `(line:${e.line} col:${e.column})`
                    ].join("\n")
                );
            }
        };


        // 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 any flr function 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([]);
            };


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


        return JSON.stringify(bikeMain());
    })
        .toString()



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


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



    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                [
                    e.message,
                    `(line:${e.line} col:${e.column})`
                ].join("\n")
            );
        }
    };

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

To test in Script Editor,

  1. Use the copy icon at top right of the code panel above to make sure that you have copied all of the code, and
  2. set the language selector at top left to JavaScript, rather than AppleScript.

Using Scripts | Bike 2 (Preview) | Bike


Keyboard Maestro version:
Cycle any traffic-light flags in front Bike Document.kmmacros.zip (4.3 KB)

1 Like