summaryrefslogtreecommitdiffstats
path: root/js/dbusServices
diff options
context:
space:
mode:
Diffstat (limited to 'js/dbusServices')
-rw-r--r--js/dbusServices/dbus-service.in5
-rw-r--r--js/dbusServices/dbus-service.service.in3
-rw-r--r--js/dbusServices/dbusService.js188
-rw-r--r--js/dbusServices/extensions/css/application.css9
-rw-r--r--js/dbusServices/extensions/extensionPrefsDialog.js178
-rw-r--r--js/dbusServices/extensions/extensionsService.js161
-rw-r--r--js/dbusServices/extensions/main.js23
-rw-r--r--js/dbusServices/extensions/ui/extension-error-page.ui112
-rw-r--r--js/dbusServices/meson.build44
-rw-r--r--js/dbusServices/notifications/main.js11
-rw-r--r--js/dbusServices/notifications/notificationDaemon.js160
-rw-r--r--js/dbusServices/org.gnome.ScreenSaver.src.gresource.xml11
-rw-r--r--js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml19
-rw-r--r--js/dbusServices/org.gnome.Shell.Notifications.src.gresource.xml11
-rw-r--r--js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml11
-rw-r--r--js/dbusServices/screencast/main.js14
-rw-r--r--js/dbusServices/screencast/screencastService.js498
-rw-r--r--js/dbusServices/screensaver/main.js11
-rw-r--r--js/dbusServices/screensaver/screenSaverService.js70
19 files changed, 1539 insertions, 0 deletions
diff --git a/js/dbusServices/dbus-service.in b/js/dbusServices/dbus-service.in
new file mode 100644
index 0000000..5241661
--- /dev/null
+++ b/js/dbusServices/dbus-service.in
@@ -0,0 +1,5 @@
+imports.package.start({
+ name: '@PACKAGE_NAME@',
+ prefix: '@prefix@',
+ libdir: '@libdir@',
+});
diff --git a/js/dbusServices/dbus-service.service.in b/js/dbusServices/dbus-service.service.in
new file mode 100644
index 0000000..3b0d09a
--- /dev/null
+++ b/js/dbusServices/dbus-service.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=@service@
+Exec=@gjs@ @pkgdatadir@/@service@
diff --git a/js/dbusServices/dbusService.js b/js/dbusServices/dbusService.js
new file mode 100644
index 0000000..8b6a0a1
--- /dev/null
+++ b/js/dbusServices/dbusService.js
@@ -0,0 +1,188 @@
+/* exported DBusService, ServiceImplementation */
+
+const { Gio, GLib } = imports.gi;
+
+const Signals = imports.signals;
+
+const IDLE_SHUTDOWN_TIME = 2; // s
+
+const { programArgs } = imports.system;
+
+var ServiceImplementation = class {
+ constructor(info, objectPath) {
+ this._objectPath = objectPath;
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(info, this);
+
+ this._injectTracking('return_dbus_error');
+ this._injectTracking('return_error_literal');
+ this._injectTracking('return_gerror');
+ this._injectTracking('return_value');
+ this._injectTracking('return_value_with_unix_fd_list');
+
+ this._senders = new Map();
+ this._holdCount = 0;
+
+ this._hasSignals = this._dbusImpl.get_info().signals.length > 0;
+ this._shutdownTimeoutId = 0;
+
+ // subclasses may override this to disable automatic shutdown
+ this._autoShutdown = true;
+
+ this._queueShutdownCheck();
+ }
+
+ // subclasses may override this to own additional names
+ register() {
+ }
+
+ export() {
+ this._dbusImpl.export(Gio.DBus.session, this._objectPath);
+ }
+
+ unexport() {
+ this._dbusImpl.unexport();
+ }
+
+ hold() {
+ this._holdCount++;
+ }
+
+ release() {
+ if (this._holdCount === 0) {
+ logError(new Error('Unmatched call to release()'));
+ return;
+ }
+
+ this._holdCount--;
+
+ if (this._holdCount === 0)
+ this._queueShutdownCheck();
+ }
+
+ /**
+ * _handleError:
+ * @param {Gio.DBusMethodInvocation}
+ * @param {Error}
+ *
+ * Complete @invocation with an appropriate error if @error is set;
+ * useful for implementing early returns from method implementations.
+ *
+ * @returns {bool} - true if @invocation was completed
+ */
+
+ _handleError(invocation, error) {
+ if (error === null)
+ return false;
+
+ if (error instanceof GLib.Error) {
+ invocation.return_gerror(error);
+ } else {
+ let name = error.name;
+ if (!name.includes('.')) // likely a normal JS error
+ name = `org.gnome.gjs.JSError.${name}`;
+ invocation.return_dbus_error(name, error.message);
+ }
+
+ return true;
+ }
+
+ _maybeShutdown() {
+ if (!this._autoShutdown)
+ return;
+
+ if (GLib.getenv('SHELL_DBUS_PERSIST'))
+ return;
+
+ if (this._holdCount > 0)
+ return;
+
+ this.emit('shutdown');
+ }
+
+ _queueShutdownCheck() {
+ if (this._shutdownTimeoutId)
+ GLib.source_remove(this._shutdownTimeoutId);
+
+ this._shutdownTimeoutId = GLib.timeout_add_seconds(
+ GLib.PRIORITY_DEFAULT, IDLE_SHUTDOWN_TIME,
+ () => {
+ this._shutdownTimeoutId = 0;
+ this._maybeShutdown();
+
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _trackSender(sender) {
+ if (this._senders.has(sender))
+ return;
+
+ this.hold();
+ this._senders.set(sender,
+ this._dbusImpl.get_connection().watch_name(
+ sender,
+ Gio.BusNameWatcherFlags.NONE,
+ null,
+ () => this._untrackSender(sender)));
+ }
+
+ _untrackSender(sender) {
+ const id = this._senders.get(sender);
+
+ if (id)
+ this._dbusImpl.get_connection().unwatch_name(id);
+
+ if (this._senders.delete(sender))
+ this.release();
+ }
+
+ _injectTracking(methodName) {
+ const { prototype } = Gio.DBusMethodInvocation;
+ const origMethod = prototype[methodName];
+ const that = this;
+
+ prototype[methodName] = function (...args) {
+ origMethod.apply(this, args);
+
+ if (that._hasSignals)
+ that._trackSender(this.get_sender());
+
+ that._queueShutdownCheck();
+ };
+ }
+};
+Signals.addSignalMethods(ServiceImplementation.prototype);
+
+var DBusService = class {
+ constructor(name, service) {
+ this._name = name;
+ this._service = service;
+ this._loop = new GLib.MainLoop(null, false);
+
+ this._service.connect('shutdown', () => this._loop.quit());
+ }
+
+ run() {
+ // Bail out when not running under gnome-shell
+ Gio.DBus.watch_name(Gio.BusType.SESSION,
+ 'org.gnome.Shell',
+ Gio.BusNameWatcherFlags.NONE,
+ null,
+ () => this._loop.quit());
+
+ this._service.register();
+
+ let flags = Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT;
+ if (programArgs.includes('--replace'))
+ flags |= Gio.BusNameOwnerFlags.REPLACE;
+
+ Gio.DBus.own_name(Gio.BusType.SESSION,
+ this._name,
+ flags,
+ () => this._service.export(),
+ null,
+ () => this._loop.quit());
+
+ this._loop.run();
+ }
+};
diff --git a/js/dbusServices/extensions/css/application.css b/js/dbusServices/extensions/css/application.css
new file mode 100644
index 0000000..4ef4066
--- /dev/null
+++ b/js/dbusServices/extensions/css/application.css
@@ -0,0 +1,9 @@
+.error-page preferencespage { margin: 30px; }
+
+.expander { padding: 12px; }
+.expander.expanded { border: 0 solid @borders; border-bottom-width: 1px; }
+.expander-toolbar {
+ border: 0 solid @borders;
+ border-top-width: 1px;
+ padding: 3px;
+}
diff --git a/js/dbusServices/extensions/extensionPrefsDialog.js b/js/dbusServices/extensions/extensionPrefsDialog.js
new file mode 100644
index 0000000..7155c1a
--- /dev/null
+++ b/js/dbusServices/extensions/extensionPrefsDialog.js
@@ -0,0 +1,178 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ExtensionPrefsDialog */
+
+const { Adw, Gdk, Gio, GLib, GObject, Gtk } = imports.gi;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+
+var ExtensionPrefsDialog = GObject.registerClass({
+ GTypeName: 'ExtensionPrefsDialog',
+}, class ExtensionPrefsDialog extends Adw.PreferencesWindow {
+ _init(extension) {
+ super._init({
+ title: extension.metadata.name,
+ search_enabled: false,
+ });
+
+ try {
+ ExtensionUtils.installImporter(extension);
+
+ // give extension prefs access to their own extension object
+ ExtensionUtils.setCurrentExtension(extension);
+
+ const prefsModule = extension.imports.prefs;
+ prefsModule.init(extension.metadata);
+
+ if (prefsModule.fillPreferencesWindow) {
+ prefsModule.fillPreferencesWindow(this);
+
+ if (!this.visible_page)
+ throw new Error('Extension did not provide any UI');
+ } else {
+ const widget = prefsModule.buildPrefsWidget();
+ const page = this._wrapWidget(widget);
+ this.add(page);
+ }
+ } catch (e) {
+ this._showErrorPage(e);
+ logError(e, 'Failed to open preferences');
+ }
+ }
+
+ set titlebar(w) {
+ this.set_titlebar(w);
+ }
+
+ // eslint-disable-next-line camelcase
+ set_titlebar() {
+ // intercept fatal libadwaita error, show error page instead
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this._showErrorPage(
+ new Error('set_titlebar() is not supported for Adw.Window'));
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _showErrorPage(e) {
+ while (this.visible_page)
+ this.remove(this.visible_page);
+
+ const extension = ExtensionUtils.getCurrentExtension();
+ this.add(new ExtensionPrefsErrorPage(extension, e));
+ }
+
+ _wrapWidget(widget) {
+ if (widget instanceof Adw.PreferencesPage)
+ return widget;
+
+ const page = new Adw.PreferencesPage();
+ if (widget instanceof Adw.PreferencesGroup) {
+ page.add(widget);
+ return page;
+ }
+
+ const group = new Adw.PreferencesGroup();
+ group.add(widget);
+ page.add(group);
+
+ return page;
+ }
+});
+
+const ExtensionPrefsErrorPage = GObject.registerClass({
+ GTypeName: 'ExtensionPrefsErrorPage',
+ Template: 'resource:///org/gnome/Shell/Extensions/ui/extension-error-page.ui',
+ InternalChildren: [
+ 'expander',
+ 'expanderArrow',
+ 'revealer',
+ 'errorView',
+ ],
+}, class ExtensionPrefsErrorPage extends Adw.PreferencesPage {
+ static _classInit(klass) {
+ super._classInit(klass);
+
+ klass.install_action('page.copy-error',
+ null,
+ self => {
+ const clipboard = self.get_display().get_clipboard();
+ clipboard.set(self._errorMarkdown);
+ });
+ klass.install_action('page.show-url',
+ null,
+ self => Gtk.show_uri(self.get_root(), self._url, Gdk.CURRENT_TIME));
+
+ return klass;
+ }
+
+ _init(extension, error) {
+ super._init();
+
+ this._addCustomStylesheet();
+
+ this._uuid = extension.uuid;
+ this._url = extension.metadata.url || '';
+
+ this.action_set_enabled('page.show-url', this._url !== '');
+
+ this._gesture = new Gtk.GestureClick({
+ button: 0,
+ exclusive: true,
+ });
+ this._expander.add_controller(this._gesture);
+
+ this._gesture.connect('released', (gesture, nPress) => {
+ if (nPress === 1)
+ this._revealer.reveal_child = !this._revealer.reveal_child;
+ });
+
+ this._revealer.connect('notify::reveal-child', () => {
+ this._expanderArrow.icon_name = this._revealer.reveal_child
+ ? 'pan-down-symbolic'
+ : 'pan-end-symbolic';
+ this._syncExpandedStyle();
+ });
+ this._revealer.connect('notify::child-revealed',
+ () => this._syncExpandedStyle());
+
+ this._errorView.buffer.text = `${error}\n\nStack trace:\n`;
+ // Indent stack trace.
+ this._errorView.buffer.text +=
+ error.stack.split('\n').map(line => ` ${line}`).join('\n');
+
+ // markdown for pasting in gitlab issues
+ let lines = [
+ `The settings of extension ${this._uuid} had an error:`,
+ '```',
+ `${error}`,
+ '```',
+ '',
+ 'Stack trace:',
+ '```',
+ error.stack.replace(/\n$/, ''), // stack without trailing newline
+ '```',
+ '',
+ ];
+ this._errorMarkdown = lines.join('\n');
+ }
+
+ _syncExpandedStyle() {
+ if (this._revealer.reveal_child)
+ this._expander.add_css_class('expanded');
+ else if (!this._revealer.child_revealed)
+ this._expander.remove_css_class('expanded');
+ }
+
+ _addCustomStylesheet() {
+ let provider = new Gtk.CssProvider();
+ let uri = 'resource:///org/gnome/Shell/Extensions/css/application.css';
+ try {
+ provider.load_from_file(Gio.File.new_for_uri(uri));
+ } catch (e) {
+ logError(e, 'Failed to add application style');
+ }
+ Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(),
+ provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+});
diff --git a/js/dbusServices/extensions/extensionsService.js b/js/dbusServices/extensions/extensionsService.js
new file mode 100644
index 0000000..d8234d2
--- /dev/null
+++ b/js/dbusServices/extensions/extensionsService.js
@@ -0,0 +1,161 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ExtensionsService */
+
+const { Gio, GLib, Shew } = imports.gi;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+
+const { loadInterfaceXML } = imports.misc.dbusUtils;
+const { ExtensionPrefsDialog } = imports.extensionPrefsDialog;
+const { ServiceImplementation } = imports.dbusService;
+
+const ExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');
+const ExtensionsProxy = Gio.DBusProxy.makeProxyWrapper(ExtensionsIface);
+
+var ExtensionsService = class extends ServiceImplementation {
+ constructor() {
+ super(ExtensionsIface, '/org/gnome/Shell/Extensions');
+
+ this._proxy = new ExtensionsProxy(Gio.DBus.session,
+ 'org.gnome.Shell', '/org/gnome/Shell');
+
+ this._proxy.connectSignal('ExtensionStateChanged',
+ (proxy, sender, params) => {
+ this._dbusImpl.emit_signal('ExtensionStateChanged',
+ new GLib.Variant('(sa{sv})', params));
+ });
+
+ this._proxy.connect('g-properties-changed', () => {
+ this._dbusImpl.emit_property_changed('UserExtensionsEnabled',
+ new GLib.Variant('b', this._proxy.UserExtensionsEnabled));
+ });
+ }
+
+ get ShellVersion() {
+ return this._proxy.ShellVersion;
+ }
+
+ get UserExtensionsEnabled() {
+ return this._proxy.UserExtensionsEnabled;
+ }
+
+ set UserExtensionsEnabled(enable) {
+ this._proxy.UserExtensionsEnabled = enable;
+ }
+
+ async ListExtensionsAsync(params, invocation) {
+ try {
+ const res = await this._proxy.ListExtensionsAsync(...params);
+ invocation.return_value(new GLib.Variant('(a{sa{sv}})', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async GetExtensionInfoAsync(params, invocation) {
+ try {
+ const res = await this._proxy.GetExtensionInfoAsync(...params);
+ invocation.return_value(new GLib.Variant('(a{sv})', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async GetExtensionErrorsAsync(params, invocation) {
+ try {
+ const res = await this._proxy.GetExtensionErrorsAsync(...params);
+ invocation.return_value(new GLib.Variant('(as)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async InstallRemoteExtensionAsync(params, invocation) {
+ try {
+ const res = await this._proxy.InstallRemoteExtensionAsync(...params);
+ invocation.return_value(new GLib.Variant('(s)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async UninstallExtensionAsync(params, invocation) {
+ try {
+ const res = await this._proxy.UninstallExtensionAsync(...params);
+ invocation.return_value(new GLib.Variant('(b)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async EnableExtensionAsync(params, invocation) {
+ try {
+ const res = await this._proxy.EnableExtensionAsync(...params);
+ invocation.return_value(new GLib.Variant('(b)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async DisableExtensionAsync(params, invocation) {
+ try {
+ const res = await this._proxy.DisableExtensionAsync(...params);
+ invocation.return_value(new GLib.Variant('(b)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ LaunchExtensionPrefsAsync([uuid], invocation) {
+ this.OpenExtensionPrefsAsync([uuid, '', {}], invocation);
+ }
+
+ async OpenExtensionPrefsAsync(params, invocation) {
+ const [uuid, parentWindow, options] = params;
+
+ try {
+ const [serialized] = await this._proxy.GetExtensionInfoAsync(uuid);
+
+ if (this._prefsDialog)
+ throw new Error('Already showing a prefs dialog');
+
+ const extension = ExtensionUtils.deserializeExtension(serialized);
+
+ this._prefsDialog = new ExtensionPrefsDialog(extension);
+ this._prefsDialog.connect('realize', () => {
+ let externalWindow = null;
+
+ if (parentWindow)
+ externalWindow = Shew.ExternalWindow.new_from_handle(parentWindow);
+
+ if (externalWindow)
+ externalWindow.set_parent_of(this._prefsDialog.get_surface());
+ });
+
+ if (options.modal)
+ this._prefsDialog.modal = options.modal.get_boolean();
+
+ this._prefsDialog.connect('close-request', () => {
+ delete this._prefsDialog;
+ this.release();
+ return false;
+ });
+ this.hold();
+
+ this._prefsDialog.show();
+
+ invocation.return_value(null);
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async CheckForUpdatesAsync(params, invocation) {
+ try {
+ await this._proxy.CheckForUpdatesAsync(...params);
+ invocation.return_value(null);
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+};
diff --git a/js/dbusServices/extensions/main.js b/js/dbusServices/extensions/main.js
new file mode 100644
index 0000000..306fe36
--- /dev/null
+++ b/js/dbusServices/extensions/main.js
@@ -0,0 +1,23 @@
+/* exported main */
+
+imports.gi.versions.Adw = '1';
+imports.gi.versions.Gdk = '4.0';
+imports.gi.versions.Gtk = '4.0';
+
+const { Adw, GObject } = imports.gi;
+const pkg = imports.package;
+
+const { DBusService } = imports.dbusService;
+const { ExtensionsService } = imports.extensionsService;
+
+function main() {
+ Adw.init();
+ pkg.initFormat();
+
+ GObject.gtypeNameBasedOnJSPath = true;
+
+ const service = new DBusService(
+ 'org.gnome.Shell.Extensions',
+ new ExtensionsService());
+ service.run();
+}
diff --git a/js/dbusServices/extensions/ui/extension-error-page.ui b/js/dbusServices/extensions/ui/extension-error-page.ui
new file mode 100644
index 0000000..5ce6a62
--- /dev/null
+++ b/js/dbusServices/extensions/ui/extension-error-page.ui
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ExtensionPrefsErrorPage" parent="AdwPreferencesPage">
+ <style>
+ <class name="error-page"/>
+ </style>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Something’s gone wrong</property>
+ <style>
+ <class name="title-1"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">We’re very sorry, but there’s been a problem: the settings for this extension can’t be displayed. We recommend that you report the issue to the extension authors.</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="margin-top">12</property>
+ <child>
+ <object class="GtkBox">
+ <property name="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="expander">
+ <property name="spacing">6</property>
+ <style>
+ <class name="expander"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="expanderArrow">
+ <property name="icon-name">pan-end-symbolic</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Technical Details</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="revealer">
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkTextView" id="errorView">
+ <property name="monospace">True</property>
+ <property name="editable">False</property>
+ <property name="wrap-mode">word</property>
+ <property name="left-margin">12</property>
+ <property name="right-margin">12</property>
+ <property name="top-margin">12</property>
+ <property name="bottom-margin">12</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <style>
+ <class name="expander-toolbar"/>
+ </style>
+ <child>
+ <object class="GtkButton">
+ <property name="receives-default">True</property>
+ <property name="action-name">page.copy-error</property>
+ <property name="has-frame">False</property>
+ <property name="icon-name">edit-copy-symbolic</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="homeButton">
+ <property name="visible"
+ bind-source="homeButton"
+ bind-property="sensitive"
+ bind-flags="sync-create"/>
+ <property name="hexpand">True</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Homepage</property>
+ <property name="tooltip-text" translatable="yes">Visit extension homepage</property>
+ <property name="receives-default">True</property>
+ <property name="has-frame">False</property>
+ <property name="action-name">page.show-url</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/js/dbusServices/meson.build b/js/dbusServices/meson.build
new file mode 100644
index 0000000..48b7f89
--- /dev/null
+++ b/js/dbusServices/meson.build
@@ -0,0 +1,44 @@
+launcherconf = configuration_data()
+launcherconf.set('PACKAGE_NAME', meson.project_name())
+launcherconf.set('prefix', prefix)
+launcherconf.set('libdir', libdir)
+
+dbus_services = {
+ 'org.gnome.Shell.Extensions': 'extensions',
+ 'org.gnome.Shell.Notifications': 'notifications',
+ 'org.gnome.Shell.Screencast': 'screencast',
+ 'org.gnome.ScreenSaver': 'screensaver',
+}
+
+config_dir = '@0@/..'.format(meson.current_build_dir())
+
+foreach service, dir : dbus_services
+ configure_file(
+ input: 'dbus-service.in',
+ output: service,
+ configuration: launcherconf,
+ install_dir: pkgdatadir,
+ )
+
+ serviceconf = configuration_data()
+ serviceconf.set('service', service)
+ serviceconf.set('gjs', gjs.full_path())
+ serviceconf.set('pkgdatadir', pkgdatadir)
+
+ configure_file(
+ input: 'dbus-service.service.in',
+ output: service + '.service',
+ configuration: serviceconf,
+ install_dir: servicedir
+ )
+
+ gnome.compile_resources(
+ service + '.src',
+ service + '.src.gresource.xml',
+ dependencies: [config_js],
+ source_dir: ['.', '..', dir, config_dir],
+ gresource_bundle: true,
+ install: true,
+ install_dir: pkgdatadir
+ )
+endforeach
diff --git a/js/dbusServices/notifications/main.js b/js/dbusServices/notifications/main.js
new file mode 100644
index 0000000..7944cd1
--- /dev/null
+++ b/js/dbusServices/notifications/main.js
@@ -0,0 +1,11 @@
+/* exported main */
+
+const { DBusService } = imports.dbusService;
+const { NotificationDaemon } = imports.notificationDaemon;
+
+function main() {
+ const service = new DBusService(
+ 'org.gnome.Shell.Notifications',
+ new NotificationDaemon());
+ service.run();
+}
diff --git a/js/dbusServices/notifications/notificationDaemon.js b/js/dbusServices/notifications/notificationDaemon.js
new file mode 100644
index 0000000..b22f4ec
--- /dev/null
+++ b/js/dbusServices/notifications/notificationDaemon.js
@@ -0,0 +1,160 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported NotificationDaemon */
+
+const { Gio, GLib } = imports.gi;
+
+const { loadInterfaceXML } = imports.misc.dbusUtils;
+const { ServiceImplementation } = imports.dbusService;
+
+const NotificationsIface = loadInterfaceXML('org.freedesktop.Notifications');
+const NotificationsProxy = Gio.DBusProxy.makeProxyWrapper(NotificationsIface);
+
+Gio._promisify(Gio.DBusConnection.prototype, 'call');
+
+var NotificationDaemon = class extends ServiceImplementation {
+ constructor() {
+ super(NotificationsIface, '/org/freedesktop/Notifications');
+
+ this._autoShutdown = false;
+
+ this._activeNotifications = new Map();
+
+ this._proxy = new NotificationsProxy(Gio.DBus.session,
+ 'org.gnome.Shell',
+ '/org/freedesktop/Notifications',
+ (proxy, error) => {
+ if (error)
+ log(error.message);
+ });
+
+ this._proxy.connectSignal('ActionInvoked',
+ (proxy, sender, params) => {
+ const [id] = params;
+ this._emitSignal(
+ this._activeNotifications.get(id),
+ 'ActionInvoked',
+ new GLib.Variant('(us)', params));
+ });
+ this._proxy.connectSignal('NotificationClosed',
+ (proxy, sender, params) => {
+ const [id] = params;
+ this._emitSignal(
+ this._activeNotifications.get(id),
+ 'NotificationClosed',
+ new GLib.Variant('(uu)', params));
+ this._activeNotifications.delete(id);
+ });
+ }
+
+ _emitSignal(sender, signalName, params) {
+ if (!sender)
+ return;
+ this._dbusImpl.get_connection()?.emit_signal(
+ sender,
+ this._dbusImpl.get_object_path(),
+ 'org.freedesktop.Notifications',
+ signalName,
+ params);
+ }
+
+ _untrackSender(sender) {
+ super._untrackSender(sender);
+
+ this._activeNotifications.forEach((value, key) => {
+ if (value === sender)
+ this._activeNotifications.delete(key);
+ });
+ }
+
+ _checkNotificationId(invocation, id) {
+ if (id === 0)
+ return true;
+
+ if (!this._activeNotifications.has(id))
+ return true;
+
+ if (this._activeNotifications.get(id) === invocation.get_sender())
+ return true;
+
+ const error = new GLib.Error(Gio.DBusError,
+ Gio.DBusError.INVALID_ARGS, 'Invalid notification ID');
+ this._handleError(invocation, error);
+ return false;
+ }
+
+ register() {
+ Gio.DBus.session.own_name(
+ 'org.freedesktop.Notifications',
+ Gio.BusNameOwnerFlags.REPLACE,
+ null, null);
+ }
+
+ async NotifyAsync(params, invocation) {
+ const sender = invocation.get_sender();
+ const pid = await this._getSenderPid(sender);
+ const replaceId = params[1];
+ const hints = params[6];
+
+ if (!this._checkNotificationId(invocation, replaceId))
+ return;
+
+ params[6] = {
+ ...hints,
+ 'sender-pid': new GLib.Variant('u', pid),
+ };
+
+ try {
+ const [id] = await this._proxy.NotifyAsync(...params);
+ this._activeNotifications.set(id, sender);
+ invocation.return_value(new GLib.Variant('(u)', [id]));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async CloseNotificationAsync(params, invocation) {
+ const [id] = params;
+ if (!this._checkNotificationId(invocation, id))
+ return;
+
+ try {
+ await this._proxy.CloseNotificationAsync(...params);
+ invocation.return_value(null);
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async GetCapabilitiesAsync(params, invocation) {
+ try {
+ const res = await this._proxy.GetCapabilitiesAsync(...params);
+ invocation.return_value(new GLib.Variant('(as)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async GetServerInformationAsync(params, invocation) {
+ try {
+ const res = await this._proxy.GetServerInformationAsync(...params);
+ invocation.return_value(new GLib.Variant('(ssss)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async _getSenderPid(sender) {
+ const res = await Gio.DBus.session.call(
+ 'org.freedesktop.DBus',
+ '/',
+ 'org.freedesktop.DBus',
+ 'GetConnectionUnixProcessID',
+ new GLib.Variant('(s)', [sender]),
+ new GLib.VariantType('(u)'),
+ Gio.DBusCallFlags.NONE,
+ -1,
+ null);
+ const [pid] = res.deepUnpack();
+ return pid;
+ }
+};
diff --git a/js/dbusServices/org.gnome.ScreenSaver.src.gresource.xml b/js/dbusServices/org.gnome.ScreenSaver.src.gresource.xml
new file mode 100644
index 0000000..d77f72a
--- /dev/null
+++ b/js/dbusServices/org.gnome.ScreenSaver.src.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/ScreenSaver/js">
+ <file>main.js</file>
+ <file>screenSaverService.js</file>
+ <file>dbusService.js</file>
+
+ <file>misc/config.js</file>
+ <file>misc/dbusUtils.js</file>
+ </gresource>
+</gresources>
diff --git a/js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml b/js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml
new file mode 100644
index 0000000..3ab92a2
--- /dev/null
+++ b/js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/Shell/Extensions/js">
+ <file>main.js</file>
+ <file>extensionsService.js</file>
+ <file>extensionPrefsDialog.js</file>
+ <file>dbusService.js</file>
+
+ <file>misc/config.js</file>
+ <file>misc/extensionUtils.js</file>
+ <file>misc/dbusUtils.js</file>
+ <file>misc/params.js</file>
+ </gresource>
+
+ <gresource prefix="/org/gnome/Shell/Extensions">
+ <file>css/application.css</file>
+ <file>ui/extension-error-page.ui</file>
+ </gresource>
+</gresources>
diff --git a/js/dbusServices/org.gnome.Shell.Notifications.src.gresource.xml b/js/dbusServices/org.gnome.Shell.Notifications.src.gresource.xml
new file mode 100644
index 0000000..4e039ba
--- /dev/null
+++ b/js/dbusServices/org.gnome.Shell.Notifications.src.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/Shell/Notifications/js">
+ <file>main.js</file>
+ <file>notificationDaemon.js</file>
+ <file>dbusService.js</file>
+
+ <file>misc/config.js</file>
+ <file>misc/dbusUtils.js</file>
+ </gresource>
+</gresources>
diff --git a/js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml b/js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml
new file mode 100644
index 0000000..292f0f1
--- /dev/null
+++ b/js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/Shell/Screencast/js">
+ <file>main.js</file>
+ <file>screencastService.js</file>
+ <file>dbusService.js</file>
+
+ <file>misc/config.js</file>
+ <file>misc/dbusUtils.js</file>
+ </gresource>
+</gresources>
diff --git a/js/dbusServices/screencast/main.js b/js/dbusServices/screencast/main.js
new file mode 100644
index 0000000..7c71971
--- /dev/null
+++ b/js/dbusServices/screencast/main.js
@@ -0,0 +1,14 @@
+/* exported main */
+
+const {DBusService} = imports.dbusService;
+
+function main() {
+ const {ScreencastService} = imports.screencastService;
+ if (!ScreencastService.canScreencast())
+ return;
+
+ const service = new DBusService(
+ 'org.gnome.Shell.Screencast',
+ new ScreencastService());
+ service.run();
+}
diff --git a/js/dbusServices/screencast/screencastService.js b/js/dbusServices/screencast/screencastService.js
new file mode 100644
index 0000000..eb3dc88
--- /dev/null
+++ b/js/dbusServices/screencast/screencastService.js
@@ -0,0 +1,498 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ScreencastService */
+
+imports.gi.versions.Gst = '1.0';
+imports.gi.versions.Gtk = '4.0';
+
+const { Gio, GLib, Gst, Gtk } = imports.gi;
+
+const { loadInterfaceXML, loadSubInterfaceXML } = imports.misc.dbusUtils;
+const { ServiceImplementation } = imports.dbusService;
+
+const ScreencastIface = loadInterfaceXML('org.gnome.Shell.Screencast');
+
+const IntrospectIface = loadInterfaceXML('org.gnome.Shell.Introspect');
+const IntrospectProxy = Gio.DBusProxy.makeProxyWrapper(IntrospectIface);
+
+const ScreenCastIface = loadSubInterfaceXML(
+ 'org.gnome.Mutter.ScreenCast', 'org.gnome.Mutter.ScreenCast');
+const ScreenCastSessionIface = loadSubInterfaceXML(
+ 'org.gnome.Mutter.ScreenCast.Session', 'org.gnome.Mutter.ScreenCast');
+const ScreenCastStreamIface = loadSubInterfaceXML(
+ 'org.gnome.Mutter.ScreenCast.Stream', 'org.gnome.Mutter.ScreenCast');
+const ScreenCastProxy = Gio.DBusProxy.makeProxyWrapper(ScreenCastIface);
+const ScreenCastSessionProxy = Gio.DBusProxy.makeProxyWrapper(ScreenCastSessionIface);
+const ScreenCastStreamProxy = Gio.DBusProxy.makeProxyWrapper(ScreenCastStreamIface);
+
+const DEFAULT_PIPELINE = 'videoconvert chroma-mode=GST_VIDEO_CHROMA_MODE_NONE dither=GST_VIDEO_DITHER_NONE matrix-mode=GST_VIDEO_MATRIX_MODE_OUTPUT_ONLY n-threads=%T ! queue ! vp8enc cpu-used=16 max-quantizer=17 deadline=1 keyframe-mode=disabled threads=%T static-threshold=1000 buffer-size=20000 ! queue ! webmmux';
+const DEFAULT_FRAMERATE = 30;
+const DEFAULT_DRAW_CURSOR = true;
+
+const PipelineState = {
+ INIT: 0,
+ PLAYING: 1,
+ FLUSHING: 2,
+ STOPPED: 3,
+};
+
+const SessionState = {
+ INIT: 0,
+ ACTIVE: 1,
+ STOPPED: 2,
+};
+
+var Recorder = class {
+ constructor(sessionPath, x, y, width, height, filePath, options,
+ invocation,
+ onErrorCallback) {
+ this._startInvocation = invocation;
+ this._dbusConnection = invocation.get_connection();
+ this._onErrorCallback = onErrorCallback;
+ this._stopInvocation = null;
+
+ this._x = x;
+ this._y = y;
+ this._width = width;
+ this._height = height;
+ this._filePath = filePath;
+
+ try {
+ const dir = Gio.File.new_for_path(filePath).get_parent();
+ dir.make_directory_with_parents(null);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
+ throw e;
+ }
+
+ this._pipelineString = DEFAULT_PIPELINE;
+ this._framerate = DEFAULT_FRAMERATE;
+ this._drawCursor = DEFAULT_DRAW_CURSOR;
+
+ this._applyOptions(options);
+ this._watchSender(invocation.get_sender());
+
+ this._initSession(sessionPath);
+ }
+
+ _applyOptions(options) {
+ for (const option in options)
+ options[option] = options[option].deepUnpack();
+
+ if (options['pipeline'] !== undefined)
+ this._pipelineString = options['pipeline'];
+ if (options['framerate'] !== undefined)
+ this._framerate = options['framerate'];
+ if ('draw-cursor' in options)
+ this._drawCursor = options['draw-cursor'];
+ }
+
+ _addRecentItem() {
+ const file = Gio.File.new_for_path(this._filePath);
+ Gtk.RecentManager.get_default().add_item(file.get_uri());
+ }
+
+ _watchSender(sender) {
+ this._nameWatchId = this._dbusConnection.watch_name(
+ sender,
+ Gio.BusNameWatcherFlags.NONE,
+ null,
+ this._senderVanished.bind(this));
+ }
+
+ _unwatchSender() {
+ if (this._nameWatchId !== 0) {
+ this._dbusConnection.unwatch_name(this._nameWatchId);
+ this._nameWatchId = 0;
+ }
+ }
+
+ _senderVanished() {
+ this._unwatchSender();
+
+ this.stopRecording(null);
+ }
+
+ _notifyStopped() {
+ this._unwatchSender();
+ if (this._onStartedCallback)
+ this._onStartedCallback(this, false);
+ else if (this._onStoppedCallback)
+ this._onStoppedCallback(this);
+ else
+ this._onErrorCallback(this);
+ }
+
+ _onSessionClosed() {
+ switch (this._pipelineState) {
+ case PipelineState.STOPPED:
+ break;
+ default:
+ this._pipeline.set_state(Gst.State.NULL);
+ log(`Unexpected pipeline state: ${this._pipelineState}`);
+ break;
+ }
+ this._notifyStopped();
+ }
+
+ _initSession(sessionPath) {
+ this._sessionProxy = new ScreenCastSessionProxy(Gio.DBus.session,
+ 'org.gnome.Mutter.ScreenCast',
+ sessionPath);
+ this._sessionProxy.connectSignal('Closed', this._onSessionClosed.bind(this));
+ }
+
+ _startPipeline(nodeId) {
+ if (!this._ensurePipeline(nodeId))
+ return;
+
+ const bus = this._pipeline.get_bus();
+ bus.add_watch(bus, this._onBusMessage.bind(this));
+
+ this._pipeline.set_state(Gst.State.PLAYING);
+ this._pipelineState = PipelineState.PLAYING;
+
+ this._onStartedCallback(this, true);
+ this._onStartedCallback = null;
+ }
+
+ startRecording(onStartedCallback) {
+ this._onStartedCallback = onStartedCallback;
+
+ const [streamPath] = this._sessionProxy.RecordAreaSync(
+ this._x, this._y,
+ this._width, this._height,
+ {
+ 'is-recording': GLib.Variant.new('b', true),
+ 'cursor-mode': GLib.Variant.new('u', this._drawCursor ? 1 : 0),
+ });
+
+ this._streamProxy = new ScreenCastStreamProxy(Gio.DBus.session,
+ 'org.gnome.ScreenCast.Stream',
+ streamPath);
+
+ this._streamProxy.connectSignal('PipeWireStreamAdded',
+ (proxy, sender, params) => {
+ const [nodeId] = params;
+ this._startPipeline(nodeId);
+ });
+ this._sessionProxy.StartSync();
+ this._sessionState = SessionState.ACTIVE;
+ }
+
+ stopRecording(onStoppedCallback) {
+ this._pipelineState = PipelineState.FLUSHING;
+ this._onStoppedCallback = onStoppedCallback;
+ this._pipeline.send_event(Gst.Event.new_eos());
+ }
+
+ _stopSession() {
+ this._sessionProxy.StopSync();
+ this._sessionState = SessionState.STOPPED;
+ }
+
+ _onBusMessage(bus, message, _) {
+ switch (message.type) {
+ case Gst.MessageType.EOS:
+ this._pipeline.set_state(Gst.State.NULL);
+ this._addRecentItem();
+
+ switch (this._pipelineState) {
+ case PipelineState.FLUSHING:
+ this._pipelineState = PipelineState.STOPPED;
+ break;
+ default:
+ break;
+ }
+
+ switch (this._sessionState) {
+ case SessionState.ACTIVE:
+ this._stopSession();
+ break;
+ case SessionState.STOPPED:
+ this._notifyStopped();
+ break;
+ default:
+ break;
+ }
+
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ _substituteThreadCount(pipelineDescr) {
+ const numProcessors = GLib.get_num_processors();
+ const numThreads = Math.min(Math.max(1, numProcessors), 64);
+ return pipelineDescr.replaceAll('%T', numThreads);
+ }
+
+ _ensurePipeline(nodeId) {
+ const framerate = this._framerate;
+ const needsCopy =
+ Gst.Registry.get().check_feature_version('pipewiresrc', 0, 3, 57) &&
+ !Gst.Registry.get().check_feature_version('videoconvert', 1, 20, 4);
+
+ let fullPipeline = `
+ pipewiresrc path=${nodeId}
+ always-copy=${needsCopy}
+ do-timestamp=true
+ keepalive-time=1000
+ resend-last=true !
+ video/x-raw,max-framerate=${framerate}/1 !
+ ${this._pipelineString} !
+ filesink location="${this._filePath}"`;
+ fullPipeline = this._substituteThreadCount(fullPipeline);
+
+ try {
+ this._pipeline = Gst.parse_launch_full(fullPipeline,
+ null,
+ Gst.ParseFlags.FATAL_ERRORS);
+ } catch (e) {
+ log(`Failed to create pipeline: ${e}`);
+ this._notifyStopped();
+ }
+ return !!this._pipeline;
+ }
+};
+
+var ScreencastService = class extends ServiceImplementation {
+ static canScreencast() {
+ const elements = [
+ 'pipewiresrc',
+ 'filesink',
+ ...DEFAULT_PIPELINE.split('!').map(e => e.trim().split(' ').at(0)),
+ ];
+ return Gst.init_check(null) &&
+ elements.every(e => Gst.ElementFactory.find(e) != null);
+ }
+
+ constructor() {
+ super(ScreencastIface, '/org/gnome/Shell/Screencast');
+
+ this.hold(); // gstreamer initializing can take a bit
+ this._canScreencast = ScreencastService.canScreencast();
+
+ Gst.init(null);
+ Gtk.init();
+
+ this.release();
+
+ this._recorders = new Map();
+ this._senders = new Map();
+
+ this._lockdownSettings = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.lockdown',
+ });
+
+ this._proxy = new ScreenCastProxy(Gio.DBus.session,
+ 'org.gnome.Mutter.ScreenCast',
+ '/org/gnome/Mutter/ScreenCast');
+
+ this._introspectProxy = new IntrospectProxy(Gio.DBus.session,
+ 'org.gnome.Shell.Introspect',
+ '/org/gnome/Shell/Introspect');
+ }
+
+ get ScreencastSupported() {
+ return this._canScreencast;
+ }
+
+ _removeRecorder(sender) {
+ this._recorders.delete(sender);
+ if (this._recorders.size === 0)
+ this.release();
+ }
+
+ _addRecorder(sender, recorder) {
+ this._recorders.set(sender, recorder);
+ if (this._recorders.size === 1)
+ this.hold();
+ }
+
+ _getAbsolutePath(filename) {
+ if (GLib.path_is_absolute(filename))
+ return filename;
+
+ const videoDir =
+ GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) ||
+ GLib.get_home_dir();
+
+ return GLib.build_filenamev([videoDir, filename]);
+ }
+
+ _generateFilePath(template) {
+ let filename = '';
+ let escape = false;
+
+ [...template].forEach(c => {
+ if (escape) {
+ switch (c) {
+ case '%':
+ filename += '%';
+ break;
+ case 'd': {
+ const datetime = GLib.DateTime.new_now_local();
+ const datestr = datetime.format('%Y-%m-%d');
+
+ filename += datestr;
+ break;
+ }
+
+ case 't': {
+ const datetime = GLib.DateTime.new_now_local();
+ const datestr = datetime.format('%H-%M-%S');
+
+ filename += datestr;
+ break;
+ }
+
+ default:
+ log(`Warning: Unknown escape ${c}`);
+ }
+
+ escape = false;
+ } else if (c === '%') {
+ escape = true;
+ } else {
+ filename += c;
+ }
+ });
+
+ if (escape)
+ filename += '%';
+
+ return this._getAbsolutePath(filename);
+ }
+
+ ScreencastAsync(params, invocation) {
+ let returnValue = [false, ''];
+
+ if (this._lockdownSettings.get_boolean('disable-save-to-disk')) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const sender = invocation.get_sender();
+
+ if (this._recorders.get(sender)) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const [sessionPath] = this._proxy.CreateSessionSync({});
+
+ const [fileTemplate, options] = params;
+ const [screenWidth, screenHeight] = this._introspectProxy.ScreenSize;
+ const filePath = this._generateFilePath(fileTemplate);
+
+ let recorder;
+
+ try {
+ recorder = new Recorder(
+ sessionPath,
+ 0, 0,
+ screenWidth, screenHeight,
+ filePath,
+ options,
+ invocation,
+ _recorder => this._removeRecorder(sender));
+ } catch (error) {
+ log(`Failed to create recorder: ${error.message}`);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ this._addRecorder(sender, recorder);
+
+ try {
+ recorder.startRecording(
+ (_, result) => {
+ if (result) {
+ returnValue = [true, filePath];
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ } else {
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+ });
+ } catch (error) {
+ log(`Failed to start recorder: ${error.message}`);
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+ }
+
+ ScreencastAreaAsync(params, invocation) {
+ let returnValue = [false, ''];
+
+ if (this._lockdownSettings.get_boolean('disable-save-to-disk')) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const sender = invocation.get_sender();
+
+ if (this._recorders.get(sender)) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const [sessionPath] = this._proxy.CreateSessionSync({});
+
+ const [x, y, width, height, fileTemplate, options] = params;
+ const filePath = this._generateFilePath(fileTemplate);
+
+ let recorder;
+
+ try {
+ recorder = new Recorder(
+ sessionPath,
+ x, y,
+ width, height,
+ filePath,
+ options,
+ invocation,
+ _recorder => this._removeRecorder(sender));
+ } catch (error) {
+ log(`Failed to create recorder: ${error.message}`);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ this._addRecorder(sender, recorder);
+
+ try {
+ recorder.startRecording(
+ (_, result) => {
+ if (result) {
+ returnValue = [true, filePath];
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ } else {
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+ });
+ } catch (error) {
+ log(`Failed to start recorder: ${error.message}`);
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+ }
+
+ StopScreencastAsync(params, invocation) {
+ const sender = invocation.get_sender();
+
+ const recorder = this._recorders.get(sender);
+ if (!recorder) {
+ invocation.return_value(GLib.Variant.new('(b)', [false]));
+ return;
+ }
+
+ recorder.stopRecording(() => {
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(b)', [true]));
+ });
+ }
+};
diff --git a/js/dbusServices/screensaver/main.js b/js/dbusServices/screensaver/main.js
new file mode 100644
index 0000000..2a08d14
--- /dev/null
+++ b/js/dbusServices/screensaver/main.js
@@ -0,0 +1,11 @@
+/* exported main */
+
+const { DBusService } = imports.dbusService;
+const { ScreenSaverService } = imports.screenSaverService;
+
+function main() {
+ const service = new DBusService(
+ 'org.gnome.ScreenSaver',
+ new ScreenSaverService());
+ service.run();
+}
diff --git a/js/dbusServices/screensaver/screenSaverService.js b/js/dbusServices/screensaver/screenSaverService.js
new file mode 100644
index 0000000..2c1546e
--- /dev/null
+++ b/js/dbusServices/screensaver/screenSaverService.js
@@ -0,0 +1,70 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ScreenSaverService */
+
+const { Gio, GLib } = imports.gi;
+
+const { loadInterfaceXML } = imports.misc.dbusUtils;
+const { ServiceImplementation } = imports.dbusService;
+
+const ScreenSaverIface = loadInterfaceXML('org.gnome.ScreenSaver');
+const ScreenSaverProxy = Gio.DBusProxy.makeProxyWrapper(ScreenSaverIface);
+
+var ScreenSaverService = class extends ServiceImplementation {
+ constructor() {
+ super(ScreenSaverIface, '/org/gnome/ScreenSaver');
+
+ this._autoShutdown = false;
+
+ this._proxy = new ScreenSaverProxy(Gio.DBus.session,
+ 'org.gnome.Shell.ScreenShield',
+ '/org/gnome/ScreenSaver',
+ (proxy, error) => {
+ if (error)
+ log(error.message);
+ });
+
+ this._proxy.connectSignal('ActiveChanged',
+ (proxy, sender, params) => {
+ this._dbusImpl.emit_signal('ActiveChanged',
+ new GLib.Variant('(b)', params));
+ });
+ this._proxy.connectSignal('WakeUpScreen',
+ () => this._dbusImpl.emit_signal('WakeUpScreen', null));
+ }
+
+ async LockAsync(params, invocation) {
+ try {
+ await this._proxy.LockAsync(...params);
+ invocation.return_value(null);
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async GetActiveAsync(params, invocation) {
+ try {
+ const res = await this._proxy.GetActiveAsync(...params);
+ invocation.return_value(new GLib.Variant('(b)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async SetActiveAsync(params, invocation) {
+ try {
+ await this._proxy.SetActiveAsync(...params);
+ invocation.return_value(null);
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+
+ async GetActiveTimeAsync(params, invocation) {
+ try {
+ const res = await this._proxy.GetActiveTimeAsync(...params);
+ invocation.return_value(new GLib.Variant('(u)', res));
+ } catch (error) {
+ this._handleError(invocation, error);
+ }
+ }
+};