Our terminal app is getting better. The next step is adding some ways to interact with commands we run. These are three primary ways:
- input some text (by default in whole lines; not by character)
- tell command that input is done (Control-D in traditional terminal)
- tell command to stop (Control-C in traditional terminal)
runCommand
in preload.js
We're changing it again. There's a lot of events coming from the app (input
, endInput
, kill
), and a lot of events we send from the app (onout
, onerr
, ondone
):
let runCommand = ({command, onout, onerr, ondone}) => {
const proc = child_process.spawn(
command,
[],
{
shell: true,
stdio: ["pipe", "pipe", "pipe"],
},
)
proc.stdout.on("data", (data) => onout(data.toString()))
proc.stderr.on("data", (data) => onerr(data.toString()))
proc.on("close", (code) => ondone(code))
return {
kill: () => proc.kill(),
input: (data) => proc.stdin.write(data),
endInput: () => proc.stdin.end(),
}
}
We changed stdin
from ignore
to pipe
as it's now active, and now we return an object with three methods for app to use to talk to our process.
Move all the logic out of App.svelte
Initially all the logic for dealing with commands was in App.svelte
and HistoryEntry.svelte
was display only class.
This needs to be flipped - there's way too much in App.svelte
, so let's rename HistoryEntry.svelte
to Command.svelte
and move all the logic there instead.
<script>
import Command from "./Command.svelte"
import CommandInput from "./CommandInput.svelte"
let history = []
async function onsubmit(command) {
let entry = {command}
history.push(entry)
history = history
}
</script>
<h1>Svelte Terminal App</h1>
<div id="terminal">
<div id="history">
{#each history as entry}
<Command command={entry.command} />
{/each}
</div>
<CommandInput {onsubmit} />
</div>
<style>
:global(body) {
background-color: #444;
color: #fff;
font-family: monospace;
}
</style>
Input box styling in CommandInput.svelte
It's a small thing, but because now we have multiple input boxes at the same time, I changed its color a bit to make it more distinct.
input {
background-color: #666;
}
Command.svelte
template
There's a lot of things we want to do:
- add input field for entering text
- add some buttons for end of input, and for killing command
- remove spinner icon as it's redundant now - running command will have input field, done command will not
- instead of interactions being stdout first, then stderr, we want to intertwine stdin, stdout, and stderr as they are happening, so we can see things better
<div class='history-entry'>
<div class='input-line'>
<span class='prompt'>$</span>
<span class='command'>{command}</span>
</div>
{#each interactions as interaction}
<div class={interaction.type}>{interaction.data}</div>
{/each}
{#if running}
<form on:submit|preventDefault={submit}>
<input type="text" bind:value={input} />
<button type="button" on:click={endInput}>End Input</button>
<button type="button" on:click={kill}>Kill</button>
</form>
{/if}
{#if error}
<Icon data={exclamationTriangle} />
{/if}
</div>
Command.svelte
script
All the existing logic from App.svelte
as well as a bunch of new logic goes here.
The code should be clear enough. interactions
is an array of objects, each of which has a type
and data
property. type
is either stdin
, stdout
, or stderr
. data
is the actual text that was send or received.
<script>
import Icon from "svelte-awesome"
import { exclamationTriangle } from "svelte-awesome/icons"
export let command
let running = true
let interactions = []
let error = false
let input = ""
function onout(data) {
interactions.push({data, type: "stdout"})
interactions = interactions
}
function onerr(data) {
interactions.push({data, type: "stderr"})
interactions = interactions
}
function ondone(code) {
running = false
error = (code !== 0)
}
function endInput() {
proc.endInput()
}
function kill() {
proc.kill()
}
function submit() {
let data = input+"\n"
interactions.push({data, type: "stdin"})
interactions = interactions
proc.input(data)
input = ""
}
let proc = window.api.runCommand({command,onout,onerr,ondone})
</script>
Command.svelte
styling
Styling just matches what we already did, except I changed input's background color a little to distinguish inputs from the rest of the terminal.
<style>
.history-entry {
padding-bottom: 0.5rem;
}
.stdin {
color: #ffa;
white-space: pre;
}
.stdout {
color: #afa;
white-space: pre;
}
.stderr {
color: #faa;
white-space: pre;
}
.input-line {
display: flex;
gap: 0.5rem;
}
.command {
color: #ffa;
flex: 1;
}
form {
flex: 1;
display: flex;
}
input {
flex: 1;
font-family: inherit;
background-color: #666;
color: inherit;
border: none;
}
</style>
Result
And here's the result:
The terminal still has some limitations, most obviously:
- running a command creates new unfocused input box, so you need to focus on it manually; then when command finishes, you need to manually focus on input for the new command
- keyboard shortcuts like Control-D and Control-C don't work
-
cd
command doesn't work - any command that generates binary data, too much data, or data that's not line-based text, will work very poorly
But it's still going quite well.
For the next episode we'll take a break from our terminal app and try to code something different.
As usual, all the code for the episode is here.
Top comments (0)