Bike – Open Link ⌘↩?

Any particular apps – that we could look at – come to mind ?


UPDATE

Ah, ok … got one – iA Writer

Screenshot 2024-04-08 at 2.23.20 PM


Possibly hard to script … there may be a way around it, that hasn’t immediately come to mind, but I don’t think the osascript (AppleScript|JavaScript) interface to current builds includes information about cursor position.

( i.e. simple enough with rows which contain just one link, but in rows containing two or more links, you would need to know which link the text cursor was closer to – or contained by )


One hack might be to script a temporary extension of the selection by one character – enough to fire a copy event and inspect the new keyboard contents for the html of any link – before collapsing it again.

In the meanwhile, if you have Keyboard Maestro version 11, then you could try this, which is bound to ⌘↩

Open link.kmmacros.zip (15.6 KB)

in a macro folder which makes macros available only to Bike, as in this pattern:

Screenshot 2024-04-08 at 3.19.08 PM


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

    ObjC.import("AppKit");

    // Open the first link (if any)
    // found in any Bike XML content in the clipboard.

    const main = () =>
        bindLR(
            clipOfTypeLR("com.hogbaysoftware.bike.xml")
        )(
            html => {
                const urls = linksInHTML(html);

                return 0 < urls.length
                    ? Right(urls[0])
                    : Left("No links found in clipboard.");
            }
        );


    // ---------------------- HTML -----------------------

    // linksInHTML :: HTML String -> [URL]
    const linksInHTML = html =>
        // A (possibly empty) list of URLs.
        bindLR(
            parseFromHTML(html)
        )(
            compose(
                map(x => x.attributes.href),
                filterTree(x => "a" === x.name)
            )
        );


    // parseFromHTML :: String -> Either String Tree Dict
    const parseFromHTML = html => {
        const
            error = $(),
            node = $.NSXMLDocument.alloc
            .initWithXMLStringOptionsError(
                html, 0, error
            );

        return node.isNil()
            ? Left(`Not parseable as XML: ${html}`)
            : Right(xmlNodeDict(node));
    };


    // xmlNodeDict :: NSXMLNode -> Tree Dict
    const xmlNodeDict = xmlNode => {
    // A Tree of dictionaries derived from an NSXMLNode
    // in which the keys are:
    // name (tag), content (text), attributes (array)
    // and XML (source).
        const
            uw = ObjC.unwrap,
            hasChildren = 0 < parseInt(
                xmlNode.childCount, 10
            );

        return Node({
            name: uw(xmlNode.name),
            content: hasChildren
                ? undefined
                : (uw(xmlNode.stringValue) || " "),
            attributes: (() => {
                const attrs = uw(xmlNode.attributes);

                return Array.isArray(attrs)
                    ? attrs.reduce(
                        (a, x) => Object.assign(a, {
                            [uw(x.name)]: uw(
                                x.stringValue
                            )
                        }),
                        {}
                    )
                    : {};
            })(),
            xml: uw(xmlNode.XMLString)
        })(
            hasChildren
                ? uw(xmlNode.children).map(xmlNodeDict)
                : []
        );
    };


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

    // clipOfTypeLR :: String -> Either String String
    const clipOfTypeLR = utiOrBundleID => {
        const
            clip = ObjC.deepUnwrap(
                $.NSString.alloc.initWithDataEncoding(
                    $.NSPasteboard.generalPasteboard
                    .dataForType(utiOrBundleID),
                    $.NSUTF8StringEncoding
                )
            );

        return 0 < clip.length
            ? Right(clip)
            : Left(
                "No clipboard content found " + (
                    `for type '${utiOrBundleID}'`
                )
            );
    };

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

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

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


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


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


    // filterTree (a -> Bool) -> Tree a -> [a]
    const filterTree = p =>
    // List of all values in the tree
    // which match the predicate p.
        foldTree(x => xs =>
            p(x)
                ? [x, ...xs.flat()]
                : xs.flat()
        );

    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = f => {
    // The catamorphism on trees. A summary
    // value obtained by a depth-first fold.
        const go = tree => f(
            tree.root
        )(
            tree.nest.map(go)
        );

        return go;
    };


    // map :: (a -> b) -> [a] -> [b]
    const map = f =>
    // The list obtained by applying f
    // to each element of xs.
    // (The image of xs under f).
        xs => [...xs].map(f);

    // MAIN ---
    return JSON.stringify(main(), null, 2);
})();

1 Like

Sure. Bear uses cmd+shift+K, Obsidian uses cmd+Return, and you’ve already found iA Writer. All of them also support a variant with the option modifier to open the link in a new tab/window.

Unfortunately, I don’t own KM, perhaps others might find this useful! I’m hoping for a native solution.

1 Like

Maybe I don’t understand the main point of the discussion. Why can’t changing the shortcut key of the bike menu item ‘open link’ to ⌘↵ solve the problem?

3 Likes

Yeah, same…

(And here’s a way to do i, btw. :point_down:t2: )

Thanks, completely missed that menu option.

@eno

I seem to have missed it too :slight_smile:

So, in terms of things like Keyboard Maestro, just:

1 Like

Yeah, much less clunky to add stuff like this in Keyboard Maestro. Also, it can be backed up, as opposed to the native solution!

But if you’re just doing a couple, the native is fine - and that updates the text in the menu as well. :slightly_smiling_face:

1 Like