Electron Adventures: Episode 94: Opal Ruby Terminal App

Now that we have Opal setup, let's try to use it to write an app - the classic terminal app we've done so many times already, starting all the way back in episode 8.

index.js

Normally we'd have full isolation and preload code, but to not complicate things in this already complicated setup, let's just let Opal Ruby do whatever it wants by turning on nodeIntegration and contextIsolation:

let { app, BrowserWindow } = require("electron")

function createWindow() {
  let win = new BrowserWindow({
    height: 600,
    width: 800,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    }
  })
  win.loadFile(`${__dirname}/public/index.html`)
}

app.on("ready", createWindow)

app.on("window-all-closed", () => {
  app.quit()
})

As a side note, Opal Ruby can run in both browser, and node, and printing stuff to standard output prints them to either browser console (in browser), or to the terminal (in node). This mode makes Opal Ruby think it's running in a node, and its debug output will go to the terminal, even from the frontend process.

In a more proper app, we'd have a separate preload file as the only place with node integrations, so printing would go to browser's console as expected.

public/index.html

Just bringing back what we already had before:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Ruby Opal Application</title>
    <link href="app.css" rel="stylesheet" type="text/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="./build/app.js"></script>
  </body>
</html>

public/app.css

Again, just what we had before:

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;
}

src/app.rb

And the app itself! I took the existing JavaScript app we had, turned it into Ruby, and then cleaned it up a bit to look more presentable like real Ruby code.

Opal Ruby looks fairly awkward in places where it needs to integrate with JavaScript, and you can see a lot of that stuff here. In any "real" app, we'd have all that wrapping logic stuff into some library, so our main code can remain clean.

require "native"

ChildProcess = Native(`require("child_process")`)

def element(query)
  $$.document.querySelector(query)
end

def create_element(tag, className=nil, children=[])
  el = $$.document.createElement(tag)
  el.className = className if className
  children.each do |child|
    el.append child
  end
  el
end

def create_input_line(command)
  create_element("div", "input-line", [
    create_element("span", "prompt", ["$"]),
    create_element("span", "input", [command])
  ])
end

def create_terminal_history_entry(command, output)
  terminal_history = element("#history")
  terminal_history.append(create_input_line(command))
  terminal_history.append(
    create_element("div", "output", [output])
  )
end

element("form").addEventListener("submit") do |e|
  Native(e).preventDefault
  input = element("input")
  command = input.value
  output = ChildProcess.execSync(command).toString
  create_terminal_history_entry(command, output)
  input.value = ""
  input.scrollIntoView
end

Results

Here's the results:

Overall I wouldn't recommend coding like this. Opal Ruby makes sense in context of Rails, but writing standalone applications with it is really difficult. It's not quite the same as Ruby (for example - for this I tried instance_eval on Native object, and that silently didn't work), and you pretty much need to understand Opal Ruby internals to figure things out. Source maps still pointed to incorrect places.

It would be great if we reached a point where we can run non-JavaScript languages in the browser with same ease as we can do JavaScript and its special flavors, but right now we're nowhere close to that point.

If you want to try Opal Ruby anyway, there's a project that sets it all up. It might need some updating, but it could be a decent starting point.

45