Showing GUIs from Shell Scripts

Posted on October 12, 2021 by Olivier Goffart and Simon Hausmann

Ever written a quick (shell) script to automate a small task at some point? Then that script grew over the years and perhaps ended up in the hands of way more users than you originally anticipated?

If you were lucky enough to experience this, maybe you wondered: Wouldn't it be nice to have a GUI for that script?

Now, you probably don't want to rewrite this script in another language to get access to powerful GUI libraries. But you still want to use a proper GUI library. And you also want something that accepts and returns complex, structured data. Finally, this GUI should run eveywhere that script runs.

Teaser

If you have rust/cargo installed, copy this code snippet and paste it into your shell on Linux or macOS.

#!/bin/bash

# install sixtyfps-viewer (do nothing if it is already installed)
cargo install sixtyfps-viewer

output=$(sixtyfps-viewer - --save-data - << EOF
import { StandardButton, LineEdit, GridBox } from "sixtyfps_widgets.60";
_ := Dialog {
    StandardButton { kind: ok; }
    StandardButton { kind: cancel; }
    property name <=> name-le.text;
    property address <=> address-le.text;
    GridBox {
        Row {
            Text { text: "Enter your name:"; }
            name-le := LineEdit { }
        }
        Row {
            Text { text: "Address:"; }
            address-le := LineEdit { }
        }
    }
}
EOF
)
if [ $? -eq 0 ]; then
    name=`echo $output | grep -o '"name": *"[^"]*"' | grep -o '"[^"]*"$'`
    address=`echo $output | grep -o '"address": *"[^"]*"' | grep -o '"[^"]*"$'`
    echo "Your name is $name and you live in $address"
fi
#!/bin/bash

# install sixtyfps-viewer
# (do nothing if it is already installed)
cargo install sixtyfps-viewer

out=$(sixtyfps-viewer - --save-data - \
<< EOF
import {
  StandardButton, LineEdit, GridBox
} from "sixtyfps_widgets.60";
_ := Dialog {
  StandardButton { kind: ok; }
  StandardButton { kind: cancel; }
  property name <=> name-le.text;
  property address <=> address-le.text;
  GridBox {
    Row {
      Text {
        text: "Enter your name:";
      }
      name-le := LineEdit { }
    }
    Row {
      Text { text: "Address:"; }
      address-le := LineEdit { }
    }
  }
}
EOF
)
if [ $? -eq 0 ]; then
  name=`echo $out \
      | grep -o '"name": *"[^"]*"' \
      | grep -o '"[^"]*"$'`
  address=`echo $out \
      | grep -o '"address": *"[^"]*"' \
      | grep -o '"[^"]*"$'`
  echo "Your name is $name and you \
      live in $address"
fi
You should see the following dialog. When you click OK, it prints Your name is "Olivier" and you live in "Berlin".

Screenshot of sixtyfps-viewer

Using SixtyFPS from Shell Scripts

SixtyFPS is a new GUI framework for desktop and embedded applications. It is written in Rust but can be used from other programming languages.

The sixtyfps-viewer utility is a tool that we introduced to preview .60 design markup files. Since it is written in Rust, you can install it by running cargo install sixtyfps-viewer. Besides the preview functionality, we have added features that help you show GUIs from shell scripts.

The .60 design markup language describes your UI in a declarative way with a familiar syntax. Check out our language reference for details.

With sixtyfps-viewer, you can either load a .60 file, or pass a "-" to load from stdin. The design is then shown on the screen and you can interact with it like a real application.

What's new is that any properties declared at the top-level can be loaded and saved with JSON. The --save-data - option causes the the viewer to write the properties as key-value pairs to stdout when quitting, or you could also write it into a temporary file. The --load-data option reads a JSON object and populates the key-value pairs to the properties declared at the top-level.

To persist the value of widgets like LineEdit, declare aliases, similar to the teaser example: property name <=> name-le.text; This re-exports the name-le's text property as "name".

SysInfo Example

You can find a more advanced example in our Git repository: This sysinfo_linux.sh script collects information about your system, formats it to JSON, feeds it into sixtyfps-viewer and presents a system information dialog. Next to it is sysinfo_mac.sh script that does the same for macOS, and they both use the same user interface file.

On Linux it looks like this:

Conclusion

These examples demonstrate a simple way of separating a dialog user interface from input and output data. There are many ways how this could be extended, for example by feeding dynamically data updates via another file descriptor on the side, or by specifying custom callbacks.

What kind of features would you be interested in using to enhance your shell scripts with a UI? Let us know in the comments.