diff options
Diffstat (limited to '')
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); + } + } +}; |