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):
- The
bike://UUID#itemID
links are working well (Copy > Copy Link
) - 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)