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
}
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
}
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
}
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})
})
})
}
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
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>
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>
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>
Result
And here's the result:
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.
Top comments (0)