Electron Adventures: Episode 94: Opal Ruby Terminal App
Tomasz Wegrzanowski
Posted on November 11, 2021
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.
As usual, all the code for the episode is here.
Posted on November 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.