nogui.js

Render GTK controls from NoGui Spec

Usage no npm install needed!

<script type="module">
  import noguiJs from 'https://cdn.skypack.dev/nogui.js';
</script>

README

NoGui

NoGui

NoGui is a widget-free, XML-free, boilerplate-free notation for specifying user interfaces.

Rendering

NoGui is rendering-agnostic. The UI tree should be easy to process and you can use any technology to draw widgets on any device.

NoGui GTK/GJS

This project provides a first NoGui implementation for GJS/GTK. The nogui module allows for loading a NoGui spec and rendering the corresponding GTK widgets.

Example

assets/player.js

// define your UI in one file
module.exports = {
  icons: {
    card: { name: 'audio-card' },
    play: { name: 'media-playback-start' },
    stop: { name: 'media-playback-stop' },
    exit: { name: 'application-exit-symbolic' },
    info: { name: "dialog-information-symbolic" },
    gears: { name: "settings-gears-symbolic" },
    back:  { name: "go-previous-symbolic" },
    vol_max: { name: 'audio-volume-high-symbolic' },
    vol_min: { name: 'audio-volume-muted-symbolic' },    
  },
  dialogs: {
    about: { info: 'About Audio Player',  file: 'about.md',  icon: 'info' },
    close: { ask:  'Close Audio Player?', call: 'respClose', icon: 'exit' },
  },
  views: {
    main: [
      { title: 'My Audio App', icon: 'card' },
      { action: 'Play Audio', call: 'playAudio',  icon: 'play' },
      { action: 'Stop Audio', call: 'stopAudio',  icon: 'stop' },
      { switch: 'Mute Audio', bind: 'muted', icons: ['vol_max', 'vol_min'] },
      { action: 'About',    dialog: 'about',  icon: 'info' },
      { action: 'Settings', view: 'settings', icon: 'gears' },
      { action: 'Close',    dialog: 'close',  icon: 'exit' },
    ],
    settings: [
      { title: 'Settings', icon: 'gears' },
      { switch: 'Mute Audio', bind: 'muted', icons: ['vol_max', 'vol_min'] },
      { action: 'Back', view: 'main',  icon: 'back' },
    ]
  },
  main: 'main',
}

This example is written in plain JavaScript. However, json or yaml and loading these into memory should also work fine.

OK, now we have a clean user interface as NoGui "spec". Let's build some business logic for it.

gtk4-player.js

// To allow the app to do something, we need to define
// some callbacks and a data model as used in the NoGui spec.
const data = {
    muted: false,
}
const callbacks = {
    playAudio() { print("PLAY") },
    stopAudio() { print("STOP") },
    respClose(id, code) { if(code == 'OK') app.quit() },
}

// Now we can bring everything together into the GTK app.
const Gtk   = require('gtk4')  // webpack import for `imports.gi.Gtk`
const nogui = require('nogui') // webpack import for `imports.<path>.nogui`

let app = new Gtk.Application()
app.connect('activate', (app) => {
    let window = new Gtk.ApplicationWindow({      
      title: 'My Audio Player', default_width: 240, application:app,
    })
    let stack = new Gtk.Stack()  // use a Gtk.Stack to manage views
    window.set_child(stack)
    window.show()

    // `nogui.Controller` manages data and connects controls to the parents
    let ctl = new nogui.Controller({
        window, data, callbacks,
        showView: (name) => stack.set_visible_child_name(name),
    })

    // `nogui.Builder` builds the UI
    let spec = require('./assets/player')  // read + load `player.js`
    let ui = new nogui.Builder(spec, ctl, './assets')
    ui.buildWidgets()

    // The builder now has all `ui.views`, `ui.icons`, and `ui.dialogs`.
    // Only the views need to added to the parent controls.
    for (const v of ui.views) stack.add_named(v.widget, v.name)

    // The ctl.showView handler allows switching views manually or
    // via the spec action "view":"<name>".
    ctl.showView(spec.main)
    
    data.muted = true  // data bindings are set up automatically
})
app.run(null)

That is it! Here is what the app will look like.

Player Main Player Settings Player Dialog

Packaging

The example uses Node.js require which is not available in gjs. However, this is currently the smartest way of managing packages for GJS apps without having modifications of your imports.searchPath all over the place. Also for Gnome Extensions it is discouraged to modify the searchPath anyway.

Using require and webpack you can generate minified files (see webpack.config.js) that include all required modules. And the best is that you then can use npm modules. For instance, this project uses md2pango to convert Markdown to Pango Markup in the about dialog.