quicksilver / Quicksilver

Quicksilver Project Source
http://qsapp.com
Apache License 2.0
2.74k stars 286 forks source link

Custom AppleScript actions: support JavaScript for automations (JXA) #2604

Closed n8henrie closed 2 years ago

n8henrie commented 2 years ago

Copied from: https://groups.google.com/g/blacktree-quicksilver/c/I6dkTGN0bI8/m/ebbi1s46AAAJ

Low priority. I will take another look at this after 1.7.0


After a couple days of StackOverflow threads, I can't figure out whether I can use JavaScript for Automation (JXA) in Quicksilver actions.

I have a ton of AppleScript actions, and they're one of the best parts of Quicksilver IMO, but I really hate working with AppleScript. I'm not huge on JavaScript either, but having map / filter and a more traditional syntax really is a blessing compared to AppleScript. (For context, most of my current AppleScript actions just shell out to python or what have you, which is workable.)

If you open up Script Editor, set the language to JavaScript, then open the Quicksilver dictionary, it looks like it supports the same handlers, just with slightly different names (instead of on process text it's processText).

Unfortunately I've had no luck! I've tried a million or so different configurations of the below.

const Quicksilver = Application("Quicksilver")
Quicksilver.includeStandardAdditions = true

function processText(text) {
    return "bar"
}

function openFiles(files) {
    return
}

function getIndirectTypes() {
    return [$.NSStringPboardType]
}

function getDirectTypes() {
    return [$.NSStringPboardType]
}

function getArgumentCount() {
    return 3
}

Looking through the source, I don't know enough Obj C to say what the issue may be. I do see `"AppleScript Action: No handler? Aborting..."`` in the logs, and poking around that place in the source and adding a few debug logs, I can see that it isn't finding any handlers in my test file.

Any ideas on this? I'd sure rather be writing these in JS (or python, or go, or swift, or rust, or ...) rather than AppleScript!

image

n8henrie commented 2 years ago

I've made a little progress on this by replacing NSAppleScript with OSAScript from OSAKit/OSAKit.h; it seems like the AppleScript-related functionality continues to work as a drop-in replacement, but should give QS the ability to use other scripting language like JavaScript.

but having a hard time finding much documentation on the little magic strings like aevtoapp that are used to find handlers. A few relevant links:

An example of how Hammerspoon is doing it:

Are these better documented somewhere by Apple?: @"aevtoapp", @"DAEDopnt", @"aevtodoc", @"DAEDopfl"]

Getting no hits with https://developer.apple.com/search/?q=aevtoapp&type=Documentation or https://duckduckgo.com/?t=ffab&q=aevtoapp+site%3Ahttps%3A%2F%2Fdeveloper.apple.com

n8henrie commented 2 years ago

Are these better documented somewhere by Apple?: @"aevtoapp", @"DAEDopnt", @"aevtodoc", @"DAEDopfl"]

No, these are defined in https://github.com/quicksilver/Quicksilver/blob/master/Quicksilver/Scripting/Quicksilver.sdef; each app that wants to implement "scriptability" can apparently define their own magic strings in this way.

Moreover, the key phrases from the AppleScript Dictionary such as on process text get compiled to these strings in the saved AppleScript. For example, if one opens and saves this as QSTest.scpt in Script Editor.app:

using terms from application "Quicksilver"
end using terms from

You could then see:

$ strings QStest.scpt
FasdUAS 1.101.10
daed
alis
NateSSD
Quicksilver.app
Debug
-/:private:tmp:QS:build:Debug:Quicksilver.app/
*private/tmp/QS/build/Debug/Quicksilver.app
ascr

But if you expand it to:

using terms from application "Quicksilver"
    on process text
    end process text
end using terms from

You'll now see DAEDopnt showing up:

$ strings QStest.scpt
FasdUAS 1.101.10
.DAEDopnt****
utxt
daed
alis
NateSSD
Quicksilver.app
Debug
-/:private:tmp:QS:build:Debug:Quicksilver.app/
*private/tmp/QS/build/Debug/Quicksilver.app
.DAEDopnt****
utxt
.DAEDopnt****
utxt
ascr

Interestingly, Quicksilver seems to determine what handlers a script supports by doing a plain old search for the magic string^1.

I was extremely surprised this morning to discover that one can run JXA scripts with no modifications to Quicksilver just by including these magic strings somewhere in the script. In contrast to AppleScript, when saved in Script Editor (configured as JavaScript), there is relatively little "compilation" done to JXA scripts -- one can cat them and it's mostly unchanged. I think this is why the same "search for the string" strategy works if you just put the magic string in your script.

For example, the below should work find in a current build of QS; the magic strings like DAEDopnt could be included anywhere, but I think they make sense as a comment above the function in question.

const app = Application.currentApplication()
// When *not* running from XCode, you can also use:
// const app = Application("Quicksilver")
// but from XCode, this will hang on `app.showNotification` or `app.displayDialog`
app.includeStandardAdditions = true

// DAEDopnt
function processText(dobj, {with: iobj}) {
    app.showNotification(typeof(iobj))
    app.showNotification("dobj: " + dobj + ", iobjc: " + iobj)
    return "bar"
}

// DAEDgdob
function getDirectTypes() {
    return ["NSStringPboardType", "NSFilenamesPboardType"]
}

// DAEDgiob
function getIndirectTypes() {
  return ["NSFilenamesPboardType"]
}

// Remove the asterisk to enable this handler; without disrupting the magic string,
// QS will try to use it preferentially
// D*AEDopfl
// function openFiles(dobj, {with: iobj}) {
//  return
// }

// DAEDgarc
function getArgumentCount() {
  return 3
}

Once I'd figured this out, it was pretty each to add the JavaScript version of the functions in question to the list of magic strings: https://github.com/quicksilver/Quicksilver/pull/2745

With that PR, the below JXA works as expected, including indirect

const app = Application.currentApplication()

function processText(dobj, {with: iobj}) {
    app.showNotification(typeof(iobj))
    app.showNotification("dobj: " + dobj + ", iobjc: " + iobj)
    return "bar"
}

function getDirectTypes() {
    return ["NSStringPboardType", "NSFilenamesPboardType"]
}

function getIndirectTypes() {
  return ["NSFilenamesPboardType"]
}

function getArgumentCount() {
  return 3
}

openFiles also seems to work, but I've left it out because it seems to override the other logic and disables it from accepting text input:

function openFiles(dobj, {with: iobj}) {
    app.showNotification("dobj: " + dobj + ", iobjc: " + iobj)
    return
}

I know it seems like JXA support may not be long for this world, but it certainly is much more pleasant to work with than AppleScript in my opinion; if this seems reasonable to merge, I'll be happy to update the wiki:

pjrobertson commented 2 years ago

Nice! I never knew this was a thing.

My only concern is that the function names are a bit generic (not prefixed with say QS) but I think the risk of collision is very low, and the readability benefit is worth it.

Having to hard-code all the strings is ugly - I was looking at whether there's a cleaner way to do it (e.g. here ) but I don't think it's worth the effort.

Documentation is definitely needed, but let's do away with the Wiki :) Can you copy the contents of those two files to create some new Markdown files, then add them here: https://github.com/quicksilver/manual

Once done, I'll merge this.

n8henrie commented 2 years ago

Nice! I never knew this was a thing.

Oh man, using ~QS~ JS instead of AS is such an upgrade -- map / filter alone makes it worthwhile IMO. Really a shame its support seems to be withering away. A few examples:

My only concern is that the function names are a bit generic (not prefixed with say QS)

I agree and had considered this, but the function names seem to be (automatically?) determined from the .sdef file; they're not something I came up with:

Screen Shot 2022-04-23 at 10 38 26

I think the risk of collision is very low

I agree, I included at least the function prefix and the opening parenthesis suffix in the magic search string to this effect (to hopefully avoid false positives with people making references in documentation). It should be fairly uncommon for people to unintentionally make a reference to function processText() in code that is not also going to be using that handler. Hopefully.

Of note, the existing codebase suffers from this same potential drawback, though accidentally including DAEDopnt may be less likely than accidentally including function processText( :) As I noted above, if you do happen to include the magic string DAEDopfl (or function openFiles() somewhere in your code, it will cause problems.

Ah, I'd forgotten about the manual when bemoaning the sprawl. The wiki pages for these topics are actually pretty good; I'd like to make a new section under the Features tab for Custom Actions and I'll migrate the existing stuff from the wiki and update with the JXA syntax (and perhaps a link to https://n8henrie.com/2013/03/template-for-writing-quicksilver-actions-in-applescript/ as well).

n8henrie commented 2 years ago

Not working for text objects for some reason. I'll investigate.

Obvious issues: I included closing parenthesis for function processText() in validActionsForDirectObject, and omitted function openFiles entirely. But fixing this doesn't seem to resolve the issue in my first round of testing.

n8henrie commented 2 years ago

Working now, should be fixed by https://github.com/quicksilver/Quicksilver/pull/2883