Bike 2.0 (Preview 245)

New

  • Support for opening row links
  • Multiple views of the same outline:
    • Window ▸ Duplicate Tab
    • Window ▸ Duplicate Tab to New Window

Improved

  • Drag and drop
    • Better animations
    • Can’t drop on or next to self without modifier key
  • Typewriting scrolling animations

Changed

  • Use space key in block mode to toggle done
  • Dropping a branch on itself now requires holding a modifier key

Fixed

  • Document file extension settings
  • Row handle clicks sometimes not registering
  • Crash when opening a context menu in block-mode rows

API

  • Extended Keybinding active modifiers API
  • Changed URL property names and mutability

Download:

2 Likes
  • Support for opening row links

This is very good – thank you !

What is the best approach to pasting row links in the middle of existing text, rather than as a new line ?


I can do this in two steps:

  • Paste after ⇧⌘C, which defaults, I think, in both block and text-editing mode, to creating a new row from the public.url and public.url-name in the clipboard,
  • then copy again from selected blue link text, and paste the copied OutlineFragment to cursor point in an existing row.

but I may be missing something more direct.

( If not I could perhaps write a script for a direct Copy Row Link(s) to Outline Fragment )

Expand disclosure triangle to view rough sketch
(() => {
    "use strict";

    // Rough sketch of a JXA Copy As Row Link for Bike 2.0
    // which allows for pasting a link inline (in text selection mode)
    // as well as pasting as a new row (in block selection mode)

    // Rob Trew @2025
    // Ver 0.0

    ObjC.import("AppKit");

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

        return either(
            alert("Copy Row Link")
        )(
            compose(
                setClipOfTextType("com.hogbaysoftware.bike.xml"),
                xmlFragment
            )
        )(
            bindLR(
                doc.exists()
                    ? Right(doc.selectionRows() || [])
                    : Left("No document open in Bike")
            )(
                rows => bindLR(
                    0 < rows.length
                        ? Right(
                            rows.flatMap(row => {
                                const name = row.name().trim();

                                return 0 < name.length
                                    ? [Tuple(name)(row.id())]
                                    : [];
                            })
                        )
                        : Left(`Nothing selected in "${doc.name()}"`)
                )(
                    bikeLiXMLFromNameIDPairsLR(doc)
                )
            )
        );
    }

    // bikeXMLFromNameIDPairsLR :: Bike Document -> 
    // [(String, String)] -> Either String XML
    const bikeLiXMLFromNameIDPairsLR = doc =>
        nameIDPairs => {
            const docID = doc.id();

            return 0 < nameIDPairs.length
                ? Right(
                    nameIDPairs.map(
                        ([rowName, rowId]) => (
                            `<li><p><a href="bike://${docID}/${rowId}">${rowName}</a></p></li>`
                        )
                    )
                        .join("\n\t\t")
                )
                : Left(`Only empty rows selected in "${doc.name()}"`);
        };


    const xmlFragment = liXML =>
        `<?xml version="1.0" encoding="UTF-8"?>
<html>
  <head>
    <meta charset="utf-8"/>
  </head>
  <body>
    <ul id="OutlineFragment">
        ${liXML}
    </ul>
  </body>
</html>`

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

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                includeStandardAdditions: true
            });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

    // setClipOfTextType :: String -> String -> IO String
    const setClipOfTextType = utiOrBundleID =>
        txt => {
            const pb = $.NSPasteboard.generalPasteboard;

            return (
                pb.clearContents,
                pb.setStringForType(
                    $(txt),
                    utiOrBundleID
                ),
                txt
            );
        };

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


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


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr
            ? lr
            : mf(lr.Right);


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        // A function defined by the right-to-left
        // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Left" in e
            ? fl(e.Left)
            : fr(e.Right);


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e
            ? e
            : Right(f(e.Right));


    return main()
    // return sj(main());
})();

Good catch, will fix. Answer should be if copied text is not multiline then paste will work like any other pasted text. I just need to add logic for that. I also need to add special case logic to detect when link is pasted, and if you have a range of selected text I will apply the link to that text, as in Bike 1. Will get this fixed!

1 Like