Draft script :: Toggle task-completed status of selected row(s)

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