Find Space After 145 Characters and Divide into Multiple Clipboards

My apologies for this long and possibly convoluted process I am trying to describe. I wish I could sum it up a bit better than I did.

I am scouring the forums trying to figure out a way to count the first 160 characters of selected text and parse out the messages. I have to send text messages with 160 characters in length or less. If it is longer than that I have to type (1/2) or (1/3) at the end by breaking up the message that was typed. Then send (2/2) or (2/3) etc. I do this hundreds of times and I figured this was a job for Keyboard Maestro to select the text and parse it out into either two clipboards or three depending on how long the message is (it won't be longer than three).

If anyone has an idea to get me pointed in the right direction how to divide the message out that would be great. The added complication is that I would really like to end after a word and not cut a word off so basically look for the first space after 145 characters and chop the message into one, two or three different groups.

This seemed useful but not sure how to piece it together.

I am thinking depending on how long it is put it in clipboard 1, 2 and if it is over 190 characters (145+145) put the remaining contents into clipboard 3 and use that to paste into the third message.

Possible code for a KM Execute a JavaScript for Automation action:

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    const test = `My apologies for this long and possibly convoluted process I am trying to describe. I wish I could sum it up a bit better than I did.

I am scouring the forums trying to figure out a way to count the first 160 characters of selected text and parse out the messages. I have to send text messages with 160 characters in length or less. If it is longer than that I have to type (1/2) or (1/3) at the end by breaking up the message that was typed. Then send (2/2) or (2/3) etc. I do this hundreds of times and I figured this was a job for Keyboard Maestro to select the text and parse it out into either two clipboards or three depending on how long the message is (it won't be longer than three).

If anyone has an idea to get me pointed in the right direction how to divide the message out that would be great. The added complication is that I would really like to end after a word and not cut a word off so basically look for the first space after 145 characters and chop the message into one, two or three different groups.

This seemed useful but not sure how to piece it together.`;

    const main = () => {
        const
            maxTweetLength = 160,
            // " (1/n)"
            padLength = 6;

        const
            targetSize = maxTweetLength - padLength,
            experimentalTweak = 5;

        return twitterChunks(
            targetSize - experimentalTweak
        )(test)
        // optionally checking lengths:
        // .map(x => x.length)
        .join("\n\n***\n\n");
    };

    // ---------------- TWITTER SEGMENTS -----------------

    const twitterChunks = limit => s => {
        const
            wcs = mapAccumL(a => w => {
                // Cost of an additional word and space.
                const n = 1 + a + w.length;

                return Tuple(n)(
                    Tuple(w)(n)
                );
            })(0)(
                words(s)
            )[1];

        const go = allSofar =>
            measuredWords => {
                const
                    nextLimit = limit + allSofar,
                    chunk = takeWhile(
                        ab => nextLimit >= ab[1]
                    )(
                        measuredWords
                    ),
                    rest = drop(chunk.length)(
                        measuredWords
                    );

                return 0 < rest.length ? (
                    [chunk].concat(
                        go(last(chunk)[1])(rest)
                    )
                ) : [chunk];
            };

        const
            chunks = 0 < wcs.length ? (
                go(0)(wcs)
            ) : [],
            total = chunks.length;

        return chunks.map(
            (wns, i) =>
            `${wns.map(fst).join(" ")} (${1 + i}/${total})`
        );
    };


    // --------------------- GENERIC ---------------------

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2
        });


    // drop :: Int -> [a] -> [a]
    // drop :: Int -> Generator [a] -> Generator [a]
    // drop :: Int -> String -> String
    const drop = n =>
        xs => xs.slice(n);


    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];


    // last :: [a] -> a
    const last = xs =>
        // The last item of a list.
        0 < xs.length ? (
            xs.slice(-1)[0]
        ) : null;


    // mapAccumL :: (acc -> x -> (acc, y)) ->
    // acc -> [x] -> (acc, [y])
    const mapAccumL = f =>
        // A tuple of an accumulation and a list
        // obtained by a combined map and fold,
        // with accumulation from left to right.
        acc => xs => [...xs].reduce(
            (a, x) => {
                const tpl = f(a[0])(x);

                return [tpl[0], a[1].concat(tpl[1])];
            },
            [acc, []]
        );


    // takeWhile :: (a -> Bool) -> [a] -> [a]
    // takeWhile :: (Char -> Bool) -> String -> String
    const takeWhile = p =>
        // The longest prefix of xs in which
        // every element satisfies p.
        xs => {
            const n = xs.length;

            return xs.slice(
                0, 0 < n ? until(
                    i => n === i || !p(xs[i])
                )(i => 1 + i)(0) : 0
            );
        };


    // until :: (a -> Bool) -> (a -> a) -> a -> a
    const until = p =>
        // The value resulting from repeated applications
        // of f to the seed value x, terminating when
        // that result returns true for the predicate p.
        f => x => {
            let v = x;

            while (!p(v)) {
                v = f(v);
            }

            return v;
        };


    // words :: String -> [String]
    const words = s =>
        // List of space-delimited sub-strings.
        s.split(/\s+/u);

    // MAIN ---
    return main();
})();

Sample output from preceding post:


My apologies for this long and possibly convoluted process I am trying to describe. I wish I could sum it up a bit better than I did. I am scouring (1/8)


the forums trying to figure out a way to count the first 160 characters of selected text and parse out the messages. I have to send text messages (2/8)


with 160 characters in length or less. If it is longer than that I have to type (1/2) or (1/3) at the end by breaking up the message that was typed. (3/8)


Then send (2/2) or (2/3) etc. I do this hundreds of times and I figured this was a job for Keyboard Maestro to select the text and parse it out into (4/8)


either two clipboards or three depending on how long the message is (it won't be longer than three). If anyone has an idea to get me pointed in the (5/8)


right direction how to divide the message out that would be great. The added complication is that I would really like to end after a word and not (6/8)


cut a word off so basically look for the first space after 145 characters and chop the message into one, two or three different groups. This seemed (7/8)


useful but not sure how to piece it together. (8/8)

Hey @skillet,

This task isn't terribly difficult, but knowing the general format of your input text would help.

-Chris

Thanks @ComplexPoint I will have to research how to get the selected text into the JavaScript since it seems to be part of the code. I am guessing you copy selected text to a clipboard and then output that to a variable that you reference directly in that JavaScript code.

@ccstone It's just basically plain text in a browser that I would select and copy to a clipboard and then parse out to different clipboards and stick (1/2) and (2/2) at the end of the text. I hope that answers your question. I am thinking I would just build in a Command+A to select and copy to a specific clipboard.

One route, after binding a KM Variable name to the content of the clipboard,

would be, from JS, to use the .getvariable method of the Keyboard Maestro Engine Application object.

The next question is how you want to feed them to Twitter (perhaps from a command line API ?)

The ideal UI might, I guess, retain some manual control over which word boundaries to split at.

but here's one approach to the capture and split side, returning a JSON list of strings:

Text chunked for tweeting.kmmacros (6.3 KB)

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Ver 0.02

    const main = () => {
        const
            maxTweetLength = 160,
            // " (1/n)"
            padLength = 6;

        const
            targetSize = maxTweetLength - padLength,
            experimentalTweak = 5;

        return JSON.stringify(
            twitterChunks(
                targetSize - experimentalTweak
            )(
                Application("Keyboard Maestro Engine")
                .getvariable("textToChunk")
            ), null, 2
        );
    };

    // ---------------- TWITTER SEGMENTS -----------------

    // twitterChunks :: Int -> String -> [String]
    const twitterChunks = limit => s => {
        const
            wcs = mapAccumL(a => w => {
                // Cost of an additional word and space.
                const n = 1 + a + w.length;

                return Tuple(n)(
                    Tuple(w)(n)
                );
            })(0)(
                words(s)
            )[1];

        const go = allSofar =>
            measuredWords => {
                const
                    nextLimit = limit + allSofar,
                    chunk = takeWhile(
                        ab => nextLimit >= ab[1]
                    )(
                        measuredWords
                    ),
                    rest = drop(chunk.length)(
                        measuredWords
                    );

                return 0 < rest.length ? (
                    [chunk].concat(
                        go(last(chunk)[1])(rest)
                    )
                ) : [chunk];
            };

        const
            chunks = 0 < wcs.length ? (
                go(0)(wcs)
            ) : [],
            total = chunks.length;

        return chunks.map(
            (wns, i) =>
            `${wns.map(fst).join(" ")} (${1 + i}/${total})`
        );
    };

    // --------------------- GENERIC ---------------------

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2
        });


    // drop :: Int -> [a] -> [a]
    // drop :: Int -> Generator [a] -> Generator [a]
    // drop :: Int -> String -> String
    const drop = n =>
        xs => xs.slice(n);


    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];


    // last :: [a] -> a
    const last = xs =>
        // The last item of a list.
        0 < xs.length ? (
            xs.slice(-1)[0]
        ) : null;


    // mapAccumL :: (acc -> x -> (acc, y)) ->
    // acc -> [x] -> (acc, [y])
    const mapAccumL = f =>
        // A tuple of an accumulation and a list
        // obtained by a combined map and fold,
        // with accumulation from left to right.
        acc => xs => [...xs].reduce(
            (a, x) => {
                const tpl = f(a[0])(x);

                return [tpl[0], a[1].concat(tpl[1])];
            },
            [acc, []]
        );


    // takeWhile :: (a -> Bool) -> [a] -> [a]
    // takeWhile :: (Char -> Bool) -> String -> String
    const takeWhile = p =>
        // The longest prefix of xs in which
        // every element satisfies p.
        xs => {
            const n = xs.length;

            return xs.slice(
                0, 0 < n ? until(
                    i => n === i || !p(xs[i])
                )(i => 1 + i)(0) : 0
            );
        };


    // until :: (a -> Bool) -> (a -> a) -> a -> a
    const until = p =>
        // The value resulting from repeated applications
        // of f to the seed value x, terminating when
        // that result returns true for the predicate p.
        f => x => {
            let v = x;

            while (!p(v)) {
                v = f(v);
            }

            return v;
        };


    // words :: String -> [String]
    const words = s =>
        // List of space-delimited sub-strings.
        s.split(/\s+/u);

    // MAIN ---
    return main();
})();

@ComplexPoint thank you for this. I am actually doing this to help people in need though a texting platform in Google Chrome and not for Twitter. My apologies if something I typed made it sound like it was for Twitter.

I am on a computer and typing messages in a little box and it doesn't allow me to just send the message and break it up for me automatically on the platform. If it is too long it says it can't send so I am constantly selecting the second have of my text cutting (command+x) then pressing return to send the message. Then pasting (command+v) and depending on how long the message is typing (2/2) or (2/3) and pressing return.

I didn't get into all those details because I thought it would be too much information and that I could just figure out how to parse it out to different macros or put timed breaks into a single macro if there was a way to get it into different Keyboard Maestro clipboards.

Thanks for all your help with this, this will be a big time saver and make conversations with texters so much smoother and faster.

1 Like

Not your fault – I read too fast and projected Twitter into the context : -)

Are you all set for the next stage of the process ?

Here, FWIW is a pair of draft macros:

  • one for copying a text (to a KM variable) as a sequence of short numbered chunks
  • another for pasting one numbered chunk at a time (from that KM variable) until none remain.

Text chunking Macros.kmmacros (16.4 KB)

1 Like

I spent over an hour trying to figure out how to break out segments and didn't want to be too needy so I am just coming back to it to see if I can make sense of it. I just didn't understand how to get it into clipboards. I don't know any JavaScript and that stuff was way beyond my mind.

Thanks I will see how I can piece these two together and get what I am looking for. Thanks for the handholding and sending all this.

Well all that paster is doing is:

  1. Reading in a JSON array (from a KM Var) to a live array
  2. Putting the first item of the array into the clipboard
  3. Updating the KM Var with a shortened array, in JSON format

Good luck !

Very cool, thank you!

Hey @skillet,

Here's my take on the task.

I've used the Keyboard Maestro Clipboard History Switcher to hold the data, which is progressively removed as you paste.

  1. Select your text and activate the Copy macro.
  2. Place your cursor and serially activate the Paste macro.

Test Group.ccstone Macros.kmmacros (13 KB)

Macro-Image-Copy

image

Macro-Image-Paste

image

There's a way to do this with only Keyboard Maestro native actions, but I don't want to fool with that until KM 10 comes out with a few bug-fixes and enhancements.

-Chris

2 Likes

Thanks @ccstone that works amazingly with those two macros and only two key commands needed which I was quite surprised about. AppleScript for me is a little more digestible though there is a lot to digest and learn in this. Thank you both for all your help!

1 Like