DEV Community

Håvard Wormdal Høiby
Håvard Wormdal Høiby

Posted on • Edited on

Control your desktop layout with React

This post is a brief introduction to writing flow files for workflow. If you are new to workflow, then the introduction post is a good place to start. The next post shows you how to add support for new apps.

Updated code samples to support workflow@2.x.

code-sample

Flow files are layout declarations, where you declare which applications you want to open and where on the screen they should be appear. This post will show you how to write a flow file as a small React application using the workflow-react package.

Running example.

As a simple example, we will build a flow file for writing unit tests. This flow will contain two text editors [1], and a terminal. The editors will open the source file and the corresponding test file, while the terminal will execute the tests in watch mode. We will assume that the project is using a file name convention, so that we can derive the location and name of the test file from the source file and vice-versa. We will also assume that the project is under source control.

Further more we assume that you are using a workflow-home folder which is derived or equivalent to the one created by workflow on the first execution.

The shell

Every flow file has a default export, the exported object should be a tree structure with a workspace node as the root node. When using workflow-react, this is achieved by calling render on a Workspace component.

import React from "react";
import { render, Workspace} from "workflow-react";

export const flow = render(
  <Workspace name="workflow-watch-test" />
);
Enter fullscreen mode Exit fullscreen mode

Accepting arguments

The workspace node has a property called args which can be used to pass a list of command line arguments that the flow will require. The parameter for our example will be the absolute path of either the test file or the source file. As we only have a single argument, we set the args prop of the component to the string name that we want to give the argument. An array of strings is passed if more values are needed. The position in this array denotes the position of the argument on the command line. [2]

<Workspace name="workflow-watch-test" args="path" />
Enter fullscreen mode Exit fullscreen mode

Defining the layout

Workflow has two main packages for defining layouts. The workflow-layout-yoga package provides an api similar to Flexbox of the CSS language. The workflow-layout-tiled is a simpler approach which exposes two nodes; a vertical and a horizontal split (SplitV and SplitH). These can be used to split the parent node into a number of child nodes horizontally or vertically with the size given by the percent property. We will use one of each of these nodes to make a 80%-20% split between the terminal and the text editors and a 50%-50% split between the text editors.

import {requireComponent} from "workflow-react";

const {SplitV, SplitH} = requireComponent("workflow-layout-tiled");

export const flow = render(
  <Workspace ...>
    <SplitV percent={1.0}>
      <SplitH percent={0.8}>
        <... percent={0.5} />
        <... percent={0.5} />
      </SplitH>
      <... percent={0.2} />
    </SplitV>
  </Workspace>
);
Enter fullscreen mode Exit fullscreen mode

Notice that the SplitV and SplitH components are imported with a custom require function which is exported by the workflow-react package. This function will internally require the npm package workflow-layout-tiled and wrap the contents into React components. This approach is used so that both layout and app definitions are in no way tied to React and can be shared with other frontends too, like workflow-angular. You can read more about this functionality here.

Instantiating the apps

Now the last piece of the puzzle is to pass the appropriate props into the app components so that they will be able to launch with the expected files and commands.

Importing the default apps

The first thing to take note of is the import. We are using the default application collection package called workflow-apps-defaults. This package exports TextEditor, Terminal, and Browser. These have defaults for each platform, but they can be overridden by the user in the workflow-home directory. By using this package, the flow file can be shared without modification by users on different platforms and with different preferences for applications.

const {Terminal, TextEditor} = requireComponent("workflow-apps-defaults");
Enter fullscreen mode Exit fullscreen mode

Loading the source file

By default [3], workflow will call all properties in app nodes which are functions [4] with the command line arguments. In this example we gave the argument the name path. We thus pass a function to the file property of the TextEditor component to make it evaluate to the path to the source file from the command line argument. The function will handle the conversion from a path to the test file, if the argument was the test file.

function sourceFile({path}) {
  if (path.includes("src")) {
    return path;
  } else {
    return path.replace("test/unit", "src").replace("_tests.js", ".js");
  }
}

export const flow = render(
     <...>
        <TextEditor ... file={sourceFile} />
        <... />
      </...>
);
Enter fullscreen mode Exit fullscreen mode

Loading the test file

The test file is loaded in exactly the same way as the source file. Just with the opposite conversion.

function testFile({path}) {
  if (path.includes("test/unit")) {
    return path;
  } else {
    return path.replace("src", "test/unit").replace(".js", "_tests.js");
  }
}

export const flow = render(
     <...>
        <TextEditor ... file={testFile} />
        <... />
      </...>
);
Enter fullscreen mode Exit fullscreen mode

Launching the test in a terminal

The last component make use of the npm package execa to execute a git command to figure out the root of the repository. The root path is used as the current working directory when opening the terminal. We reuse the testFile function to compute the the test path. This is passed to the args property of the terminal. If a property is set to an array which contains functions, these functions are also called with the command line arguments.

import {dirname} from "path";
import execa from "execa";

function projectRoot({path}) {
  const cwd = dirname(path);
  const {stdout} = execa.sync('git', ["rev-parse", "--show-toplevel"], {cwd});
  return stdout.trim();
}

export const flow = render(
    <...>
      <Terminal 
        percent={0.2} 
        cwd={projectRoot}
        cmd={"npm run test -- --watch "}
        args={[testFile]}
      />
    </...>
);
Enter fullscreen mode Exit fullscreen mode

Executing the flow

If you add the full example as the file WorkflowRunTest.js under your WORKFLOW_HOME/flows directory, and install the execa package with npm i execa. Then the following command will execute our example flow.

$  workflow WorkflowWatchTest.js /path/to/workflow-repo/packages/workflow-resolver-absolute/src/index.js
Enter fullscreen mode Exit fullscreen mode

This should open packages/workflow-resolver-absolute/src/index.js and packages/workflow-resolver-absolute/test/unit/index_tests.js in the default editor and run the npm run test -- --watch /path/to/workflow-repo/packages/workflow-resolver-absolute/test/unit/index_tests.js command in /path/to/workflow-repo.

The full example

This is the full flow file for our example.

import {dirname} from "path";
import React from "react";
import execa from "execa";
import { render, Workspace, requireComponent} from "workflow-react";

const {SplitV, SplitH} = requireComponent("workflow-layout-tiled");
const {Terminal, TextEditor} = requireComponent("workflow-apps-defaults");

function sourceFile({path}) {
  if (path.includes("src")) {
    return path;
  } else {
    return path.replace("test/unit", "src").replace("_tests.js", ".js");
  }
}

function testFile({path}) {
  if (path.includes("test/unit")) {
    return path;
  } else {
    return path.replace("src", "test/unit").replace(".js", "_tests.js");
  }
}

function projectRoot({path}) {
  const cwd = dirname(path);
  const {stdout} = execa.sync('git', ["rev-parse", "--show-toplevel"], {cwd});
  return stdout.trim();
}

export const flow = render(
  <Workspace name="workflow-watch-test" args="path">
    <SplitV percent={1.0}>
      <SplitH percent={0.8}>
        <TextEditor percent={0.5} file={sourceFile} />
        <TextEditor percent={0.5} file={testFile} />
      </SplitH>
      <Terminal
        percent={0.2}
        cwd={projectRoot}
        cmd={"npm run test -- --watch "}
        args={[testFile]}
      />
    </SplitV>
  </Workspace>
);


Enter fullscreen mode Exit fullscreen mode

Footnotes

[1]: You might be thinking that we would want one text editor with a split inside it. This is possible with workflow, but is currently only supported by a few applications like emacs. This is how:

<Emacs percent={0.8}>
  <SplitH >
    <File percent={0.5} file={sourceFile} />
    <File percent={0.5} file={testFile} />
  </SplitH>
</Emacs>
Enter fullscreen mode Exit fullscreen mode

See a complete example here.

[2]: Due to a bug in argument parsing you might need to specify the arguments as <Workspace ... args={["ignore", "path]} /> for the example to work.

[3]: If you let workflow set up your home folder on the first execution you are using the default workflow-home. It contains the workflow-transformer-apply-arguments-to-fields. This transformer implemented the functionality which passes command line arguments into properties on the app nodes.

[4]: Except for the open function. This function will be described in detail in an upcoming post about creating new apps.

Top comments (0)