Writing Native Apps With JavaScript
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
.
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:
- install Xcode or its Xcode Essential counter part via Apple store.
- once you have Xcode installed, open it once to be sure there are no additional components that needs to be installed. After that, open a new terminal and write
sudo xcodebuild -license
in order to read the Apple license and terms of use and, eventually, agree following the procedure (pressing space, reading, reaching the part you need to type agree) - test that everything is fine by typing
brew install gawk
and installing actuallygawk
needed to build anyway - download and install MacPorts since it’s the official place to retrieve WebKitGTK (I wish Homebrew had an updated recipe that won’t fail … if you know some, please share!)
- open a new terminal and write
sudo port install webkit-gtk3
. If that complains at the very beginning, follow the instructions to have all you need to build via macports (xcode-select --install
or agreeing on the license)
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:
child_process
to spawn synchronously or asynchronously external programs, with the ability to read and write in itsstdin/out/err
streams.stream
to interact with unix pipesevents
to haveEventEmitter
compatible objectsfs
to read and write synchronously or asynchronously files and read directories tooos
to have same exact infonodejs
would return with itsos
module, it’s indeed a spawned call to it!path
to normalize and simplify common path operationsSystem.global
and a globally availableglobal
reference,console.log/info/warn/assert
used to testjsgtk
itself,process
with few methods,setTimeout
andsetInterval
with theirclear
counterparts and last, but not least,require
to include core modules or actually include files.
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:
- the Giovanni’s doc which includes pretty much all possible imports and libraries from the GTK world
- the Unofficial Seed Documentation which has details shown in a different way (a bit more handy than the Giovanni’s page)
- this partially (I guess) Japanese page, but you are better off via
jsgtk
andrequire("fs")
if you want to deal with file system - this repository with few examples and this one with many more
- this page from GNOME
- these links from GNOME
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.