Wibbly Stuff

First time for everything - GNOME JavaScript

It's no secret that web technologies are everywhere now. Be it embedded platforms, cloud software, mobile and desktop apps, your toaster and microve and yes, also the web, imagine that. More and more people are starting to write code in JavaScript, and it's slowly, but surely becoming a universal programming language. Given the low barrier of the language, it's only natural that GNOME also has decided to go with JavaScript for the desktop apps. However to rephrase a quote infamous meme - it is not the same JavaScript that you're used to.

Recently, I tried my hands on GJS, or should I say GNOME JavaScript? I decided to rewrite Fedy's UI in GJS, which is currently written in a mix of Bash and Yad and as you can imagine has less than ideal UI. Now, I didn't go all guns blazing giddy on power with the new GJS UI, but I got a chance to take my first look at GJS.


GJS is also a JavaScript interpreter, but it's different in a way from node/io.js that it's based on Mozilla's Spidermonkey engine, while node and io.js are based on the V8 engine which powers Google Chrome. Apart from that, there are quite a few visible differences, due to the different conventions followed in GNOME, and the differences in the engine itself.

Syntax

While the syntax is mostly the same, node still uses old ECMAScript 5, while with GJS you can take advantages of some newer ES6 features (though all features aren't supported yet). The unsupported features include classes, enhanced object initializer, template strings, spread operator etc.

As far as I've tried, the following features are supported,

Variable declaration

Variables can be defined using the let keyword, which introduces lexical (block-level) scoping.

function makeEven(num) {
    let even;

    if (num % 2) {
        let odd = num;
        
        even = odd + 1;
    } else {
        even = num;
    }

    print(odd); // undefined

    return even;
}

Constants can be defined using the const keyword, which is a one time assignment.

const PI = 3.14159;

Variables can also be declared by using matching arrays or objects.

let [ a, b, c ] = [ 1, 2, 3 ];
let { a, b } = { a: "apple", b: "banana" };

Arrow functions

Arrow functions are shorter to write and can maintain the same this variable in the callback.

let squares = [ 1, 2, 3 ].map(n => n * n);

For...Of

Looping over arrays using for...of is much more convenient.

let fruits = [ "apple", "orange", "banana" ];

for (let fruit of fruits) {
    print(fruit);
} // apple, orange, banana

Default and rest parameters

Function parameters can accept default values,

function add(x, y = 0) {
    return x + y;
}

The trailing parameters can be bound to an array, eliminating the need for the arguments object,

function largest(...args) {
    let num = args[0];

    for (let n of args) {
        if (n > num) {
            num = n;
        } 
    }

    return num;
}

Unfortunately, while rest parameters work, array spread doesn't work. So you still have to do stuff like fun.apply(this, args).

Common differences

The basic operations in GJS such as importing modules, logging etc, are different vary a lot from node.

Modules

Like node, you can use modules in GJS. But the syntax differs from node. Instead of require statements, you import objects from the imports object instead.

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;

If you want to include your own files, you've to add the current directory to the search path.

imports.searchPath.unshift(".");

const Awesome = imports.awesome; // import awesome.js from current directory

If your file is under a sub-directory, you can use . instead of / for the separator.

const Awesome = imports.lib.awesome; // import lib/awesome.js

All variables, constants and functions declared inside the global scope of the file lib/awesome.js are now available under the constant Awesome.

Logging

We all use console.log a lot. But Console APIs are not present in GJS. With GJS, you have the log (for logging) and print methods instead. You also have the logError and printerr to use with error objects.

Timing events

In the browser and node, we're all used to setTimeout and setInterval functions. But since setTimeout and setInterval are part of the browser's window object, and not JavaScript native methods, they don't exist in GJS. You can use GLib.timeout_add instead.

const GLib = imports.gi.GLib;

GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000, function() {
    print("This will print after 2 seconds");

    return false; // Don't repeat
}, null);

Event listeners

In browser environments, we are used to adding event listeners with the addEventListener method, and for those who use jQuery, the on method. Though this isn't strictly a GJS thing, when writing GTK apps, you can attach event listeners to the GTK widgets with the connect method.

const Gtk = imports.gi.Gtk;

let button = new Gtk.Button();

button.connect("clicked", () => print("Button clicked!"));

Linting

If you're using a linter to lint your GJS code, you can add log, logError, print, printerr, imports and ARGV as globals. These methods are part of the global window object.

If you use ESLint, then you can selectively enable or disable ECMA6 features. Here are my tweaks for GJS,

{
  "env": {
    "es6": true
  },

  "ecmaFeatures": {
    "arrowFunctions": true,
    "binaryLiterals": false,
    "blockBindings": true,
    "classes": false,
    "defaultParams": true,
    "destructuring": true,
    "forOf": true,
    "generators": false,
    "modules": false,
    "objectLiteralComputedProperties": false,
    "objectLiteralDuplicateProperties": false,
    "objectLiteralShorthandMethods": false,
    "objectLiteralShorthandProperties": false,
    "octalLiterals": false,
    "regexUFlag": false,
    "regexYFlag": true,
    "spread": false,
    "superInFunctions": false,
    "templateStrings": false
  },

  "globals": {
    "log": true,
    "logError": true,
    "print": true,
    "printerr": true,
    "imports": true,
    "ARGV": true
  },

  "rules": {

  }
}


Doing stuff

So, we know the syntax, but there are more things we need to know before we could write something useful. We might want to access the file system, os functions, build GUI applications etc. To do these things, we will need to use the libraries provided by the system.

For example, GLib provides a lot of system APIs, whereas GTK provides various widgets to build UI applications. You can start by checking the samples on GNOME's website and Giovanni's GJS docs.

I have setup a repo with polyfills for setTimeout, setInterval and Promise, a small promise based library for file operations etc. It's not complete yet, but hoping to make it better in the future. Feel free to use them if you need.

When you're trying to write an app which accepts arguments, you have a ARGV array containing all the arguments passed to the app. Handy!

One issue you might face with GJS apps is, when you try to get the path of current directory with GLib.get_current_dir(), what it actually returns is the directory from where the application was called, not where the application files exist. It might be undesirable in many cases, for example where you're reading some files in the application directory. What I ended up doing is, write a small bash script to launch the app, which switches to the application directory and then launches the app.

#!/bin/bash

cd $(dirname "${BASH_SOURCE[0]}") && gjs app.js $@

Note that the GJS docs are far from complete. While you'll be able find the docs for most simple stuff, it might be difficult to find and understand the docs for complex tasks, as most of them are generated and include C terminology.

If you have anything to add, please leave a comment. It's much appreciated.

No comments :

Post a Comment