Run External Application from XD Plugin

Hello,

I’m looking for a way to run an application from Adobe XD plugin (like Zeplin does - it exports selected artboard and then launches Zepplin application).

I tried to find something in Adobe XD documentation and Adobe XD samples, but didn’t manage to do that.

I also know that I can register special URI prefix in system and launch specific application for that (like mailto: links are opened in mail application) but I didn’t find a way to “launch” such link from Adobe XD Plugin.

Thanks

@overflowapp I know you have implemented this. Do you mind sharing what you’ve done?

Hey @stevekwak, sure!
We use WebSockets to establish a communication between XD and Overflow.
When the Overflow app fires up it writes a port number in /plugin_settings and awaits for any package from the Overflow plugin. Same goes for the Overflow app. If the user would like to cancel syncing artboards from XD within the interface of the Overflow app we use the same channel to send stuff.

Hope this helps
Natalie

3 Likes

@overflowapp, @stevekwak thank you for such prompt response.
But what if your app is not running? Is there any way to run this app from plugin?

Unfortunately, there is no way to fire up the app.
We are too looking forward to this :slight_smile:

1 Like

@stevekwak maybe you have ideas how Zepplin application handles this case (cause I can see that Zeplin launches its application even if it’s closed)? Maybe there is some URL launcher in Adobe XD API (so I could just map my app to custom URL and then open it)?

Zeplin doesn’t actually have any plugins but is a “direct” integration (which isn’t a public API). There is a URL launcher (shell.openExternal, see https://adobexdplatform.com/plugin-docs/reference/uxp/class/Shell.html#shellopenexternalurl), but this only works for the HTTP(S) protocol (and not for “custom” protocols like file://, abc:// etc.).

There is – in fact – currently no way to launch an external application with an Adobe XD plugin (this would also mean many security risks, which is why it also shouldn’t be as easy as “just launching” something).

You could –e.g. – check if the app is running and otherwise promt the user to start it if it doesn’t.

2 Likes

Sounds reasonable. Thank you for prompt and comprehensive answer.

1 Like

On Mac you can set a link in a UI control that when clicked will launch with the file: protocol and launch the registered application.

This works on Mac:

// open page on the file system
var openLocationLink = h("a", {href:""}, "Open");
openLocationLink.setAttribute("href", "file:///Users/user/documents/project/page.html");

// open page in your plugin directory
var folder = await fileSystem.getPluginFolder();
var file = await folder.getEntry("instructions.html");
openLocationLink.setAttribute("href", "file://" + file.nativePath);

// open a folder in Finder
var folder = await fileSystem.getPluginFolder();
openLocationLink.setAttribute("href", "file://" + folder.nativePath);

I haven’t had this work on Windows.

But if the file type isn’t registered you can launch the browser and use that to redirect to a page in your plugin directory to redirect to a registered application types. Apple does this with iTunes for links to podcasts or apps. A window pops up and asks if you want you want to launch the registered link.

There are security concerns sure but they have chosen to install your plugin so there is a level of trust you have granted and in that there is a trust that an application you attempt to open would have to do with your plugin.

There has been discussion around the openExternal in the past and @kerrishotts might have some updated information on this.

2 Likes

Just to clarify: I’m not saying it should be impossible. I’m just saying this shouldn’t be as easy as just using a simple command like openExternal(). For example, there could be some sort of way where the user gets prompted if link xy should get openend.

The thing is that it would be very easy to get harmful stuff into the plugin if Adobe isn’t extremely careful here (also, there could be things like an API activating it after review by Adobe is already done), and there are always some black sheep. If Adobe isn’t extremely careful, this could potentially lead to a big damage to the image of plugins and/or even Adobe XD in general (you could even dynamically download your harmful files so you don’t have to get them through review :slightly_frowning_face:).

Therefore, while I agree there is a certain level of trust by users installing the plugin, this shouldn’t get taken “as easily” as saying “there’s some level of trust, so just do it” :wink:

Hi @Velara,

probably I’m missing something to make it work (I’m on Mac)

var openLocationLink = h(“a”, {href:“”}, “Open”);

what does that ‘h’ mean?

@PaoloBiagini It’s some sort of micro-library for creating DOM objects (cf. https://github.com/AdobeXD/plugin-samples/tree/master/ui-hello-h).

Edit: It also seems to be known (if that’s the same, I’m not quite sure :wink:) under the name ‘hyperscript’ (see https://github.com/hyperhype/hyperscript).

1 Like

thank you very much @pklaschka, I will check that as soon as possible.

1 Like

Wow – lots of great discussion on here… apologies for not getting in on this earlier, but it’s been a hectic week!

Anyway – to the points here, this is something we do have to be very careful about. It’s not so much specific developers we’re worried about here (after all, I don’t think anyone on this forum would do something malicious to their users), but more in the aggregate, and we do have to keep in mind that arbitrary code comes with considerable risks and the user is rarely in an educated position to make that determination of trust. (As we can see from the browsers and their continued refinement of secure vs unsecure pages, forms, submissions, etc.)

Specifically, when it comes to running an external application from XD, we have the following issues:

  • Security risks – already mentioned in these threads
  • Privacy concerns – access to low level information could potentially breach the user’s privacy in ways our current API surface hasn’t considered.
  • Cross-platform concerns – plugins that launch external programs are that much harder to write in a cross-platform manner.
  • Permissions and UX – how do we communicate the attempt to the end user to get access (or do we at all), and if so, how do we ensure that users have the information they need to make an educated decision. (And then, what happens if the user rejects the attempt?)

None of these are necessarily insurmountable, but we have to be very careful here. Right now we are exploring what this would look like, but I’ve no additional news to share at this point precisely because there are huge boulders to address.

For now, I think the best thing the community can do is to continue submitting specific use cases – as in, why do you need to launch a particular external application (the Zepplin example is a good point)? That knowledge will hopefully lead us to a better solution that can be done in a secure, cross-platform, user-trustable manner.

3 Likes

That’s about right but it is only a single function that is used in some of the plugin examples to create elements for your UI.

It’s the same as calling createElement, setting properties then adding any nested elements. It’s useful in that it may be easier to read.

In fact here is the whole function:

function h(tag, props, ...children) {
    let element = document.createElement(tag);
    if (props) {
        if (props.nodeType || typeof props !== "object") {
            children.unshift(props);
        }
        else {
            for (let name in props) {
                let value = props[name];
                if (name == "style") {
                    Object.assign(element.style, value);
                }
                else {
                    element.setAttribute(name, value);
                    element[name] = value;
                }
            }
        }
    }
    for (let child of children) {
        element.appendChild(typeof child === "object" ? child : document.createTextNode(child));
    }
    return element;
}

And here it is creating an Alert dialog:

let alertDialog =
	 h("dialog", {name:"Alert"},
		h("form", { method:"dialog", style: { width: 380 }, },
		  alertForm.header = h("h1", "Header"),
		  h("label", { class: "row" },
			 alertForm.message = h("span", { }, "Message"),
		  ),
		  h("footer",
			 h("button", { uxpVariant: "cta", type: "submit", onclick(e){ closeDialog(alertDialog) } }, "OK")
		  )
		)
	 )
2 Likes

many thanks for all explanations!

I directly inserted the h function inside my main.js but, calling this function the folder doesn’t open in Finder (‘exportFolder’ being set early in the code)

function openExportFolder()
{
    var openLocationLink = h("a", {href:""}, "Open");
    openLocationLink.setAttribute("href", "file://" + exportFolder.nativePath);

    console.log(exportFolder.nativePath);
 }

though exportFolder.nativePath printed in the console is right.

This is not the same as shell.openExternal() and you don’t have to use h.

You have to create a hyperlink in your plugin’s UI and then when the user clicks on it it will open your folder.

So something like this:

/**
* Shorthand for creating Elements.
* @param {*} tag The tag name of the element.
* @param {*} [props] Optional props.
* @param {*} children Child elements or strings
*/
function h(tag, props, ...children) {
    let element = document.createElement(tag);
    if (props) {
        if (props.nodeType || typeof props !== "object") {
            children.unshift(props);
        }
        else {
            for (let name in props) {
                let value = props[name];
                if (name == "style") {
                    Object.assign(element.style, value);
                }
                else {
                    element.setAttribute(name, value);
                    element[name] = value;
                }
            }
        }
    }
    for (let child of children) {
        element.appendChild(typeof child === "object" ? child : document.createTextNode(child));
    }
    return element;
}

function onsubmit() {
    //  dialog is automatically closed after submit unless you call e.preventDefault()
}


let labelWidth = 75;
let hyperlinkLabel = null;
let dialog =
    h("dialog",
        h("form", { method:"dialog", style: { width: 380 }, onsubmit },
            h("h1", "Export Complete"),

            h("label", { class: "row", marginTop:10 },
                h("span", { style: { width: labelWidth, flex: "1" } }, "Export completed successfully!")
            ),
                
            h("footer",
                h("label", { class: "row", paddingTop:0, style: {border:"0px solid #888888",  alignItems:"center"} },
                    hyperlinkLabel = h("a", { href:"",
                        style: {
                            textAlign: "center", marginTop:"0", position:"relative", top:"0", height:23, paddingTop:"4.5",
                            flex: "1" , backgroundColor:"#2D96F0", border:"0px solid #888888", verticalAlign:"middle",
                            paddingLeft:"16", paddingRight:"16", marginRight:"0", borderRadius: "12",
                            fontSize:"11px", fontWeight:"700", color:"#FFFFFF"
                        }
                    }, "Reveal in Finder")
                ),
                h("button", { uxpVariant: "cta", type: "submit", onclick(e){ onsubmit(); dialog.close(); e.preventDefault; } }, "Close")
            )
        )
    )
    
async function openAlert() {
    const fileSystem = require("uxp").storage.localFileSystem;
    document.body.appendChild(dialog);
    dialog.showModal();
    const pluginFolder = await fileSystem.getPluginFolder();
    var path = "file://" + pluginFolder.nativePath;
    hyperlinkLabel.href = path;
    hyperlinkLabel.title = path;
}

module.exports = {
    commands: {
        menuCommand: openAlert
    }
};

Manifest:

{
    "id": "UI_OPEN_IN_FINDER",
    "name": "(UI) Open in Finder",
    "version": "1.0.0",
    "host": {
        "app": "XD",
        "minVersion": "13.0.0"
    },
    "uiEntryPoints": [
        {
            "type": "menu",
            "label": "Open in Finder",
            "commandId": "menuCommand"
        }
    ]
}

OpenInFinder.xdx (2.7 KB)

Using hyperlink works, thank you @Velara!
Too bad we can’t use any hover/click effect to simulate a button.

2 Likes

@PaoloBiagini Did you test this on Windows? Just as a “warning”: I once tried to achieve this and it didn’t work on WIndows, so you might have to test if this really works on Windows machines (please note: I’m not saying it doesn’t work – I could also have done something wrong when I tried it – I’m just saying you’ll need to test it thoroughly :wink:) .

hi @pklaschka, thank you.
unfortunately I can’t make any test on a Windows system at the moment.
Did you get a warning in the console?