Electron Adventures: Episode 76: NodeGui React Terminal App

taw

Tomasz Wegrzanowski

Posted on October 7, 2021

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 and CommandInput handle display logic
  • since we can use arbitrary node we just use child_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)
Enter fullscreen mode Exit fullscreen mode

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

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

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:

Episode 76 Screenshot

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.

💖 💪 🙅 🚩
taw
Tomasz Wegrzanowski

Posted on October 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related