Skip to content

D-Bus

This is a fairly long guide on how to use D-Bus in GJS. It covers most topics, from low-level usage to high-level conveniences and how to easily use D-Bus in GTK applications. If you're already familiar with D-Bus or you just want to get started the quickest way possible, you can jump to the high-level code examples for Clients and Services.

While working with D-Bus you can use D-Spy or the built-in inspector in GNOME Builder to introspect D-Bus on your desktop. The guide for GLib.Variant values will also be useful while learning how to use D-Bus.

Related Guides

Introduction to D-Bus

D-Bus is a messaging system that can be used to communicate between processes, enforce single-instance applications, start those services on demand and more. This section is an overview of D-Bus including the structure of services, bus types and connections.

Bus Structure

To get our bearings, let's first take a look at the hierarchy of a common D-Bus service that most users will have on their desktop. UPower is a good example because it exports an object for the application itself and additional objects for each power device (eg. laptop battery):

org.freedesktop.UPower
    /org/freedesktop/UPower
        org.freedesktop.DBus.Introspectable
        org.freedesktop.DBus.Peer
        org.freedesktop.DBus.Properties
        org.freedesktop.UPower
    /org/freedesktop/UPower/devices/battery_BAT0
        org.freedesktop.DBus.Introspectable
        org.freedesktop.DBus.Peer
        org.freedesktop.DBus.Properties
        org.freedesktop.UPower.Device

Names

At the top-level we have the well-known name, org.freedesktop.UPower. This is a "reverse-DNS" style name that should also be the Application ID. The D-Bus server will resolve this to a unique name like :1.67, sometimes referred to as the name owner. Think of this like a DNS server resolving a web site address (www.gnome.org) to an IP address (8.43.85.13).

This facilitates what is known as D-Bus Activation which allows the D-Bus server to automatically start services when they are accessed at a well-known name. Once the process starts it will "own the name", and thus become the name owner.

Object Paths

At the second level we have two object paths: /org/freedesktop/UPower representing the application and /org/freedesktop/UPower/devices/battery_BAT0 representing a laptop battery. These objects aren't really used for anything themselves, but rather are containers for various interfaces.

Notice the convention of using the well-known name (in the form of a path) as the base path for objects belonging to the service.

Interfaces

At the third level we have interfaces and that's what we're really interested in. Just like a GObject these can have methods, properties and signals.

Like object paths, the convention is to use the well-known name as the base for the interface name. Each path will also have a set of common interfaces, those beginning with org.freedesktop.DBus, for introspecting the service and property management.

In D-Bus the method arguments and return values, property values and signal parameters are all D-Bus Variants. When using the GNOME API these are GLib.Variant objects, which is covered in the GVariant guide. Note in particular that D-Bus does not support maybe types (m), which means there is no null value.

Bus Connections

A Gio.DBusConnection is used to communicate with the services on a bus and an average desktop has two bus types.

The system bus is used for services that are independent of a user session and often represent real devices like a laptop battery (UPower) or Bluetooth devices (bluez). You probably won't be exporting any services on this bus, but you might be a client to a service.

The session bus is far more common to use and many user applications and desktop services are exported here. Some examples include notification servers, search providers for GNOME Shell, or even regular applications such as the file browser that wants to expose actions like EmptyTrash().

You can get a bus connection using the convenience properties in GJS:

js
const sessionConnection = Gio.DBus.session;
const systemConnection = Gio.DBus.system;

Interface Definitions

Most projects declare interface definitions in XML, either as files or inline in the code. In GJS, describing exported interfaces in XML is mandatory. GJS includes convenience functions for creating client proxies directly from an XML string, which is covered in the High-Level Proxies section.

The official D-Bus documentation has API Design Guidelines, so we'll just show a simple example of an XML definition:

xml
<node>
  <!-- Notice that neither the well-known name or object path are defined -->
  <interface name="guide.gjs.Test">
  
    <!-- A method with no arguments and no return value -->
    <method name="SimpleMethod"/>
    
    <!-- A method with both arguments and a return value -->
    <method name="ComplexMethod">
      <arg type="s" direction="in" name="input"/>
      <arg type="u" direction="out" name="length"/>
    </method>
    
    <!-- A read-only property -->
    <property name="ReadOnlyProperty" type="s" access="read"/>
    
    <!-- A read-write property -->
    <property name="ReadWriteProperty" type="b" access="readwrite"/>
    
    <!-- A signal with two arguments -->
    <signal name="TestSignal">
      <arg name="type" type="s"/>
      <arg name="value" type="b"/>
    </signal>
  </interface>
</node>

Clients

Clients for D-Bus services are often referred to as proxies, and libraries for many services like Evolution Data Server are either wrappers or subclasses of Gio.DBusProxy. To get started using proxies quickly, jump ahead to High-Level Proxies and use the convenience classes offered by GJS.

It was mentioned earlier that some services support D-Bus Activation which allows the D-Bus server to start the service process automatically. See the documentation for Gio.BusNameWatcherFlags, Gio.DBusCallFlags and Gio.DBusProxyFlags for more information about this and other flags you can pass on the client side.

Watching a Name

To know when a D-Bus service appears and vanishes from the message bus, you can watch for the well-known name to become owned by a name owner. Note that you can still hold a client proxy and be connected to signals while a service is unavailable though.

js
import Gio from 'gi://Gio';


// These two functions are the callbacks for when either the service appears or
// disappears from the bus. At least one of these two functions will be called
// when you first start watching a name.


/**
 * Invoked when the name being watched is known to have to have an owner.
 *
 * This will be called when a process takes ownership of the name, which is to
 * say the service actually became active.
 *
 * @param {Gio.DBusConnection} connection - the connection the name is being
 *     watched on
 * @param {string} name - the name being watched
 * @param {string} nameOwner - the unique name of the owner of the name being
 *     watched
 */
function onNameAppeared(connection, name, nameOwner) {
    console.log(`The well-known name ${name} has been owned by ${nameOwner}`);
}

/**
 * Invoked when the name being watched is known not to have to have an owner.
 *
 * Likewise, this will be invoked when the process that owned the name releases
 * the name.
 *
 * @param {Gio.DBusConnection} connection - ...
 * @param {string} name - ...
 */
function onNameVanished(connection, name) {
    console.log(`The name owner of ${name} has vanished`);
}

// Like signal connections and many other similar APIs, this function returns an
// integer that is later passed to Gio.bus_unwatch_name() to stop watching.
const busWatchId = Gio.bus_watch_name(
    Gio.BusType.SESSION,
    'guide.gjs.Test',
    Gio.BusNameWatcherFlags.NONE,
    onNameAppeared,
    onNameVanished);

Gio.bus_unwatch_name(busWatchId);

Direct Calls

In this section, we'll see by example that all operations we perform as a client are actually performed on a bus connection. Whether it's calling methods, getting and setting property values or connecting to signals, these are all ultimately being passed through a bus connection.

Although you usually won't need to do this, it is sometimes more convenient if you only need to perform a single operation. In other cases it may be useful to work around problems with introspected APIs that use D-Bus since the data exchanged as GLib.Variant objects are fully supported.

Method Calls

Below is an example of sending a libnotify notification and getting the resulting reply ID. In this first example we will demonstrate the following steps:

  1. Packing the method arguments
  2. Calling the method
  3. Unpacking the method reply
  4. Handling D-Bus errors
js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';


try {
    /* 1. Packing the method arguments
     *
     *    Note that calling methods directly in this way will require you to
     *    find documentation or introspect the interface. D-Spy can help here.
     */
    const notification = new GLib.Variant('(susssasa{sv}i)', [
        'GJS D-Bus Tutorial',
        0,
        'dialog-information-symbolic',
        'Example Title',
        'Example Body',
        [],
        {},
        -1,
    ]);

    /* 2. Calling the method
     *
     *    To call a method directly, you will need to know the well-known name,
     *    object path and interface name. You will also need to know whether
     *    the service is on the session bus or the system bus.
     */
    const reply = await Gio.DBus.session.call(
        'org.freedesktop.Notifications',
        '/org/freedesktop/Notifications',
        'org.freedesktop.Notifications',
        'Notify',
        notification,
        null,
        Gio.DBusCallFlags.NONE,
        -1,
        null);

    /* 3. Unpacking the method reply
     *
     *    The reply of a D-Bus method call is always a tuple. If the
     *    method has no return value the tuple will be empty, otherwise
     *    it will be a packed variant.
     */

    // Our method call has a reply, so we will extract it by getting the
    // first child of the tuple, which is the actual method return value.
    const value = reply.get_child_value(0);

    // The return type of this method is a 32-bit unsigned integer or `u`,
    // although the JavaScript type will be `Number`.
    const replaceId = value.get_uint32();

    // And log the reply
    console.log(`Notification ID: ${replaceId}`);
} catch (e) {
    /* 4. Handling D-Bus errors
     *
     *    Errors returned by D-Bus may contain extra information we don't
     *    want to present to users. See the documentation for more
     *    information about `Gio.DBusError`.
     */
    if (e instanceof Gio.DBusError)
        Gio.DBusError.strip_remote_error(e);

    logError(e);
}

Properties

Getting or setting the value of properties are also just method calls, made to the standard org.freedesktop.DBus.Properties interface. The name of the interface holding the property is passed as the first argument of the method arguments.

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';


/*
 * Getting a property value
 */
try {
    const reply = await Gio.DBus.session.call(
        'org.gnome.Shell',
        '/org/gnome/Shell',
        'org.freedesktop.DBus.Properties',
        'Get',
        new GLib.Variant('(ss)', [
            'org.gnome.Shell',
            'ShellVersion',
        ]),
        null,
        Gio.DBusCallFlags.NONE,
        -1,
        null);

    const [version] = reply.recursiveUnpack();

    console.log(`GNOME Shell Version: ${version}`);
} catch (e) {
    if (e instanceof Gio.DBusError)
        Gio.DBusError.strip_remote_error(e);

    logError(e);
}

/*
 * Setting a property value
 */
try {
    await Gio.DBus.session.call(
        'org.gnome.Shell',
        '/org/gnome/Shell',
        'org.freedesktop.DBus.Properties',
        'Set',
        new GLib.Variant('(ssv)', [
            'org.gnome.Shell',
            'OverviewActive',
            GLib.Variant.new_boolean(true),
        ]),
        null,
        Gio.DBusCallFlags.NONE,
        -1,
        null);
} catch (e) {
    if (e instanceof Gio.DBusError)
        Gio.DBusError.strip_remote_error(e);

    logError(e);
}

Signal Connections

Connecting signal handlers directly on a connection is also possible. See the Gio.DBusConnection.signal_subscribe() documentation for details about signal matching.

js
import Gio from 'gi://Gio';


/**
 * The callback for a signal connection.
 *
 * @param {Gio.DBusConnection} connection - the emitting connection
 * @param {string|null} sender - the unique bus name of the sender of the
 *     signal, or %null on a peer-to-peer D-Bus connection
 * @param {string} path - the object path that the signal was emitted on
 * @param {string} iface - the name of the interface
 * @param {string} signal - the name of the signal
 * @param {GLib.Variant} params - a variant tuple with parameters for the signal
 */
function onActiveChanged(connection, sender, path, iface, signal, params) {
    const [locked] = params.recusiveUnpack();

    console.log(`Screen Locked: ${locked}`);
}

// Connecting a signal handler returns a handler ID, just like GObject signals
const handlerId = Gio.DBus.session.signal_subscribe(
    'org.gnome.ScreenSaver',
    'org.gnome.ScreenSaver',
    'ActiveChanged',
    '/org/gnome/ScreenSaver',
    null,
    Gio.DBusSignalFlags.NONE,
    onActiveChanged);

// Disconnecting a signal handler
Gio.DBus.session.signal_unsubscribe(handlerId);

Low-Level Proxies

The reason Gio.DBusProxy objects are so much more convenient is they allow you to treat the collection of methods, properties and signals of a service interface as a discrete object. They can automatically cache the values of properties as they change, connect and group signals, watch for the name owner appearing or vanishing, and generally reduce the amount of code you have to write.

Note that the synchronous constructors will block the main thread while getting the D-Bus connection and caching the initial property values. To avoid this, you can use asynchronous constructors like Gio.DBusProxy.new() and Gio.DBusProxy.new_for_bus().

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';


// Constructing a proxy
try {
    const proxy = await Gio.DBusProxy.new_for_bus(
        Gio.BusType.SESSION,
        Gio.DBusProxyFlags.NONE,
        null,
        'org.gnome.Shell',
        '/org/gnome/Shell',
        'org.gnome.Shell',
        null);

    /* Properties
     *
     * Similar to `GObject.Object::notify`, this signal is emitted when one or
     * more properties have changed on the proxy.
     */
    proxy.connect('g-properties-changed', (_proxy, changed, invalidated) => {
        const properties = changed.deepUnpack();

        /* These properties are already cached when the signal is emitted.
         */
        for (const [name, value] of Object.entries(properties))
            console.log(`Property ${name} set to ${value.unpack()}`);

        /* These properties have been marked as changed, but not cached.
         *
         * This is usually done for performance reasons, but you can set the
         * `Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES` flag at construction
         * to override this, in which case this will always be empty.
         */
        for (const name of invalidated)
            console.log(`Property ${name} changed`);
    });

    /* Signals
     *
     * This GObject signal is emitted when the service emits an D-Bus signal on
     * the interface the proxy is watching.
     */
    proxy.connect('g-signal', (_proxy, senderName, signalName, parameters) => {
        if (signalName === 'AcceleratorActivated')
            console.log(`Accelerator Activated: ${parameters.print(true)}`);
    });

    /* Service Status
     *
     * The `g-name-owner` property changes between a unique name and `null`
     * when the service appears or vanishes from the bus, respectively. The
     * proxy remains valid, allowing you to track the service state.
     */
    proxy.connect('notify::g-name-owner', (_proxy, _pspec) => {
        if (proxy.g_name_owner === null)
            console.log(`${proxy.g_name} has vanished`);
        else
            console.log(`${proxy.g_name} has appeared`);
    });

    /* Method calls only require the method name as the well-known name,
     * object path and interface name are bound to the proxy.
     */
    const reply = await proxy.call('FocusSearch', null,
        Gio.DBusCallFlags.NONE, -1, null);
} catch (e) {
    if (e instanceof Gio.DBusError)
        Gio.DBusError.strip_remote_error(e);

    logError(e);
}

High-Level Proxies

The D-Bus conveniences in GJS are the easiest way to get a client and cover most use cases. All you need to do is call Gio.DBusProxy.makeProxyWrapper() with the interface XML and it will create a reusable class you can use to create proxies.

Creating a Proxy Wrapper

Below is an example of how a proxy wrapper is created from an XML interface:

js
const interfaceXml = `
<node>
  <interface name="guide.gjs.Test">
    <method name="SimpleMethod"/>
    <method name="ComplexMethod">
      <arg type="s" direction="in" name="input"/>
      <arg type="u" direction="out" name="length"/>
    </method>
    <signal name="TestSignal">
      <arg name="type" type="s"/>
      <arg name="value" type="b"/>
    </signal>
    <property name="ReadOnlyProperty" type="s" access="read"/>
    <property name="ReadWriteProperty" type="b" access="readwrite"/>
  </interface>
</node>`;


// Pass the XML string to create a proxy class for that interface
const TestProxy = Gio.DBusProxy.makeProxyWrapper(interfaceXml);

Now that we have created a reusable class, we can easily create client proxies for an interface. The proxy will be created synchronously or asynchronously depending on whether a callback is passed in the constructor arguments.

The arguments for the constructor are as follows:

  • bus - a Gio.DBusConnection
  • name - a well-known name
  • object - an object path
  • asyncCallback - an optional callback
  • cancellable - an optional Gio.Cancellable
  • flags - an optional Gio.DBusProxyFlags value

The arguments passed to the asyncCallback are as follows:

  • proxy - a Gio.DBusProxy, or null on failure
  • error - a GLib.Error, or null on success

Here are examples for both synchronous and asynchronous construction:

js
// Synchronous, blocking method
const proxySync = TestProxy(Gio.DBus.session, 'guide.gjs.Test',
    '/guide/gjs/Test');

// Asynchronous, non-blocking method (Promise-wrapped)
const proxyAsync = await new Promise((resolve, reject) => {
    TestProxy(
        Gio.DBus.session,
        'guide.gjs.Test',
        '/guide/gjs/Test',
        (proxy, error) => {
            if (error === null)
                resolve(proxy);
            else
                reject(error);
        },
        null, /* cancellable */
        Gio.DBusProxyFlags.NONE);
});


// Create a proxy synchronously, making sure to catch errors
let proxy = null;

try {
    proxy = TestProxy(Gio.DBus.session, 'guide.gjs.Test', '/guide/gjs/Test');
} catch (e) {
    console.warn(e);
}

Using a Proxy Wrapper

With our proxy wrapper class, we can create a Gio.DBusProxy that has methods, properties and signals all set up for us.

Methods are defined for three variants: a synchronous variant, asynchronous with callback, and a variant that returns a Promise:

js
try {
    proxy.SimpleMethodSync();
} catch (e) {
    console.warn(e);
}

try {
    await proxy.SimpleMethodAsync();
} catch (e) {
    console.warn(e);
}

proxy.ComplexMethodRemote('input string', (returnValue, errorObj, fdList) => {
    // If @errorObj is `null`, then the method call succeeded and the variant
    // will already be unpacked with `GLib.Variant.prototype.deepUnpack()`
    if (errorObj === null) {
        console.debug(`ComplexMethod('input string'): ${returnValue}`);

        if (fdList !== null) {
            // Methods that return file descriptors are fairly rare, so you
            // will know if you should expect one or not. Consult the API
            // documentation for `Gio.UnixFDList` for more information.
        }

    // If there was an error, then @returnValue will be an empty list and
    // @errorObj will be an Error object
    } else {
        console.warn(errorObj);
    }
});

Properties work like native JavaScript properties and you can watch the g-properties-changed signal to be notified of changes:

js
console.log(`ReadOnlyProperty: ${proxy.ReadOnlyProperty}`);
console.log(`ReadWriteProperty: ${proxy.ReadWriteProperty}`);

proxy.ReadWriteProperty = true;
console.log(`ReadWriteProperty: ${proxy.ReadWriteProperty}`);

proxy.connect('g-properties-changed', (_proxy, _changed, _invalidated) => {
});

Signals are connected and disconnected with the functions connectSignal() and disconnectSignal(), so they don't conflict with the GObject methods:

js
const handlerId = proxy.connectSignal('TestSignal', (_proxy, nameOwner, args) => {
    console.log(`TestSignal: ${args[0]}, ${args[1]}`);

    proxy.disconnectSignal(handlerId);
});

Services

There are a number of reasons why exporting services over D-Bus can be useful for an application developer. It can help you establish a client-server architecture to separate the backend from the front-end, but it can also provide a language agnostic entry point for your application.

Owning a Name

The first thing we're going to cover is how to acquire a well-known name on a bus connection and at what point you will want to actually export your service. This is similar to watching a name:

js
import Gio from 'gi://Gio';


/**
 * Invoked when a connection to a message bus has been obtained.
 *
 * If there is a client waiting for the well-known name to appear on the bus,
 * you probably want to export your interfaces here. This way the interfaces
 * are ready to be used when the client is notified the name has been owned.
 *
 * @param {Gio.DBusConnection} connection - the connection to a message bus
 * @param {string} name - the name that is requested to be owned
 */
function onBusAcquired(connection, name) {
    console.log(`${name}: connection acquired`);
}

/**
 * Invoked when the name is acquired.
 *
 * On the other hand, if you were using something like GDBusObjectManager to
 * watch for interfaces, you could export your interfaces here.
 *
 * @param {Gio.DBusConnection} connection - the connection that acquired the name
 * @param {string} name - the name being owned
 */
function onNameAcquired(connection, name) {
    console.log(`${name}: name acquired`);
}

/**
 * Invoked when the name is lost or @connection has been closed.
 *
 * Typically you won't see this callback invoked, but it might happen if you
 * try to own a name that was already owned by someone else.
 *
 * @param {Gio.DBusConnection|null} connection - the connection on which to
 *     acquire the name, or %null if the connection was disconnected
 * @param {string} name - the name being owned
 */
function onNameLost(connection, name) {
    console.log(`${name}: name lost`);
}

// Just like a signal handler ID, the `Gio.bus_own_name()` function returns a
// unique ID we can use to unown the name when we're done with it.
const ownerId = Gio.bus_own_name(
    Gio.BusType.SESSION,
    'guide.gjs.Test',
    Gio.BusNameOwnerFlags.NONE,
    onBusAcquired,
    onNameAcquired,
    onNameLost);

// Note that `onNameLost()` is NOT invoked when manually unowning a name.
Gio.bus_unown_name(ownerId);

Exporting Interfaces

Now that we know how to own a well-known name, let's get to exporting our service interface. We'll use the same XML definition from the earlier example, since it has one of everything:

js
const interfaceXml = `
<node>
  <interface name="guide.gjs.Test">
    <method name="SimpleMethod"/>
    <method name="ComplexMethod">
      <arg type="s" direction="in" name="input"/>
      <arg type="u" direction="out" name="length"/>
    </method>
    <signal name="TestSignal">
      <arg name="type" type="s"/>
      <arg name="value" type="b"/>
    </signal>
    <property name="ReadOnlyProperty" type="s" access="read"/>
    <property name="ReadWriteProperty" type="b" access="readwrite"/>
  </interface>
</node>`;

GJS provides a convenience function for creating a service, which takes an XML definition and a class instance. The function is called Gio.DBusExportedObject.wrapJSObject() and is documented with the other Gio overrides in GJS.

The class can be either a plain JavaScript class or a GObject subclass, as long as it implements the defined methods and properties. Changes to property values and other signals must be emitted manually.

js
class Service {
    // Properties
    get ReadOnlyProperty() {
        return GLib.Variant.new_string('a string');
    }

    get ReadWriteProperty() {
        if (this._readWriteProperty === undefined)
            return false;

        return this._readWriteProperty;
    }

    set ReadWriteProperty(value) {
        if (this._readWriteProperty === value)
            return;

        this._readWriteProperty = value;
        this._impl.emit_property_changed('ReadWriteProperty',
            GLib.Variant.new_boolean(this.ReadWriteProperty));
    }

    // Methods
    SimpleMethod() {
        console.log('SimpleMethod() invoked');
    }

    ComplexMethod(input) {
        console.log(`ComplexMethod() invoked with '${input}'`);

        return input.length;
    }

    // Signals
    emitTestSignal() {
        this._impl.emit_signal('TestSignal',
            new GLib.Variant('(sb)', ['string', true]));
    }
}

When a property is set by a client of your service, the setter will be passed the return value of GLib.Variant.deepUnpack(). When a client requests the value of a property, the getter may return either a native value (e.g. 'a string') or a GLib.Variant that matches the expected signature.

Note that the object returned by Gio.DBusExportedObject.wrapJSObject() and the class instance are separate. The class definition above expects that the D-Bus object has been assigned to this._impl, so that it can emit property changes and signals.

If you choose to follow the same pattern, just be sure that is done before you export the service:

js
let serviceInstance = null;
let exportedObject = null;


function onBusAcquired(connection, _name) {
    // Create the class instance, then the D-Bus object
    serviceInstance = new Service();
    exportedObject = Gio.DBusExportedObject.wrapJSObject(interfaceXml,
        serviceInstance);

    // Assign the exported object to the property the class expects, then export
    serviceInstance._impl = exportedObject;
    exportedObject.export(connection, '/guide/gjs/Test');
}

function onNameAcquired(_connection, _name) {
    // Clients will typically start connecting and using your interface now.
}

function onNameLost(_connection, _name) {
    // Well behaved clients will know not to call methods on your interface now
}

const ownerId = Gio.bus_own_name(
    Gio.BusType.SESSION,
    'guide.gjs.Test',
    Gio.BusNameOwnerFlags.NONE,
    onBusAcquired,
    onNameAcquired,
    onNameLost);

Other APIs

There are a number of APIs in the GNOME platform that can make use of D-Bus, notably Gio.Application, Gio.Action and Gio.Menu. Although a little less flexible, these higher-level APIs make exporting functionality on D-Bus extremely easy and the built-in support in GTK makes them an excellent choice for applications.

In this section we'll cover the basics of Gio.Action and Gio.Menu, which provide an easy way to export functionality and structured presentation. The GNOME Developer Documentation for GActions and GMenus have a more complete overview of these APIs.

GAction

Gio.Action is actually a GObject Interface that can be implemented by objects, but you will almost always use the provided implementation Gio.SimpleAction. There are basically two kinds of actions: a functional type that emits a signal when activated and stateful actions that hold some kind of value.

Gio.Action objects are usually added to objects that implement the Gio.ActionGroup and Gio.ActionMap interfaces. The Gio.SimpleActionGroup implementation supports both of these.

Below are a few examples of how to create each type of action:

js
// This is the most basic an action can be. It has a name and can be activated
// with no parameters, which results in the callback being invoked.
const basicAction = new Gio.SimpleAction({
    name: 'basicAction',
});

basicAction.connect('activate', (action, _parameter) => {
    console.log(`${action.name} activated!`);
});

// An action with a parameter
const paramAction = new Gio.SimpleAction({
    name: 'paramAction',
    parameter_type: new GLib.VariantType('s'),
});

paramAction.connect('activate', (action, parameter) => {
    console.log(`${action.name} activated: ${parameter.unpack()}`);
});

// And a stateful action. The state type is set at construction from the initial
// value, and can't be changed afterwards.
const stateAction = new Gio.SimpleAction({
    name: 'stateAction',
    state: GLib.Variant.new_boolean(true),
});

stateAction.connect('notify::state', (action, _pspec) => {
    console.log(`${action.name} changed: ${action.state.print(true)}`);
});

Once you've created actions, you will want to add them to a group so that they can be exported over D-Bus. The exported actions will be playing the part of the "server" in this case.

js
// Adding actions to an action group
const actionGroup = new Gio.SimpleActionGroup();
actionGroup.add_action(basicAction);
actionGroup.add_action(paramAction);
actionGroup.add_action(stateAction);


// Exporting action groups on the session bus
const connection = Gio.DBus.session;
const groupId = connection.export_action_group('/guide/gjs/Test',
    actionGroup);


// Once exported, action groups can be unexported using the returned ID
connection.unexport_action_group(groupId);

Now any number of other processes can act as clients by using a Gio.DBusActionGroup. This class implements the Gio.ActionGroup interface, but not the Gio.ActionMap interface. This means clients can activate actions and change their state value, but they can not enable, disable, add or remove them.

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';


// #region action
// This is the most basic an action can be. It has a name and can be activated
// with no parameters, which results in the callback being invoked.
const basicAction = new Gio.SimpleAction({
    name: 'basicAction',
});

basicAction.connect('activate', (action, _parameter) => {
    console.log(`${action.name} activated!`);
});

// An action with a parameter
const paramAction = new Gio.SimpleAction({
    name: 'paramAction',
    parameter_type: new GLib.VariantType('s'),
});

paramAction.connect('activate', (action, parameter) => {
    console.log(`${action.name} activated: ${parameter.unpack()}`);
});

// And a stateful action. The state type is set at construction from the initial
// value, and can't be changed afterwards.
const stateAction = new Gio.SimpleAction({
    name: 'stateAction',
    state: GLib.Variant.new_boolean(true),
});

stateAction.connect('notify::state', (action, _pspec) => {
    console.log(`${action.name} changed: ${action.state.print(true)}`);
});
// #endregion action


// #region action-group
// Adding actions to an action group
const actionGroup = new Gio.SimpleActionGroup();
actionGroup.add_action(basicAction);
actionGroup.add_action(paramAction);
actionGroup.add_action(stateAction);


// Exporting action groups on the session bus
const connection = Gio.DBus.session;
const groupId = connection.export_action_group('/guide/gjs/Test',
    actionGroup);


// Once exported, action groups can be unexported using the returned ID
connection.unexport_action_group(groupId);
// #endregion action-group

What makes Gio.Action even more convenient, is that GTK already knows how to do all of this for you. Simply insert the Gio.ActionGroup into any Gtk.Widget and that widget or any of its children that implement the Gtk.Actionable interface can activate or set the state of the action.

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';


// Initialize the GTK environment and prepare an event loop
Gtk.init();
const loop = GLib.MainLoop.new(null, false);


// Create a top-level window
const window = new Gtk.Window({
    title: 'GJS GAction Example',
    default_width: 320,
    default_height: 240,
});
window.connect('close-request', () => loop.quit());

const box = new Gtk.Box({
    orientation: Gtk.Orientation.VERTICAL,
    margin_start: 12,
    margin_end: 12,
    margin_top: 12,
    margin_bottom: 12,
    spacing: 12,
});
window.set_child(box);


// GTK will search upwards in the hierarchy of widgets for the group, so we will
// insert our action group into the top-level window.
const remoteGroup = Gio.DBusActionGroup.get(
    Gio.DBus.session,
    'guide.gjs.Test',
    '/guide/gjs/Test'
);
window.insert_action_group('test', remoteGroup);


// We can now refer to our actions using the group name chosen when inserting
// it and the action name we want to activate
const button = new Gtk.Button({
    label: 'Click Me!',
    action_name: 'test.paramAction',
    action_target: new GLib.Variant('s', 'Button was clicked!'),
});
box.append(button);

const check = new Gtk.CheckButton({
    label: 'Toggle Me!',
    action_name: 'test.stateAction',
});
box.append(check);


// Open up the window
window.present();
await loop.runAsync();

GMenu

Gio.MenuModel is another GObject Interface for defining ordered, nested groups of menu items, sections and submenus. The defacto implementation of this interface is Gio.Menu.

Unlike Gio.Action, menu models contain presentation information like labels and icon names. It's also possible to define menu models in XML UI files, but we're only going to cover the basic API usage in GJS here because we're really just covering this for D-Bus usage.

js
import Gio from 'gi://Gio';


// Here we're creating the top-level menu. Submenus and sections can be created
// the same way and can be added to a parent menu with `append_submenu()` and
// `append_section()`.
const menuModel = new Gio.Menu();


// For the most common use case you can simply use Gio.Menu.prototype.append()
menuModel.append('Basic Item Label', 'test.basicAction');


// In cases you need the `Gio.MenuItem` instance to add more attributes, you
// can build an item manually. Notice that the second argument is a "detailed"
// action string, which can handle some simple types inline. Consult the
// documentation for how these can be used.
const paramItem = Gio.MenuItem.new('Parameter Item', 'test.paramAction::string');

// Icons are `Gio.Icon` instances, an abtsraction of icons that is serialized as
// a `a{sv}` variant when sent over D-Bus. Note that it's up to the client-side
// to actually do something useful with this.
const paramIcon = new Gio.ThemedIcon({
    name: 'dialog-information-symbolic',
});

paramItem.set_icon(paramIcon);

// Once we add the item to the menu, making changes to the `paramItem` instance
// or the GIcon won't affect the menu in any way.
menuModel.append_item(paramItem);


// A number of the Gtk Widgets that are built from GMenuModels can automatically
// handle simple action types like stateful actions with booleans. This item
// will be turned into a Gtk.CheckButton for us.
const stateItem = Gio.MenuItem.new('State Item', 'test.stateAction');
menuModel.append_item(stateItem);


// Export and unexport a menu just like GActionGroup
const connection = Gio.DBus.session;

const menuId = connection.export_menu_model(
    '/guide/gjs/Test',
    menuModel
);

connection.unexport_menu_model(menuId);

Now, assuming we have a remote process exporting both the action group and menu model from above, we can get clients for both and populate a menu:

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';


// Initialize the GTK environment and prepare an event loop
Gtk.init();
const loop = GLib.MainLoop.new(null, false);


// Create a top-level window
const window = new Gtk.Window({
    title: 'GJS GMenu Example',
    default_width: 320,
    default_height: 240,
});
window.connect('close-request', () => loop.quit());

const headerBar = new Gtk.HeaderBar({
    show_title_buttons: true,
});
window.set_titlebar(headerBar);


// As before, we'll insert the action group
const remoteGroup = Gio.DBusActionGroup.get(Gio.DBus.session,
    'guide.gjs.Test', '/guide/gjs/Test');
window.insert_action_group('test', remoteGroup);


// Get the remote menu model
const remoteMenu = Gio.DBusMenuModel.get(Gio.DBus.session,
    'guide.gjs.Test', '/guide/gjs/Test');

// And now we'll add a menu button to a header bar with our menu model
const menuButton = new Gtk.MenuButton({
    icon_name: 'open-menu-symbolic',
    menu_model: remoteMenu,
});
headerBar.pack_end(menuButton);


// Show the window and start the event loop
window.present();
await loop.runAsync();

MIT Licensed | GJS, A GNOME Project