Copy as Markdown Link

Dear All,

Thanks to ComplexPoint for posting the copy-as-md-link, which has become very helpful in my workflow!

Recently, I think when moving to Ventura, the included macro code for Things 3 on Mac failed: copy link for the things url does not work anymore, probably due to changes of the share menu.

This problem might have been solved elsewhere in this forum, but I did not find anything. With help of the very quick and kind Things support, I was able to solve my problem by replacing the copy-link code in com.culturedcode.ThingsMac from GitHub - RobTrew/copy-as-md-link by this Apple Script

tell application id "com.culturedcode.ThingsMac"

   set selToDos to selected to dos
   set toDoLinks to ""
   set toDoName to ""

   repeat with selToDo in selToDos
      set toDoLink to "things:///show?id=" & (id of selToDo) as text
      set toDoName to (name of selToDo) as text
      set toDoLinks to toDoLinks & "[" & toDoName & "](" & toDoLink & ") "
      -- set toDoLinks to toDoLinks & toDoLink
   end repeat
   
   set AppleScript's text item delimiters to {return}
   return toDoLinks as text

end tell

So, I hope this was not old news:)

4 Likes

I get an error when I try this. I am new to Keyboard Maestro.

How do I fix this error?

Macro Cancelled
Execute a JavaScript For Automation failed
with script error: osascript: no such
component "JavaScript". Macro "Copy as
Markdown link" cancelled (while executing
From front application and selection to
mdLink string value).

osascript: no such component "JavaScript".

That seems quite a surprising message – I don't know whether @peternlewis has any thoughts ?

Can you tell us what version of macOS you are using, and show us the details of:

  1. How you have installed the macro
  2. Where you installed it from (the Github source is the current version)
  3. How you are launching it

I wonder whether this is relevant:

applescript - OSA Script failing for JavaScript - Ask Different

2 Likes
  • macOS 12.6.3 (21G419)
  • Installed from Keyboard Maestro, File, Import, Import Macros Safely.
  • Installed from the latest GitHub source.
  • Testing by entering the key combination from a link in an email in Mac mail.
1 Like

and simple Execute JavaScript for Automation actions seem to be working ?

No idea - your link to stackexchange sounds like the right path. I'd try restarting, and then failing that, the technique described in the stackexchange answer.

2 Likes

Thank you.

Looks like it was not Keyboard Maestro. This fixed it:

2 Likes

Has anybody tried this with Orion browser?

Its identifier is: com.kagi.kagimacOS.

I tried just updating Copy as Markdown link like the following, but of course that does not work.

const linkForBundleLR = bundleID =>
        // ------------ BROWSER ? ------------

        [
            "com.Apple.Safari",
            "com.google.Chrome",
            "com.microsoft.edgemac",
            "com.vivaldi.Vivaldi",
            "com.kagi.kagimacOS"

Screen Shot 2023-02-02 at 11.55.54

Orion is a webkit browser, and shares the Safari scripting interface.

In addition to adding its bundleID to the list you show above, we also need to extend the test for Safari which results in using a method named currentTab rather than activeTab.

See:

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

    // Rob Trew @ 2020

    // Copy Markdown Link to front document, URL, or resource.
    // Ver 0.31

    // Switched to running app-specific macros by UUID
    // fetched from a JSON dictionary stored in a
    // uuidsForMDLink KM variable.

    // If this variable is not found, or a UUID retrieved
    // from it is not found, then the dictionary is regenerated.

    // The regeneration, which will happen on the first
    // run, but should only be needed thereafter when
    // new sub-macros are added, will activate Keyboard Maestro.app

    // Normally use of the macro will, however, normally
    // bypass Keyboard Maestro.app and run through
    // Keyboard Maestro Engine instead.

    ObjC.import("AppKit");

    const kmGroupName = "MD link tools";

    // ---------------------- MAIN -----------------------
    // main :: IO ()
    // eslint-disable-next-line max-lines-per-function
    const main = () => {
        const bundleID = frontAppBundleId();

        return either(
            msg => (
                alert("Copy as Markdown link")(msg),
                msg
            )
        )(
            mdLink => mdLink
        )(
            bindLR(
                void 0 !== bundleID ? (
                    Right(bundleID)
                ) : Left(
                    "No active application detected"
                )
            )(linkForBundleLR)
        );
    };

    // linkForBundleLR :: String -> Either String String
    const linkForBundleLR = bundleID =>
    // ------------ BROWSER ? ------------

        [
            "com.apple.Safari",
            "com.google.Chrome",
            "com.microsoft.edgemac",
            "com.vivaldi.Vivaldi",
            "com.kagi.kagimacOS"
        ].includes(bundleID) ? (
                browserLinkLR(bundleID)
            ) : (() => {
            // ---- APP-SPECIFIC MACRO ? -----
                const
                    kme = Application("Keyboard Maestro Engine"),
                    dctUUID = either(
                        msg => (
                        // eslint-disable-next-line no-console
                            console.log(
                                "BundleID map had to be regenerated",
                                msg
                            ),
                            // Regenerated UUID dictionary
                            updatedUUIDMap()
                        )
                    )(
                    // UUID dictionary from existing
                    // KM Variable
                        dct => dct
                    )(
                        jsonParseLR(
                            kme.getvariable("uuidsForMDLink")
                        )
                    );

                return linkFromUUID(kme)(bundleID)(
                    dctUUID[bundleID]
                );
            })();

    // linkFromUUID :: Application ->
    // String -> String -> String
    const linkFromUUID = kme =>
        bundleID => maybeUUID => Boolean(maybeUUID) ? (
            either(
                // If the UUID wasn"t found,
                // then run a new one from an
                // updated dictionary.
                () => (
                    bindLR(
                        doScriptLR(kme)(
                            updatedUUIDMap()[bundleID]
                        )
                    )(
                        // Link after use of alternate UUID
                        () => Right(
                            kme.getvariable("mdLink")
                        )
                    )
                )
            )(
                // Link read after  with UUID
                () => Right(kme.getvariable("mdLink"))
            )(
                // Run macro with this UUID if possible.
                doScriptLR(kme)(maybeUUID)
            )
        ) : appFrontWindowMDLinkLR(bundleID);


    // doScriptLR :: UUID -> Either String String
    const doScriptLR = kme =>
        uuid => {
            try {
                return (
                    kme.doScript(uuid),
                    Right(uuid)
                );
            } catch (e) {
                return Left(
                    `Macro UUID :: ${uuid}\n\n${e.message}`
                );
            }
        };

    // -------------- BUNDLEID -> UUID MAP ---------------

    // updatedUUIDMap :: IO () -> { bundleID :: UUID }
    const updatedUUIDMap = () => {
        const
            macroGroupName = "MD Link tools",
            mdLinkToolsGroups = Application(
                "Keyboard Maestro"
            ).macroGroups.where({
                name: macroGroupName
            });

        return either(
            alert("Copy as MD Link - Map bundle to UUID")
        )(
            dictUUIDs => (
                Application("Keyboard Maestro Engine")
                .setvariable("uuidsForMDLink", {
                    to: JSON.stringify(
                        dictUUIDs, null, 2
                    )
                }),
                dictUUIDs
            )
        )(
            0 < mdLinkToolsGroups.length ? (() => {
                const
                    instances = mdLinkToolsGroups.at(0)
                    .macros()
                    .flatMap(macro => {
                        const k = macro.name();

                        return k.includes(".") ? (
                            [
                                [k, macro.id()]
                            ]
                        ) : [];
                    });

                return Right(
                    instances.reduce(
                        (a, [bundle, uuid]) => Object.assign(
                            a, {
                                [bundle]: uuid
                            }
                        ), {}
                    )
                );
            })() : Left(
                `Macro group not found:\n\n\t${macroGroupName}`
            )
        );
    };


    // --------------------- BROWSERS ----------------------

    // browserLinkLR :: String -> Either String IO String
    const browserLinkLR = bundleID => {
        const
            app = Application(bundleID),
            ws = app.windows;

        return bindLR(
            0 < ws.length ? (
                Right(ws.at(0))
            ) : Left(`No windows open in ${bundleID}`)
        )(
            w => {
                const tabs = w.tabs;

                return 0 < tabs.length ? (() => {
                    const
                        tab = w[
                        [
                            "com.apple.Safari",
                            "com.kagi.kagimacOS"
                        ].includes(bundleID) ? (
                                "currentTab"
                            ) : "activeTab"
                        ]();

                    return Right(
                        `[${tab.name()}](${tab.url()})`
                    );
                })() : Left(
                    `No open tabs in front window of ${bundleID}`
                );
            }
        );
    };

    // ----------------------- JXA -----------------------

    // frontAppBundleId :: () -> String
    const frontAppBundleId = () => {
        const uw = ObjC.unwrap;

        return uw(uw(
            $.NSWorkspace.sharedWorkspace.activeApplication
        ).NSApplicationBundleIdentifier);
    };

    // ------- DEFAULT - DOCUMENT OF FRONT WINDOW --------

    // appFrontWindowMDLinkLR :: String -> Either String String
    const appFrontWindowMDLinkLR = bundleID => {
        const
            procs = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                })
            .applicationProcesses.where({
                bundleIdentifier: bundleID
            });

        return bindLR(
            bindLR(
                procs.length > 0 ? (
                    Right(procs.at(0).windows)
                ) : Left(`Application not found: ${bundleID}`)
            )(ws => ws.length > 0 ? (
                Right(ws.at(0))
            ) : Left(`No windows found for ${bundleID}`))
        )(w => {
            const
                uw = ObjC.unwrap,
                [winTitle, maybeDocURL] = [
                    "AXTitle", "AXDocument"
                ]
                .map(appID => uw(
                    w.attributes.byName(appID).value()
                ));

            return Boolean(maybeDocURL) ? (
                Right(`[${winTitle}](${maybeDocURL})`)
            ) : Left(
                [
                    `Window "${winTitle}" of:\n\n\t${bundleID}`,
                    "\nmay not be a document window.",
                    `\nConsider adding a macro named "${bundleID}"`,
                    `to the KM Group "${kmGroupName}".`,
                    "\n(Or request such a macro, which should",
                    "save a [label](url) string) in the",
                    "KM variable \"mdLink\")",
                    "on the Keyboard Maestro forum)."
                ].join("\n")
            );
        });
    };

    // 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
            );
        };


    // ----------------- GENERIC FUNCTIONS -----------------
    // https://github.com/RobTrew/prelude-jxa

    // 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 => undefined !== m.Left ? (
            m
        ) : mf(m.Right);


    // 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 => "Either" === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;


    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        // Either a message, or a JS value obtained
        // from a successful parse of s.
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                `${e.message} (line:${e.line} col:${e.column})`
            );
        }
    };

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

I've added support for com.kagi.kagimacOS (the Orion browser) at:

RobTrew/copy-as-md-link: macOS Keyboard Maestro macro group – single keystroke to copy MD links from different applications.

3 Likes

Thank you! :tada:

1 Like

hello @ComplexPoint !

Out of more than a thousand KM macros, this one is in my top ten.
It works with Finder but I always use Pathfinder.
I tried creating a pathfinder version, but it does not work.
My attempt at modifying the script and the error message below.
thanks very much

(() => {
    'use strict';
    // Rob Trew @2020
    const
        finder = Application('com.cocoatech. PathFinder'),
        xs = PathFinder.selection();
    return 0 < xs.length ? (
        xs.map(
            x => `[${x.name()}](${x.url()})`
        ).join('\n')
    ) : (() => {
        const
            url = finder.insertionLocation().url(),
            fp = decodeURI(url).slice(7);
        return `[${fp}](${url})`;
    })();
})();

The first thing to remove is that redundant space before the P.

(Should be "com.cocoatech.PathFinder")

Beyond that, I would have to install PathFinder to test.

Does it emulate Finder's osascript interface ?


I wonder which version of Copy as Markdown Link you are using ?

The current version is always at:

RobTrew/copy-as-md-link: macOS Keyboard Maestro macro group – single keystroke to copy MD links from different applications.

( click the green Code button on that page )

I ask because having downloaded a copy of PathFinder here, it seems to be working "as is", without needing adjustment.

1 Like

Hi and thank you for your prompt reply.
I installed the latest version. It works with all apps except Pathfinder, just like the last version.
I tried without creating a new MD Link and hoping that it would use the finder script but that did not work.

Error message same as above.

So I'm back to creating a new Pathfinder MD Link, for which I simply modified the actual Finder script.

thank you for pointing out the space before Pathfinder which I corrected, and it still did not work.

I notice that in the script I see "finder" in 2 more lines. Should I modify this ?

thanks

OK, next thing:

JS value naming is case-sensitive. You've bound the name finder, but JS has no means of knowing what Finder refers to.


For which I simply modified the actual Finder script.

It's not entirely clear to me what that would mean.
You have created a new macro with PathFinder's bundle name ?


I installed the latest version.

Also a little difficult to interpret, given that I'm seeing different results here ...

1 Like

Yes, as per ScreenShot above with is a duplicate of the Finder macro.
I was wondering what to do with the 2 other lines: replace Finder.selection with Pathfnder.selection and replace finder.insertion with pathfinder.insertion ?

The screenshot doesn't show the bundle name of the macro in which you have embedded your modification of the Finder script.

It works with all apps except Pathfinder,

What are you actually seeing when you run the macro now ?

OK, so even before we get to any more glitches in your modification of the JS, we need to notice that your new macro isn't yet being found in any case.

I'll send you a PM.

1 Like

thanks very much

Added com.cocoatech.PathFinder to version at:

RobTrew/copy-as-md-link: macOS Keyboard Maestro macro group – single keystroke to copy MD links from different applications.

3 Likes