Pasting to Mail.app (Format > Indentation) format

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 ?

1 Like

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)

Expand disclosure triangle to view JS Source
(() => {
    "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)

1 Like

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):

Screenshot 2022-05-28 at 19.43.46

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>
Expand disclosure triangle to view JS Source
(() => {
    "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:

Screenshot 2022-05-28 at 20.17.49

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>
Expand disclosure triangle to view JS Source
(() => {
    "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();
})();
1 Like

Nice, thanks for sharing. I’ll add a link from the Bike script wiki to this thread.

1 Like

Here is a draft  Copy As Mail-indented   script for Bike 1.1

If you:

  1. select some lines in Bike, and
  2. run this script

you should find that you can then paste into Mail.app and get mail-indented paragraphs like:

Expand disclosure triangle to view JS Source
(() => {
    "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 || "&nbsp;"}</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();
})();

See Using Scripts - Bike

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.

Copy selected lines as Mail-indented.kmmacros.zip (3.4 KB)

2 Likes

@complexpoint

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:

Screenshot 2022-06-16 at 18.22.45

Screenshot 2022-06-16 at 18.22.53

Screenshot 2022-06-16 at 18.22.32

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 :

  • paste this kind of thing into an Execute JavaScript for Automation action in Keyboard Maestro, and
  • set a keyboard shortcut hot key for it.

Copy selected lines from BIKE for Mail-indented pasting - Macro Library - Keyboard Maestro Discourse

Paste text outline to Mail.app in Format > Indentation style - Macro Library - Keyboard Maestro Discourse

1 Like

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:

  • summon a fresh document with ⌘N
  • paste in the whole of the script, (making sure you have scrolled right down to the bottom to copy)
  • click the compile (hammer) icon

?

The end of the script looks like this:

    return main();
})();

See: Choose a script language in Script Editor on Mac - Apple Support (IE)

1 Like

May also be worth trying, in the Script Editor Menu:

View > Show Navigation Bar

I think that’s the issue.

@complexpoint
@jessegrosjean

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…

2 Likes