Electron Adventures: Episode 76: NodeGui React Terminal App
Tags: javascript, electron, react, qt
Published: 2021-10-07 19:18:29

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

```javascript
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.

```javascript
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`.

```javascript
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:

![Episode 76 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxmz6qg81rcpummadi1a.png)

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](https://github.com/taw/electron-adventures/tree/master/episode-76-nodegui-react-terminal-app).