Get text from Named Clipboard from KM & use in AppleScript?

Hi @Jono,

You have the syntax/formatting right; you just need a different command. Since the contents of named clipboards aren’t variables, the getvariable command won’t help you here. Instead, since named clipboards can be accessed via KM text tokens using the %NamedClipboard%[CLIPBOARD NAME]% syntax, you can use the process tokens command, like this:

tell application "Keyboard Maestro Engine"
	set kmClipboard_01 to process tokens "%NamedClipboard%Clipboard 01%"
	log kmClipboard_01
end tell
1 Like

The KM Engine method that you need is processTokens (rather than getvariable)

For Execute Applescript actions:

-- Name of clipboard -> contents of named clipboard

-- namedClipContents :: String -> String
on namedClipContents(strClipName)
    tell application "Keyboard Maestro Engine"
        process tokens ("%NamedClipboard%" & strClipName & "%")
    end tell
end namedClipContents

on run
    namedClipContents("Gamma")
end run

for Execute JavaScript for Automation actions:

(() => {
    // Name of clipboard -> contents of named clipboard
    
    // namedClipContents :: String -> String
    const namedClipContents = strClipName =>
        Application('Keyboard Maestro Engine')
        .processTokens('%NamedClipboard%' + strClipName + '%')

       return namedClipContents('Gamma');
})();
1 Like

Ah, that’s why it wouldn’t work. Thanks!

How would I do that in reverse?
(Send some text from an AppleScript in Script Editor to Keyboard Maestro’s Named Clipboard called Clipboard 01?)

This is a little more complex, but it can be done. The easiest way (that I’ve found, anyway, is to copy a “Set Named Clipboard to text” action as XML, paste the XML in the AppleScript, and replace the part of the XML containing the string with "& VariableName &" like this:

tell application "Keyboard Maestro Engine"
	set clipText to "Test"
	do script "<dict>
		<key>JustDisplay</key>
		<false/>
		<key>MacroActionType</key>
		<string>SetClipboardToText</string>
		<key>TargetNamedClipboardRedundantDisplayName</key>
		<string>Sig</string>
		<key>TargetNamedClipboardUID</key>
		<string>15F896EC-ABA6-476A-BA07-4FA02B9B71B7</string>
		<key>TargetUseNamedClipboard</key>
		<true/>
		<key>Text</key>
		<string>" & clipText & "</string>
	</dict>
</array>
</plist>
"
end tell

I’m afraid you’ll need to replicate this with your own clipboards, since this one is unique to my system, but the principle should be the same.

1 Like

That’s great, thanks again!

Just tried it and works fine :+1:

1 Like

The unique UUID for a named clipboard can be obtained automatically, if you want.

See:

2 Likes

That’s useful, thanks.

…so now I’m trying to grab the contents of the Keyboard Maestro Named Clipboards and show them in LaunchBar (with the idea being I could show the named clipboards in LaunchBar and paste them into other apps from LaunchBar).

This will show the contents of each clipboard (where return isn’t commented out)

(() => {

    const kmClipboard_01 = getClipboard =>
        Application('Keyboard Maestro Engine')
        .processTokens('%NamedClipboard%Clipboard 01%')

		 return kmClipboard_01()
		
		
	const kmClipboard_02 = getClipboard =>
        Application('Keyboard Maestro Engine')
        .processTokens('%NamedClipboard%Clipboard 02%')

		// return kmClipboard_02()


	const kmClipboard_03 = getClipboard =>
        Application('Keyboard Maestro Engine')
        .processTokens('%NamedClipboard%Clipboard 03%')

		// return kmClipboard_03()	


	const kmClipboard_04 = getClipboard =>
        Application('Keyboard Maestro Engine')
        .processTokens('%NamedClipboard%Clipboard 04%')

		// return kmClipboard_04()


	const kmClipboard_05 = getClipboard =>
        Application('Keyboard Maestro Engine')
        .processTokens('%NamedClipboard%Clipboard 05%')

		// return kmClipboard_05()

})();

I guess I need to somehow turn the contents of each clipboard into variables? And then combine them in a script like this, inserting the variables in place of the existing titles?
function run() {

    return [{ 'title' : 'Library', 'subtitle' : '~/Library', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library' },
			      { 'title' : 'Preferences', 'subtitle' : '~/Library/Preferences', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library/Preferences' },
            { 'title' : 'Application Support', 'subtitle' : '~/Library/Application Support', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library/Application Support' },
			      { 'title' : 'Containers', 'subtitle' : '~/Library/Containers', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library/Containers' },
            ];

}

Given that your set of clipboard names may vary, it might be helpful to derive that list at run-time, rather than hard-wiring your current list into your script.

Something, for example, like this, to obtain a list of {name: clipName, text: String} records:

(() => {
    'use strict';

    // clipNames :: () -> [String]
    const clipNames = () =>
        ObjC.deepUnwrap(
            $.NSArray.arrayWithContentsOfFile(
                $(
                    "~/Library/Application\ Support/" +
                    "Keyboard\ Maestro/Keyboard\ Maestro" +
                    "\ Clipboards.plist"
                )
                .stringByStandardizingPath
            )
        )
        .map(clip => clip.Name)
        .sort();

    const
        kme = Application('Keyboard Maestro Engine'),
        kvs = clipNames()
        .map(k => ({
            name: k,
            text: kme.processTokens('%NamedClipboard%' + k + '%')
        }));

    return kvs; //:: [{name::String, text::String}]
})();

That's great, thanks a lot!

If I run it in Script Editor it runs fine, but if I use the code in a LaunchBar action (paste the code into the script in LaunchBar's Action Editor) it shows the following error:

ReferenceError: Can't find variable: Application (Line 21)

So I guess this is something that LaunchBar doesn't like, and not a problem but the script.
Any idea why it would throw up that error, as it looks like the variable makes sense to me? (But I don't know anything, haha :blush: )

LaunchBar uses its own internal copy of the macOS (JSContext ) JavaScript interpreter, which doesn’t include the Application object, but does have some additional objects and methods of its own.

Application is a special library added to the JSContext used by Script Editor, osascript etc.

Same JS, different set of available libraries.

Ah, OK. I’ll look at LaunchBar’s documentation and see if there’s a way to make this work.

Thanks again :slight_smile:

The details are here:

https://developer.obdev.at/launchbar-developer-documentation/#/javascript-reference

The Javascript for Automation ObjC object and its methods are incidentally, also provided by the parent Automation object:

The object tree looks like this – none of these are part of vanilla JS, or available to the JSContext used by LaunchBar:

Automation
    Application
        currentApplication
    Library
    ObjC
        $
        Ref
            equals
        bindFunction
        block
        castObjectToRef
        castRefToObject
        deepUnwrap
        dict
        import
        interactWithUser
        registerSubclass
        super
        unwrap
        wrap
    ObjectSpecifier
    Path
    Progress
    delay
    getDisplayString
    initializeGlobalObject
    log

Ah, so that means it won’t work in LaunchBar? Oh well, thanks anyway.

I think you should be able to use the LB instructions for AppleScript-based scripts, adapting them to JXA equivalents:

https://developer.obdev.at/launchbar-developer-documentation/#/implementing-actions-applescript

(Where they say JavaScript, they mean their own JSContext)

Another, possibly simpler, approach would be to follow the instructions for using a shell script, and to construct a shell script with this kind of pattern:

#!/bin/bash

osascript -l JavaScript <<JXA_END 2>/dev/null

( JXA code pasted here )

JXA_END

Ah, so I’ve added this as a shell script:

#!/bin/bash

osascript -l JavaScript <<JXA_END 2>/dev/null

(() => {
	'use strict';

	// clipboardNames :: () -> [String]
	const clipboardNames = () =>
		ObjC.deepUnwrap(
			$.NSArray.arrayWithContentsOfFile(
				$(
					"/Users/jono/Library/Application Support/Keyboard Maestro/Keyboard Maestro Clipboards.plist"
				)
				.stringByStandardizingPath
			)
		)
		.map(clip => clip.Name)
		.sort();

	const
		kme = Application('Keyboard Maestro Engine'),
		kvs = clipboardNames()
		.map(clipboardNumber => ({
			name: clipboardNumber,
			text: kme.processTokens('%NamedClipboard%' + clipboardNumber + '%'),
			icon: 'Clipboard.icns'
		}));

	return kvs; //:: [{name::String, text::String, icon::String}]
})();

And when I run it, it says in the Console:

line 4: /Users/jono/Library/Application Support/Keyboard Maestro/Keyboard Maestro Clipboards.plist: Permission denied

Any idea how to get around that?

OK – useful result, looks like the shell doesn’t give osascript permission to read the plist.

Next option, go the the LB documentation of working with AppleScripts, and try this AS version of the same thing:

use framework "Foundation"
use scripting additions


-- clipboardNames :: () -> [String]
on clipboardNames()
    set ca to current application
    set strPath to "~/Library/Application Support/Keyboard Maestro/Keyboard Maestro Clipboards.plist"
    set oPath to (ca's NSString's stringWithString:strPath)'s ¬
        stringByStandardizingPath
    script clipName
        on |λ|(x)
            |name| of x
        end |λ|
    end script
    sort(map(clipName, unwrap((ca's NSArray's arrayWithContentsOfFile:(oPath)))))
end clipboardNames


on run
    clipboardNames()
end run


-- GENERIC FUNCTIONS ------------------------------------------------------------------

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- sort :: Ord a => [a] -> [a]
on sort(xs)
    ((current application's NSArray's arrayWithArray:xs)'s ¬
        sortedArrayUsingSelector:"compare:") as list
end sort

-- unwrap :: NSObject -> a
on unwrap(objCValue)
    if objCValue is missing value then
        missing value
    else
        set ca to current application
        item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

Tho curiously enough, the AS version is allowed by the shell to read the plist.

This seems to work (though simpler to use the AppleScript directly)

#!/bin/bash

osascript <<AS_END 2>/dev/null
use framework "Foundation"
use scripting additions


-- clipboardNames :: () -> [String]
on clipboardNames()
    set ca to current application
    set strPath to "~/Library/Application Support/Keyboard Maestro/Keyboard Maestro Clipboards.plist"
    set oPath to (ca's NSString's stringWithString:strPath)'s ¬
        stringByStandardizingPath
    script clipName
        on |λ|(x)
            |name| of x
        end |λ|
    end script
    sort(map(clipName, unwrap((ca's NSArray's arrayWithContentsOfFile:(oPath)))))
end clipboardNames


on run
    unlines(clipboardNames())
end run


-- GENERIC FUNCTIONS ------------------------------------------------------------------

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- sort :: Ord a => [a] -> [a]
on sort(xs)
    ((current application's NSArray's arrayWithArray:xs)'s ¬
        sortedArrayUsingSelector:"compare:") as list
end sort

-- unlines :: [String] -> String
on unlines(xs)
    intercalate(linefeed, xs)
end unlines

-- intercalate :: [a] -> [[a]] -> [a]
-- intercalate :: String -> [String] -> String
on intercalate(sep, xs)
    concat(intersperse(sep, xs))
end intercalate

-- concat :: [[a]] -> [a]
-- concat :: [String] -> String
on concat(xs)
    if length of xs > 0 and class of (item 1 of xs) is string then
        set acc to ""
    else
        set acc to {}
    end if
    repeat with i from 1 to length of xs
        set acc to acc & item i of xs
    end repeat
    acc
end concat

-- intersperse(0, [1,2,3]) -> [1, 0, 2, 0, 3]
-- intersperse :: Char -> String -> String
-- intersperse :: a -> [a] -> [a]
on intersperse(sep, xs)
    set lng to length of xs
    if lng > 1 then
        set acc to {item 1 of xs}
        repeat with i from 2 to lng
            set acc to acc & {sep, item i of xs}
        end repeat
        if class of xs is string then
            concat(acc)
        else
            acc
        end if
    else
        xs
    end if
end intersperse

-- unwrap :: NSObject -> a
on unwrap(objCValue)
    if objCValue is missing value then
        missing value
    else
        set ca to current application
        item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap
AS_END

Ah - I see the problem in the JXA version, a couple of $ characters (ObjC references in JXA, but need escaping for the shell).

This should return a JSON version of the key value pairs from the shell

(There should be no need to change the path the (~ tilde) will expand to your user path

#!/bin/bash

osascript -l JavaScript <<JXA_END 2>/dev/null
(() => {
    'use strict';

    // clipNames :: () -> [String]
    const clipNames = () =>
        ObjC.deepUnwrap(
            \$.NSArray.arrayWithContentsOfFile(
                \$(
                    '~/Library/Application\ Support/' +
                    'Keyboard\ Maestro/Keyboard\ Maestro' +
                    '\ Clipboards.plist'
                )
                .stringByStandardizingPath
            )
        )
        .map(clip => clip.Name)
        .sort();

    const
        kme = Application('Keyboard Maestro Engine'),
        kvs = clipNames()
        .map(k => ({
            name: k,
            title: kme.processTokens('%NamedClipboard%' + k + '%')
        }));

    return JSON.stringify(kvs, null, 2); //:: [{name::String, title::String}]
})();
JXA_END

1 Like

thanks again for all the help on this, I really appreciate it!

The shell script now doesn’t show any errors in LaunchBar, but doesn’t show any results when run.

I tried running it in Code Runner and it works fine, and shows the results correctly in the pane below the code.

So I guess (hope) it’s ‘not quite formatting the results the way LaunchBar likes it’ or something?

Here’s an example that successfully shows the items in LaunchBar when run:

function run() {

    return [{ 'title' : 'Library', 'subtitle' : '~/Library', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library' },
			{ 'title' : 'Preferences', 'subtitle' : '~/Library/Preferences', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library/Preferences' },
            { 'title' : 'Application Support', 'subtitle' : '~/Library/Application Support', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library/Application Support' },
		    { 'title' : 'Containers', 'subtitle' : '~/Library/Containers', 'icon' : 'Library_Folder_Icon.icns', 'path' : '~/Library/Containers' },
            ];

}

is there anything in there that shows how ‘LaunchBar likes it to be formatted’? :blush: