nogui

1.0.44 • Public • Published

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.

GJS Example

// First define your UI inline in one plain JS `Object`.
// You can also load the `spec` from JSON, YAML, or another module.
const spec = {
  icons: {                                                                // define all icons used by the app
    card: { name: 'audio-card' },                                         // this example uses the standard
    play: { name: 'media-playback-start' },                               // GTK themed icons by their name
    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: {                                                              // simple text-based `dialogs`
    about: { info: 'About Audio Player',  file: '../README.md', icon: 'info' },  // with text in separate file
    close: { ask:  'Close Audio Player?', call: 'onClose',      icon: 'exit' },  // or inline
  },
  parts: {                                                                // `parts` are reusable components
    controls: [
      { act: 'Play', call: 'play', icon: 'play', vis: '!playing' },       // `act` is a small unlabeled action
      { act: 'Stop', call: 'stop', icon: 'stop', vis: 'playing'  },       // button with callbacks, icons, and
    ],                                                                    // the `act` text as tooltip
  },
  views: {                                                                // apps can have multiple views
    player: [
      { title: '{{ playing? "Playing: $song" : "Next Song: $song" }}' },  // use nogui expressions for dynamic text
      { use: 'controls' },                                                // just `use` the `parts` anywhere
      '------------------------------------------------------------',     // easy-peasy separators
      { action: 'About',    dialog: 'about',    icon: 'info' },           // `action` is a labelled button
      { action: 'Settings', view:   'settings', icon: 'gears' },          // actions and acts can also
      { action: 'Close',    dialog: 'close',    icon: 'exit' },           // show dialogs and switch views
    ],
    settings: [
      { title: 'Settings', icon: 'gears' },
      { use: 'controls' },                                                // just `use` the `parts` again
      '------------------------------------------------------------',
      { switch: '{{muted? "Muted" : "Not Muted"}}', bind: 'muted',        // controls can `bind` to the data
        icons: ['vol_max', 'vol_min'] },
      { act: 'Back to Player', view: 'player', icon: 'back' },            // basic view navigation with acts
    ]
  },
  main: 'player',                                                         // tell the app where to start
}

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

const nogui = require('nogui')   // webpack import for `imports.<path>.nogui`
const { binding, poly } = nogui  // unbox some NoGui helpers

// To allow the app to do something, we need to define some callbacks
// and a data model that can be referenced from the spec.
let data = binding.GetProxy({  // `binding.GetProxy` wraps our data in a
  playing: false,              // `Proxy` to make all fields bindable, so we
  muted:   false,              // can `bind` them in the controls, use them
  song:    'Cool Song 😎🎶'    // as `$vars` in templates (see spec!), or
})                             // create programmatic bindings in code.

// As controller of the app we can use any `object` with some callbacks.
let callbacks = {
  play() { data.playing = true  },  // callback for the Play button
  stop() { data.playing = false },  // callback for the Stop button
  onClose(id, code) {               // "close"-dialog handler
    if(code == 'OK') app.quit()
  },
}

// Now we can bring everything together into a GTK app.
const { Gtk, GLib } = imports.gi
const args = [imports.system.programInvocationName].concat(ARGV)
const here = GLib.path_get_dirname(args[0])
const app = new Gtk.Application()

app.connect('activate', (app) => {
    let stack  = new Gtk.Stack()  // use a Gtk.Stack to manage views
    let window = new Gtk.ApplicationWindow({
      title:'🎵 My Music', default_width:240, application:app, child:stack,
    })
    stack.show()   // GTK 3 requires calling "show" everywhere ¯\_(ツ)_/¯
    window.show()  // in GTK 4 only windows must be shown explicitly

    // `nogui.Controller` manages data, bindings, dialogs, and views
    let ctl = new nogui.Controller({
        window, data, callbacks,
        showView: (name) => stack.set_visible_child_name(name),
    })

    // Nogui will automatically manage bindings for expressions in the spec.
    // But you can also manually bind to the data to trigger custom logic.
    ctl.binding.bindProperty('playing', v => {
      log(v? `playing song "${data.song}"` : `song "${data.song}" stopped`)
    })

    // `nogui.Builder` builds the UI and loads assets such as icons
    // and Markdown files according to the NoGui spec.
    let ui = new nogui.Builder(spec, ctl, ctl.data, here)
    ui.build()  // `build` traverses the spec and creates all widgets

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

    // The custom `showView` handler can be used for switching `views`
    // manually in the custom parent control, i.e., the `stack` in this case.
    // The handler is also used for 'view:<view_name>' actions in the spec.s
    ctl.showView(ui.spec.main)
    // A "view" is actually just a separate `Gtk.Widget` tree that can be
    // managed in any `Gtk.Widget`. NoGui does not make any assumptions here.

    callbacks.play()  // Just use the callback to control the app.
})

app.run(args)

That's 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.

Using require and webpack you can generate minified files (see webpack.config.js) that include all required modules. This also allows you to use npm modules! For instance, this project uses md2pango to convert Markdown to Pango Markup in the about dialog. It also uses json5 to allow for rich-text JSON with comments and more.

Contributing

See CONTRIBUTING.md for a short list of things to adhere.

Package Sidebar

Install

npm i nogui

Weekly Downloads

2

Version

1.0.44

License

MIT

Unpacked Size

612 kB

Total Files

51

Last publish

Collaborators

  • uweju