A GNOME app in TypeScript, in five minutes

gjsify brings the web and Node.js APIs to GJS. Here is what that looks like in practice: a small GTK 4 app in TypeScript, Blueprint and CSS that loads data with fetch and stores it with node:fs. Install the CLI first, for example with npm install -g @gjsify/cli.

Create and run

gjsify create hello-gjsify --template gtk-minimal --install
cd hello-gjsify
gjsify run build
gjsify run start

You get a window, a Blueprint file for the interface (src/main-window.blp) and a TypeScript class that binds to it. Leave gjsify dev running in a second terminal: it rebuilds and relaunches the app whenever you save. Change the greeting in the .blp and watch it update.

Use web standards and Node.js

Replace src/main-window.ts with the following. It fetches data from the web, stores it through Node's file system API, reads it back and shows it in the hint label:

import GObject from 'gi://GObject?version=2.0';
import Gtk from 'gi://Gtk?version=4.0';
import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import Template from './main-window.blp';

export class MainWindow extends Gtk.ApplicationWindow {
    declare private _hint: Gtk.Label;

    static {
        GObject.registerClass(
            {
                GTypeName: 'MainWindow',
                Template,
                InternalChildren: ['hint'],
            },
            this,
        );
    }

    constructor(application: Gtk.Application) {
        super({ application });
        void this.load();
    }

    async load(): Promise<void> {
        const response = await fetch('https://api.github.com/repos/gjsify/gjsify', {
            signal: AbortSignal.timeout(10_000),
        });
        const file = join(await mkdtemp(join(tmpdir(), 'gjsify-')), 'repo.json');
        await writeFile(file, await response.text());
        const { full_name, stargazers_count } = JSON.parse(await readFile(file, 'utf8'));
        this._hint.set_label(`${full_name} · ${stargazers_count} ★`);
    }
}

fetch and AbortSignal work as in the browser, node:fs, node:os and node:path as in Node.js. Under the hood they run on Soup, Gio and GLib: no browser engine, no Node.js process.

Style it

Put the CSS in its own file, src/style.css:

.demo-status {
  color: #9141ac;
  font-weight: bold;
}

gjsify turns a CSS import into a string. TypeScript does not know that yet and would report Cannot find module './style.css', so declare it in src/css.d.ts:

declare module '*.css' {
    const css: string;
    export default css;
}

Load the string into a Gtk.CssProvider in src/index.ts. Do it inside the activate handler: before that, there is no display yet and Gdk.Display.get_default() returns null. Here is the complete file:

import Gtk from 'gi://Gtk?version=4.0';
import Gio from 'gi://Gio?version=2.0';
import Gdk from 'gi://Gdk?version=4.0';
import { MainWindow } from './main-window.js';
import css from './style.css';

const app = new Gtk.Application({
    applicationId: 'org.gjsify.hello-gjsify',
    flags: Gio.ApplicationFlags.FLAGS_NONE,
});

app.connect('activate', () => {
    const provider = new Gtk.CssProvider();
    provider.load_from_string(css);
    Gtk.StyleContext.add_provider_for_display(
        Gdk.Display.get_default()!,
        provider,
        Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
    );

    new MainWindow(app).present();
});

app.run([]);

Finally, give the label the class in src/main-window.blp. This is the complete file with the change in the second label:

using Gtk 4.0;

template $MainWindow: Gtk.ApplicationWindow {
  title: "hello-gjsify";
  default-width: 480;
  default-height: 280;

  child: Gtk.Box {
    orientation: vertical;
    spacing: 12;
    margin-top: 24;
    margin-bottom: 24;
    margin-start: 24;
    margin-end: 24;

    Gtk.Label {
      label: _("Hello from gjsify!");
      styles ["title-2"]
    }

    Gtk.Label hint {
      xalign: 0.5;
      styles ["demo-status"]
    }
  };
}
The finished window: a greeting and the repository name with its stars in purple

A bigger example

Learn 6502 Assembly is built the same way, with TypeScript, Blueprint, libadwaita and gjsify. Its code shows how a real app is put together.

Back to the logbook