Is the intent to have nested items in Bike indented when copy/pasted in Mail?
Thanks,
Steve
Is the intent to have nested items in Bike indented when copy/pasted in Mail?
Thanks,
Steve
Short answer is yes.
Longer answer is Bike writes .bike (html subset), .opml, and tab indented .txt to the clipboard, up to application receiving the paste to decide what to do with it. Are you not getting results that you hoped for when pasting?
Thanks for the prompt reply! I was expecting the nested text from Bike to be ‘indented’ in Mail, not tab indented. Examples attached. Mail indentation moves the whole paragraph to the right, similar to what Bike’s outline does. Three screen shots attached showing Bike document, result of pasting into Mail and then my ‘cleaned up’ version where I used Mail indentation.
Remember that Bike is copying indented plain text which you are pasting into Mail.app
’s rich text editor there. The way in which the clipboard is used, when pasted, is up to Mail.app
.
For what you are after, you would need some scripting magic to translate plain text indented structure into a rich text blockquote format that Mail.app looks for.
If we inspect the Mail.app
rich text clipboard content (the public.html
pastboard item is probably more human-legible than the public.rtf
pasteboard item) we can see that Apple is using blockquote
tag nesting for the html representation of Mail
“Indent” formatting.
It might be possible for someone to write a paste-to-Mail
script or Keyboard Maestro macro which mapped Bike’s structured text clipboard contents to a Mail.app html blockquote nest, but I wonder whether the cost / benefit would look attractive ?
Could you give us a sense of what turns on it (tab-indent vs Mail.app indent format) in your working practices ?
PS if you wanted to paste a Bike-copied clipboard into Mail in Mail’s Bulleted List
format, then you could experiment with something like this Keyboard Maestro macro, which uses only the Bike HTML pasteboard item, with a KM paste action:
Paste Bike clipboard into Mail as bullet nest.kmmacros.zip (2.1 KB)
(() => {
"use strict";
ObjC.import("AppKit");
const main = () =>
bindLR(
clipOfTypeLR("com.hogbaysoftware.bike.xml")
)(
setClipOfTextType("public.html")
);
// --------------------- 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);
// 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}'`
)
);
};
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
),
txt
);
};
return main();
})();
(after which you could also use Mail’s Format > Lists > Convert to Numbered List
on any selected runs)
And if anyone wanted to script the rewrite of the Bike clipboard to a Mail.app blockquote indent format, a starting point might be that you can paste this to Mail.app
(with indent formatting):
By placing this HTML <blockquote>
nest in a public.html
pasteboard item:
<span>Alpha</span>
<blockquote>
<div>Beta</div>
<div>Gamma</div>
</blockquote>
<blockquote>
<blockquote>
<div>Epsilon</div>
<div>Zeta</div>
</blockquote>
Eta
</blockquote>
<span>Delta</span>
(() => {
"use strict";
ObjC.import("AppKit");
const main = () =>
setClipOfTextType("public.html")(
`<span>Alpha</span>
<blockquote>
<div>Beta</div>
<div>Gamma</div>
</blockquote>
<blockquote>
<blockquote>
<div>Epsilon</div>
<div>Zeta</div>
</blockquote>
Eta
</blockquote>
<span>Delta</span>`
);
// --------------------- GENERIC ---------------------
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
),
txt
);
};
return main();
})();
And you can prune out the vertical white-space:
by adding a margin style to the <blockquote>
tags:
<span>Alpha</span>
<blockquote style="margin: 0px 0px 0px 40px;">
<div>Beta</div>
<div>Gamma</div>
</blockquote>
<blockquote style="margin: 0px 0px 0px 40px;">
<blockquote style="margin: 0px 0px 0px 40px;">
<div>Epsilon</div>
<div>Zeta</div>
</blockquote>
Eta
</blockquote>
<span>Delta</span>
(() => {
"use strict";
ObjC.import("AppKit");
const main = () =>
setClipOfTextType("public.html")(
`<span>Alpha</span>
<blockquote style="margin: 0px 0px 0px 40px;">
<div>Beta</div>
<div>Gamma</div>
</blockquote>
<blockquote style="margin: 0px 0px 0px 40px;">
<blockquote style="margin: 0px 0px 0px 40px;">
<div>Epsilon</div>
<div>Zeta</div>
</blockquote>
Eta
</blockquote>
<span>Delta</span>`
);
// --------------------- GENERIC ---------------------
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
),
txt
);
};
return main();
})();
Nice, thanks for sharing. I’ll add a link from the Bike script wiki to this thread.
Here is a draft Copy As Mail-indented
script for Bike 1.1
If you:
you should find that you can then paste into Mail.app
and get mail-indented paragraphs like:
(() => {
"use strict";
ObjC.import("AppKit");
// Draft of "Copy As Mail.app indented"
// for Bike 1.1
// Rob Trew @2022
// Ver 0.03
// main :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return either(
alert("Copy as Mail.app indentation")
)(
x => x
)(
doc.exists() ? (() => {
const
selectedRows = doc.rows.where({
selected: true
}),
n = selectedRows.length,
levels = selectedRows.level(),
minLevel = Math.min(...levels);
return (
setClipOfTextType("public.html")(
mailQuoteBlocksFromForest(
forestFromIndentedLines(
zip(
levels.map(x => x - minLevel)
)(
selectedRows.name()
)
)
)
),
Right(
`${n} row(s) copied as Mail.app indented lines.`
)
);
})() : Left("No document open in Bike")
);
};
// ------- INDENTED MAIL.APP HTML FROM FOREST --------
// mailQuoteBlocksFromForest :: [Tree String] -> String
const mailQuoteBlocksFromForest = forest => {
const
style = "style=\"margin: 0px 0px 0px 40px;\"",
go = tree => {
const xs = tree.nest;
return [
`<div>${tree.root || " "}</div>`,
...(
0 < xs.length ? [
[
`<blockquote ${style}>`,
xs.flatMap(go).join("\n"),
"</blockquote>"
]
.join("\n")
] : []
)
]
.join("\n");
};
return forest.flatMap(go).join("\n");
};
// ----------------------- 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
);
};
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
),
txt
);
};
// --------------------- 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 => e.Left ? (
fl(e.Left)
) : fr(e.Right);
// 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 || []
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = a =>
// A pair of values, possibly of
// different types.
b => ({
type: "Tuple",
"0": a,
"1": b,
length: 2,
*[Symbol.iterator]() {
for (const k in this) {
if (!isNaN(k)) {
yield this[k];
}
}
}
});
// forestFromIndentedLines :: [(Int, String)] ->
// [Tree String]
const forestFromIndentedLines = tuples => {
const go = xs =>
0 < xs.length ? (() => {
// First line and its sub-tree,
const [depth, body] = xs[0],
[tree, rest] = span(x => depth < x[0])(
xs.slice(1)
);
// followed by the rest.
return [
Node(body)(go(tree))
].concat(go(rest));
})() : [];
return go(tuples);
};
// span :: (a -> Bool) -> [a] -> ([a], [a])
const span = p =>
// Longest prefix of xs consisting of elements which
// all satisfy p, tupled with the remainder of xs.
xs => {
const i = xs.findIndex(x => !p(x));
return -1 !== i ? (
Tuple(xs.slice(0, i))(
xs.slice(i)
)
) : Tuple(xs)([]);
};
// zip :: [a] -> [b] -> [(a, b)]
const zip = xs =>
// The paired members of xs and ys, up to
// the length of the shorter of the two lists.
ys => Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => Tuple(xs[i])(ys[i]));
// MAIN ---
return main();
})();
and not that this script is written in JavaScript for Automation
rather than AppleScript
, so if you are testing it in Script Editor, the language selector at top left needs to be set to JavaScript.
Here is a Keyboard Maestro macro version, initially bound to the ⌥C keyboard shortcut, but you can adjust that in KM.
Sorry for the delay in responding so thanks for the script!
However, I’m a scripting novice and am getting an error when trying to run it. I copied script and pasted it into Script Editor. I don’t see a language selector ‘top left’ so I went to preferences and changed the default editor to Javascript.
But when I try to run the script I get an error: Expected expression but found “>”.
Suggestions?
Thanks!
In a macOS Script Editor.app
document:
I went to preferences and changed the default editor
( Preferences can’t change the setting of an existing window – it just sets the default for new windows )
Let us know how you get on. Make sure that you scroll down to select and copy the whole of the JavaScript above.
I personally :
Copy selected lines from BIKE for Mail-indented pasting - Macro Library - Keyboard Maestro Discourse
This is what I see after pasting in your javascript into Script Editor 2.11. No ability to toggle between AppleScript and JavaScript.
And FWIW, I don’t have Keyboard Maestro.
Can’t figure out what I’m missing…
Which version of macOS is this ?
What happens if (with JavaScript as the default language in Preferences), you:
⌘N
?
The end of the script looks like this:
return main();
})();
See: Choose a script language in Script Editor on Mac - Apple Support (IE)
May also be worth trying, in the Script Editor Menu:
View > Show Navigation Bar
I think that’s the issue.
Thank you both for your patience! Got it to work after revealing the nav bar and compiling.
And back to a question after my original posting, when composing longish emails I often include quotes, references to other articles, attachments, etc. Very outline-ish, but relatively tedious to do while in Mail.app. I’ve been an outliner user since ThinkTank/MORE days and that’s sorta how I think/compose. So when first trying Bike one of the first things I did was try pasting into Mail.
I really like Bike’s interface — much less cluttered than Omnioutliner — so I was hopeful of using it as a general first draft tool. Now that I have this Mail script I’ll definitely use it more…