29
Electron Adventures: Episode 96: Pywebview Terminal App
Now that we've done some hello worlds in Pywebview, 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.
The document is nearly identical to what we had many times before:
<!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>
As so is the styling:
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;
}
Only one thing is new:
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.
And finally the Python code:
#!/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
).
And here's the result:
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.
29