Electron Adventures: Episode 15: Async Command Execution
Tags: electron, javascript, svelte
Published: 2021-08-07 23:15:22

The terminal app we created has quite a few issues, the biggest of which is that it will just hang until command you try to run completes.

A second big problem is that any error we get is currently not passed to the app.

We'll start with Svelte Terminal app from episode 13, and modify just the necessary parts.

### Not good enough approach

What we've been doing so far is:

```javascript
function onsubmit(command) {
  let output = window.api.runCommand(command)
  history.push({command, output})
  history = history
}
```

Here's one idea how we could solve the async command execution:

```javascript
async function onsubmit(command) {
  let output = await window.api.runCommand(command)
  history.push({command, output})
  history = history
}
```

We could just wait for the command to complete, and then push the result to the history. The frontend wouldn't block, so that's an improvement, but it would still behave weird - the command user entered would disappear completely, and then suddenly reappear together with its output until when done.

### Better approach

What we need to do is follow two steps - first put entry in history that a command is running. Then modify that entry to include command's output once it's done.

And since we're redoing the API, we might as well include the rest of the fields we want:

```javascript
  async function onsubmit(command) {
    let entry = {command, stdout: "", stderr: "", error: null, running: true}
    history.push(entry)
    history = history

    Object.assign(entry, {running: false}, await window.api.runCommand(command))
    history = history
  }
```

`Object.assign` seemed more convenient than fiddling with indexes. In case you're confused `history = history` is just our way of telling Svelte that `history` variable changed even though we did not reassign it. It seems a bit silly at first, but "functional" version of this would be a lot more verbose.

### New `runCommand` in `preload.js`

Node's async APIs do not do promises, they still do old school callbacks. Fortunately wrapping them in a promise is easy:

```javascript
let runCommand = (command) => {
  return new Promise((resolve, reject) => {
    child_process.exec(command, (error, stdout, stderr) => {
      resolve({stdout, stderr, error})
    })
  })
}
```

### Install font-awesome for spinner

Now we just need to change `src/HistoryEntry.svelte` to display all the information we need. I want to show that command is still running, but somehow HTML5 still doesn't have builtin `<spinner>` tag. Totally baffling, it's such a universal thing.

So we need to do this, and restart our dev server:

```shell
$ npm i --save svelte-awesome
```

### `src/HistoryEntry.svelte`

First we need to import relevant icons from `font-awesome`, and list all our properties:

```html
<script>
  import Icon from "svelte-awesome"
  import { spinner, exclamationTriangle } from "svelte-awesome/icons"

  export let command, stdout, stderr, error, running
</script>
```

So in addition to `command` and `stdout` we had before, we also have `stderr`, and two flags `error` and `running` (well `error` is actually full error message, but we only check if it's present or not).

```svelte
<div class='history-entry'>
  <div class='input-line'>
    <span class='prompt'>$</span>
    <span class='input'>{command}</span>
  </div>
  <div class='stdout'>{stdout}</div>
  <div class='stderr'>{stderr}</div>
  {#if running}
    <Icon data={spinner} pulse />
  {/if}
  {#if error}
    <Icon data={exclamationTriangle} />
  {/if}
</div>
```

And finally some CSS, only slightly adjusted from before:

```html
<style>
  .history-entry {
    padding-bottom: 0.5rem;
  }

  .stdout {
    color: #afa;
    white-space: pre;
  }

  .stderr {
    color: #faa;
    white-space: pre;
  }

  .input-line {
    display: flex;
    gap: 0.5rem;
  }

  .input {
    color: #ffa;
    flex: 1;
  }
</style>
```

### Result

And here's the result:

![Episode 15 screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lrywjo36a1yu4hoe1g3m.png)

This is now a somewhat serviceable terminal app. It shows errors, it shows when command is still running. The main issue is that it waits for the command to completely finish before it shows anything. We can address this issue in the next episode.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-15-async-command-execution).