Any particular apps – that we could look at – come to mind ?
UPDATE
Ah, ok … got one – iA Writer
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 a macro folder which makes macros available only to Bike, as in this pattern:
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);
})();
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.
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?