This is a question which I was asked by a user on the Keyboard Maestro forum, but perhaps this is a helpful place to look at it.
Imagine we have an outline which:
- represents a project that we are working on,
- and contains a top level child with a name like
Todos
How could we add (and later update) a numeric affix to the project name, showing the number of items in the Todos
subfolder ?
Expand disclosure triangle to view dummy project outline
Project (10)
Status
Todos
Alpha
Beta
one
two
asdf
sdf
fds
four
Zeta
Delta
Done
Background
Doing this in the JavaScript (rather than AppleScript) flavour of Apple’s osascript
scripting interface, (i.e. with the Script Editor.app
language selector at top-left set to JavaScript
for testing purposes),
first we need a reference to the top level row of the project whose name will be decorated with a todo count.
The simplest case might just be to select the project, before running the script.
// Target project – top level row – for example a selected line.
const
firstSelectedRow = doc.rows.where({
"selected": true
}).at(0);
but maybe nothing is selected ? We can test for that.
firstSelectedRow.exists()
will return either true
or false
.
If it does exist, then let’s call it row
.
Now, does it actually contain a child row called “Todos” (or whatever name we are giving to our container of items to work through) ?
If the childName
that interests us is “Todos”, we can see if a reference to such a child actually exists for the row
we are looking at.
First we define a reference to any such child of the project:
const child = row.rows.byName(childName);
and then we test whether the value of its existence is true
or false
child.exists() ?
If it does exist, we can directly obtain the number of items it contains:
child.rows.length
Now, are we adding a numeric affix to the grandparent project name ?
Or are we updating an existing numeric affix ?
Let’s just plan to take the stem before any existing (number)
affix, and then append a fresh affix to that pruned stem.
The text of a row can be obtained like this:
const text = row.name();
and we can apply a JavaScript regular expression (regex representation of the numeric affix pattern) to the string, and see whether we get the special value:
-
null
(no match, aBoolean
false value) or, - a list (JS Array, a
Boolean
true value) containing a match.
If there’s no match, we take the whole string,
if there’s a match, we take a slice from the start, ignoring the last n
characters, where n
is the length of the matching affix.
// withOutNumericSuffix :: Bike Row -> String
const withOutNumericSuffix = row => {
const
text = row.name(),
match = (/\(\d+\)\s*$/u).exec(text);
return Boolean(match) ? (
text.slice(0, -match[0].length).trimEnd()
) : text;
};
When we want to add a fresh numeric affix, we can define it like this (which includes discarding any existing affix):
// withNumericSuffix :: Int -> Bike Row -> String
const withNumericSuffix = n =>
row => `${withOutNumericSuffix(row)} (${n})`;
and we can combine these elements into a draft script, testable in Script Editor.app
which assumes:
- that we have a Bike document open
- and the the cursor is selecting a project row, which
- contains a “Todos” row,
- which in turn contains zero or more items
The full draft script (make sure you scroll down to copy all of it), might look like this:
Expand disclosure triangle to view JS Source
(() => {
"use strict";
// Rob Trew @2022
// MAIN :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists() ? (() => {
// Target project – for example the first selected line.
const
firstSelectedRow = doc.rows.where({
"selected": true
}).at(0);
return firstSelectedRow.exists() ? (
either(
alert("Affix todo count to project")
)(
projectString => projectString
)(
suffixedWithCountOfGrandChildrenLR(
"Todos"
)(firstSelectedRow)
)
) : `Nothing selected in ${doc.name()}`;
})() : "No documents open in Bike";
};
// ---------- SUFFIXED WITH COUNT OF TODOS -----------
// suffixedWithCountOfGrandChildrenLR :: String ->
// Bike Row -> Either IO String IO String
const suffixedWithCountOfGrandChildrenLR = childName =>
row => {
const child = row.rows.byName(childName);
return child.exists() ? (() => {
const
suffixedName = withNumericSuffix(
child.rows.length
)(row);
return (
row.name = suffixedName,
Right(suffixedName)
);
})() : Left(
[
`Child not found under row: '${row.name()}'`,
`\t'${childName}'`
]
.join("\n\n")
);
};
// withNumericSuffix :: Int -> Bike Row -> String
const withNumericSuffix = n =>
row => `${withOutNumericSuffix(row)} (${n})`;
// withOutNumericSuffix :: Bike Row -> String
const withOutNumericSuffix = row => {
const
text = row.name(),
match = (/\(\d+\)\s*$/u).exec(text);
return Boolean(match) ? (
text.slice(0, -match[0].length).trimEnd()
) : text;
};
// ----------------------- 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 => e.Left ? (
fl(e.Left)
) : fr(e.Right);
// MAIN ---
return main();
})();