Skip to content
Download example

Everything Everywhere All At Once

This page is generated directly from the script that I use to test Gtk-Stream. The syntax and idioms used within are guaranteed to work with the latest version.

The script is intended to showcase all the features of GTK-Stream. Don't expect it to do anything useful. Here is what it looks like :

a mess of buttons and widgets

Initialization

First of all, you need to start the Gtk-Stream coprocess. Since version 0.11.3, gtk-stream provides a --hook bash option that writes all the boilerplate for you.

GTK-Stream boilerplate in Bash
$ gtk-stream --hook bash
coproc GTK_STREAM { exec gtk-stream; }
trap 'kill -INT "$GTK_STREAM_PID"; wait' EXIT
exec {GTK_STREAM_EVENTS}<&"${GTK_STREAM[0]}"
exec {GTK_STREAM_MESSAGES}>&"${GTK_STREAM[1]}"
function GTK.send() {
    if (($# > 0)); then
        printf "$@" >&"$GTK_STREAM_MESSAGES"
    else
        cat >&"$GTK_STREAM_MESSAGES"
    fi
}
function GTK.receive() {
    read -u "$GTK_STREAM_EVENTS" "$@"
}

Using this option, all we have to do is :

eval "$(gtk-stream --hook bash)"

We'll look for images in the same directory as the main script, so let's find out where it is

HERE="$(dirname "$0")"

And we can communicate with the GTK-Stream coprocess via the GTK.send and GTK.receive functions.

Starting the application

Now to the good part. First of all we need to open an application

GTK.send '<application application-id="net.gtk-stream.example.basic">'

And give it some style

GTK.send <<EOF
  <style>
    .frame { color: red; margin: 10px; }
    .tux { min-width: 100px; border: 1px solid teal; }
  </style>
EOF

And then open a window. This is a testing window, so it contains a little of everything that Gtk-Stream handles. If you need more features, you can request them on the issue tracker

GTK.send <<EOF
  <add-icon-path path="$HERE/img" />
  <window id="window1" title="Hello to all !" default-width="800" default-height="600" icon-name="logo-gtk-sm">
    <scrolled-window>
    <box id="box1" orientation="vertical">
      <box orientation="horizontal">
        <icon file="$HERE/img/logo-gtk-sm.png" />
        <button id="hello"><label text="Hello" /></button>
        <button id="hello_after"><label text="Hello after" /></button>
        <button id="close"><label text="Close" /></button>
        <button id="progress"><label text="Progress" /></button>
        <button id="open"><label text="Open..." /></button>
        <button id="remove"><label text="Remove me" /></button>
      </box>
      <separator orientation="horizontal" />
      <box>
        <entry id="entry_key" placeholder-text="A key" />
        <entry hexpand="true" id="entry_value" placeholder-text="A value" />
      </box>
      <button id="add_item"><label text="Add to dropdown" /></button>

      <grid>
        <cell x="0" y="0"><label hexpand="true" hexpand-set="true" text="Switch me" /></cell>
        <cell x="1" y="0"><switch managed="true" id="switch1" /></cell>
        <cell x="0" y="1"><label hexpand="true" hexpand-set="true" text="Switch me too" /></cell>
        <cell x="1" y="1"><switch id="switch2" /></cell>
      </grid>
      <link id="link1"><label text="Click me" /></link>
      <scale id="scale1" orientation="horizontal" adjustment="0:20:14" />
      <progress-bar id="prog" show-text="true" text="progress" fraction="0.2" />
      <dropdown id="dd" enable-search="true" search-match-mode="substring">
        <item key="a" value="Pick me"><label text="Pick me" /></item>
        <item key="b" value="No, pick me"><label text="No, pick me" /></item>
        <item key="c" value="No, me"><label text="No, me" /></item>
      </dropdown>
      <paned vexpand="true" vexpand-set="true" orientation="horizontal">
        <picture css-classes="tux" file="$HERE/img/tux.png" />
        <label text="Middle" />
        <label text="Right" />
      </paned>
      <frame css-classes="frame">
        <frame-label><label text="Some frame" /></frame-label>
        <grid id="main_grid" hexpand="true" hexpand-set="true">
          <cell x="0" y="0"><label hexpand="true" hexpand-set="true" text="A" /></cell>
          <cell x="1" y="0" h="2"><label hexpand="true" hexpand-set="true" text="B" /></cell>
          <cell x="0" y="1"><label hexpand="true" hexpand-set="true" text="C" /></cell>
        </grid>
      </frame>
      <box>
        <switch id="stack-switch" />
        <stack id="stack">
          <label id="stack/a" text="I'm on top" />
          <label id="stack/b" text="No, I am"    />
        </stack>
      </box>
      <box>
        <stack id="stack2">
          <stack-page title="AAA"><label id="stack2/a" text="AAAAAA" /></stack-page>
          <stack-page title="BBB"><label id="stack2/b" text="BBBBBB" /></stack-page>
        </stack>

        <box-prepend><stack-side-bar stack="stack2" /></box-prepend>
      </box>
    </box>
    </scrolled-window>
  </window>
EOF

The Main Loop (in Bash)

Now we have a window up. Let's define some logic ! Here are some variables that will track the state of our application.

i=0
progress=20
declare -A SWITCH_ACTIONS=( )
stack_visible="a"
dd_key=""
dd_val=""

And we can start receiving events, one at a time

while GTK.receive ev; do

We can print them

    printf "Received event: %s\n" "$ev"

We can also parse them. They are always of the form $WIDGET_ID:$EVENT_TYPE[:$ADDITIONAL_DATA]

    case "$ev" in

First example, but important : we can react to events by sending more actions to the coprocess (in this case, setting the visible-child property of a Gtk.Stack)

        stack-switch:switch:on)
            GTK.send '<set-prop id="stack" name="visible-child" value="stack/b" />';;
        stack-switch:switch:off)
            GTK.send '<set-prop id="stack" name="visible-child" value="stack/a" />';;

Other than that, you're still in Bash, so you can spawn background tasks or whatever, and those tasks can also communicate with the coprocess (because they inherited the file descriptor)

        switch1:switch:on)
            (
                set -e
                sleep 2
                GTK.send '<set-prop id="%s" name="state" value="true" />' "${ev%%:*}"
            ) &
            SWITCH_ACTIONS[${ev%%:*}]="$!"
            ;;
        switch1:switch:off)
            switch_id="${ev%%:*}"
            if [ -n "${SWITCH_ACTIONS[$switch_id]}" ]; then
                kill "${SWITCH_ACTIONS[$switch_id]}"
                unset SWITCH_ACTIONS[$switch_id]
            fi
            GTK.send '<set-prop id="%s" name="state" value="false" />' "$switch_id"
            ;;

This is more of the same, setting a property when a link is clicked

        link1:clicked)
            GTK.send '<set-prop id="link1" name="visited" value="true" />'
            ;;

Another similar but important feature : you can react to an event by creating new widgets, and adding them where you want.

Here, we use the i variable to count the number of times a label has been inserted that way, and show that number in the labels themselves.

        hello:clicked)
            GTK.send '<insert into="box1"><label text="Hello &quot;%d&quot;" /></insert>' "$i"
            ((i++))
            ;;
        hello_after:clicked)
            GTK.send '<insert into="box1"><box-prepend after="link1"><label text="Hello &quot;%d&quot;" /></box-prepend></insert>' "$i"
            ((i++))
            ;;
        progress:clicked)
            ((progress+=5))
            GTK.send '<set-prop id="prog" name="fraction" value="%d.%02d" />' "$((progress / 100))" "$((progress % 100))"
            ;;

In the same vein, we can add items to a dropdown, simply by adding an to it (this seems self-evident, but GTK makes it pretty convoluted internally)

        entry_key:changed:*) dd_key="${ev#entry_key:changed:}";;
        entry_value:changed:*) dd_val="${ev#entry_value:changed:}";;
        add_item:clicked)
            GTK.send '<insert into="dd"><item key="%s" value="%s"><label text="%s" /></item></insert>' "$dd_key" "$dd_val" "$dd_val";;

We can also remove any widget from its parent (including the widget that received the event in the first place)

        remove:clicked)
            GTK.send '<remove id="remove" />';;

File dialogs are cool. Let's use one

        open:clicked)
            GTK.send '<file-dialog id="file" parent="window1" />'
            ;;

When it exits, if the user has selected a file, do a ls -l

        file:selected:*)
            ls -l "${ev#file:selected:}";;

And finally, we can close a window

        close:clicked)
            GTK.send '<close-window id="window1" />';;

When the main window is closed, exit our main loop

        window1:close-request)
            break;;
    esac
done

And close the application. You don't have to do that since Gtk-Stream will be killed when the script exits.

It's good manners to do it anyway.

GTK.send '</application>'