Writing Native Apps With JavaScript

GOME Disk Utilities

There are tons of solutions to write Desktop or Mobile applications via JavaScirpt. Ionic, Electron, Telerik, AppJS, Adobe Air and Qt are just few examples but there is one common denominator between all these solutions: they try to make App development “Webbish“, creating a lot of expectations for developers that know Web and Web only, failing at introducing them to a different environment not based on nodes trees and Web related security constrains.

The only exception in the previous list is the Qt framework, since its QML bindings are between Web and Native but are something a part to learn. Is that really the only thing we can do to develop native-like looking applications? Is there any other option?

The GTK+ Project

During my early days as server side developer, using languages such PHP or Python was all I needed to render websites, interact asynchronously via ActionScript LoadVars or later on XMLHttpRequest and, before moving full stack on JavaScript, create little Desktop applications via glorious projects such AutoIt, wxPython or PHP-GTK, available in its Pythonic counterpart as PyGTK, a project that looks death accordingly with latest news from 2011 shown in the website (but I’m pretty sure it’s not).

However, The GTK+ Project never actually died and it’s still actively maintained and full of goodness and the best part of it is that recently I’ve discovered there is a PyGTK equivalent in JavaScript called GJS, which has been confirmed from its official mailing list to be actively maintained.

Not Only GNOME

The GNOME project is usually associated to a Desktop Environment, and it’s actually the best Desktop environment I could think of, indeed is the one I’m using right now. However, the entire project is made of submodules, most of them based on GTK3 which is fully cross platform. That means we can create any sort of application through GTK3 and, since there are JavaScript bindings, in JS!

Have you ever heard of GIMP, Inkscape, Scribus or other cross platform UI centric applications? All made via GTK!

How To Install GJS

When it comes to ArchLinux, simply type pacman -S --needed gjs in a terminal and you’ll have a gjs executable available.

The same goes for Ubuntu, via sudo apt-get install npm gjs command.

If you are on OSX and you don’t have MacPorts already installed (suggested), you can use Homebrew and tap TingPing/gnome repo. Following the procedure you can copy and pasate into a install-gjs.sh file and launch it via sh install-gjs.sh.

# WARNING, if you have MacPorts already installed you should use it!
if [ "$(which port)" != "" ]; then
  sudo port install gjs
else
  # verify and eventually install Homebrew
  if [ "$(which brew)" = "" ]; then
    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    # eventually confirm or add password the first time it's installed
  fi
  # install gjs via https://github.com/TingPing/homebrew-gnome
  brew tap TingPing/gnome
  brew install gtk+3
  brew install gjs
fi

The installation on OSX where some dependency might need to be built could take a while so … instead of staring at the screen we can start preparing our first working demo.

Hello GJS!

In order to have a similar structure to point at, let’s create a ~/gjs-examples folder via mkdir -p ~/gjs-examples and then cd ~/gjs-examples. This is where from now on we’ll create and test our files.

Following the content of our first hello.js file.

#!/usr/bin/env gjs

// in GJS every file can be used as module and
// defining variables will automatically export them.
// It's a good practice to avoid accidental module pollution
// so, by default, we can wrap our logic within a closure

(function (Gtk){'use strict';

  // this call is necessary to be sure that GTK3
  // is avaibale and capable of showing some UI
  Gtk.init(null);

  // part of ES6/2015 is brought in by SpiderMonkey
  // Actually the whole GJS is based on "SpiderMonkey 24"
  // const, let, arrow functions, Proxy, WeakMap and others
  // all all natively available
  const
    // create a new Window
    win = new Gtk.Window({
      // as top-level
      type : Gtk.WindowType.TOPLEVEL,
      // centered on the screen
      window_position: Gtk.WindowPosition.CENTER
    })
  ;

  // define a minimum size, by default it tries
  // to pack itself around the visible UI (in this case the label)
  win.set_default_size(200, 80);

  // add a label with a text content
  win.add(new Gtk.Label({label: 'Hello GJS!'}));

  // GTK works via "signals", somehow similar to DOM Events
  // show is invoked once the UI is shown
  // the Gtk.main() call is necessary to run  the main application loop
  // if never invoked the program will just exit (without an IDLE)
  win.connect('show', () => {
    // needed in OSX to bring the window on top
    win.set_keep_above(true);
    // needed to start the main application loop
    Gtk.main();
  });

  // once destroyed (closed it)
  // quit the main loop and exit from the application
  win.connect('destroy', () => Gtk.main_quit());

  // we can now show this window
  win.show_all();

}(imports.gi.Gtk));

Hoping that while creating and reading above file the gjs executable has been installed, we can now either start the hello file via gjs hello.js or we can make it executable via chmod +x hello.js and then launch it via ./hello.js.

Hello GJS

Creating A Browser With A Language Born For A Browser

wondering how does the browser written in JavaScript and GTK+ looks like in OSX and Linux? https://t.co/wHqTm1jQoC pic.twitter.com/wCSHoTV4Jk

— Andrea Giammarchi (@WebReflection) December 9, 2015

I’m quite fan of inceptions so, beside the classic “Hello World” like example, I’d like to provide another example mostly copied from ARDORIS’s PyGTK example.

The main difference between the previous example and the one that is following is that we need WebKitGTK+ so that we can import a WebView and load in it anything we like.

There are two versions of WebKit, the regular old one, and the WebKit2 one. In order to simplify installation and usage I am going to use the old implementation which is available in ArchLinux via sudo pacman -S --needed webkitgtk and in Ubuntu via sudo apt-get installl libwebkitgtk-3.0-dev.

On OSX though, the installation is a bit more cumbersome. Here what you might need to do:

Problems including WebKit via imports?

If you have used Homebrew instead of MacPorts in order to install gjs and you’d like to use WebKit brought in via MacPorts, you should definitively try to install gjs via MacPorts removing the other one before.

brew uninstall -f gjs and then sudo port install gjs.

This will somehow grant compatibility withing built modules, ensuring better stability.

Please note it might take long time to have the entire thing compiled. In my case I haven’t finished yet building the whole thing but I hope it’s going to work (update: it did work \o/). However, and once again, instead of staring at the screen we can go through our browser.js file.

#!/usr/bin/env gjs

// A basic GJS Webkit based browser example.
// Similar logic and basic interface found in this PyGTK example:
// http://www.eurion.net/python-snippets/snippet/Webkit%20Browser.html

(function (Gtk, WebKit) {'use strict';

  // necessary to initialize the graphic environment
  // if this fails it means the host cannot show GTK3
  Gtk.init(null);

  const
    // main program window
    window = new Gtk.Window({
      type : Gtk.WindowType.TOPLEVEL
    }),
    // the WebKit browser wrapper
    webView = new WebKit.WebView(),
    // toolbar with buttons
    toolbar = new Gtk.Toolbar(),
    // buttons to go back, go forward, or refresh
    button = {
      back: Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_BACK),
      forward: Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_FORWARD),
      refresh: Gtk.ToolButton.new_from_stock(Gtk.STOCK_REFRESH)
    },
    // where the URL is written and shown
    urlBar = new Gtk.Entry(),
    // the browser container, so that's scrollable
    scrollWindow = new Gtk.ScrolledWindow({}),
    // horizontal and vertical boxes
    hbox = new Gtk.HBox({}),
    vbox = new Gtk.VBox({})
  ;

  // Setting up optional Dark theme (gotta love it!)
  // ./browser.js google.com dark
  if (ARGV.some(color => color === 'dark')) {
    let gtkSettings = Gtk.Settings.get_default();
    gtkSettings.set_property('gtk-application-prefer-dark-theme', true);
    gtkSettings.gtk_theme_name = 'Adwaita';
  }

  // open first argument or Google
  webView.open(url(ARGV[0] || 'google.com'));

  // whenever a new page is loaded ...
  webView.connect('load_committed', (widget, data) => {
    // ... update the URL bar with the current adress
    urlBar.set_text(widget.get_main_frame().get_uri());
    button.back.set_sensitive(webView.can_go_back());
    button.forward.set_sensitive(webView.can_go_forward());
  });

  // configure buttons actions
  button.back.connect('clicked', () => webView.go_back());
  button.forward.connect('clicked', () => webView.go_forward());
  button.refresh.connect('clicked', () => webView.reload());

  // enrich the toolbar
  toolbar.add(button.back);
  toolbar.add(button.forward);
  toolbar.add(button.refresh);

  // define "enter" / call-to-action event
  // whenever the url changes on the bar
  urlBar.connect('activate', () => {
    let href = url(urlBar.get_text());
    urlBar.set_text(href);
    webView.open(href);
  });

  // make the container scrollable
  scrollWindow.add(webView);

  // pack horizontally toolbar and url bar
  hbox.pack_start(toolbar, false, false, 0);
  hbox.pack_start(urlBar, true, true, 8);

  // pack vertically top bar (hbox) and scrollable window
  vbox.pack_start(hbox, false, true, 0);
  vbox.add(scrollWindow);

  // configure main window
  window.set_default_size(1024, 720);
  window.set_resizable(true);
  window.connect('show', () => {
    // bring it on top in OSX
    window.set_keep_above(true);
    Gtk.main()
  });
  window.connect('destroy', () => Gtk.main_quit());
  window.connect('delete_event', () => false);

  // add vertical ui and show them all
  window.add(vbox);
  window.show_all();

  // little helper
  // if link doesn't have a protocol, prefixes it via http://
  function url(href) {
    return /^([a-z]{2,}):/.test(href) ? href : ('http://' + href);
  }

}(
  imports.gi.Gtk,
  imports.gi.WebKit
));

Running via gjs browser.js or doing chmod +x browser.js and then ./browser.js we should see a browser like widget coming up with google.com as default page. We can eventually change initial page passing an argument or we could use a dark theme where available via ./browser.js google.com dark … you’re going to love it if your UI fully supports the Awaita Dark Theme!

About GJS Modules

If there’s one annoying thing about modules in GJS, it’s the inability to have the current working directory included by default. There are different sort of alchemies for including such folder and the following is my very personal hack based on a syntax mix between Bash and JavaScript.

In Bash we can define a runtime environment variable and execute arbitrary code after. When we do that, we can use special chars such / which has instead a special meaning for JavaScript.

# in bash the hash is for comments
# forward slashes are valid content
ENVVAR=something//

echo $ENVVAR
# will print `something//`

In JavaScript thought, two forward slashes mean inline comment so that whatever is in it will be ignored. This is the entry point of my hack that brings automatically the current working directory in gjs.

#!/usr/bin/env bash
imports=imports// exec gjs -I "$(dirname $0)" "$0" "$@"

// the rest of the GJS JavaScript content, e.g.
print('goobye Bash, hello JS');

Being gjs a *nix compatible command line tool, the first directive will be simply ignored once executed as gjs instead of bash environment, while the latter one will be executed like exec gjs -I "$(dirname $0)" "$0" "$@".

You can save the previous output in a file and launch it after making it executable. It will work from any folder automatically adding its directory to the imports.searchPath list of paths.

In order to test a module we can create a module.js file and put const value = 123; in it. Simply writing log(imports.module.value); from another file in the same folder will print out the value 123. This is the GJS modules ABC.

Update !!!

I’ve published jsgtk as npm executable now, and all it takes to bootstrap is the following header:

#!/usr/bin/env jsgtk

console.info('Hello JSGTK!');

It works already in OSX and Linux, of course you need to have installed gjs and npm install -g jsgtk

Now it’s definitively easier and less “hacky” to bootstrap, all folders for native imports or required modules should be available too.

npm and nodejs in GJS

Nowadays, having your own JS module system means you are out from the community behind npm, the largest modules registry, and this is the exact case for GJS.

However, even if the binding was CommonJS friendly, it’s quite common that modules published on npm are developed, and tested, for node.js only (or browsers).

jsgtk to the rescue !

Highly experimental, and far from complete, I’ve been working on a porting of the most common core utilities in nodes for GJS: the repository is called jsgtk and it has already partially integrated the following core modules:

In order to test jsgtk environment we can create a node_modules folder within our ~/gjs-examples one: mkdir -p node_modules.

Now we need to install npm either via sudo pacman -S --needed npm in ArchLinux, sudo apt-get install npm in Ubuntu or brew install npm in OSX.

Now that we have npm we can bring in jsgtk locally via npm install jsgtk.

Unfortunately, there’s no native support for Promises in SpiderMonkey 24, the good news is that we can simply npm install es6-promise too so that we can require it later on … how?

Let’s create a gjs-node.js file and put the following in it!

#!/usr/bin/env bash
imports=imports// exec gjs -I "$(dirname $0)/node_modules/jsgtk" "$0" "$@"
imports.jsgtk.env;

const Promise = require('es6-promise').Promise;

new Promise((res, rej) => {
  setTimeout(res, 1000, 'It worked!!!');
}).then((text) => {
  console.info(text);
  process.exit(0);
});

// we need a main loop to have an IDLE
imports.gi.Gtk.main();

Most important difference within the automatic import hack previously discussed, is that this time instead of pointing at the very same folder, we need to make jsgtk instantly available and, since it has been included via npm, we can simply point at its folder within the node_modules one. Every other required file will be included through such node_modules folder or, if present, in upper directory until it won’t find a module. This is similar to what usually happens in nodejs too.

About Gjs-WARNINGS

The previous code most likely produced a warning like function lib$es6$promise$$internal$$tryThen does not always return a value and this is OK. The GJS interpreter has some lint guard included by default. Usually simply putting an explicit return at the end of a callback that returns in some case would fade away the warning but I wish such lint thing would go away or there will be a way to suppress it at runtime.

The Documentation

When your life is basically about writing JavaScript pretty much everywhere (client/server/IoT) and you’re not aware about the existence of wonderful projects such GJS, there’s usually one issue: lack of good documentation!

I’ve no idea for how many years GNOME developers have been writing software in GJS (few, apparently) but the only pages I could find, most of which are probably outdated, are the following:

Ultimately, I’m willing to write there and now some little tutorial while I discover more and move forward with jsgtk repository … and BTW, if you are familiar with GTK3 please help as you can, every contribution is more than welcome!!!

Last, but not least, it turned out the mailing list is very welcoming and I’ve learned already a lot about GJS and its internal libraries already via some example provided in there: kudos that!

I hope you’ll enjoy your new adventures with native applications using JavaScript and GTK instead of HTML tags and CSS (however, GTK is also compatible with some CSS, how cool is that?!)

What about Windows?

It is surely possible to create applications that work on Windows too, I’ve just no idea how! If you have an How-To-GTK-On-Windows article the share, please do!

All I can offer for now is the official page that doesn’t help much, but I remember even during my php-gtk time it was possible to use them on Windows.

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor