Two Questions: Quick Look / Paste as Bulleted List

Hello! Hoping you all can help me with these two items:

  1. Is there any way to have MacOS display the contents of a Bike document in Quick Look? I rely on Quick Look a lot but it doesn’t seem to work with Bike files at the moment.
  2. Is there a way to copy the contents of my Bike document and paste it into another app as a bulleted list?

Thanks!

You will find some earlier discussion of progress towards QuickLook support here:

Quicklook support

To copy a Bike document (or just selected rows, if the selection is extended) as a bulleted plain text (tab-indented) outline, there is a Keyboard Maestro macro here:

BIKE Outliner - Copy as Bulleted Plain Text - Keyboard Maestro Discourse

and if you don’t use Keyboard Maestro, you can also use something like FastScripts to bind a keystroke to the Copy as Bulleted Plain Text script below:

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

    ObjC.import("AppKit");

    // Copy Bike document
    // (or Bike selected rows, if selected is extended)
    // as bulleted plain text.

    // RobTrew @2022
    // Ver 0.01

    // main :: IO ()
    const main = () => {
        const doc = Application("Bike").documents.at(0);

        return doc.exists() ? (
            copyText(
                bulletedTextFromBikeDoc(doc)
            )
        ) : "No document open in Bike";
    };

    // ---------------------- BIKE -----------------------

    // bulletedTextFromBikeDoc :: Bike Doc -> IO String
    const bulletedTextFromBikeDoc = doc => {
        const
            rows = Boolean(doc.selectedText()) ? (
                doc.rows.where({selected: true})
            ) : doc.rows;

        return bulletOutlineFromForest(
            forestFromIndentedLines(
                zip(
                    rows.level()
                )(
                    rows.name()
                )
            )
        );
    };

    // ----------------------- JXA -----------------------

    // copyText :: String -> IO String
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;

        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

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

    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
    // Constructor for a Tree node which connects a
    // value of some kind to a list of zero or
    // more child trees.
        xs => ({
            type: "Node",
            root: v,
            nest: xs || []
        });


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
    // A pair of values, possibly of
    // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // bulletOutlineFromForest :: Forest String -> String
    const bulletOutlineFromForest = trees => {
        const go = tabs => tree => {
            const txt = tree.root.text;

            return [
                Boolean(txt) ? (
                    `${tabs}- ${txt}`
                ) : `${tabs}`,
                ...tree.nest.flatMap(go(`\t${tabs}`))
            ];
        };

        return trees.flatMap(go("")).join("\n");
    };


    // forestFromIndentedLines :: [(Int, String)] ->
    // [Tree {text:String, body:Int}]
    const forestFromIndentedLines = tuples => {
        const go = xs =>
            0 < xs.length ? (() => {
            // First line and its sub-tree,
                const [depth, body] = xs[0],
                    [tree, rest] = span(x => depth < x[0])(
                        xs.slice(1)
                    );

                return [
                    Node({
                        text: body,
                        level: depth
                    })(go(tree))
                ]
                // followed by the rest.
                    .concat(go(rest));
            })() : [];

        return go(tuples);
    };


    // span :: (a -> Bool) -> [a] -> ([a], [a])
    const span = p =>
    // Longest prefix of xs consisting of elements which
    // all satisfy p, tupled with the remainder of xs.
        xs => {
            const i = xs.findIndex(x => !p(x));

            return -1 !== i ? (
                Tuple(xs.slice(0, i))(
                    xs.slice(i)
                )
            ) : Tuple(xs)([]);
        };


    // zip :: [a] -> [b] -> [(a, b)]
    const zip = xs =>
    // The paired members of xs and ys, up to
    // the length of the shorter of the two lists.
        ys => Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => [xs[i], ys[i]]);

    return main();
})();

If you test the script in Script Editor.app, (making sure, of course, to copy the whole of it, scrolling right down to the bottom), you will need to set the language selector at top left to JavaScript rather than AppleScript.

See: Using Scripts - Bike

2 Likes

If you use .html file extension for your Bike files than they should show in QuickLook. You can also change the default file extension to html in Bike’s preferences.

Thanks!

Hmm…will I miss out on anything or create issues elsewhere if I change the default extension to html? I assume the files are .bike files instead of .html for some reason…

You shouldn’t miss out on features, the main reason for different file extension is to avoid confusion and for easy file associations…

  1. Avoid confusion. .bike file content is HTML text in a specific configuration. So all Bike files are HTML, but not all HTML files are Bike. Using a separate file extension makes that clear… .bike files are HTML in a specific configuration.

  2. If you double-click on a .bike file then it will open in Bike, while if you double click on HTML file it will open in web browser by default.

Other then those two issues I don’t think there are any drawbacks to using html file extension for Bike files. The actual saved text content should be exactly the same.

1 Like

Thanks for the explanation!

Not a huge issue, but if you can set .html as the default file format for Bike, I think Bike should also allow you to Save As an .html doc. It only seems to offer .bike, .opml, and .txt at the moment.

This is getting to the confusing part… but saving as HTML document is possible.

There are two separate decisions to make when saving a file… what file content to save and what file extension to use. Normally those decisions are linked (file extension determines file format), but not always. For example (just in general here, not Bike specific) when you save a text file you can save with all sorts of file extensions (.txt, .md, ,.swift, etc).

With that background in Bike’s save dialogue you have two decisions:

  1. Chose “File Format” to determine what content is saved into the file
  2. Enter a file name. Normally Bike will add a file extension (based on file format choice) to that name when you save… but you also have option to enter another extension of your choosing. So for example when you save just name the file “todo.html” and it will save Bike file format content into a file with .html extension.
1 Like