DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Updated on

Electron Adventures: Episode 15: Async Command Execution

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:

function onsubmit(command) {
  let output = window.api.runCommand(command)
  history.push({command, output})
  history = history
}
Enter fullscreen mode Exit fullscreen mode

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

async function onsubmit(command) {
  let output = await window.api.runCommand(command)
  history.push({command, output})
  history = history
}
Enter fullscreen mode Exit fullscreen mode

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:

  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
  }
Enter fullscreen mode Exit fullscreen mode

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:

let runCommand = (command) => {
  return new Promise((resolve, reject) => {
    child_process.exec(command, (error, stdout, stderr) => {
      resolve({stdout, stderr, error})
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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:

$ npm i --save svelte-awesome
Enter fullscreen mode Exit fullscreen mode

src/HistoryEntry.svelte

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

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

  export let command, stdout, stderr, error, running
</script>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

And finally some CSS, only slightly adjusted from before:

<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>
Enter fullscreen mode Exit fullscreen mode

Result

And here's the result:

Episode 15 screenshot

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.

Discussion (0)