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

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:

That's right, the code I sketched is just for data capture – others may know more about LaunchBar actions and their formats and requirements – I haven't looked at them for a long time.

(But the documentation looks good)

Perhaps something to ask about on a LaunchBar forum ?

and I see there's this:

Sure, I’ll take a look at the documentation, and play about with it. And ask on the LaunchBar forum if needed.

That link looks like it could be useful too.

Thanks again!

1 Like

…I managed it, it was just a matter of changing 'text' to 'title' in:

title: kme.processTokens('%NamedClipboard%' + k + '%'),

Here's the (almost finished version)

Thanks again!

1 Like

Quick progress ! Good work.

1 Like

I remembered that I have another use case that I wanted to setup with this :blush:

I wanted a LaunchBar action to just show the first 3 named clipboards (Clipboard 01, Clipboard 02, and Clipboard 03), instead of all of the named clipboards (that I’d use when setting up projects for work).

Would it be much effort to tweak the script for this?

If it’s the Bash version you’re after, then perhaps something like this:

#!/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();
        
    // take :: Int -> [a] -> [a]
    const take = (n, xs) => xs.slice(0, n);

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

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

Yes, that’s it. Perfect, thanks!

( You may well not have time for this at the moment, but others who find this thread (now or later), might find it very helpful to have a link to (or brief explanation of) the final LB action )

1 Like

Yes that’s a good idea, I’ll definitely do that.

I just have to finish off the action first. I’m still trying to find out how to paste the selected clipboard contents to the front most app (I’ve tried a few variations with the code, but struggling with it at the moment :blush: ).

Sorry if this has already been covered above, but may I ask why you are using Named Clipboards for plain text?

It would be much easier to use KM global Variables for plain text, and probably faster too.

I have 10 named clipboards (Clipboard 1, Clipboard 2, Clipboard 3 etc.) and copy info to the named/numbered clipboards via keyboard shortcuts. Then paste the info somewhere else via keyboard shortcuts.

Because they're named/numbered clipboards it's easy for me the know/keep track of what info is copied to which clipboard.

It works really well for me, I use it all the time.

Interesting, how would this work for my use case?