xcallbackurl
SCHEME
,
ACTION
,
PARAMETERS

The xcallbackurl statement performs an action in another application using the x-callback-url mechanism.


Parameters

This statement has three parameters:

scheme – url scheme of the target application, for example ulysses or bear.

action – action to be performed in the target application. Consult the documentation of the target application to learn what applications are available.

parameters – dictionary of parameters to be passed to the target application’s action.


Description

This statement performs an action in another application using the x-callback-url mechanism. This mechanism is an inter-app communication and messaging system that was first developed on iOS (since iOS does not support AppleEvents or AppleScript), but is also supported by some cross platform iOS/macOS applications, including Ulysses, Bear and Things. For more information on the x-callback-url system see http://x-callback-url.com.

Scheme

The scheme specifies the application you want to communicate with. If an application supports the x-callback-url communication method, it will support a url scheme. There is a list of applications that support x-callback-url at http://x-callback-url.com/apps/. Most of these apps are iOS only.

Technical note: The name “url scheme” comes from the fact that the target app is launched via a URL. For example, the scheme for Ulysses is ulysses, so internally Panorama will communicate with Ulysses using a URL in the format ulysses://x-callback-url/action?data. You don’t have to worry about the format of the URL though, because the xcallbackurl statement takes care of those details for you.

Action

The action specifies what operation should be performed by the target application. Each application that supports x-callback-url will have its own custom list of actions that can be performed. You must consult the documentation for the application you want to target to learn what actions are available.

Data

Depending on the action being performed, you may also need to supply one or more items of data. For example, if you send an action to open a document, you’ll need to specify what document should be opened. Again, you’ll need to consult the documentation for the application you are targeting to find out what data must be provided with a particular action.

The data items are passed in the form of a Panorama Data Dictionary (see Data Dictionaries). For example, we’ll assume that you have a hypothetical named MyEditor with a scheme of “myeditor”. This hypothetical application has an action of “open” that will open a document. This action requires an additional data item, the path of the document to open. Here’s how you would tell MyEditor to open the document MyBook.txt:

xcallbackurl "myeditor","open",initializedictionary("path","~/Documents/MyBook.txt")

Some actions may allow (or require) multiple data items. In that case, just add the needed items to the dictionary. For example, this code might be used to open a document in our hypothetical application in read-only mode.

xcallbackurl "myeditor","open",initializedictionary(
    "path","~/Documents/MyBook.txt",
    "editable","no")

In this example two data items are passed to the action: path and editable.

Minimizing Flashing Between Applications

Usually when xcallbackurl is used the targeted app will come to the front. If that app then passes data back to Panorama then Panorama will come back to the front. This can wind up causing a lot of annoying back and forth window “flashing” as data is passed back and forth. To eliminate this, add the data item

"in-background","true"

to the data dictionary. This value is not passed to the target application, instead, it tells Panorama that it should stay in front and not let the target application come to the front. This will make the code run faster since the windows aren’t flipping back and forth, and it will look a lot nicer to the user as well.

You will probably want to use this option whenever you are simply asking the other app to access data and give it back to Panorama. If you are asking the other application to perform an action, however, like opening a window, you probably want to leave this option off and allow the target application to come forward.

Callback Return Data

Some applications have actions that can return information back to Panorama. For example, perhaps our hypothetical MyEditor application can return information about what documents are currently open and what the contents of those documents are.

To access the information returned back to Panorama, you need to add the special label xCallbackURLSuccess: to your code. If this label exists, the code following the label will be run as a separate subroutine when the target application returns information to Panorama. There is one parameter passed to this subroutine, a dictionary that contains the data values passed back to Panorama. Consult the documentation for the target program to learn what data values are returned by each action (if any).

Let’s assume our hypothetical MyEditor application has an action of list-open-documents that returns a data value called documents. This code will display the list of documents.

xcallbackurl "myeditor","list-open-documents",
    initializedictionary("in-background","true")
return

xCallbackURLSuccess:
    let returnedData = parameter(1)
    message getdictionaryvalue(returnedData,"documents")
    return

Notice that this example is really two separate subroutines combined into one procedure. The first two lines are a subroutine, and the last four lines are a separate subroutine. Since these are separate subroutines, they run at separate times, and they cannot share local variables. You don’t know for sure when the xCallBackURLSuccess subroutine will run. In fact, if there is any kind of error in the target program, the second subroutine may not run at all.

You’ll also notice that the "in-background","true" option has been used, so that there is no window flashing as MyEditor gathers the information.

Callback Return Errors

If there is a problem with the target program, an error will be returned back to Panorama. Normally Panorama responds by displaying an error alert. However, if you wish, you can write custom code to respond to the error yourself. To do this, add the label xCallbackURLError: to your procedure, followed by the code for handling the error. This subroutine will be passed a parameter that contains a dictionary with information about the error. This dictionary will contain two values: errorMessage, which contains text describing the error, and errorCode, which contains an error number identifying the type of error (both the message and the code are generated by the target program, not by Panorama, so these messages and codes will be different depending on the program being targeted).

This example code calls MyEditor to get information about the currently edited document MyBook. If MyBook is currently being edited, it copies the title and text of the document into the Panorama database (the code assumes the database contains fields called Name and Text). If it is not being edited, it displays a notification warning.

xcallbackurl "myeditor","document-info",
    initializedictionary("document","MyBook","in-background","true")
return

xCallbackURLSuccess:
    let returnedData = parameter(1)
    let documentName = getdictionaryvalue(returnedData,"Name")
    find Name=documentName
    if info("notfound")
        addrecord
    endif
    Name = documentName
    Text = getdictionaryvalue(returnedData,"Text")
    return
xCallbackURLError:
    let errorInfo = parameter(1)
    nsnotify "x-callback-url Error!",
        "text",getdictionaryvalue(errorInfo,"errorMessage")
    return

Multiple Callbacks in a Single Procedure

If you want to use multiple callbacks in a single procedure, things get a bit more complicated. In that case, you probably want to have a separate callback subroutine for each xcallbackurl action. You can do that by using separate xCallBackURLSuccess: labels with the action name prefixed (with an underscore after the action name). Any punctuation in the action name must be omitted, just include the letters. So if the action is document-info, the label should be:

documentinfo_xCallbackURLSuccess:

Here is a real world example that takes all open documents from the MyEditor app and imports them into a Panorama database.

xcallbackurl "myeditor","list-open-documents",
    initializedictionary("in-background","true")
return

listopendocuments_xCallbackURLSuccess:
	letfileglobal myEditorOpenDocuments = getdictionaryvalue(returnedData,"documents")
	letfileglobal myEditorDocumetNumber = 1

readNextDocument:
	let documentName = nthline(myEditorOpenDocuments,myEditorDocumentNumber)
	if documentName="" return endif // if empty we are done
	myEditorDocumentNumber = myEditorDocumentNumber+1
	xcallbackurl "myeditor","document-info",
        initializedictionary(document",documentName,"in-background","true")
	return

documentinfo_xCallbackURLSuccess:
    let returnedData = parameter(1)
    let documentName = getdictionaryvalue(returnedData,"Name")
    find Name=documentName
    if info("notfound")
        addrecord
    endif
    Name = documentName
    Text = getdictionaryvalue(returnedData,"Text")
	goto readNextDocument

As you can see, it’s not possible to write this as a simple Panorama loop. This is because each callback is a separate subroutine. The Panorama code will stop after each document is imported, then resume when the target application returns the information for the next document. In the meantime, Panorama uses fileglobal variables to keep track of which document to import next.

For simplification, the example code above does not explicitly handle errors. If you do want to handle errors, you can add the action prefix to the error subroutine as well, for example

documentinfo_xCallbackURLError:

Callback Cancellation

Some target applications may give the user the option to approve or cancel a request for information. If you want your code to be notified of cancellation, add the label xCallbackURLCancel: to your code to get notified when that happens. You can also add the action prefix if you want to customize the cancellation response for just a particular action, for example newdocument_xCallbackURLCancel.

Real World Examples (Ulysses)

The examples above are purely fictional. To better illustrate how the xcallbackurl statement works, this section will show real examples of how xcallbackurl can be used to communicate between Panorama X and the writing app Ulysses. The x-callback-url API’s for Ulysses are described in a Ulysses Knowledge Base article.

Create a New Sheet in Ulysses

This first example will bring the Ulysses app forward (or open it if it is not currently open) and then create a new sheet.

xcallbackurl "ulysses","new-sheet",
    initializedictionary(
        "text","##"+datepattern(today(),"DayOfWeek, Month ddnth, yyyy")+
            lf()+lf()+
            "Now is the time for _all good men_ to quickly jump over the red fox, as fast as they possibly can. "+lf()+lf()+
            "1. Gold"+lf()+
            "2. Silver"+lf()+
            "3. Bronze"+lf()+lf()+
            "This page automatically created by Panorama X on "+info("computername")+"."
        )

Here is the result in Ulysses of running this procedure.

Since the "in-background","true" option wasn’t used, Ulysses comes to the front so that we can start editing the text.

Open a Sheet in Ulysses

This code will bring the Ulysses app forward, then navigate to the Welcome to Ulysses Overview sheet.

xcallbackurl "ulysses","open",
    initializedictionary("id","ZglGGxchbVd-0MNkvC_1Mw")

The ID value, ZglGGxchbVd-0MNkvC_1Mw, specifies what page to open. You get the ID value from Ulysses itself. To get this value, hold down the Option key and then right click on the item you are interested in. Then choose Copy Callback Identifier from the popup menu.

Now you can paste the identifier into your code. Or, you could paste the identifier into a database field, and use that field in your code. Using this technique you can link a database record with a sheet or group in Ulysses.

Display Ulysses Version

So far, we have commanded Ulysses to perform an action, but not gotten any data back. This example takes the next step, and asks Ulysses to tell us what version it is.

xcallbackurl "ulysses","get-version",initializedictionary("in-background","true")
return
    
getversion_xCallbackURLSuccess:
    alertsheet "Ulysses Version Information","text",dumpdictionary(parameter(1))
    return

The result is a dictionary that contains two values, the build number and the api version.

Note: In the code above, we used getversion_xCallbackURLSuccess: to identify the callback code, but since there is only one xcallbackurl in this procedure, the identifier could have been simply

xCallbackURLSuccess:

Get Ulysses Authorization Token

The example after this will extract text from the Ulysses database. To do that, we first have to get an authorization token from Ulysses. Here is the code that will do that.

xcallbackurl "ulysses","authorize",initializedictionary(
    "appname","PanoramaX")
return
    
xCallbackURLSuccess:
    letfileglobal ulyssesToken = getdictionaryvalue(parameter(1),"access-token")
    showvariables ulyssesToken
    return

When you run this code, Ulysses will come to the front, and it will ask if you want to allow Panorama to access information inside of Ulysses.

If you press the Allow button, Ulysses will return an access token back to Panorama. We’ll need that token later, so the code stores it in a fileglobal variable. (If the user presses the Don’t Allow button, an error is returned to Panorama.)

Displaying an Ulysses Sheet

This final example reaches into Ulysses and grabs the contents (text) of a sheet. This example code simply displays the contents, but it could just as easily copy it into a database field.

xcallbackurl "ulysses","read-sheet",initializedictionary(
    "id","ZglGGxchbVd-0MNkvC_1Mw",
    "text","yes",
    "access-token",ulyssesToken,
    "in-background","true")
return
    
xCallbackURLSuccess:
    let response = parameter(1)
    let sheetJSON = getdictionaryvalue(response,"sheet")
    let sheetDictionary = jsonimport(sheetJSON)
    let sheetText = getdictionaryvalue(sheetDictionary,"text")
    displaydata sheetText,{width=800 height=80%}
    return

xCallbackURLError:
    alertsheet "Ulysses cannot locate the requested sheet."
    return

This code uses the Ulysses read-sheet action. This action requires the sheet id, which we learned how to get from Ulysses earlier. It also requires a valid access token, so you have to run the previous example code to get the authorization token before running this code. Finally, the "text","yes" option tells Ulysses to return the full text of the sheet (in addition to a bunch of other information).

When this action is used, Ulysses returns a collection of information in JSON format. The first step is to extract this JSON text from the Panorama dictionary passed to the code.

let sheetJSON = getdictionaryvalue(response,"sheet")

The next line converts this JSON text into a Panorama dictionary.

let sheetDictionary = jsonimport(sheetJSON)

Now it is easy to extract the actual text itself.

let sheetText = getdictionaryvalue(sheetDictionary,"text")

The final line actually displays the text in a dialog.

The text is displayed in Markdown format, the native format used by Ulysses.

Note: This code also contains an xCallBackURLError: subroutine to display a custom error message if an invalid ID number is provided.

Deep Dive - How XCALLBACKURL Works

If you’re interested in the technical details behind the x-callback-url system, read on. When Panorama runs an xcallbackurl statement, it creates a special URL. For example, suppose you run this code to ask Ulysses what its version is.

xcallbackurl "ulysses","get-version",""

Panorama will convert this into a URL like this:

ulysses://x-callback-url/get-version

Panorama will then send this URL to macOS. macOS will use the scheme to figure out what application to run, in this case Ulysses. It will then pass the URL to the specified application. The application analyzes the URL to determine the action and any additional data, and performs the action.

In this example, the action involves sending the version data back to Panorama. Ulysses will assemble a URL something like this:

panoramax://x-callback-url/run/database/procedure/xCallbackURLSuccess?buildnumber=50353&apiVersion=2

Ulysses will then send this URL to macOS, which in turn will pass this URL back to Panorama. Panorama will analyze the URL, decode the data and convert it into a dictionary, and then call the xCallbackURLSuccess: label in your procedure with that dictionary as a parameter. Voila!

The URLs in the description above are actually slightly simplified from the real URLs, but you don’t need to worry about that. The xcallurl statement takes care of converting the scheme, action and data you supply into a URL in the correct format, and then decoding the URL sent back from the target program (if any). All the details are taken care of for you, and you simply need to concentrate on the API provided by the target program.


See Also


History

VersionStatusNotes
10.2NewNew in this version.