Checkboxes

I was wondering/hoping that you would add a checkbox feature to Bike. One way I use outlines is to create a checklist for my students for their online courses. Having an actual checkbox option (along with maybe a legal numbering theme/option) would make Bike my sole outlining program.

Bike is working in that direction though maybe a while and not sure exactly where it will end up. I expect the next related feature will be inline formatting and ability to format “strikethrough” text.

2 Likes

If and when you do end up implementing a concept of “checked” items, a small feature idea here that I never realised I needed until I used outliners that offered it: “Pruning” checked items in either the whole document, or a specific row (and its children).

I use outlines extensively for managing projects and like to hold on to checked items for a while, but on a regular basis delete all checked items nested under a specific row to spring clean.

Until we get actual checkboxes, I am making do with a text expansion shortcut that inserts the :heavy_check_mark:︎ unicode symbol.

1 Like

Thanks for the tip. That would work for me, but I doubt most of my students would find it easy enough to do. Plus, I generally convert the outline to a PDF that they can actually print and check-off each box (I know, very old-school analog).

Should be possible, I think, to write a script to toggle ☐☑ characters at the start of selected lines.

(I’ll take a look at the weekend)

In the meanwhile, if you just need a prefix for printing, you could toggle it on and off in selected lines by using an existing prefix-toggling script:

Screenshot 2022-06-24 at 07.42.56

Here as a Keyboard Maestro macro:

BIKE - Toggle a box character prefix in selected lines.kmmacros.zip (2.4 KB)

and here as a JS script to attach to a keystroke in some other way:

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

    // Toggle a given prefix character in selected lines
    // of Bike 1.2

    // Rob Trew @2022
    // Ver 0.01

    // --------------------- OPTION ----------------------
    // Which single-character prefix to add or clear ?
    const
        prefixChar = "☐";
        // Application("Keyboard Maestro Engine")
        // .getvariable("prefixChar");

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

        return doc.exists() ? (() => {
            const
                selectedRows = doc.rows.where({
                    selected: true
                }),
                n = selectedRows.length;

            return Boolean(n) ? (() => {
                const [f, change] = (
                    selectedRows.at(0).name()
                    .startsWith(prefixChar)
                ) ? (
                    [dePrefixed(prefixChar), "CLEARED"]
                ) : [prefixed(prefixChar), "ADDED"];

                return (
                    zipWith(row => s => row.name = s)(
                        selectedRows()
                    )(
                        selectedRows.name().map(f)
                    ),
                    [
                        `${change} '${prefixChar}' prefix`,
                        `in ${n} selected lines.`
                    ]
                    .join("\n")
                );
            })() : "No rows selected in Bike";
        })() : "No documents open in Bike";
    };

    // -------------------- PREFIXES ---------------------

    // prefixed :: Char -> String -> String
    const prefixed = c =>
        s => `${c} ${dePrefixed(c)(s)}`;


    // dePrefixed :: Char -> String -> String
    const dePrefixed = c =>
        s => c === s[0] ? (
            s.slice(" " === s[1] ? 2 : 1)
        ) : s;

    // --------------------- 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();
})();

See: Using Scripts - Bike

2 Likes

See:

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

It might make the most sense to support Markdown checkboxes, like:

[ ] unfinished task
[x] finished task

OPML doesn’t support “checklists,” though OmniOutliner, for instance, uses the attribute _status="checked" for checked items.

A Markdown checkbox toggling script:

ScreenFlow


To test in Script Editor.app (with language selector at top left set to JavaScript)

See: Using Scripts - Bike

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.01

    // --------------------- 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.
        // A space character 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();
})();

Or to use with something like FastScripts or Keyboard Maestro:

BIKE – Toggle a Markdown checkbox in selected lines.kmmacros.zip (2.8 KB)


4 Likes

Nice, thanks for the continuing collection of Keyboard Maestro scripts! Really helpful.

2 Likes