Why did I wait so long to update Bike…current version is fabulous…well done Jesse
Wanting to adapt your copy selection as csv to copy entire outline as tabbed text.
I can get the outline with the below adaption of your script, just not sure how to substitute tabs. Suggestions welcome.
Thanks
Rob
var bikeApp = Application("Bike")
bikeApp.includeStandardAdditions = true
var document = bikeApp.documents.at(0)
var zip = (a, b) => a.map((k, i) => [k, b[i]])
var ancestors = []
var results = []
var allRows = document.rows()
for (each of allRows) {
const eachLevel = each.level()
while (ancestors.length > 0 && ancestors[ancestors.length - 1].level() >= eachLevel) {
ancestors.pop()
}
ancestors.push(each)
if (!each.containsRows()) {
let line = []
for (eachAncestor of ancestors) {
let eachName = eachAncestor.name()
let eachEscapedName = eachName.replace(/"/g, '""')
line.push('"' + eachEscapedName + '"')
}
results.push(line.join())
}
}
bikeApp.setTheClipboardTo(results.join('\n'))
results
Tab-indented text is one of the formats copied to the clipboard automatically on ⌘C
What application are you pasting into ?
Choice of the format used in pasting is made by the application you paste into. If you are pasting into something like Numbers, which stops looking for plain text when it finds an HTML or OPML pasteboard item, you can reduce the clipboard contents to plain text only before pasting.
If you are using Keyboard Maestro, you can bind a keystroke to something like this:
Which uses the following JavaScript for Automation source:
Expand disclosure triangle to view JS source
(() => {
"use strict";
// BIKE :: Copy As Plain Text Only
// To get around a bug in Numbers.app,
// which seems to stop looking for plain text when
// (inside the editing field of a spreadsheet cell)
// it sees HTML and OPML outlines in the clipboard.
// Ver 0.04
// Rob Trew @2020
ObjC.import("AppKit");
// MAIN :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return either(
alert("Copy as Text Only")
)(
clip => clip
)(
doc.exists()
? (() => {
const
selectedText = doc.selectedText()
.trimStart();
return Boolean(selectedText)
? (
copyText(selectedText),
// delay(0.2),
bindLR(
clipTextLR()
)(
clip => clip === selectedText ? (
Right(clip)
) : Left(
[
"Text not copied ...",
"Try increasing delay."
]
.join("\n")
)
)
)
: Left("Nothing selected in Bike.");
})()
: Left("No document open in Bike.")
);
};
// ----------------------- 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
);
};
// 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.");
};
// copyText :: String -> IO String
const copyText = s => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
);
};
// --------------------- 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
});
// 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);
// 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);
return main();
})();
You will have spotted this, but for anyone who comes later, perhaps worth clarifying that, of course, any descendants of checked rows are also excluded by this draft.
And just to show that there are many ways to overthink this , you can also use Bike’s outline paths to perform the traversal/filtering for you:
tell application "Bike"
tell front document
set theMatches to query outline path "//* except //@done///*"
set theExport to export from theMatches as plain text format
set the clipboard to theExport
end tell
end tell
This is AppleScript, but you can use same query API in javascript. This method could be significantly faster (since query is all performed in Bike process), but it also means you get to/have to learn the outline path syntax
Edit And somehow I missed that @complexpoint already mentioned outline paths in previous post!
but I missed the trick of simplifying with .export, which, for the version excluding the whole subtree of any checked items, might, FWIW, look something like this in JS:
@jessegrosjean and @complexpoint have just clarified to me the usefulness of outline paths and the pluses and minuses of both applescript and javascript.