Sharing a variable between extension and application

I’m developing a combo of extension + application, where the GNOME extension waits for a signal from the application to do something and then replies information back to the application. For now I’ve only written the code as a GNOME Extension, but I’m already developing it with the application in mind.

I’ve been trying to use dbus/GActions by following the guide on gjs, however it seems like my “client”-side doesn’t connect to changes in the value even though my “server”-side does. This is a minimum reproducible code:

import GObject from 'gi://GObject';
import St from 'gi://St';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';

import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';

import * as Main from 'resource:///org/gnome/shell/ui/main.js';

const Indicator = GObject.registerClass(
class Indicator extends PanelMenu.Button {
    _init() {
        // Creates a minimum indicator for testing purposes
        super._init(0.0, _('My Shiny Indicator'));

        this.add_child(new St.Icon({
            icon_name: 'face-smile-symbolic',
            style_class: 'system-status-icon',
        let item = new PopupMenu.PopupMenuItem(_('Show Notification'));;

        // "Client"-side connects to the state
        const remoteGroup = Gio.DBusActionGroup.get(

        // The client waits for an update from the server-side
        // NOT WORKING
        remoteGroup.connect('action-state-changed', (action, value) => {
            console.log("I'm not called");

        // Acts as a button to communicate with the server
        item.connect('activate', () => {
            remoteGroup.activate_action('paramAction', new GLib.Variant('(s)', ['string']));

export default class IndicatorExampleExtension extends Extension {
    enable() {
        // Initialize our actions and create a GActionGroup
        const stateAction = new Gio.SimpleAction({
            name: 'stateAction',
            state: GLib.Variant.new_string(''),
        const paramAction = new Gio.SimpleAction({
            name: 'paramAction',
            parameter_type: new GLib.VariantType('(s)'),
        const actionGroup = new Gio.SimpleActionGroup();

        // "Server"-side is called whenever the value is changed
        // WORKS
        actionGroup.connect('action-state-changed', (action, value) => {
            console.log("I'm called");

        // Endpoint the "client" reaches to update the state
        paramAction.connect('activate', (action, parameter) => {
            // I also tried with stateAction.change_state
                        new GLib.Variant('s', 

        // Exposes the serverside
        this.connection = Gio.DBus.session;
        this.groupId = this.connection.export_action_group('/guide/gjs/Test',

        // Builds our testing "client"
        this._indicator = new Indicator();
        Main.panel.addToStatusArea(this.uuid, this._indicator);

    disable() {
        this._indicator = null;

No errors nor anything… Is this a bug? Is it not possible for remoteGroup to see changes in a dbus value with Gio.DBusActionGroup.get? Is there another way of doing what I’m trying to accomplish using GNOME libraries?

Thanks in advance <3

Since you’re handling the Gio.DBusActionGroup yourself, you’ll want to make a call to Gio.ActionGroup.list_actions() before attempting to use the action group.

Thanks for the response andyholmes! I don’t know how list_actions flew over my head! So, I tested it out and it’s weirder than before:

actionGroup.list_actions() yields the two actions as expected:

GNOME Shell-Message: 21:40:07.770: Array [

remoteGroup.list_actions() yields none

GNOME Shell-Message: 21:40:07.775: Array []

But somehow

remoteGroup.activate_action('paramAction', new GLib.Variant('(s)', ['string']));

works just fine (!?)

Any idea of what could be causing this? You also mentioned about handling Gio.DBusActionGroup myself, is there another implementation for it?

If you pass a Gio.DBusActionGroup to a GTK widget, it will handle things like this for you. Since you’re not and the action group is remote, you need to initialize the actions which will happen asynchronously over D-Bus.

The documentation for Gio.DBusActionGroup.get() says:

This call is non-blocking. The returned action group may or may not
already be filled in. The correct thing to do is connect the signals
for the action group to monitor for changes and then to call
Gio.ActionGroup.list_actions to get the initial list.

Whatever the results of the initial call to list_actions(), further changes will have signals emitted for them.

1 Like

Thank you very much, this definitely lead me to fixing the issue by adding a remoteActions.list_actions() and changing actionGroup.action_state_changed to actionGroup.change_state_action.

Alright, I was now writing the rust application, I’m pretty sure I’m doing the same as the gjs code line by line, but well… Same thing, I can alter the state of “stateAction” and activate “paramAction” but the watching of “stateAction” doesn’t work, remote_group.list_actions() returns an empty list (before and after activation/change of state/signal connections).

Is there a way to wait for DBusActionGroup to be filled in? Making a loop to wait for the actions doesn’t seem to work.

I’ll look for some apps that use DBusActionGroup in the mean time. Thank you again for helping me out.

Could you show your code?

Yes, here it is:

EDIT: Ah, just a fyi, I was experimenting a lot using the Glib Application’s dbus_connection method, loops and print statements, so it might look a bit messy for now.

1 Like

This sort of thing is not going to work, since the “waiting” you want to do requires the main context to iterate through pending events.

This is obviously a test program, so in this case you could do this is whatever language/syntax:

while (!some_condition)
    g_main_context_iteration (NULL, FALSE);

In a real application a GMainLoop will already be running and you don’t want to interfere with it this way. In that case, you just wait for the expected signal to be emitted, use a idle/timeout source, or in GJS (and I guess Rust probably) you can wrap that in a Promise/Future if you want it to behave in some pseudo-synchronous way.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.