DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

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>
    <meta charset="utf-8">
    <link rel="stylesheet" href="./terminal.css" />
    <h1>Very amazing terminal app</h1>
    <div id="terminal">
      <div id="history">

      <div class="input-line">
        <span class="prompt">$</span>
          <input type="text" autofocus />
    <script src="./terminal.js"></script>
Enter fullscreen mode Exit fullscreen mode


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


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"
  let inputSpan = document.createElement("span")
  inputSpan.className = "input"


  return inputLine

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

form.addEventListener("submit", async (e) => {
  let command = input.value
  let output = await window.pywebview.api.execute(command)
  createTerminalHistoryEntry(command, output)
  input.value = ""
Enter fullscreen mode Exit fullscreen mode

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 =, capture_output=True, shell=True, encoding="utf-8")
    return result.stdout + result.stderr

app = App()

window = webview.create_window(
  "Terminal App",
Enter fullscreen mode Exit fullscreen mode

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:

Episode 96 Screenshot

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.

Top comments (1)

artydev profile image

It works in windows

Image description