Can't Figure Out Simple Clipboard Regex

I'm trying to remove the return character that OmniFocus puts at the end when you select a task and "Copy as Link." After an hour of trying, web searching, reading the Keyboard Maestro wiki... I give up. What's wrong with my macro? Thanks!

2022-01-21-0939-55

Instead of using \r try \R

It's described in the KM wiki page Regular Expressions [Keyboard Maestro Wiki] in the table under the heading ICU 55+ Metacharacters

1 Like

@tiffle beat me to it but I did just test a simple string with a return and /r wasn't found on the clipboard copy of the string but \n was.

1 Like

\r matches the carriage return character while \n matches the line feed character. \R matches them both!

EDIT:
You can also try using the built-in filter
KM 0 2022-01-21_16-57-19

That won't work!

1 Like

I tried \R, \n and filtering with Unwrap, and none of those approaches. However, I know regex is working because I tried telling it to find \d and it removed all the digits:
omnifocus:///task/j9_eGinDu7I
became
omnifocus:///task/j_eGinDuI

Maybe it's not a return or newline? Is there a way I can find out the hex code or ASCII behind that invisible character?

If you’re saying nothing works then you need to provide an example of the actual text in your clipboard so we can see what’s going on.

Sure. Here it is:

omnifocus:///task/j9_eGinDu7I

(there's a newline or CR at the end of that line.)

P.S. I pasted the text into BBEdit and did a hex dump, and it showed a hex A0 (newline) at the end. However, a stackoverflow posting said "BBEdit now uses the line feed (ASCII decimal 10) as line breaks in its internal representation for text in open documents, instead of the carriage return (ASCII decimal 13)" so it may not be what was really on the clipboard.

It just so happens I have OmniFocus on my Mac, so I gave it a go. Here's my version of your macro:

Test OF.kmmacros (2.0 KB)
Keyboard Maestro Export

I tested it on an entry in OmniFocus and this is the result:

KM 0 2022-01-21_18-57-26

As you can see there's no newline (of any flavour).

This was the clipboard before running the macro:

KM 1 2022-01-21_19-03-24

So in conclusion - it works fine here. You might have a typo or something so try my version of your macro and see what happens.

1 Like

Yes, yours works. I went back to mine and did a Cmd A in the Search For field, hit Delete and retyped \R and it still didn't work. No idea what's going on. It's a new M1 MacBook Air and has been great so far, though I suspect this issue has nothing to do with the machine.

But I'll use yours in the meantime. Thanks a bunch!

1 Like

If you figure out what the problem is maybe you could write it up here so we can all learn. Cheers.

FWIW, I used Omni-Automation scripting API to create a "Copy Item Link" script that doesn't add a new line at the end of link(s).

Macro:

Copy Item Link.kmmacros (6.1 KB)

JS Code

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = () => {
        // main :: IO ()
        const main = () => {
            const
                seln = document.windows[0].selection,
                xs = seln.allObjects,
                x = xs[0],
                types = ["Tag", "Folder", "Project", "Task"],
                strLinks = compose(
                    unlines,
                    map(linkFromObject),
                    filter(compose(flip(elem)(types), x => x.constructor.name))
                )(xs)
            return (
                Pasteboard.general.string = strLinks,
                strLinks
            )
        };

        // FUNCTIONS --
        // linkFromObject :: OFItem -> String
        const linkFromObject = x => {
            const
                t = x.constructor.name;
            return `omnifocus:///${
                        toLower("Project" === t ? "Task" : t)
                    }/${x.id.primaryKey}`
        }

        // GENERICS ----------------------------------------------------------------
        // https://github.com/RobTrew/prelude-jxa
        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            // A function defined by the right-to-left
            // composition of all the functions in fs.
            fs.reduce(
                (f, g) => x => f(g(x)),
                x => x
            );


        // elem :: Eq a => a -> [a] -> Bool
        const elem = x =>
            // True if xs contains an instance of x.
            xs => {
                const t = xs.constructor.name;

                return "Array" !== t ? (
                    xs["Set" !== t ? "includes" : "has"](x)
                ) : xs.some(eq(x));
            };

        // eq (==) :: Eq a => a -> a -> Bool
        const eq = a =>
            // True when a and b are equivalent in the terms
            // defined below for their shared data type.
            b => {
                const t = typeof a;

                return t !== typeof b ? (
                    false
                ) : "object" !== t ? (
                    "function" !== t ? (
                        a === b
                    ) : a.toString() === b.toString()
                ) : (() => {
                    const kvs = Object.entries(a);

                    return kvs.length !== Object.keys(b).length ? (
                        false
                    ) : kvs.every(([k, v]) => eq(v)(b[k]));
                })();
            };

        // filter :: (a -> Bool) -> [a] -> [a]
        const filter = p =>
            // The elements of xs which match
            // the predicate p.
            xs => [...xs].filter(p);

        // flip :: (a -> b -> c) -> b -> a -> c
        const flip = op =>
            // The binary function op with
            // its arguments reversed.
            1 !== op.length ? (
                (a, b) => op(b, a)
            ) : (a => b => op(b)(a));

        // map :: (a -> b) -> [a] -> [b]
        const map = f =>
            // The list obtained by applying f
            // to each element of xs.
            // (The image of xs under f).
            xs => [...xs].map(f);

        // toLower :: String -> String
        const toLower = s =>
            // Lower-case version of string.
            s.toLocaleLowerCase();

        // unlines :: [String] -> String
        const unlines = xs =>
            // A single string formed by the intercalation
            // of a list of strings with the newline character.
            xs.join("\n");

        return main()
    };


    // OmniJS Context Evaluation ------------------------------------------------
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})()`
        )
    ) : 'No documents open in OmniFocus.'
})();

You bet. Thanks again!

1 Like

Wow! That's an amazing amount of code to just get it to do its normal function without adding a return. But that shows how little I know about this stuff! :slight_smile:

I'll keep a copy of your macro, just in case the one "tiffle" gave me succumbs to my system's mystery problem. Could the JavaScript code just go into the OmniFocus Automation menu, instead of routing through Keyboard Maestro?

Thanks,
Russell

Yes, of course. Here is an Omni-Automation Plug-In:

Copy Item Link.omnifocusjs.zip (1.6 KB)

This is working for me.
The micro pause of 0.2 seconds is needed because otherwise the "Filter" actions is executed before the Clipboard is actually written with the link from OF: there's a possibility that I need it because I have a couple of clipboard manager that might slow things down a little. So you might not need the pause.

Omnifocus - Shortcut - Copy URL.kmmacros (2.5 KB)

3 Likes

Well, that makes sense since I am re-implementing the feature using OF scripting API.