Skip to content

GTK-Stream : A stream-based approach to GUI development

Ever felt frustrated by how complex writing a simple GUI can be ? All those callbacks and the frameworks that take over your entire program ?

Ever wanted to just say "open a window, containing a button X and a label. When X is clicked, open another window. Oh, and make the label red" ?

Ever wanted your GUIs to read like regular old single-threaded code ?

Well now they can !

GTK-Stream is a plain, old, Unix command that reads a GUI description from its standard input (formatted in XML, mainly because it's a standard well-documented format, and is similar to HTML which is used for almost all Web UIs) and outputs GUI events on its standard output.

Here is an example of what that looks like, with a GUI managed entirely in the simplest of Bash (you may have to install GTK-Stream to run it) :

#!/usr/bin/env bash
# Exit the script if an error occurs
set -e

# Start the GTK-Stream coprocess
eval "$(gtk-stream --hook bash)"

GTK.send <<EOF
<application>                   <!-- Start the application -->
  <window id="window1">         <!-- then open a window -->
    <button id="hello">
      <label text="Hello" />    <!-- containing a button -->
    </button>
  </window>                     <!-- and nothing else -->
EOF

# At this point, the window pops up, and the button can 
# be clicked up to three times before the program exits
i=0
while GTK.receive ev; do
   printf "Event: %s\n" ev
   case "$ev" in
      *:clicked)
         ((i++))
         if ((i > 3)); then break; fi
         ;;
   esac
done
# Once this is done, close the window
GTK.send '<close-window id="window1" />'
# ... and the app
GTK.send '</application>'

Why use GTK-Stream ?

Writing GUIs is a complex endeavour. It doesn't have to be this way.

Ease of programming

Most, if not all, GUI toolkits provide a way to handle user input through asynchronous callbacks. When a button is clicked, a function is called. When a user types something in a text field, a function is called. When the application is ready to run, a function is called.

There are several problems with this approach, especially for simple applications :

  • Contagion

    Think of asynchronous functions in Javascript before async/await. Callbacks force you to write your code in continuation-passing style.

    Although it is a very expressive programming style, it is also extremely unwieldy, and it colors your whole codebase.

  • State management

    Callbacks force a sometimes unnatural splitting of local state, and coupling of business logic and interaction semantics.

    Take the above example of a button pressed three times. The state of the application is stored in the plain ol' variable i, and mutated when the button is pressed. A simple, linear control flow.

    Compare that to using callbacks. The same logic would be achieved by storing that state variable i some place where it could be accessed by the button being clicked (by subscribing to a 'click' event). In that context, the callback must also be able to access the window, in order to close it.

    This situation where two unrelated parts of a program are forced together is what we call coupling, and it usually makes programs and scripts harder to maintain and modify (mainly because if you change one part, you also have to change every other coupled part)

  • Control flow

    Callbacks are complicated to think about. Much more so than a basic "when this happens, do that" model.

Language-independence

Since GUI toolkits depend on callbacks, it means that GUI can only be written in languages whose function call semantics can be understood by the toolkit. This means C, or through some sort of FFI if your language supports it.

This may prove impossible, for instance, for shell languages, where functions don't adhere to any C calling convention whatsoever.

In contrast, all languages provide a way to read and write plain strings to a pipe, and read plain strings back from another pipe.

Composability

One of the interesting aspects of the Unix shell is how it allows you to easily compose different unrelated commands to arrive at a result.

Not so with GUI applications, though. GUIs are usually monolithic, and need user interaction to provide certain results.

Using GTK-Stream may enable a new kind of application : the GUI-able kind. Indeed, a script written to follow the GTK-Stream protocol may well be connected to another program, that does not spawn windows at all and instead responds with pre-recorded events, or presents a Curses-like interface, all the while triggering the exact same underlying logic.

Conclusion

For all those reasons, when writing a simple user interface (one asking to open a file, or showing a few progress bars inching along), it's not worth the effort to write a GUI.

Maybe that's why most of graphical applications are designed for complex programs. The added complexity of a GUI toolkit doesn't matter as much when your program is already doing a lot of complex things.

But it sure matters when writing basic scripts, that expect simple interactions. Those scripts seem relegated to the command-line, where interaction is linear and synchronous. And it is not pretty, nor nearly as friendly.

GTK-Stream offers a simpler, more maintainable approach to creating GUIs for simple applications, and allows for easier integration with existing shell scripts and other command-line tools.