Electron Adventures: Episode 96: Pywebview Terminal App
Tags: javascript, electron, python, qt
Published: 2021-11-12 15:09:07

Now that we've done some hello worlds in [Pywebview](https://pywebview.flowrl.com/), let's try to build something more complicated - a terminal app.

As I mentioned previously, Pywebview lacks any sort of debugging tools on the frontend, so it would be a terrible idea to try writing any serious code in it. Fortunately we already have a working terminal app, and we just need to port it to Pywebview.

### `terminal.html`

The document is nearly identical to what we had many times before:

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="./terminal.css" />
  </head>
  <body>
    <h1>Very amazing terminal app</h1>
    <div id="terminal">
      <div id="history">
      </div>

      <div class="input-line">
        <span class="prompt">$</span>
        <form>
          <input type="text" autofocus />
        </form>
      </div>
    </div>
    <script src="./terminal.js"></script>
  </body>
</html>
```

### `terminal.css`

As so is the styling:

```css
body {
  background-color: #444;
  color: #fff;
}

h1 {
  font-family: monospace;
}

#terminal {
  font-family: monospace;
}

.input-line {
  display: flex;
}

.input-line > * {
  flex: 1;
}

.input-line > .prompt {
  flex: 0;
  padding-right: 0.5rem;
}

.output {
  padding-bottom: 0.5rem;
}

.input {
  color: #ffa;
}

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

form {
  display: flex;
}

input {
  flex: 1;
  font-family: monospace;
  background-color: #444;
  color: #fff;
  border: none;
}
```

### `terminal.js`

Only one thing is new:

```javascript
let form = document.querySelector("form")
let input = document.querySelector("input")
let terminalHistory = document.querySelector("#history")

function createInputLine(command) {
  let inputLine = document.createElement("div")
  inputLine.className = "input-line"

  let promptSpan = document.createElement("span")
  promptSpan.className = "prompt"
  promptSpan.append("$")
  let inputSpan = document.createElement("span")
  inputSpan.className = "input"
  inputSpan.append(command)

  inputLine.append(promptSpan)
  inputLine.append(inputSpan)

  return inputLine
}

function createTerminalHistoryEntry(command, commandOutput) {
  let inputLine = createInputLine(command)
  let output = document.createElement("div")
  output.className = "output"
  output.append(commandOutput)
  terminalHistory.append(inputLine)
  terminalHistory.append(output)
}

form.addEventListener("submit", async (e) => {
  e.preventDefault()
  let command = input.value
  let output = await window.pywebview.api.execute(command)
  createTerminalHistoryEntry(command, output)
  input.value = ""
  input.scrollIntoView()
})
```

That thing being `let output = await window.pywebview.api.execute(command)`. The `execute(command)` function needs to be exposed by the Python backend.

### `terminal`

And finally the Python code:

```python
#!/usr/bin/env python3

import webview
import subprocess

class App:
  def execute(self, command):
    result = subprocess.run(command, capture_output=True, shell=True, encoding="utf-8")
    return result.stdout + result.stderr

app = App()

window = webview.create_window(
  "Terminal App",
  "terminal.html",
  js_api=App()
)
webview.start()
```

We just expose a single method. We need to remember to convert it to `string` (with `encoding="utf-8"`), as pywebview can't send `bytes` over, even though technically that's a valid JavaScript types these days (`Uint8Array`).

### Results

And here's the result:

![Episode 96 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2jlg7oifyjbnxoqqvypw.png)

Oh wait, what is this crap in the middle? As it turns out, our shitty OS specific webview decided to automatically turn "--" into a long dash, something nobody ever asked it to do. Neither Chrome nor Safari does that, nor any other program I've seen, it's just whichever crappy frontend Pywebview is using.

I already mentioned all the other problems with Pywebview, but this just shows again what a terrible idea it is to use whatever happens to be bundled with the OS. People often whine about Electron apps being big due to bundled browser, but that those few MBs avoid all such issues at once.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-96-pywebview-terminal-app).