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:
- The
Read
method, to read a setting - The
SettingChanged
signal, emitted when a setting changes - The
version
property, which contains the version of the portal
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!