Control your desktop layout with React
Tags: workflowwm, react, showdev
Published: 2018-08-30 14:10:36

---
title: Control your desktop layout with React
published: true
description: An introduction to writing flow files for workflow
tags: workflow-wm, react, showdev
---

This post is a brief introduction to writing flow files for [`workflow`](https://github.com/havardh/workflow). If you are new to `workflow`, then the [introduction post](https://dev.to/havardh/introducing-workflow-18h6) is a good place to start. The next [post](https://dev.to/havardh/spotify-on-workflow-4fo6) shows you how to add support for new apps.

_Updated code samples to support [workflow@2.x](https://github.com/havardh/workflow/commit/ca77860)_. 

![code-sample](https://thepracticaldev.s3.amazonaws.com/i/f8v95okq5cqmio7pooit.png)

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`](https://github.com/havardh/workflow/tree/master/packages/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]](#footnotes), 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. 

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

export const flow = render(
  <Workspace name="workflow-watch-test" />
);
```

### 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]](#footnotes)

```jsx
<Workspace name="workflow-watch-test" args="path" />
```

### Defining the layout

Workflow has two main packages for defining layouts. The [`workflow-layout-yoga`](https://github.com/havardh/workflow/tree/master/packages/workflow-layout-yoga) package provides an api similar to `Flexbox` of the CSS language. The [`workflow-layout-tiled`](https://github.com/havardh/workflow/tree/master/packages/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. 

```jsx
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>
);
```

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`](https://github.com/havardh/workflow/tree/master/packages/workflow-angular). You can read more about this functionality [here](https://github.com/havardh/workflow/tree/master/packages/workflow-react#the-requirecomponent-function).

### 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](https://github.com/havardh/workflow/tree/master/packages/create-workflow-home/files/static#appsdefaultsjs) 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.

```jsx
const {Terminal, TextEditor} = requireComponent("workflow-apps-defaults");
```

#### Loading the source file

By default [[3]](#footnotes), `workflow` will call all properties in app nodes which are functions [[4]](#footnotes) 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.

```jsx
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} />
        <... />
      </...>
);
```

#### Loading the test file

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

```jsx
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} />
        <... />
      </...>
);
```

#### Launching the test in a terminal

The last component make use of the `npm` package [`execa`](https://github.com/sindresorhus/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.

```jsx
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]}
      />
    </...>
);
```


### 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. 

```bash
$  workflow WorkflowWatchTest.js /path/to/workflow-repo/packages/workflow-resolver-absolute/src/index.js
```

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.

```jsx
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>
);


```

### <a name="footnotes"></a>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:

```jsx
<Emacs percent={0.8}>
  <SplitH >
    <File percent={0.5} file={sourceFile} />
    <File percent={0.5} file={testFile} />
  </SplitH>
</Emacs>
```

See a complete example [here](https://github.com/havardh/workflow/blob/17411a81a4b2af02c487c11751ed8b94bc2b5005/packages/workflow-app-emacs/flows/Example.js).

[2]: Due to a [bug](https://github.com/havardh/workflow/issues/108) 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`](https://github.com/havardh/workflow/tree/master/packages/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.