myxine-client

Get a GUI fast in any language under the sea!


Keywords
library, mit, Propose Tags , Myxine, Myxine.Direct, command-line, gui, html, reactive, rust, scripting
License
MIT
Install
cabal install myxine-client

Documentation

Myxine: a slithery sea-friend to help you get GUI fast

woodcut sketch of myxine glutinosa, the hagfish

Hagfish, a.k.a. myxine glutinosa, are an eel-like sea creatures best known for their ability to make a lot of slime.

By analogy, myxine quickly reduces the friction in creating a dynamic graphical interface, helping you to get GUI fast in any language under the sea.

TL;DR:

If you write a function in any programming language that makes some HTML, Myxine can give you a dynamic webpage whose content instantly reflects whatever you'd like it to show. You can then listen to events within that page to quickly prototype a reactive user interface—with only a knowledge of HTML and your favorite language.

Q: Could you show me something cool, then tell me the details after?
A: Happily, let's get started!

Q: I want to know all about how it works, then can you show me the demo after?
A: Sure thing, let's dig in!

Show me!

First, install myxine, get a cup of tea while it builds, and then come back here :)

Second, make sure you have Python 3 and the requests library installed. Myxine doesn't itself depend on them, but we'll need them presently because this example happens to be written in Python. If you have Python 3 (and therefore hopefully pip3) on your system, you can install requests with:

$ pip3 install requests

Now, in one terminal window, run:

$ cargo run --release

And in another window, run:

$ ./examples/circles.py

Then open up http://localhost:1123/ in your web browser, and mouse around! See if you can figure out what's going on by reading the Python source for this example, or read on for the full story...

Getting started

Installation

You will need a recent version of the Rust programming langauge and its build tool, cargo. If you don't have that, here's the quick-start for installing Rust. Once you have cargo installed, run:

$ cargo build --release

We're not yet on crates.io but will be soon! Once we are, you'll be able to install with cargo install myxine.

Running

Myxine is meant to run in the background. It might live longer than any individual program that uses it, and it's meant to be a service many programs might use at the same time. To get started, run:

$ cargo run --release

Let's play!

Myxine speaks to the world through HTTP requests and responses. If you can make a web request to localhost from your program, you can use myxine.

Open your browser to http://localhost:1123/, then watch what happens when you run this command in your terminal:

$ curl 'localhost:1123/' \
       -d '<h1 style="color: blue; padding: 20pt; font-family: Helvetica">
             Splish splash!
           </h1>'

What's going on:

  1. If you POST some HTML to localhost:1123/some/arbitrary/path, and then
  2. GET (i.e. navigate with your web browser) from localhost:1123/some/arbitrary/path: you'll see a web page with the HTML fragment you just posted set as the contents of its <body>.
  3. When you POST some more HTML to that same path, the changes will be instantly updated on the web page before your eyes!

Some more things you can do:

  • Set the page title: use the ?title query parameter, like this:

    $ curl 'localhost:1123/?title=Hello%20Atlantic%20Ocean!' \
           -d '<h1 style="color: blue; padding: 20pt; font-family: Helvetica">
                  What a fine day it is!
               </h1>'

    Titles will be URL-decoded, so you can use, e.g. %20 to put a space in your title.

  • Store static content: You can store other kinds of data with myxine (such as assets you want to link to). If you append to your request path the query parameter ?static, myxine will interpret your data as raw bytes, and forego injecting them into an interactive page. For best results, set the Content-Type header of your request so myxine knows what kind of data to tell your browser it's receiving.

    To publish a static piece of JSON data with curl, you might say:

    $ curl -H "Content-Type: application/json"   \
           'localhost:1123/swimming.json?static' \
           -d '{ "splish": "splash" }'

    You can still update the content with further POST requests, but a web browser won't see those changes until you reload the page.

  • Store binary files: A common gotcha is trying to upload non-text content but forgetting to send it in binary mode—this will corrupt your data in transmission. To make sure non-text things get transmitted okay, make sure you send the request in binary mode. For example, to upload an image ocean.png with curl, you could say:

    $ curl -H "Content-Type: image/png"      \
           'localhost:1123/ocean.png?static' \
           --data-binary @"ocean.png"

Interactivity

Interfaces are meant to be interactive: myxine lets you listen to events happening in the page without writing a lick of JavaScript.

Listening to the event stream

To listen to the events happening in a page, send a GET request to that page's URL, with the query string ?events. Using curl, this might look like below (some data has been elided for brevity).

$ curl 'localhost:1123/some/path?events'
id: [{"attributes":{},"tagName":"html"}]
event: mousemove
data: {"x":352,"y":237, ... }

id: []
event: blur
data: {}

:

id: []
event: focus
data: {}

id: [{"attributes":{"style":"margin: 0px; padding: 0px"},"tagName":"body"},{"attributes":{},"tagName":"html"}]
event: keydown
data: {"altKey":false,"ctrlKey":false,"key":"f","metaKey":false,"shiftKey":false, ... }

This will return an endless stream of events in the text/event-stream format, a line-based text format for streams of events with attached data.

Understanding events

In myxine's case, every event will have:

  1. id: A JSON list of "target" objects identifying the path from the most specific location of the event in the document to the least specific. Each target object in this path has a tagName string identifying the HTML tag of the corresponding element, and an attributes dictionary giving the value of each attribute of that element. The list of targets may be empty, in which case it corresponds to an event that fired directly on some top-level object in the browser (as is the case for the blur and focus events in the above example).
  2. event: The name of the JavaScript event to which this item corresponds.
  3. data: A JSON dictionary holding the properties of the event. Different events have different sets of properties associated with them, so the contents of this dictionary may vary depending on the event you are examining.

So, for instance, you click on an element <div id="something", class="cool"></div>, the corresponding event will look something like:

id: [{"tagName":"div","attributes":{"id":"something","class":"cool"}}, ... ]
event: click
data: {"x":352,"y":237, ... }

If your language doesn't implement a parser for this format, check out the 17-line Python implementation as a reference. For the technical details I used when writing this parser, see the W3C Recommendation for Server-Sent Events and look at the sections for parsing and interpretation. You can ignore everything about what to do "as a user-agent" because you are not a user-agent :)

Example: For an example of an interactive page using event subscriptions, check out the circles example in Python. Make sure myxine is running, then run:

$ ./examples/circles.py

Then load up http://localhost:1123/ and mouse around!

Supported events

There are many kinds of user-interface events which can happen in the browser. Most of them are supported by myxine, but not all. Those which aren't usually are one or more of:

  • "non-bubbling" events which need to be attached to a specific element rather than the document as a whole
  • high-frequency repeated events that fire continuously (and therefore would be an automatic performance problem)
  • events which are difficult to test support for using the hardware I have available as a developer

The master list of supported events is programmatically defined in the JSON file enabled-events.json, and the hierarchy of events and their interfaces can be visualized in this clickable graphic. This file defines a subset of the standardized DOM events in JavaScript, as well as the inheritance hierarchy for the interfaces of those events and the fields which are to be reported for each event interface. This list is intentionally conservative: if you are in need of support for another event or set of events, feel free to submit a PR with changes to this file.

The escape hatch: evaluating arbitrary JavaScript

It occasionally might become necessary for you to directly evaluate some JavaScript within the context of the page. The most frequent reason for this is to query the value of some object, such as the current contents of a text-box, or the current window dimensions. To allow this, myxine exposes a simple API to send arbitrary JavaScript to the page and return its result: the ?evaluate query string.

There are two ways to use this API, corresponding to JavaScript's notions of "expression" and "statement". The more convenient of the two evaluates a given string as an expression and returns its value:

$ curl -X POST "http://localhost:1123/?evaluate=window.innerWidth"
1224

This form is succinct: you don't have to use JavaScript's return keyword, and you can specify everything in the URL itself. However, you can't evaluate multiple lines delimited by semicolons (since the input is interpreted as an expression), and you must percent-escape all special characters like spaces.

To circumvent these limitations, myxine also provides a "statement" form of the ?evaluate API, where the POST body is used as a multi-line block of statements to be evaluated:

$ curl "http://localhost:1123/?evaluate" -d \
    'let x = 100; let y = 200; return x + y;'
300

In this form, return is mandatory to send back a value, but there is no need to escape special characters, and multiple statements can be executed as a block.

Further details

  • Return values of undefined are reported as null, and therefore calls which don't return anything (i.e. if you did not use return in a statement-type request) result in null.
  • Return types are limited to those which can be serialized via the JavaScript method JSON.stringify, which does not work for cyclic objects (like window, document, and all DOM nodes), and may fail to serialize some properties for other non-scalar values. If you want to return a non-scalar value like a list or dictionary, construct it explicitly yourself by copying from the fields of the object you're interested in.
  • There is a default timeout of 1 second for reporting evaluation results. This is not enforced in the browser -- your JavaScript will continue to run indefinitely -- but is a guarantee of the myxine API. To alter this timeout, use the timeout query parameter to specify the desired limit in milliseconds.
  • All errors in evaluation, including timeouts, serialization errors, and other exceptions, are caught and reported with status code 400 Bad Request. If the status code is 200 OK, it's guaranteed that the response will be valid JSON. Otherwise, the response body is a human-readable description of the error.
  • For both forms, the JavaScript given is evaluated in the window (global) scope of the browser. For nitty-gritty details, see the docs on window.Function and the MDN writeup "Never use eval()!".