"No User Interaction Allowed" in Execute an AppleScript

[OS X 11.6, KM 7.3.1]

In an Execute an AppleScript action with a trivial text script that contains a display alert command, the alert causes an error, with KM showing the result of the step as

/var/folders/9l/wcyszrfx3kg7s1sv0bp68_740000j7/T/Keyboard-Maestro-Script-47157B54-2550-4DFF-B146-F998828FB65E:5:23: execution error: No user interaction allowed. (-1713)

Really?

An (Apple | Java)Script launched by KM is running in an osascript bash process, and lacks an application like Script Editor, Atom etc on which to call the display dialog|alert etc methods.

You can provide one by flanking the calls with tell application (Something) end tell

System Events is one option, but remember to activate it so that the dialog gets focus.

Ah, of course. Duh. Thanks.

I was trying to use a script that began by prompting the user for a file, then going on to several commands that called handlers in the script. I was thinking in terms of straight traditional programming: ask the user, do this, do that, with this and that handlers beginning with tell commands. The script wasn't yet at a point where what it was doing needed a context for its own purposes, so it didn't occur to me that it did need a context for user interaction.[quote="ComplexPoint, post:2, topic:5728"]
System Events is one option, but remember to activate it
[/quote]

Interesting point (without which I might have gone off another mysterious deep endI. I don't think I have ever explicitly activated System Events, just addressed in with tell.

Maybe the Finder would be better — it often serves (though more so many years ago than now) as a stand-in for OS X itself.

The real illusion was the fact that it worked when run from one of the editors (Script Debugger in my case). Running from an editor in essence wraps a tell editor around the whole script. I should have noticed that the display alert included the editor‘s icon, but details like that fade back over the edge of awareness as one routinely runs scripts in editors.

It also isn't obvious that the script runs without an application context — I always assumed the context would be the KM Engine, so even if this issue had occurred to me I think I would still have found it confusing.

I don’t know about AppleScript, but I have some JXA code I use that actually uses KM prompts. It builds a Prompt action and gets KM to run it. Works pretty well.

I think that would be worth extracting and posting, including in the Best Macros. It is really useful to break out commonly used idioms and mechanisms to share with the rest of us. It is not the same to trudge through a bunch of scripts looking for good ideas, although that is useful too.

In fact, I bet you even have already defined a function that does this, maybe even in a Script Library. Maybe you could put together a Script Library of your most general/useful JXA functions and AppleScript handlers. Maybe each should be posted separately as well, to aid search. I know you have nothing else to do, so…

I haven’t had a lot of luck with Script Libraries.

But I do have a lot of code. It’s somewhat organized, with almost non-existent documentation, and some old files I need to remove.

With that out of the way, see my GitHub repository. Specifically: KMEngine.md.

Here’s how to use the Prompt functions:

(function() {
    'use strict';

    // Insert "var KMEngine..." code here

    KMEngine.showOkMessagePrompt("My Title", "My Message");
    KMEngine.showOkMessagePrompt("My Title", "My Message", "Done");
    
    console.log("Result(1) = '" + KMEngine.showOkCancelMessagePrompt("My Title", "My Message") + "'");
    console.log("Result(2) = '" + KMEngine.showOkCancelMessagePrompt("My Title", "My Message", "OkeyDokey", "No Way") + "'");
})();

One final note about my GitHub repository (and I may clean this up shortly, or never): If you go to the JXA folder, ignore all the subfolders and only look at the files. The subfolders are obsolete (although most of the code works, I think).

Sorry, forgot about that, and I have looked at it before.[quote="DanThomas, post:7, topic:5728"]

// Insert "var KMEngine..." code here

[/quote]
Why is this commented out — is there something special here beyond

var KMEngine = Application("Keyboard Maestro Engine")

What am I missing?

Truthfully, I just posted the version with the Prompt code. So you wouldn't have seen it before. :rolling_eyes:

And as always, feel free to steal the code and use it however, including slicing and dicing. The only reason I post it is on the chance that even a small portion of it might benefit someone else. Besides, I probably stole half of the code from someone else anyway!

Dang, I just knew that wasn't intuitive. Should have re-worded it. That's where you're supposed to put the code from the repository. Here is all you need for the example above:

var KMEngine = (function() {
    var _engineApp;

    function _escapeXml(str) {
        return str.replace(/[<>&'"]/g, function (c) {
            switch (c) {
                case '<': return '&lt;';
                case '>': return '&gt;';
                case '&': return '&amp;';
                case '\'': return '&apos;';
                case '"': return '&quot;';
            }
        });
    }

    function _replaceAll(str, find, replace) {
        return str.replace(new RegExp(_escapeXml(find), 'g'), replace);
    }

    return {
        deleteVariable: function(name) {
            this.setVariable(name, "%Delete%");
        },

        doScript: function(uuidOrUniqueNameOrScript, parameter, timeout) {
            if (parameter) {
                if (timeout) {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript, { withParameter: parameter }, { timeout: timeout });
                } else {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript, { withParameter: parameter });
                }
            } else {
                if (timeout) {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript, { timeout: timeout });
                } else {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript);
                }
            }
        },

        getEngineAppName: function() {
            return "Keyboard Maestro Engine";
        },

        getEngineApp: function() {
            if (!_engineApp)
                _engineApp = Application(this.getEngineAppName());
            return _engineApp;
        },

        getVariable: function(name, required) {
            var result = this.getEngineApp().getvariable(name);
            if (!result && required)
                throw Error("Variable '" + name + "' is empty");
            return result;
        },

        setVariable: function(name, value) {
            this.getEngineApp().setvariable(name, {
                to: value
            });
        },

        // You should pass values for "title" and "message". The rest have default values.
        showOkCancelMessagePrompt: function(title, message, okButtonText, cancelButtonText, resultVariableName) {
            title = title || "";
            message = message || "";
            okButtonText = okButtonText || "OK";
            cancelButtonText = cancelButtonText || "Cancel";
            resultVariableName = resultVariableName || "showOkCancelMessagePromptResult";
            var script =
                '<dict> \n' +
                '    <key>Actions</key> \n' +
                '    <array> \n' +
                '        <dict> \n' +
                '            <key>MacroActionType</key> \n' +
                '            <string>SetVariableToText</string> \n' +
                '            <key>Text</key> \n' +
                '            <string>##cancelButtonText##</string> \n' +
                '            <key>Variable</key> \n' +
                '            <string>##resultVariableName##</string> \n' +
                '        </dict> \n' +
                '        <dict> \n' +
                '            <key>Buttons</key> \n' +
                '            <array> \n' +
                '                <dict> \n' +
                '                    <key>Button</key> \n' +
                '                    <string>##okButtonText##</string> \n' +
                '                    <key>Cancel</key> \n' +
                '                    <false/> \n' +
                '                </dict> \n' +
                '                <dict> \n' +
                '                    <key>Button</key> \n' +
                '                    <string>##cancelButtonText##</string> \n' +
                '                    <key>Cancel</key> \n' +
                '                    <true/> \n' +
                '                </dict> \n' +
                '            </array> \n' +
                '            <key>MacroActionType</key> \n' +
                '            <string>PromptForUserInput</string> \n' +
                '            <key>Prompt</key> \n' +
                '            <string>##message##</string> \n' +
                '            <key>TimeOutAbortsMacro</key> \n' +
                '            <true/> \n' +
                '            <key>Title</key> \n' +
                '            <string>##title##</string> \n' +
                '            <key>Variables</key> \n' +
                '            <array/> \n' +
                '        </dict> \n' +
                '        <dict> \n' +
                '            <key>MacroActionType</key> \n' +
                '            <string>SetVariableToText</string> \n' +
                '            <key>Text</key> \n' +
                '            <string>%Variable%Result Button%</string> \n' +
                '            <key>Variable</key> \n' +
                '            <string>##resultVariableName##</string> \n' +
                '        </dict> \n' +
                '    </array> \n' +
                '    <key>MacroActionType</key> \n' +
                '    <string>Group</string> \n' +
                '    <key>TimeOutAbortsMacro</key> \n' +
                '    <true/> \n' +
                '</dict>';
            script = _replaceAll(script, "##title##", _escapeXml(title));
            script = _replaceAll(script, "##message##", _escapeXml(message));
            script = _replaceAll(script, "##okButtonText##", _escapeXml(okButtonText));
            script = _replaceAll(script, "##cancelButtonText##", _escapeXml(cancelButtonText));
            script = _replaceAll(script, "##resultVariableName##", _escapeXml(resultVariableName));
            this.doScript(script);
            var result = this.getVariable(resultVariableName);
            this.deleteVariable(resultVariableName);
            return result;
        },

        // You should pass values for "title" and "message". The rest have default values.
        showOkMessagePrompt: function(title, message, okButtonText) {
            title = title || "";
            message = message || "";
            okButtonText = okButtonText || "OK";
            var script =
                '<dict> \n' +
                '    <key>Buttons</key> \n' +
                '    <array> \n' +
                '        <dict> \n' +
                '            <key>Button</key> \n' +
                '            <string>##okButtonText##</string> \n' +
                '        </dict> \n' +
                '    </array> \n' +
                '    <key>MacroActionType</key> \n' +
                '    <string>PromptForUserInput</string> \n' +
                '    <key>Prompt</key> \n' +
                '    <string>##message##</string> \n' +
                '    <key>TimeOutAbortsMacro</key> \n' +
                '    <true/> \n' +
                '    <key>Title</key> \n' +
                '    <string>##title##</string> \n' +
                '    <key>Variables</key> \n' +
                '    <array/> \n' +
                '</dict>';
                script = script
                    .replace("##title##", _escapeXml(title))
                    .replace("##message##", _escapeXml(message))
                    .replace("##okButtonText##", _escapeXml(okButtonText));
            this.doScript(script);
        }

    };

})();


And for completeness's sake, here's the complete object, with all functions:

var KMEngine = (function() {
    var _engineApp;

    function _escapeXml(str) {
        return str.replace(/[<>&'"]/g, function (c) {
            switch (c) {
                case '<': return '&lt;';
                case '>': return '&gt;';
                case '&': return '&amp;';
                case '\'': return '&apos;';
                case '"': return '&quot;';
            }
        });
    }

    function _replaceAll(str, find, replace) {
        return str.replace(new RegExp(_escapeXml(find), 'g'), replace);
    }

    return {
        calculate: function(str) {
            return this.getEngineApp().calculate(str);
        },

        convertStringToPlist: function(str) {
            return ObjC.deepUnwrap(
                $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(
                    $(str).dataUsingEncoding($.NSUTF8StringEncoding), 0, 0, null));
        },

        deleteVariable: function(name) {
            this.setVariable(name, "%Delete%");
        },

        doScript: function(uuidOrUniqueNameOrScript, parameter, timeout) {
            if (parameter) {
                if (timeout) {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript, { withParameter: parameter }, { timeout: timeout });
                } else {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript, { withParameter: parameter });
                }
            } else {
                if (timeout) {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript, { timeout: timeout });
                } else {
                    this.getEngineApp().doScript(uuidOrUniqueNameOrScript);
                }
            }
        },

        executing: function() {
            return this.getEngineApp().executing();
        },

        getActionsClipboardType: function() {
            return "com.stairways.keyboardmaestro.actionarray";
        },

        getAllMacrosSourceFileName: function() {
            return this.getAppSupportFolderName() + "Keyboard Maestro Macros.plist";
        },

        getAllVariableNames: function() {
            return this.getEngineApp().variables.name();
        },

        getAppSupportFolderName: function() {
            var app = Application.currentApplication();
            app.includeStandardAdditions = true;
            return app.pathTo('application support', { from: 'user domain' }) +
                "/Keyboard Maestro/";
        },

        getEngineAppName: function() {
            return "Keyboard Maestro Engine";
        },

        getEngineApp: function() {
            if (!_engineApp)
                _engineApp = Application(this.getEngineAppName());
            return _engineApp;
        },

        getHotKeys: function(asString, getAll) {
            return this.getEngineApp().gethotkeys({ asstring: !!asString, getall: !!getAll });
        },

        getHotKeysAsPlist: function(getAll) {
            return this.convertStringToPlist(this.getHotKeys(true, getAll));
        },

        getMacrosClipboardType: function() {
            return "com.stairways.keyboardmaestro.macrosarray";
        },

        getMacros: function(binary) {
            return this.getEngineApp().getmacros({
                asstring: !binary
            });
        },

        getMacrosAsPlist: function(binary) {
            return this.convertStringToPlist(this.getMacros(false));
        },

        getNamedClipboardInfo: function() {
            var path = this.getNamedClipboardsSourceFileName();
            var plist = this.readPlistBinaryFile(path);

            var result = plist.map(function(item) {
                return {name: item.Name, UID: item.UID};
            });
            result = result.sort(function(a, b) {
                if (a.name < b.name) return -1;
                if (a.name > b.name) return 1;
                return 0;
            });
            return result;
        },

        getNamedClipboardsSourceFileName: function() {
            return this.getAppSupportFolderName() +
                "Keyboard Maestro Clipboards.plist";
        },

        getVariable: function(name, required) {
            var result = this.getEngineApp().getvariable(name);
            if (!result && required)
                throw Error("Variable '" + name + "' is empty");
            return result;
        },

        playSound: function(file, soundEffect, volume) {
            var options = {};
            if (soundEffect !== undefined)
                options.soundeffect = soundEffect;
            if (volume !== undefined)
                options.volume = volume;
            this.getEngineApp().playSound(file, options);
        },

        processTokens: function(str) {
            return this.getEngineApp().processTokens(str);
        },

        readPlistBinaryFile: function(path) {
            var data = $.NSData.dataWithContentsOfFile(path);
            return ObjC.deepUnwrap(
                $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(
                    data, $.NSPropertyListBinaryFormat_v1_0, 0, null));
        },

        reload: function() {
            this.getEngineApp().reload();
        },

        setVariable: function(name, value) {
            this.getEngineApp().setvariable(name, {
                to: value
            });
        },

        // You should pass values for "title" and "message". The rest have default values.
        showOkCancelMessagePrompt: function(title, message, okButtonText, cancelButtonText, resultVariableName) {
            title = title || "";
            message = message || "";
            okButtonText = okButtonText || "OK";
            cancelButtonText = cancelButtonText || "Cancel";
            resultVariableName = resultVariableName || "showOkCancelMessagePromptResult";
            var script =
                '<dict> \n' +
                '    <key>Actions</key> \n' +
                '    <array> \n' +
                '        <dict> \n' +
                '            <key>MacroActionType</key> \n' +
                '            <string>SetVariableToText</string> \n' +
                '            <key>Text</key> \n' +
                '            <string>##cancelButtonText##</string> \n' +
                '            <key>Variable</key> \n' +
                '            <string>##resultVariableName##</string> \n' +
                '        </dict> \n' +
                '        <dict> \n' +
                '            <key>Buttons</key> \n' +
                '            <array> \n' +
                '                <dict> \n' +
                '                    <key>Button</key> \n' +
                '                    <string>##okButtonText##</string> \n' +
                '                    <key>Cancel</key> \n' +
                '                    <false/> \n' +
                '                </dict> \n' +
                '                <dict> \n' +
                '                    <key>Button</key> \n' +
                '                    <string>##cancelButtonText##</string> \n' +
                '                    <key>Cancel</key> \n' +
                '                    <true/> \n' +
                '                </dict> \n' +
                '            </array> \n' +
                '            <key>MacroActionType</key> \n' +
                '            <string>PromptForUserInput</string> \n' +
                '            <key>Prompt</key> \n' +
                '            <string>##message##</string> \n' +
                '            <key>TimeOutAbortsMacro</key> \n' +
                '            <true/> \n' +
                '            <key>Title</key> \n' +
                '            <string>##title##</string> \n' +
                '            <key>Variables</key> \n' +
                '            <array/> \n' +
                '        </dict> \n' +
                '        <dict> \n' +
                '            <key>MacroActionType</key> \n' +
                '            <string>SetVariableToText</string> \n' +
                '            <key>Text</key> \n' +
                '            <string>%Variable%Result Button%</string> \n' +
                '            <key>Variable</key> \n' +
                '            <string>##resultVariableName##</string> \n' +
                '        </dict> \n' +
                '    </array> \n' +
                '    <key>MacroActionType</key> \n' +
                '    <string>Group</string> \n' +
                '    <key>TimeOutAbortsMacro</key> \n' +
                '    <true/> \n' +
                '</dict>';
            script = _replaceAll(script, "##title##", _escapeXml(title));
            script = _replaceAll(script, "##message##", _escapeXml(message));
            script = _replaceAll(script, "##okButtonText##", _escapeXml(okButtonText));
            script = _replaceAll(script, "##cancelButtonText##", _escapeXml(cancelButtonText));
            script = _replaceAll(script, "##resultVariableName##", _escapeXml(resultVariableName));
            this.doScript(script);
            var result = this.getVariable(resultVariableName);
            this.deleteVariable(resultVariableName);
            return result;
        },

        // You should pass values for "title" and "message". The rest have default values.
        showOkMessagePrompt: function(title, message, okButtonText) {
            title = title || "";
            message = message || "";
            okButtonText = okButtonText || "OK";
            var script =
                '<dict> \n' +
                '    <key>Buttons</key> \n' +
                '    <array> \n' +
                '        <dict> \n' +
                '            <key>Button</key> \n' +
                '            <string>##okButtonText##</string> \n' +
                '        </dict> \n' +
                '    </array> \n' +
                '    <key>MacroActionType</key> \n' +
                '    <string>PromptForUserInput</string> \n' +
                '    <key>Prompt</key> \n' +
                '    <string>##message##</string> \n' +
                '    <key>TimeOutAbortsMacro</key> \n' +
                '    <true/> \n' +
                '    <key>Title</key> \n' +
                '    <string>##title##</string> \n' +
                '    <key>Variables</key> \n' +
                '    <array/> \n' +
                '</dict>';
                script = script
                    .replace("##title##", _escapeXml(title))
                    .replace("##message##", _escapeXml(message))
                    .replace("##okButtonText##", _escapeXml(okButtonText));
            this.doScript(script);
        }

    };

})();

1 Like

Thanks for sharing, Dan.

Just to make sure I understand the purpose of this script, is it the following?

Thanks.

@DanThomas, sorry, I must be having a "newbie" moment. :wink:
I don't understand how to call your script/function.

I tried these, but neither worked:

###Put call at bottom of Script

    };

showOkCancelMessagePrompt("MY Test Title", "This is a test", "OK", "Cancel", "TEST__KM_Prompt");

})();

###Put Call at very top of script file:

showOkCancelMessagePrompt("MY Test Title", "This is a test", "OK", "Cancel", "TEST__KM_Prompt");

Thanks for your help.

See here:

1 Like

I wish I had posted the script that was giving me the trouble, because I can't reproduce the problem. The following script works just fine in an Execute an AppleScript text action. Mysterious?

use scripting additions
to test()
	display alert "test"
end test
test()

From the AppleScript Release Notes 10.9:

osascript(1) is now a “UI element” process and can therefore display its own UI, such as using display dialog; telling another application such as System Events is no longer necessary. [12365409]

1 Like

diisplay dialog and display alert are brought to the foreground, but things like choose folder and choose from list are not, so it would appear that Apple didn't bother to make that very uniform.

-Chris

2 Likes

This approach works well for dialogs in “Execute an Applescript” Action:

set frontApp to path to frontmost application as text

tell application frontApp
  --- Enter Your User Prompt Statements Here ---
  display dialog "this is a test"  
end tell -- frontApp

I got this approach from someone/somewhere, but I don’t remember who/where.
If you originated this, please feel free to take credit.

1 Like

:sunglasses:

I'm getting the hint that this came from the illustrious Mr. Stone, as do most cool and great things about AppleScript. That would be @ccstone for those of you who don't know Chris.