October 12, 2021 by Olivier Goffart and Simon Hausmann
Showing GUIs from Shell Scripts
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 everywhere 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"
.
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.
Slint is a declarative GUI toolkit to build native user interfaces for desktop and embedded applications written in Rust, C++, or JavaScript. Find more information at https://slint.dev/ or check out the source code at https://github.com/slint-ui/slint