Notes after 3 days of heavy use

In the meanwhile – as a stop gap – I’ve been using a Keyboard Maestro macro (copies text of selected node, extracts first link, adds a document UUID if it’s a bike:// link and doesn’t have one)

Rough, and temporary, but others are welcome to experiment if they happen to use Keyboard Maestro:

Follow first link in Bike item.kmmacros.zip (3.9 KB)

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

    ObjC.import("AppKit");

    // Rough interim script for:
    // extracting first link from the clipboard
    // supplying a document id if its a bike:// link
    // without one, and opening the link through the shell

    // Draft 0.00  Rob Trew @2022

    // main :: IO ()
    const main = () =>
        either(
            alert("Open link from Bike")
        )(
            url => (
                Object.assign(
                    Application.currentApplication(), {
                        includeStandardAdditions: true
                    }
                ).doShellScript(`open ${url}`),
                url
            )
        )(
            bindLR(
                clipTextLR()
            )(txt => bindLR(
                firstLinkFoundLR(txt)
            )(url => url.startsWith(
                "bike://#"
            ) ? (
                bindLR(
                    filePathFromFrontWindowLR()
                )(fp => bindLR(
                    readFileLR(fp)
                )(xml => bindLR(
                    xmlDocFromStringLR(xml)
                )(doc => bindLR(
                    bikeDocIdFromXmlDocLR(doc)
                )(id => Right(
                    `bike://${id}${url.slice(7)}`
                )))))
            ) : Right(url)))
        );

    // --------------- FIRST LINK IN TEXT ----------------

    // firstLinkFoundLR :: String -> Either String URI
    const firstLinkFoundLR = s => {
        // Either a message or the first URI
        // found in the given string.
        const parts = s.split("://");

        return 1 < parts.length ? (() => {
            const [as, bs] = parts.map(words);
            const
                a = 0 < as.length ? (
                    last(as)
                ) : "",
                b = 0 < bs.length ? (
                    bs[0]
                ) : "";

            const
                scheme = last(a.split(/\b/u)),
                resource = takeWhile(
                    c => "!#$&'*+,/:;=?@[]".includes(s) || (
                        !(/[\s()]/u).test(c)
                    )
                )([...b]).join("");

            return Boolean(scheme) ? (
                Boolean(resource) ? (
                    Right(`${scheme}://${resource}`)
                ) : Left(`No resource found: ${scheme}://`)
            ) : Left(`No scheme found: ://${resource}`);
        })() : Left("No link found in string.");
    };

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


    // bikeDocIDFromXmlDoc :: NSXMLDoc ->
    // Either String String
    const bikeDocIdFromXmlDocLR = doc => {
        // Either a message or the ID string of the root
        // <ul> tag of a Bike document parsed as XML.
        const
            uw = ObjC.unwrap,
            e = $(),
            rootUL = uw(
                doc.nodesForXPathError("//body/ul", e)
            )[0];

        return rootUL.isNil() ? (
            Left(uw(e.localizedDescription))
        ) : Right(
            uw(rootUL.attributeForName("id").stringValue)
        );
    };


    // filePathFromFrontWindowLR  :: () -> Either String FilePath
    const filePathFromFrontWindowLR = () => {
        // ObjC.import ('AppKit')
        const
            appName = ObjC.unwrap(
                $.NSWorkspace.sharedWorkspace
                .frontmostApplication.localizedName
            ),
            ws = Application("System Events")
            .applicationProcesses.byName(appName).windows;

        return bindLR(
            0 < ws.length ? Right(
                ws.at(0).attributes.byName("AXDocument")
                .value()
            ) : Left(
                `No document windows open in ${appName}.`
            )
        )(
            docURL => null !== docURL ? (
                Right(decodeURIComponent(docURL.slice(7)))
            ) : Left(
                `No saved document active in ${appName}.`
            )
        );
    };


    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
        // Either a message, (if no clip text is found),
        // or the string contents of the clipboard.
        const
            v = ObjC.unwrap(
                $.NSPasteboard.generalPasteboard
                .stringForType($.NSPasteboardTypeString)
            );

        return Boolean(v) && 0 < v.length ? (
            Right(v)
        ) : Left("No utf8-plain-text found in clipboard.");
    };


    // xmlDocFromStringLR ::
    // XML String -> Either String NSXMLDocument
    const xmlDocFromStringLR = xml => {
        const
            error = $(),
            node = $.NSXMLDocument.alloc
            .initWithXMLStringOptionsError(
                xml, 0, error
            );

        return node.isNil() ? (
            Left("File could not be parsed as XML")
        ) : Right(node);
    };


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


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => m.Left ? (
            m
        ) : mf(m.Right);


    // 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 => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // last :: [a] -> a
    const last = xs =>
        // The last item of a list.
        0 < xs.length ? (
            xs.slice(-1)[0]
        ) : null;


    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        // Either a message or the contents of any
        // text file at the given filepath.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );

        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };


    // takeWhile :: (a -> Bool) -> [a] -> [a]
    const takeWhile = p =>
        xs => {
            const i = xs.findIndex(x => !p(x));

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


    // words :: String -> [String]
    const words = s =>
        // List of space-delimited sub-strings.
        s.split(/\s+/u);

    // ---
    return main();
})();

FWIW I notice that on this system (macOS 11.6.3, Bike Preview 26):

  1. The bike://UUID#itemID links are working well (Copy > Copy Link)
  2. but from browsers and applications supporting live links, I get a message if I try to follow one of the (Copy > Copy Path Link) variants which include a file path:

(Failed to find any documents matching the link)

(Perhaps that part of the bike url scheme is not yet implemented in build 26)

3 Likes