Interacting With D-Bus

D-Bus is a message bus that allows applications to communicate to the system or other applications. This is a powerful mechanism, but it can be a little intimidating at first.

We are going to make our app respect and react to the system appearance: if it’s dark, the app will be dark, if it’s light, the app will be light.

For that, we’ll use Portals. They are a set of well-known D-Bus interfaces that permit interaction with the system, even in a sandbox. The Settings portal will allow us to get the user color scheme preference and change the appearance of our application accordingly.

The easiest way to use D-Bus in GJS is to first describe the interface we’re interested in, and create a proxy that will connect to a service implementing that interface.

# Describing the Interface

An interface is all the methods, properties and signals provided by a service.

To create a proxy to a service implementing the interface, we have to describe it in XML. We don’t have to describe all of the methods, properties and signals, only the ones we are interested in.

For the Settings portal, we’ll use these:

We’ll store the interface in a constant at the top of src/Application.js:

12const SETTINGS_PORTAL_INTERFACE = `
13
14`;

Let’s start by putting the interface element in a node element. The name attribute is the name of the interface, as given by the documentation:

13<node>
14	<interface name="org.freedesktop.portal.Settings">
15	</interface>
16</node>

Now, let’s describe the Read method. We can read in the documentation that it has three arguments: two in the IN direction, namespace and key, both of type s, and one in the OUT direction, value, of type v. IN means we have to provide these arguments, and OUT means we’ll get it as a result. The types are described in the D-Bus specification.

We add a method element with the name attribute set to the name of the method, and a child arg element for each argument, with its direction, type and name attributes filled:

13<node>
14	<interface name="org.freedesktop.portal.Settings">
15		<method name="Read">
16			<arg direction="in" type="s" name="namespace"/>
17			<arg direction="in" type="s" name="key"/>
18			<arg direction="out" type="v" name="value"/>
19		</method>
20	</interface>
21</node>

We do the same for the SettingChanged signal, with the signal element. This time, the arg elements don’t have a direction attribute since we will always get them.

13<node>
14	<interface name="org.freedesktop.portal.Settings">
15		 <!-- ... -->
16		<signal name="SettingChanged">
17			<arg type="s" name="namespace"/>
18			<arg type="s" name="key"/>
19			<arg type="v" name="value"/>
20		</signal>
21	</interface>
22</node>

And finally, for the version property, we use the property element. The access attribute value tells if it’s a read-only property (read) or also a writable one (readwrite).

13<node>
14	<interface name="org.freedesktop.portal.Settings">
15		<!-- ... -->
16		<property name="version" access="read" type="u"/>
17	</interface>
18</node>

# Creating the Proxy

Now that we have described the interface, we can use the convenience Gio.DBusProxy.makeProxyWrapper() function. It will create for us a Gio.DBusProxy class with convenience methods and properties to the interface’s ones.

We’ll keep the proxy around in the #settingsPortalProxy property. Let’s also add a #connectToSettingsPortal() method to our Application class and call it when it is started:

33class extends Gtk.Application {
34	#settingsPortalProxy = null;
35
36	vfunc_startup() {
37		/* ... */
38		this.#connectToSettingsPortal();
39	}
40
41	/* ... */
42
43	#connectToSettingsPortal() {}
44});

In the method, we create the proxy and instantiate it. We have to give it which bus to use, and under which bus name and object path the interface is: this is where the service can be called.

The documentation tells us that the portals are on the session bus, under the org.freedesktop.portal.Desktop name at the /org/freedesktop/portal/desktop path, so we use these values:

79#connectToSettingsPortal() {
80	// Create the D-Bus proxy to the settings portal
81	const SettingsPortalProxy = Gio.DBusProxy.makeProxyWrapper(SETTINGS_PORTAL_INTERFACE);
82	this.#settingsPortalProxy = new SettingsPortalProxy(
83		Gio.DBus.session,
84		'org.freedesktop.portal.Desktop',
85		'/org/freedesktop/portal/desktop'
86	);
87}

# Reading a Property

The Settings portal has a version property that we’ll use to make sure we’ll only interact with a compatible version. To get its value, we simply have to access the version property on the proxy:

79#connectToSettingsPortal() {
80	/* ... */
81
82	// Check that we're compatible with the settings portal
83	if (this.#settingsPortalProxy.version !== 1)
84		return;
85}

If we don’t get the expected version, we don’t go further.

# Calling a Method

To get the color scheme preference of the user, as told by the documentation, we have to read the color-scheme value of the org.freedesktop.appearance namespace. Its value is a number: 0 if the user has no preference, 1 if the user prefers a dark appearance, and 2 if they prefer a light appearance.

To use the Read method, the proxy wrapper created two convenience methods for us: ReadSync() and ReadRemote(). The first one is blocking, the second one takes a callback function to deal with the result.

We’ll use ReadSync(). It will give us an array containing all the OUT arguments. The type of the value returned argument is v, which means it’s a GLib.Variant, and that we can use the convenience method recursiveUnpack() to get its value.

Translated in code, that means:

79#connectToSettingsPortal() {
80	/* ... */
81
82	// Get the color scheme and change the appearance accordingly
83	const colorScheme = this.#settingsPortalProxy.ReadSync('org.freedesktop.appearance', 'color-scheme')[0].recursiveUnpack();
84}

Let’s add a #changeAppearance() method to the application to deal with the color scheme:

33class extends Gtk.Application {
34	/* ... */
35
36	#changeAppearance(colorScheme = 0) {
37		// Possible color scheme values:
38		// 0: no preference
39		// 1: prefer dark appearance
40		// 2: prefer light appearance
41		const gtkSettings = Gtk.Settings.get_default();
42		switch (colorScheme) {
43			case 1:
44				gtkSettings.gtkApplicationPreferDarkTheme = true;
45				break;
46			default:
47				gtkSettings.gtkApplicationPreferDarkTheme = false;
48		}
49	}
50}

It changes the gtk-application-prefer-dark-theme property of Gtk.Settings to change the appearance of the application. We call it with the value we get from the portal:

79#connectToSettingsPortal() {
80	/* ... */
81	this.#changeAppearance(colorScheme);
82}

Now, when launching the application, it will respect the preferred color scheme!

# Connecting to a Signal

However, if the color scheme changes while the application is running, nothing will happen. That’s where signals come in!

We’ll connect to the SettingChanged signal, and check that the changed setting is the one we’re interested in. The proxy has a convenient connectSignal() method which takes the signal name and a callback function. It returns an ID that we can pass to the disconnectSignal() method.

The callback function takes the proxy, the bus name owner, and an array containing the signal’s arguments.

79#connectToSettingsPortal() {
80	/* ... */
81
82	// Update the appearance when the color scheme changes
83	this.#settingsPortalProxy.connectSignal(
84		'SettingChanged',
85		(_proxy, _nameOwner, [namespace, key, value]) => {
86			// If this is not the setting we want that changed, return
87			if (namespace !== 'org.freedesktop.appearance' || key !== 'color-scheme')
88				return;
89			// Unpack the value
90			const colorScheme = value.recursiveUnpack();
91			this.#changeAppearance(colorScheme);
92		}
93	);
94}

That’s it, our application now reacts to color scheme changes!