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: