App modules are at the center of workflow. They are the important glue between the internals of workflow and actual applications running on your platform. If you are new to workflow
, then the introduction post is a good place to start.
Updated code samples to support workflow@2.x.
This post will guide you through a practical example of extending workflow with support for Spotify. Workflow works cross platform, and the implementation for the various platform differs. If you need to make an app run on a specific platform, the other platforms can be skipped. This post will show how to write apps for osx
and i3
.
Running example
As a running example in this post we will create a new app component for Spotify. Spotify does not fall into the previously defined categories of applications that workflow
supports, being Terminals, Browser and Text Editors. So lets pick a simple use case for our initial version of the Spotify app. Spotify has defined a uri
specification which can be used for automation. Let us use the uri
to open Spotify with a given playlist.
Initializing the application
To get started with writing applications there is a npm
initializer called create-workflow-app
. Lets run it with the npx
cli.
npx create-workflow-app workflow-app-spotify
This will generate an example application which opens emacs
it the terminal. The three notable files are flows/Example.js
, cli.js
and src/index.js
. The cli.js
implements a simple workflow config which lets us test our app, by default it will use workflow-wm-terminal
. The yarn example
command is set up to run the flows/Example.js
.
Putting our example into code.
Lets start defining our interface by implementing the example. The spotify
cli takes an uri
parameter, one variant of this parameter lets us specify a playlist. The uri
's format is spotify:user:<username>:playlist:<playlist-id>
. So lets define an example of using this specification from jsx.
<Spotify minimized play>
<Playlist user={'<username>'} id={'<playlist id>'} />
</Spotify>
We have given our top level Spotify
component a parameter called minimized
which will cause the Spotify application to be launched minimized. And we have given it a child for specifying the playlist to open. This component has the username
and playlist id
properties. The play
prop on the Spotify
component will trigger autoplay.
The application scaffold
In the src/index.js
file we have the scaffold for making any app for workflow
. The following properties are mandatory for any app.
const Spotify = {
type: 'app',
name: 'Spotify',
params: ['minimized', 'play'],
open: ({minimized, play}, context, children) => {
// code for the app
}
};
The type
property is used by workflow
to distinguish the app node from layout
and workspace
nodes. The name
property is used in debugging information an is exposed to the wm
adapter layer. The params
is used to validate the arguments passed to the node in the open
function. The open
function is responsible for opening the application and making sure it is placed in the expected position on the screen. The parameters to the open
function are the parameters to the node in the flow, a context variable which is specific to the underlying platform and windows manager, and any child nodes passed to the node. The application it self is free to define the specification of allowed children and arguments.
Supporting workflow-wm-i3
Let us start with adding support for the i3 windows manager. The i3 windows manager is identified by context = {platform: "linux", wm: "i3"}
. It requires the app to define an additional property called xClass
[1]. For Spotify this is simply Spotify
. The open function should return a shell command which can be executed to open the application, this is specific to i3. workflow-wm-i3
will generate a layout tree
based on the xClass
which will match the various applications when opened [2].
const Spotify = {
xClass: 'Spotify',
open: ({ minimized, play }, context, children) => {
if (children.length !== 1) {
throw new Error('Spotify does not support more or less than one child node');
}
const [child] = children;
const uri = child.open(child, context, child.children);
return `spotify --uri='${uri}' &`;
}
};
We do also need to define the Platform
child node [3]. Inside the platform node we build up the uri which the spotify
node will return to workflow-wm-i3
. This design lets us easily add new types of child nodes, which will be called by the spotify node.
const Platform = {
type: "app",
name: "Platform",
params: ["user", "id"],
open: ({user, id}, context, children) => {
return `spotify:user:${user}:playlist:${id}`;
}
};
That is all it takes to add support for spotify running under workflow-wm-i3
.
Note The example above does not actually trigger autoplay on linux. If you figure out how to activate it, please have a look at this issue.
Supporting workflow-wm-osx
The OSX integration follows a more standardized method of writing apps for workflow
. workflow-wm-osx
will call each apps open function with the arguments passed to the app and the absolute position on the screen. The app is responsible for opening the application and positioning it on the given position. This is usually done with JXA [4]. For convenience, workflow-wm-osx
will pass a function called run
through the context
parameter which can be used to execute JXA
code. The basic scaffold for psudo implementation is given below.
const Spotify = {
open: async (app, context, children) => {
const uri = getUri(children, context);
await context.run(({ minimized, play, position }, uri) => {
const spotify = Application("Spotify");
spotify.activate();
const window = spotify.windows[0];
window.bounds = app.position;
spotify.playTrack(uri);
}, app, uri);
}
};
Now the most notable thing about the code above is invocation of the run
function. This will call into @jxa/run
which executes the function parameter with osascript
and returns a promise. This means that the function passed cannot be a closure and must only reference its parameters and the context provided by the osascript
environment. The code opens spotify
and sets the position of the window
to the position
in from the app
. The Spotify specific function playTrack
is used to start the playlist.
The api available on the Spotify application can be found in the Script Editor
application on OSX. It is possible to generate TypeScript definitions for the api, check out this for getting started.
Now, the actual code to make this work properly on OSX is a bit more complex. Check out the source code for the working version.
Footnotes
[1]: This is the X11 WM_CLASS
as found by the xprop
utility.
[2]: This simplistic implementation causes this bug.
[3]: For now these nodes will either use the type layout
or app
. Where a layout
node is used as a support node for positioning other nodes, and the app
node denotes something that will be visible on the screen.
[4]: JXA, or Javascript for Automation, the OSX way of writing automation scripts using Javascript
Top comments (0)