![]()
I keep an overview file of ongoing projects, and each day flag three things to work on.
At start of day:
- one
greenflag for immediate work, - one
orangefor whatever comes next - one
redfor 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,
- Use the
copyicon at top right of the code panel above to make sure that you have copied all of the code, and - set the language selector at top left to
JavaScript, rather thanAppleScript.
Using Scripts | Bike 2 (Preview) | Bike
Keyboard Maestro version:
Cycle any traffic-light flags in front Bike Document.kmmacros.zip (4.3 KB)