Checkboxes

@jessegrosjean I don’t know if this belongs under features requests or deserves its own topic, but I thought you might check out the implementation of checkboxes in Tot … something they are calling “smart bullets” and the appear to work in plain text as well.

As an avid user of Taskpaper, Tot and now Bike I thought it might be informative.

Those are unicode characters, so the approach is essentially that same as in this script:

Script :: Toggling checked ⇄ unchecked box in selected rows - Bike - Hog Bay Software Support

(in which, if you wanted, you could replace the "☐☑ " used by default with one of the unicode character pairs adopted by Tot, for example "○● " or "□■ ")


The main drawback of special unicode characters is, of course, that in other plain text contexts they may be hard for people to find and type.

(Even inside Tot there appears to be no way of toggling the check state of several selected lines together)

I think that’s probably why some users express a preference for the Markdown convention of more easily typed prefix strings like [ ] and [x], which you can toggle (in one or more rows) with this script:

BIKE Outliner – Toggle a Markdown checkbox in selected lines - Macro Library - Keyboard Maestro Discourse

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Cycle a set of prefixes (single or multi character)
    // in selected (non-empty) lines of Bike 1.3

    // Rob Trew @2022
    // Ver 0.02

    // --------------------- OPTION ----------------------
    // Which prefixes to cycle ?
    // (An empty string is interpreted as a
    //  no-prefix stage in the cycle).
    const prefixCycle = ["[ ]", "[x]", ""];

    // ----------------- PREFIX TOGGLED ------------------
    // main :: IO ()
    const main = () => {
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return doc.exists() ? (() => {
            const
                selectedNonEmptyRows = doc.rows.where({
                    selected: true,
                    _not: [{
                        name: ""
                    }]
                });

            return Boolean(selectedNonEmptyRows.length) ? (
                bikeRowsPrefixCycled([...prefixCycle])(
                    selectedNonEmptyRows
                )
            ) : "No non-empty rows selected in Bike.";
        })() : "No documents open in Bike.";
    };

    // ------------------ PREFIX CYCLED ------------------

    // bikeRowsPrefixCycled :: [String] ->
    // IO Bike Rows -> IO String
    const bikeRowsPrefixCycled = prefixes =>
        // Any selected non-empty rows in Bike cycled
        // to the the next prefix in prefixes
        selectedNonEmptyRows => {
            const
                n = selectedNonEmptyRows.length,
                plural = 1 < n ? "s" : "",
                [f, change] = (() => {
                    const
                        iPrefix = prefixes
                        .findIndex(
                            k => selectedNonEmptyRows
                            .at(0).name()
                            .startsWith(k)
                        ),
                        nextPrefix = prefixes[
                            -1 !== iPrefix ? (
                                (1 + iPrefix) % prefixes
                                .length
                            ) : 0
                        ];

                    return [
                        updatedLine(prefixes)(nextPrefix),
                        "" !== nextPrefix ? (
                            `Set ${nextPrefix}`
                        ) : "Cleared"
                    ];
                })();

            return (
                zipWith(row => s => row.name = s)(
                    selectedNonEmptyRows()
                )(
                    selectedNonEmptyRows.name().map(f)
                ),
                [
                    `${change} prefix`,
                    `in ${n} selected line${plural}.`
                ]
                .join("\n")
            );
        };


    // updatedLine :: [String] -> String -> String -> String
    const updatedLine = prefixes =>
        // A line in which the prefix has been
        // replaced by the next option in a cycle.
        // An empty string in the prefixes list is
        // interpreted as no-prefix stage in the cycle.
        pfx => s => {
            const pre = Boolean(pfx) ? `${pfx} ` : "";

            return Boolean(s) ? (() => {
                const
                    iExisting = prefixes.findIndex(
                        k => Boolean(k) && s.startsWith(k)
                    );

                return -1 !== iExisting ? (
                    pre + s.slice(
                        1 + prefixes[iExisting].length
                    )
                ) : (pre + s.trim());
            })() : pre;
        };


    // --------------------- GENERIC ---------------------

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );

    // MAIN
    return main();
})();

(Both scripts can be found in the Bike Extensions Wiki)

3 Likes

This is great. Thank you.

1 Like