Ah, OK. I’ll look at LaunchBar’s documentation and see if there’s a way to make this work.
Thanks again
Ah, OK. I’ll look at LaunchBar’s documentation and see if there’s a way to make this work.
Thanks again
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}]
})();
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
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’?
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!
…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!
Quick progress ! Good work.
I remembered that I have another use case that I wanted to setup with this
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
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 )
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 ).
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?