Electron Adventures: Episode 76: NodeGui React Terminal App
Now that we've setup NodeGui with React, let's write a small app with it. It will yet be another terminal app, but this time there's not much code we can share, as we'll be using Qt not HTML+CSS stack.
DRY CSS
This is my first program in NodeGui. With CSS it's obvious how to write styling code in a way that doesn't repeat itself - that's what CSS have been doing for 25 years now. It's not obvious at all how to do this with NodeGui, as it doesn't seem to have any kind of CSS selectors. So prepare for a lot of copypasta.
src/App.jsx
This file isn't too bad:
- state is in
history
HistoryEntry
andCommandInput
handle display logic- since we can use arbitrary
node
we just usechild_process.execSync
to run the command we want
let child_process = require("child_process")
import { Window, hot, View } from "@nodegui/react-nodegui"
import React, { useState } from "react"
import CommandInput from "./CommandInput"
import HistoryEntry from "./HistoryEntry"
function App() {
let [history, setHistory] = useState([])
let onsubmit = (command) => {
let output = child_process.execSync(command).toString().trim()
setHistory([...history, { command, output }])
}
return (
<Window
windowTitle="NodeGui React Terminal App"
minSize={{ width: 800, height: 600 }}
>
<View style={containerStyle}>
{history.map(({ command, output }, index) => (
<HistoryEntry key={index} command={command} output={output} />
))}
<CommandInput onsubmit={onsubmit} />
</View>
</Window>
)
}
let containerStyle = `
flex: 1;
`
export default hot(App)
src/HistoryEntry.jsx
The template here is simple enough, but the CSS is quite ugly. font-family: monospace
doesn't work, I needed explicit font name. I tried gap
or flex-gap
but that's not supported, so I ended up doing old style margin-right
. And since there's no cascading everything about font-size
and font-family
is duplicated all over. There's also style duplication between this component and CommandInput
- which could be avoided by creating additional mini-components. In HTML+CSS it wouldn't be necessary, as CSS can be set on the root element and inherited, or scoped with class selectors. I don't think we have such choices here.
import { Text, View } from "@nodegui/react-nodegui"
import React from "react"
export default ({ command, output }) => {
return <>
<View styleSheet={inputLineStyle}>
<Text styleSheet={promptStyle}>$</Text>
<Text styleSheet={inputStyle}>{command}</Text>
</View>
<Text styleSheet={outputStyle}>{output}</Text>
</>
}
let inputLineStyle = `
display: flex;
flex-direction: row;
`
let promptStyle = `
font-size: 18px;
font-family: Monaco, monospace;
flex: 0;
margin-right: 0.5em;
`
let inputStyle = `
font-size: 18px;
font-family: Monaco, monospace;
color: #ffa;
flex: 1;
`
let outputStyle = `
font-size: 18px;
font-family: Monaco, monospace;
color: #afa;
white-space: pre;
padding-bottom: 0.5rem;
`
src/CommandInput.jsx
And finally the CommandInput
component. It shares some CSS duplication between elements and with the HistoryEntry
component. One nice thing is on={{ textChanged, returnPressed }}
, having explicit event for Enter being pressed looks nicer than wrapping things in form
with onsubmit
+preventDefault
.
import { Text, View, LineEdit } from "@nodegui/react-nodegui"
import React from "react"
export default ({ onsubmit }) => {
let [command, setCommand] = React.useState("")
let textChanged = (t) => setCommand(t)
let returnPressed = () => {
if (command !== "") {
onsubmit(command)
}
setCommand("")
}
return <View styleSheet={inputLineStyle}>
<Text styleSheet={promptStyle}>$</Text>
<LineEdit
styleSheet={lineEditStyle}
text={command}
on={{ textChanged, returnPressed }}
/>
</View>
}
let inputLineStyle = `
display: flex;
flex-direction: row;
`
let promptStyle = `
font-size: 18px;
font-family: Monaco, monospace;
flex: 0;
margin-right: 0.5em;
`
let lineEditStyle = `
flex: 1;
font-size: 18px;
font-family: Monaco, monospace;
`
Overall impressions
So my impressions of dev experience are mostly negative because I'm used to HTML+CSS, and there's a lot of stuff that I take for granted in HTML+CSS that's absent here. But still, it's familiar enough that it doesn't feel like a completely alien environment.
Leaving browsers with their extremely complex APIs for Qt will likely mean it's going to be much easier to secure apps like this than Electron apps.
And for what it's worth, Qt has its own ecosystem of libraries and widgets, so it's totally possible there's something there that would be difficult to achieve with browser APIs.
Of all Electron alternatives I've tred, NodeGui has the most obvious story why you should consider it. NW.js is basically Electron with slightly different API and less popular; Neutralino is a lot more limited for no obvious benefit; NodeGui is Electron-like but it comes with very different set of features and also limitations.
Results
Here's the results:
There are more "Electron alternatives", but I think I covered the most direct competitors, as I have zero interest in writing frontends in Dart, Rust, or C#. In the next episode we'll go back to the regular Electron and try some of the features we haven't covered yet.
As usual, all the code for the episode is here.