Lens: An alernative to desktop agnostic UIs

written on 22 January 2015

I've been playing with a little side project that aims to help build lightweight GUI applications that look native-ish on multiple Desktop Environments (DEs). The immediate use case was to look native-ish in both Gtk and Qt based environments. My definition of native-ish is the little things such as using Gtk windows when in Gnome or Qt event loop bindings when in KDE. Of course, if the developer so chooses, any combination thereof.

The project started rather organically over at Korora where I had just finished up refactoring our small Welcome app. It's a Python app that uses Webkit as the main UI container which then provides HTML5, CSS3 and JS for the design of the UI. I was also in the middle of refactoring our hardware driver manager (Pharlap) and had way too much copy and paste in the UI department. My motivation for replicating the UI across the projects (Welcome and Pharlap) is due to Korora supporting multiple DEs and I'm rather pedantic on providing a consistent look across what we support.

It was becoming more obvious that the Korora ecosystem would benefit from breaking out the duplicated pieces in order to be reused for some of the other small-ish applications we have planned. And thus, Lens was born.

Broadly speaking the Welcome app is largely static content, though it does provide some buttons to external links that when pressed spawn a seperate session using the default browser. The problem was how does one breakout of the Webkit sandbox and allow bi-directional communication between JS and Python. A bridge or tunnel of sorts was needed to close that gap.

The bridge constructed, what I consider a rather elegant hack largely inspired by Davig Baird's tutorial HOWTO Create Python GUIs using HTML, involved manipulation of the Webkit page title. The page title makes a lot of sense in a browser when it's used to identify the page but in the case of a single window application it's a little redundant. The good news for our use case is that it's relatively trivial to hook into page title change notifications - on both the Python and JS sides. Applying structure with a tiny prefix, some stringified JSON, and some signal handling on both sides resulted in a stable solution.

So what do Lens app internals look like?

The high level view of a Lens app is shown below. There is a Python base which has an embedded Webkit layer. With in the Webkit layer HTML5, CSS3 an JS are used to design and define the visual components and interactivity. AngularJS is used as the primary JS framework due it's awesome binding mechanisms and ability to create custom directives.

Show me some code!

Lens allows apps to be built in a pure OO way or via some syntatic sugar that leverages Python's decorator features. The following python snippet is a tiny extract from one of the Lens app samples that uses decorators, it represents one side of the bridge.

from lens.app import App

app = App()

app.namespaces.append('./sample-data')
app.load_ui('app.html')

@app.connect('close')
def _close_app_cb(*args):
  app.close()

app.emit('init', {'foo':'bar'})
app.start()

Firstly, the appropriate lens components are imported and an App class is instatiated. The namespace is then tweaked to show Lens where to find the UI assets. In the HTML you have two additional schemas lens:// and app:// which are bound to the Lens system path and the app namespace paths respectively.

With the namespace configured, load_ui tells Lens which page to load once the event loop is started via start.

A signal endpoint called 'close' is established, via the decorator @app.connect. This signal will be monitored on the Python side via the _def_close_app function will which will close our app.

A signal called 'init' is then set to be emitted when the event loop begins which can then be captured on the other side of the bridge.

Finally we start the event loop via the start method.

Now for the other side of the bridge, the Javascript snippet.

var app = angular.module('lens-app', ['lens.bridge', 'lens.ui']);

function AppCtrl($scope) {
  $scope.foo = '';

  $scope.$on('init', function(e, data) {
    $scope.foo = data.foo;
  });

  $scope.closeApp = function() {
    $scope.emit('close');
  };
}

If you're familiar with (AngularJS)[https://www.angularjs.org] then the snipped above will be reasonably familiar. The essential parts to make things just work is the 'lens-bridge' module. This module ensures that signals coming in from the Python side are broadcasted on the $rootScope. Signals can also be sent back to Python using emit function which is also available on the $rootScope.

So is this for Gtk or Qt?

Lens abstracts just enough to ensure that things look reasonably native on Gtk or Qt desktops. By default it will attempt to identify the primary toolkit in use and use that. Failing that it will fallback to other toolkits if they're available. You can also hint towards your preferred toolkit if you wish.

Designing the actual UI is akin to developing a webpage. In this case the web page is the app.

Considerations

There are some considerations of using the Lens approach:

  • Communicating through the page title is by definition a "hack" - using things other than their intended purpose. Consequently is should not be considered a high-speed, high-bandwidth bi-directional pipe. The upper echelon of reliable speeds has not been benchmarked but I have managed to push about a 1000 messages per second across it.

  • Lens apps require a Python component and HTML5/CSS/JS component. It's similar to the compsition of other widget frameworks however the signalling is sufficiently different to require some additional thought in design.

  • It's still early days, documentation is essentially non-existant. The sample apps are designed to be lightweight and readable whilst demonstrating most of the current semantics.

The road ahead

The concept behind Lens is far from new, though it does represent a refined approach to facilitate the creation of beautiful UIs with reasonable ease. Lens is also quite lean due to abstraction of only the essential elements to Window creation and we aim to keep it that way.

Overall Lens is still in early days and it will be used in at least two Korora apps in the 21 release. If you're interested in learning more or helping out with development then head on over to GitHub and check it out.

Comments, issues and patches are always welcome!