Is it possible to import Mutter in a shell extension?

I am starting out with writing extensions attempting to make something that will give me access to the current session’s user activity. This information is to be consumed by a foreign language (Go) application via D-Bus. At the moment, the Go application needs to make three D-Bus calls: org.gnome.Mutter.IdleMonitor.GetIdletime, org.gnome.ScreenSaver.GetActive and the extension I am writing (pasted below).

I would like to be able to reduce this to a single D-Bus call. In order to do this, I will need to import Mutter and ScreenSaver. Is this possible? Is there documentation for this? I have attempted to import Mutter from 'gi://Mutter'; but this fails with “Error: Requiring Mutter, version none: Typelib file for namespace ‘Mutter’ (any version) not found#012require@resource:///org/gnome/gjs/modules/esm/gi.js:16:28#012@gi://Mutter:3:25#012@resource:///org/gnome/shell/ui/init.js:21:20”. Alternatively, can I get the information I’m using Mutter and ScreenSaver for from the Shell API?

I would also like to know whether it is possible for more than one element in the array returned by global.get_window_actors() can have the property has_focus be true. I would assume/hope not, but I cannot find documentation around this.

import Gio from 'gi://Gio';

const interfaceXML = `
<node>
   <interface name="org.gnome.Shell.Extensions.UserActivity">
      <method name="Details">
         <arg type="s" direction="out" name="windows"/>
      </method>
   </interface>
</node>`;

export default class Extension {
  enable() {
    this._dbus = Gio.DBusExportedObject.wrapJSObject(interfaceXML, this);
    this._dbus.export(Gio.DBus.session, '/org/gnome/Shell/Extensions/UserActivity');
  }

  disable() {
    this._dbus.flush();
    this._dbus.unexport();
    delete this._dbus;
  }

  Details() {
    const now = new Date().toISOString(); // Place holder: will be now-idletime.
    const det = global.get_window_actors()
    .filter(w => w.meta_window[`has_focus`]?.())
    .map(w => {
      function get(name) {
        return w.meta_window[`get_${name}`]?.();
      }
      return {
        'ts':     now,
        'wid':    get('id'),
        'class':  get('wm_class'),
        'name':   get('wm_class_instance'),
        'window': get('title'),
        'layer':  get('layer')
      };
    });
    return JSON.stringify(det);
  }
}

Is that actually in an extension? Extensions are effectively patches that are applied to GNOME Shell when enabled, and so you can’t do this in a subprocess that just happens to live in the extension’s directory.

Unless you have a good reason to use an out-of-process script, you really don’t need to make calls to D-Bus at all, since Mutter is a library used by GNOME Shell and you can probably make those calls natively.

I’m completely open to alternative approaches.

At the moment, the actual application only works with X11 and gets this information via Xlib calls. I want to be able to support Wayland and AFAICS this means that I’ll need to have adapters for each of the possible Wayland clients, because Wayland. Since I don’t know that any given lib will be present on a system, making D-Bus calls to adapters based on user configuration seems like the most sensible approach.

I am a little confused by the question though; I want to ask Mutter and ScreenSaver the questions above without D-Bus from within the extension; the only D-Bus call is from Go to the extension.

I have tried using GnomeDesktop.IdleMonitor.get_idle_time(), but this always returns 0 AFAICS, and I cannot find an alternative to get the screen saver GetActive call.

export default class Extension {
  enable() {
    this._idle_monitor = new GnomeDesktop.IdleMonitor();
    this._dbus = Gio.DBusExportedObject.wrapJSObject(interfaceXML, this);
    this._dbus.export(Gio.DBus.session, '/org/gnome/Shell/Extensions/UserActivity');
  }

  disable() {
    this._dbus.flush();
    this._dbus.unexport();
    delete this._dbus;
    delete this._idle_monitor;
  }

  Details() {
    const idle = this._idle_monitor.get_idletime();
    const now = new Date(new Date().getTime()-idle).toISOString();
    const det = global.get_window_actors()
    .filter(w => w.meta_window[`has_focus`]?.())
    .map(w => {
      function get(name) {
        return w.meta_window[`get_${name}`]?.();
      }
      return {
        'ts':     now,
	'idle':   idle,
        'wid':    get('id'),
        'class':  get('wm_class'),
        'name':   get('wm_class_instance'),
        'window': get('title'),
        'layer':  get('layer')
      };
    });
    return JSON.stringify(det);
  }
}

Ah, sorry it’s getting later here, so I misread quite a few things there. You want this:

import Meta from 'gi://Meta'; // <-- Meta, not Mutter

const idleMonitor = global.backend.get_core_idle_monitor();
const idleTime = idleMonitor.get_idletime();

The API documentation for the latest stable Meta 14 and older versions are on https://gjs-docs.gnome.org, and more general documentation is on https://gjs.guide.

The architecture overview might clear some things up, as well.

Thanks, yeah I found the API docs and saw the background which looked promising, but I didn’t know how to hold it. I also found the guide, but have not found it helpful for these kinds of issues (it did help in getting started, though some things are broken with links, for example the Gio.DBusExportedObject.wrapJSObject here — links to this 404 — and the suggestion to import {Extension} here which breaks my code in ways I don’t understand).

This gets my idle time nicely.

I don’t see any accessors for the screen saver status (or any screen save API) in the GJS API docs, so I imagine that is not available. I can manage with two D-Bus, calls, so if that’s the case, I will have to do that. If I’m missing anything though, please let me know.

Thanks for your help.

1 Like

Honestly, even when there’s API documentation, the quickest way to find your entry points is usually just searching GNOME/gnome-shell, filtered for JavaScript. Aside from that you get some examples and code comments for best practices.

In this case, it reminds me that the screensaver service is run out-of-process, so you could use a proxy like GNOME Shell does, but maybe that doesn’t save you enough to make it worth it.

1 Like