A draft script for toggling the task completed (data-done) status of one or more selected rows.
Only non-empty rows are affected. Any selected non-empty rows which are not yet of the task type become tasks and acquire task-completion checkboxes.
Expand disclosure triangle to view JS source
(() => {
"use strict";
// Toggle task-completed for one or more selected Bike rows.
// Affects non-empty rows only.
// Affected rows are set to type "task" where necessary.
// All selected rows are taken to the same status.
// Where their starting status is mixed, the new status
// is the opposite of that of the first selected task.
// Rob Trew @2023
// Ver 2.3
// ---------------------- MAIN -----------------------
// main :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists()
? toggleCompleted(bike)(
doc.rows.where({
selected: true,
_not: [{name: ""}]
})()
)
: "No document open in Bike.";
};
// ---------------------- BIKE -----------------------
// bikeAttribFoundOrCreated :: Bike Application ->
// String -> Bike Row -> Bike Attribute
const bikeAttribFoundOrCreated = bikeApp =>
// A reference to an attribute (of the given name)
// for a particular row.
name => row => {
const maybeAttrib = row.attributes.byName(name);
return maybeAttrib.exists()
? maybeAttrib
: (() => {
const
attrib = new bikeApp.Attribute({
name
});
return (
row.attributes.push(attrib),
attrib
);
})();
};
// taskCleared :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCleared = bike =>
// A row with any `data-done` attribute removed.
row => {
const mb = row.attributes.byName("data-done");
return (
mb.exists() && bike.delete(mb),
row
);
};
// taskCompleted :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCompleted = bike =>
// A row with an ISO8601 time stamp value for
// the `data-done` attribute.
// If the attribute is newly created the
// time-stamp is taken from current system time.
row => {
const
attrib = bikeAttribFoundOrCreated(bike)(
"data-done"
)(row);
return (
attrib.value = attrib.value() || (
new Date()
)
.toISOString(),
row
);
};
// taskTypeChecked :: Bike Row -> IO Bike Row
const taskTypeChecked = row =>
// A task with its type set to "task".
(
("task" !== row.type()) && (row.type = "task"),
row
);
// toggleCompleted :: Application ->
// [Bike Row] -> IO String
const toggleCompleted = bikeApp =>
// A message reporting on the number and new
// (toggled) task-completion status of the
// given list of Bike rows.
rows => {
const n = rows.length;
return 0 < n
? (() => {
const
iTask = rows.findIndex(
x => "task" === x.type()
),
newStatus = !(
-1 !== iTask
? rows[iTask].attributes
.byName("data-done")
.exists()
: false
),
statusName = newStatus
? "completed"
: "reset to incomplete";
const f = newStatus
? x => taskCompleted(bikeApp)(
taskTypeChecked(x)
)
: x => taskCleared(bikeApp)(
taskTypeChecked(x)
);
return (
rows.forEach(f),
`${n} tasks ${statusName}.`
);
})()
: "No rows selected in Bike.";
};
// MAIN ---
return main();
})();
See: Using Scripts - Bike
To test in Script Editor.app set the language selector at top left to JavaScript rather than AppleScript.
You can assign a Bike script to a keyboard shortcut using utilities like Keyboard Maestro and FastScripts.
For a Keyboard Maestro version, see:
1 Like
A variant of the JavaScript source which gives the option of automatically including (in the toggle) all descendants of selected rows:
The value of includeDescendants at the top of the script can be edited to either true or false.
Expand disclosure triangle to view JS source
(() => {
"use strict";
// Toggle task-completed for one or more selected Bike rows.
// Affects non-empty rows only.
// Affected rows are set to type "task" where necessary.
// All selected rows are taken to the same status.
// Where their starting status is mixed, the new status
// is the opposite of that of the first selected task.
// Rob Trew @2023, 2024
// Ver 2.4
// ---------- OPTION (INCLUDE DESCENDANTS) -----------
// Set `includeDescendants` :: false to toggle only the
// done status of directly selected rows.
// If `includeDescendants` :: true, then the done status
// of descendants of selected rows will also be toggled.
const includeDescendants = true;
// ---------------------- MAIN -----------------------
// main :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists()
? toggleCompleted(bike)(
(() => {
const
// Selected non-empty rows.
selectedRows = doc.rows.where({
selected: true,
_not: [{name: ""}]
})();
return includeDescendants
? selectedRows.flatMap(row => [
row,
...row.entireContents()
])
: selectedRows;
})()
)
: "No document open in Bike.";
};
// ---------------------- BIKE -----------------------
// bikeAttribFoundOrCreated :: Bike Application ->
// String -> Bike Row -> Bike Attribute
const bikeAttribFoundOrCreated = bikeApp =>
// A reference to an attribute (of the given name)
// for a particular row.
name => row => {
const maybeAttrib = row.attributes.byName(name);
return maybeAttrib.exists()
? maybeAttrib
: (() => {
const
attrib = new bikeApp.Attribute({
name
});
return (
row.attributes.push(attrib),
attrib
);
})();
};
// taskCleared :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCleared = bike =>
// A row with any `data-done` attribute removed.
row => {
const mb = row.attributes.byName("data-done");
return (
mb.exists() && bike.delete(mb),
row
);
};
// taskCompleted :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCompleted = bike =>
// A row with an ISO8601 time stamp value for
// the `data-done` attribute.
// If the attribute is newly created the
// time-stamp is taken from current system time.
row => {
const
attrib = bikeAttribFoundOrCreated(bike)(
"data-done"
)(row);
return (
attrib.value = attrib.value() || (
new Date()
)
.toISOString(),
row
);
};
// taskTypeChecked :: Bike Row -> IO Bike Row
const taskTypeChecked = row =>
// A task with its type set to "task".
(
("task" !== row.type()) && (row.type = "task"),
row
);
// toggleCompleted :: Application ->
// [Bike Row] -> IO String
const toggleCompleted = bikeApp =>
// A message reporting on the number and new
// (toggled) task-completion status of the
// given list of Bike rows.
rows => {
const n = rows.length;
return 0 < n
? (() => {
const
iTask = rows.findIndex(
x => "task" === x.type()
),
newStatus = !(
-1 !== iTask
? rows[iTask].attributes
.byName("data-done")
.exists()
: false
),
statusName = newStatus
? "completed"
: "reset to incomplete";
const f = newStatus
? x => taskCompleted(bikeApp)(
taskTypeChecked(x)
)
: x => taskCleared(bikeApp)(
taskTypeChecked(x)
);
return (
rows.forEach(f),
`${n} tasks ${statusName}.`
);
})()
: "No rows selected in Bike.";
};
// MAIN ---
return main();
})();
To use in Keyboard Maestro, setting the option value through a Keyboard Maestro variable in KM11:
BIKE Outliner β Toggle Task Completion in selected row(s) - Macro Library - Keyboard Maestro Discourse
1 Like
In the Bike 2 osascript interface (AppleScript or JavaScript for Automation), I think the toggled attribute name needs to be referenced simply as "done" rather than (as in Bike 1) "data-done"
hence a Bike 2 variant of the code (again allowing for toggling all descendants of selections)
Expand disclosure triangle to view JS source for Keyboard Maestro
return (() => {
"use strict";
// Toggle task-completed for one or more selected Bike rows.
// Affects non-empty rows only.
// Affected rows are set to type "task" where necessary.
// All selected rows are taken to the same status.
// Where their starting status is mixed, the new status
// is the opposite of that of the first selected task.
// Rob Trew @2023, 2024, 2025)
// Ver 2.5
// ---------- OPTION (INCLUDE DESCENDANTS) -----------
// Set `includeDescendants` :: false to toggle only the
// done status of directly selected rows.
// If `includeDescendants` :: true, then the done status
// of descendants of selected rows will also be toggled.
const includeDescendants = JSON.parse(
kmvar.local_IncludeDescendants
);
// ---------------------- MAIN -----------------------
// main :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists()
? toggleCompleted(bike)(
(() => {
const
// Selected non-empty rows.
selectedRows = doc.rows.where({
selected: true,
_not: [{ name: "" }]
})();
return includeDescendants
? selectedRows.flatMap(row => [
row,
...row.entireContents()
])
: selectedRows;
})()
)
: "No document open in Bike.";
};
// ---------------------- BIKE -----------------------
// bikeAttribFoundOrCreated :: Bike Application ->
// String -> Bike Row -> Bike Attribute
const bikeAttribFoundOrCreated = bikeApp =>
// A reference to an attribute (of the given name)
// for a particular row.
name => row => {
const maybeAttrib = row.attributes.byName(name);
return maybeAttrib.exists()
? maybeAttrib
: (() => {
const
attrib = new bikeApp.Attribute({
name
});
return (
row.attributes.push(attrib),
attrib
);
})();
};
// taskCleared :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCleared = bike =>
// A row with any `done` attribute removed.
row => {
const mb = row.attributes.byName("done");
return (
mb.exists() && bike.delete(mb),
row
);
};
// taskCompleted :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCompleted = bike =>
// A row with an ISO8601 time stamp value for
// the `done` attribute.
// If the attribute is newly created the
// time-stamp is taken from current system time.
row => {
const
attrib = bikeAttribFoundOrCreated(bike)(
"done"
)(row);
return (
attrib.value = attrib.value() || (
new Date()
)
.toISOString(),
row
);
};
// taskTypeChecked :: Bike Row -> IO Bike Row
const taskTypeChecked = row =>
// A task with its type set to "task".
(
("task" !== row.type()) && (row.type = "task"),
row
);
// toggleCompleted :: Application ->
// [Bike Row] -> IO String
const toggleCompleted = bikeApp =>
// A message reporting on the number and new
// (toggled) task-completion status of the
// given list of Bike rows.
rows => {
const n = rows.length;
return 0 < n
? (() => {
const
iTask = rows.findIndex(
x => "task" === x.type()
),
newStatus = !(
-1 !== iTask
? rows[iTask].attributes
.byName("done")
.exists()
: false
),
statusName = newStatus
? "completed"
: "reset to incomplete";
const f = newStatus
? x => taskCompleted(bikeApp)(
taskTypeChecked(x)
)
: x => taskCleared(bikeApp)(
taskTypeChecked(x)
);
return (
rows.forEach(f),
`${n} tasks ${statusName}.`
);
})()
: "No rows selected in Bike.";
};
// MAIN ---
return main();
})();
and the Keyboard Maestro macro which uses it:
BIKE 2 β Toggle Task Completion in selected row(s).kmmacros.zip (3.1 KB)
Or a stand-alone Bike 2 version for binding to keystrokes in some other way:
Expand disclosure triangle to view stand-alone JS source
(() => {
"use strict";
// Toggle task-completed for one or more selected Bike rows.
// Affects non-empty rows only.
// Affected rows are set to type "task" where necessary.
// All selected rows are taken to the same status.
// Where their starting status is mixed, the new status
// is the opposite of that of the first selected task.
// Rob Trew @2023, 2024, 2025, 2026)
// Ver 2.5
// ---------- OPTION (INCLUDE DESCENDANTS) -----------
// Set `includeDescendants` :: false to toggle only the
// done status of directly selected rows.
// If `includeDescendants` :: true, then the done status
// of descendants of selected rows will also be toggled.
const includeDescendants = false;
// ---------------------- MAIN -----------------------
// main :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists()
? toggleCompleted(bike)(
(() => {
const
// Selected non-empty rows.
selectedRows = doc.rows.where({
selected: true,
_not: [{ name: "" }]
})();
return includeDescendants
? selectedRows.flatMap(row => [
row,
...row.entireContents()
])
: selectedRows;
})()
)
: "No document open in Bike.";
};
// ---------------------- BIKE -----------------------
// bikeAttribFoundOrCreated :: Bike Application ->
// String -> Bike Row -> Bike Attribute
const bikeAttribFoundOrCreated = bikeApp =>
// A reference to an attribute (of the given name)
// for a particular row.
name => row => {
const maybeAttrib = row.attributes.byName(name);
return maybeAttrib.exists()
? maybeAttrib
: (() => {
const
attrib = new bikeApp.Attribute({
name
});
return (
row.attributes.push(attrib),
attrib
);
})();
};
// taskCleared :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCleared = bike =>
// A row with any `done` attribute removed.
row => {
const mb = row.attributes.byName("done");
return (
mb.exists() && bike.delete(mb),
row
);
};
// taskCompleted :: Bike Application ->
// Bike Row -> IO Bike Row
const taskCompleted = bike =>
// A row with an ISO8601 time stamp value for
// the `done` attribute.
// If the attribute is newly created the
// time-stamp is taken from current system time.
row => {
const
attrib = bikeAttribFoundOrCreated(bike)(
"done"
)(row);
return (
attrib.value = attrib.value() || (
new Date()
)
.toISOString(),
row
);
};
// taskTypeChecked :: Bike Row -> IO Bike Row
const taskTypeChecked = row =>
// A task with its type set to "task".
(
("task" !== row.type()) && (row.type = "task"),
row
);
// toggleCompleted :: Application ->
// [Bike Row] -> IO String
const toggleCompleted = bikeApp =>
// A message reporting on the number and new
// (toggled) task-completion status of the
// given list of Bike rows.
rows => {
const n = rows.length;
return 0 < n
? (() => {
const
iTask = rows.findIndex(
x => "task" === x.type()
),
newStatus = !(
-1 !== iTask
? rows[iTask].attributes
.byName("done")
.exists()
: false
),
statusName = newStatus
? "completed"
: "reset to incomplete";
const f = newStatus
? x => taskCompleted(bikeApp)(
taskTypeChecked(x)
)
: x => taskCleared(bikeApp)(
taskTypeChecked(x)
);
return (
rows.forEach(f),
`${n} tasks ${statusName}.`
);
})()
: "No rows selected in Bike.";
};
// MAIN ---
return main();
})();
And for a Bike 2 version which:
- still runs from JavaScript for Automation, making it accessible to convenient option setting and key binding from Keyboard Maestro
- offers the option of toggling all descendants of selected lines (as well as the selected lines themselves)
- doesnβt change row type, or require the
task type (to show strikethrough with the standard stylesheet)
- uses the new Bike Extensions API (from JXA, through the
.evaluate method)
- and reports how many rows were toggled, and in which direction.
we could write something like:
(() => {
"use strict";
// Toggling done status of selected rows
// and, optionally, their descendants
// in Bike 2 Preview
// using the Extensions interface from osascript
// through the JavaScript for Automation
// `Application("Bike").evaluate` method.
// Rob Trew @2026
const kmvar = { local_IncludeDescendants: true };
const main = () => {
const toggleDescendants = kmvar.local_IncludeDescendants;
return Application("Bike").evaluate({
script: `${bikeContext}`,
input: `${toggleDescendants}`
});
}
const bikeContext = toggleDescendants => {
const
editor = bike.frontmostOutlineEditor,
rows = editor?.selection?.rows,
boolDescendants = JSON.parse(toggleDescendants);
const doneToggle = withDescendants =>
xs => {
const
nextDoneDate = rows[0].attributes['done']
? null
: new Date(),
toggle = row => (
row[`${nextDoneDate ? "set" : "remove"}Attribute`](
"done", nextDoneDate
)
),
rowCount = (a, x) => (
toggle(x),
withDescendants
? x.descendants.reduce(
(m, d) => (
toggle(d),
1 + m
),
1 + a
)
: (1 + a)
),
nRows = editor.transaction(
{
label: nextDoneDate ? 'Mark Done' : 'Mark Undone',
animate: 'default',
},
() => xs.reduce(rowCount, 0)
);
return `Toggled ${nRows} row(s) to ${nextDoneDate ? "DONE" : "NOT DONE"}`;
};
return undefined !== rows && (0 < rows.length)
? doneToggle(boolDescendants)(rows)
: "Nothing selected in Bike 2";
};
return main();
})();
As a Keyboard Maestro macro, with a variable for setting the option to include descendants:
BIKE 2 β Toggle Task Completion in selected row(s) (Extensions API).kmmacros.zip (2.8 KB)
1 Like